mirror of
https://github.com/markqvist/Reticulum.git
synced 2026-06-22 20:12:37 -07:00
Compare commits
575 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3517d2e4b | |||
| c59f1e3dd6 | |||
| 2cf07099e3 | |||
| 0292465666 | |||
| b4e15503c1 | |||
| a5ca1ee41e | |||
| f9c786fa28 | |||
| bcf35030bc | |||
| 237eada209 | |||
| afb984d3d4 | |||
| 675a25c90e | |||
| f6d6314842 | |||
| 22ab5c29bd | |||
| 4931c6a54c | |||
| d3fcc2a38c | |||
| e9609b7f25 | |||
| 70cd51f0fa | |||
| fa7699a37d | |||
| 2c9b794182 | |||
| eae84ed8ba | |||
| 31539c5a0e | |||
| abca32bca4 | |||
| 1d7cfe7c20 | |||
| a516960e6f | |||
| 870bee16b1 | |||
| 3814102936 | |||
| aaadff547d | |||
| f0a3fadcd1 | |||
| 7cbce84cbd | |||
| fe334c0d7c | |||
| 33d5a8e2a8 | |||
| e80bf471ec | |||
| 7dfdea2395 | |||
| 74b61aebd2 | |||
| ce9071e2d3 | |||
| d6cf59dcc8 | |||
| de61652d37 | |||
| 6181f62d93 | |||
| ed66b4873e | |||
| 26869941a4 | |||
| ee7b4e7ae5 | |||
| 7866484453 | |||
| 817b3b1a12 | |||
| 17f6968467 | |||
| a96a1d6692 | |||
| c1081fa9a4 | |||
| dd3104094b | |||
| dc68eea313 | |||
| 794e437f6d | |||
| ebf544d335 | |||
| 939f30fef2 | |||
| 4549bbfdb9 | |||
| 6c989eb38e | |||
| 137d73ad0d | |||
| 58d4162f6d | |||
| 67625395fe | |||
| f62512381a | |||
| 888e3102de | |||
| f83435c697 | |||
| 5243d646f0 | |||
| bdb284ce5d | |||
| 1b34820601 | |||
| 01010dd599 | |||
| da32709f7c | |||
| cdc6159a15 | |||
| eb2f7ae455 | |||
| a74f1bd89f | |||
| 603f709139 | |||
| 0dd063a32e | |||
| 9885a70a88 | |||
| e4a85de089 | |||
| ca0d2dffbe | |||
| 511d169c77 | |||
| 19bc8ef85c | |||
| 1e33d3eebb | |||
| d18f434583 | |||
| 64749b4d18 | |||
| 875d8ef7eb | |||
| 20283f1536 | |||
| c4af328802 | |||
| a2193b9ffd | |||
| d2542fd49b | |||
| 0333884877 | |||
| 63947ed69a | |||
| d6d18ce29c | |||
| 2ef58d8b59 | |||
| 15f2d1635e | |||
| c83b71f49a | |||
| f0824fd71e | |||
| 8dde60658f | |||
| 9437648ae5 | |||
| 71b19aca2c | |||
| 7d320f8cd5 | |||
| 340d0883a7 | |||
| 66096acc29 | |||
| e35100d865 | |||
| 128455ef01 | |||
| 10156cc90e | |||
| 4f5482f2ae | |||
| 2b9fdae74b | |||
| 7506caa0da | |||
| b1f522277c | |||
| af6e0c9ecf | |||
| 176567e3f1 | |||
| 15cd4268ac | |||
| 9307db16c4 | |||
| 0f29ab629a | |||
| b2a4ceb853 | |||
| 6c7f1d068b | |||
| b76beb602d | |||
| 0c68f6491a | |||
| 038981474a | |||
| df0b4a5165 | |||
| db7359f56d | |||
| 12e45b6483 | |||
| ba8fca6f87 | |||
| 9b99b72f61 | |||
| 03cfbc2eb6 | |||
| c92872a81b | |||
| f3f4d9bca3 | |||
| e7a317f0a0 | |||
| d5b64a4af3 | |||
| 5667a0bbac | |||
| 7e46422c16 | |||
| 869a803149 | |||
| f744e4d9a3 | |||
| 1a7607cba3 | |||
| d881c111f6 | |||
| bdac57ec0b | |||
| c15f566cfa | |||
| bdc79b9097 | |||
| 102eccb77d | |||
| e8b236c7d8 | |||
| d69491eb80 | |||
| 256a4d0b92 | |||
| c5add012c1 | |||
| 6ecc8933b4 | |||
| 42b5661979 | |||
| 6333fb39bf | |||
| ea27a8b8a7 | |||
| 358f9c3b0c | |||
| cb3ef69072 | |||
| eee9354657 | |||
| ff86a1d7e6 | |||
| e49f31322c | |||
| 95502e2c21 | |||
| 3dd4145e62 | |||
| 1d7ddc3f8a | |||
| d731b4396c | |||
| c186a1f6b0 | |||
| a049ec8b7b | |||
| 4c93f6c7f4 | |||
| 35c7a89b19 | |||
| c86b9c9703 | |||
| 64ebdd0ee3 | |||
| 9179b914d5 | |||
| eb5d46b20b | |||
| 54c36f515b | |||
| 5c5668a4fc | |||
| eeefb60c89 | |||
| 018df10a26 | |||
| 93ead77435 | |||
| bd0e1ad0ca | |||
| d0ceeacb37 | |||
| 7d5fb6a13f | |||
| 855ef7bfd1 | |||
| 323890021a | |||
| e004e7592b | |||
| 0ebec014e5 | |||
| 1b624cc0e2 | |||
| e8d161c0d5 | |||
| e5c7dd7ec7 | |||
| 7d6ed59e6e | |||
| 11e4e7953a | |||
| a5b292ee81 | |||
| d619bafb8d | |||
| 0119a589dc | |||
| b7346bed4d | |||
| fcea57cb8e | |||
| 8d8af5e60a | |||
| 1a732ac1c1 | |||
| f827d945be | |||
| e03c4ee455 | |||
| 35e7ccb773 | |||
| a932a10492 | |||
| c5108c3a19 | |||
| 767782e425 | |||
| 60c440a3b6 | |||
| 6551a25877 | |||
| 70db2c5369 | |||
| 8ed31d0dc8 | |||
| ef1ecb35e1 | |||
| 6768f10631 | |||
| fee6a53473 | |||
| bbfa3b0aa0 | |||
| 325ae654ef | |||
| 8655a4fb37 | |||
| b30d272ee6 | |||
| cc90ac2853 | |||
| 55473f39cb | |||
| 6d73881b07 | |||
| d107cd4b42 | |||
| 33247e21b2 | |||
| 6bdc769af3 | |||
| e923ccbf1b | |||
| d402ee33a2 | |||
| d8d420745f | |||
| 524f2068cd | |||
| 5db089ff19 | |||
| 08d6780c73 | |||
| ca3f0bba6d | |||
| 830327e4a2 | |||
| f96409dfa9 | |||
| 18e2da7d2b | |||
| dfd046afb6 | |||
| 63d7f1e295 | |||
| 9d076d6a19 | |||
| c6fa33a8aa | |||
| 37fa4392a5 | |||
| 90c88ade00 | |||
| bb08f63a9f | |||
| bdfad57d3f | |||
| 7ceb2d2078 | |||
| 304acdd0c1 | |||
| 8b6609c588 | |||
| 8a1d3aedd4 | |||
| d49f100edd | |||
| 83d9ee1c5f | |||
| b527c59735 | |||
| 23498a7a0a | |||
| ac2cf79451 | |||
| 42b7426ed8 | |||
| 928c02099b | |||
| c0ae63e27a | |||
| 62532e1c54 | |||
| 3136b53277 | |||
| 9352cff870 | |||
| 9e5fd0f079 | |||
| 1d37ba4780 | |||
| 134c1fb6ac | |||
| 24df04f304 | |||
| 26595bb25a | |||
| 5ee7dcf5a3 | |||
| 8b2ba9907f | |||
| d1c59ef3b6 | |||
| 2dd23b15a8 | |||
| 93ad11f193 | |||
| ec27d8bfde | |||
| 4d6e164d62 | |||
| d82ffce504 | |||
| 7ecd435911 | |||
| 49f56e7d0d | |||
| b8d6a14599 | |||
| 9c166936ad | |||
| 69db87cc24 | |||
| 5d86232fbe | |||
| 607e80bc82 | |||
| f9625b2b88 | |||
| 3d8079c02b | |||
| 5c05a7fa58 | |||
| 2fa959a560 | |||
| c39494d9fa | |||
| a3cd1ea83d | |||
| d4ddf6bb13 | |||
| 8661a3886b | |||
| 2ddbef70fe | |||
| bb051e5a11 | |||
| 080085e813 | |||
| 85454b1f25 | |||
| 3f5653f650 | |||
| b1357eb146 | |||
| 7731e799f4 | |||
| 15320e4d2c | |||
| 78596b687a | |||
| 729dc8dc11 | |||
| 3c08eb8122 | |||
| 9d12c86ac8 | |||
| 3bd573688c | |||
| 07ff87974e | |||
| e8fa92950d | |||
| ab6532742e | |||
| 4e583770e5 | |||
| f9b6dc2ab8 | |||
| 1c2bc0c7b8 | |||
| 05760f914c | |||
| 3f6e8605af | |||
| b6bfd1655c | |||
| 8cbd0e22ff | |||
| 15ec64e974 | |||
| 3de16e085e | |||
| 4cbd4ed60c | |||
| b8fbd616e5 | |||
| f8a79d2f51 | |||
| 0218ff4e26 | |||
| 1f3ce7e78f | |||
| 9009e1d232 | |||
| cc73b2c2b9 | |||
| dbf19ed054 | |||
| a1cff4e8ab | |||
| c9822968c8 | |||
| 8acabd95b5 | |||
| 49f6a6924d | |||
| 8d73265cf4 | |||
| fceb7d18d7 | |||
| 337007cf70 | |||
| 4733d6d75a | |||
| c8235544e8 | |||
| 3d1111ff02 | |||
| 83c9f2b10a | |||
| 734eb53aa7 | |||
| 6d39cb8e7c | |||
| 3c3f38b239 | |||
| 86d52d3884 | |||
| 6782672cb8 | |||
| 7fada7e5ab | |||
| 4380026a4e | |||
| 5143ea3d02 | |||
| 4802bcd829 | |||
| 6038096b95 | |||
| 2acfc31350 | |||
| 2742e5253f | |||
| 46f2e994b9 | |||
| 2c97a20c12 | |||
| 9be10ebd47 | |||
| 93cbfe7f7e | |||
| 4589de2115 | |||
| 662054ae25 | |||
| 3cf186f3cb | |||
| 7a91c82e4b | |||
| 72aace40d3 | |||
| 0c9a65b5f1 | |||
| ea749499c3 | |||
| 828cbe7f20 | |||
| 1d8d547872 | |||
| 16c53221e3 | |||
| 74936010c4 | |||
| f3245e1d65 | |||
| 1f74570ed9 | |||
| 88d1b7d2d1 | |||
| fb5dcf0631 | |||
| a23086d3fc | |||
| a4cbcbca97 | |||
| 9dd008d42b | |||
| 76fa07cb90 | |||
| 35d72f27ed | |||
| 852891c779 | |||
| f4aa7dc389 | |||
| d7c3859f61 | |||
| 85d77c10a1 | |||
| 95222c7793 | |||
| 0a18b47e8c | |||
| 70f5126499 | |||
| b60eab0fcf | |||
| 17310fc294 | |||
| 9c892dc1a4 | |||
| c596dab806 | |||
| fcb590e661 | |||
| 328017cca0 | |||
| 63dba562ae | |||
| cf20f26098 | |||
| e1e6063d17 | |||
| ccbbe6f2f8 | |||
| 55c95bf59a | |||
| 043a5dc4e7 | |||
| 32a1cdf494 | |||
| f924086198 | |||
| 6abb31e469 | |||
| 3eee369704 | |||
| 695d4d8684 | |||
| 015692d51e | |||
| 86004a89e5 | |||
| 86031ef3f8 | |||
| 034239daf3 | |||
| a7b0f9924e | |||
| a1d35b34b9 | |||
| 8d7e337dff | |||
| de7e0996ce | |||
| 7377b69144 | |||
| c933cfdaa3 | |||
| 726185cee2 | |||
| de1000bfda | |||
| 555e8c0376 | |||
| d836de3fe7 | |||
| 6ade1269ea | |||
| a8b519e06e | |||
| 7d502306ea | |||
| e9fa57c660 | |||
| 7d4ab17f0d | |||
| d532902320 | |||
| e592244443 | |||
| c1def5da19 | |||
| 6a7f081f12 | |||
| 11555198eb | |||
| 6c77e27a50 | |||
| 17e8159fd8 | |||
| c71f5d8c5e | |||
| 31cc9fc7d1 | |||
| 1d2421b0af | |||
| a5df765951 | |||
| 622019ee06 | |||
| 45e12cc668 | |||
| a21024a57e | |||
| c175491bb0 | |||
| 09b0469faf | |||
| 3d63bbf4bf | |||
| 56d5d01497 | |||
| a70bd44426 | |||
| 8c082b2fcc | |||
| 1732cac806 | |||
| e1340e87eb | |||
| e9bfef2131 | |||
| b408699e65 | |||
| 3d1c508868 | |||
| 84e0746c9c | |||
| b5658c4865 | |||
| d413a4bc53 | |||
| ce5ab902b6 | |||
| 294408b0bb | |||
| 53372fbe4c | |||
| 7fdac2118b | |||
| 1dbf78ed71 | |||
| c9101a0c21 | |||
| 2e6264c04b | |||
| e0aa46ba22 | |||
| 8093c3cd2c | |||
| c6778e4e29 | |||
| c77548d299 | |||
| 26d435ea64 | |||
| c3f0d98e41 | |||
| 3c50f4aee9 | |||
| 4a930ba82a | |||
| 866e63f0fe | |||
| d461cfa8ce | |||
| 18708636fb | |||
| 1901cca2f3 | |||
| 344019f108 | |||
| e22a8021d3 | |||
| 111c9c0ed0 | |||
| 2445d18149 | |||
| 739523d559 | |||
| 23c0a493b1 | |||
| fa353fb0b3 | |||
| 9f817bd918 | |||
| 2e5480a6bd | |||
| 1b50b7f446 | |||
| ecc413ee01 | |||
| 0b1bf13b84 | |||
| 1fc6e68f3f | |||
| 1bee46ed81 | |||
| a7772ffcd9 | |||
| 1263444b2b | |||
| 286a78ef8c | |||
| 0accff3e18 | |||
| 5f62481e62 | |||
| 82b8e1f79a | |||
| 85e2ca96bc | |||
| fdbf287fee | |||
| fa4b69181f | |||
| a32641d9f4 | |||
| 44d8db043e | |||
| be89b12c96 | |||
| fd954589b5 | |||
| a2f44668b6 | |||
| ab2ab37844 | |||
| b280a734a2 | |||
| 5c1bd15639 | |||
| 24fc67f242 | |||
| 642e0fc87e | |||
| 1528c09049 | |||
| 0f4617e9c4 | |||
| a496e22ad1 | |||
| a420565ded | |||
| b3f0a479c2 | |||
| 9e18a6d1a8 | |||
| 34fd72dc97 | |||
| ed9df7b211 | |||
| 965dbca514 | |||
| f08272c853 | |||
| 843891cdd3 | |||
| a6d59b1fa7 | |||
| 51d1d9fbfd | |||
| de1358be8b | |||
| 4eb5dbc633 | |||
| a1e6ce2357 | |||
| 16e833ddb7 | |||
| 4af35bd7ea | |||
| 7d305527e9 | |||
| 1d84dc94a0 | |||
| f825ba38a0 | |||
| f076c2d143 | |||
| 58a20fffb5 | |||
| 15a123875f | |||
| 7cadb3af8b | |||
| 01984a33eb | |||
| 7329817d95 | |||
| ad4af7dd50 | |||
| f2a778ffa4 | |||
| 1a77b5752c | |||
| 2b3d6a0989 | |||
| 0b508a04b8 | |||
| 13aebeecf9 | |||
| 47d3c640d6 | |||
| 19f27598d9 | |||
| f2ef22e1a0 | |||
| 251e1b8a35 | |||
| 5de4e24a9f | |||
| 5e4d32c4c0 | |||
| e1327842b1 | |||
| c13412369a | |||
| 18e4e66db8 | |||
| 5392d635dd | |||
| e56e80aade | |||
| 994c4fd699 | |||
| ef64fefa96 | |||
| 344ff21c1e | |||
| d34e06cb8c | |||
| 8f65a0320b | |||
| b42e1c93da | |||
| e0ca14eb21 | |||
| 48fe97291b | |||
| f400fd7b60 | |||
| fd1d464f06 | |||
| 28afdb36fe | |||
| 6c7db096fc | |||
| 5a7fcb0ec3 | |||
| d647da7a4a | |||
| d7df390bb4 | |||
| 9d36ff48dd | |||
| 8743388263 | |||
| 58486654d5 | |||
| 326d719a49 | |||
| c9b6dc007a | |||
| 1bcac5e234 | |||
| dad58e14e2 | |||
| db85939322 | |||
| 4f4eb1fce5 | |||
| e55000ee1a | |||
| 9c2bf9fba8 | |||
| 563784573b | |||
| e2903f18da | |||
| 2f47456668 | |||
| 79b3101fe0 | |||
| 9788675934 | |||
| 10c63fcaa2 | |||
| 707c012318 | |||
| 3f30e17eb4 | |||
| 9eff138c3c | |||
| b0fb5d1898 | |||
| d542da38b2 | |||
| c8b446ecaf | |||
| 6ed6af5b98 | |||
| 12d39916b9 | |||
| 12d4de0619 | |||
| 7ab87f688a | |||
| 9024a277ac | |||
| fc00d9a5aa | |||
| 106a773f22 | |||
| 93d9cb3b69 | |||
| 99504b7f7d | |||
| 72c1995551 | |||
| 3d8c6c3839 | |||
| 0a06ffd074 | |||
| 12abb544bf | |||
| 78fe132cc2 | |||
| b516d7f092 | |||
| 0961df316f | |||
| 8ad2986877 | |||
| 6214487fb3 | |||
| 2219a5454c | |||
| 712a5d1b06 | |||
| cbc3b800fb | |||
| e7348d0812 | |||
| 59e638402c | |||
| bcd6de015d | |||
| b798c84160 |
@@ -93,6 +93,6 @@ jobs:
|
||||
# .artifacts/documentation/latex/reticulumnetworkstack.pdf
|
||||
# .artifacts/documentation/epub/ReticulumNetworkStack.epub
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
prerelease: ${{ contains(github.ref, '-') }}
|
||||
fail_on_unmatched_files: true
|
||||
|
||||
@@ -13,3 +13,4 @@ tests/rnsconfig/storage
|
||||
tests/rnsconfig/logfile*
|
||||
*.data
|
||||
*.result
|
||||
.buildinfo.bak
|
||||
|
||||
+599
-1
@@ -1,6 +1,604 @@
|
||||
### 2026-05-28: RNS 1.3.2
|
||||
|
||||
This release adds commit signing and validation support to the `rngit` system, as well as improvements to the blackhole functionality.
|
||||
|
||||
**Changes**
|
||||
- Extended blackhole functionality to immediately terminate links from blackholed identities
|
||||
- Added commit signing and validation to `rngit`
|
||||
- Added commit hash inclusion in generated release manifest to `rngit`
|
||||
- Added local `verify` operation shorthand to `rngit release`
|
||||
- Added option to configure blackhole update interval
|
||||
- Added configuration option to log without timestamps
|
||||
|
||||
**Verified Retrieval**
|
||||
You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To retrieve only the installation `.whl` package, and the release manifest for future updates, you can use:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch "latest:rns-*.whl" --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
To download all artifacts, including the documentation and source archive, you can use the following command:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:all --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include a signed `rsm` release manifest and `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rngit` or `rnid`. To perform an offline verification of all release artifacts using a manifest:
|
||||
|
||||
```sh
|
||||
rngit release rns_*.rsm verify --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
To verify release artifacts using individual `rsg` files, while also verifying the manifest itself, download the `rsm` and `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns_*.rsm *.rsg
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
|
||||
|
||||
|
||||
### 2026-05-22: RNS 1.3.1
|
||||
|
||||
This maintenance release fixes a single bug.
|
||||
|
||||
**Changes**
|
||||
- Fixed regression in request response transfer size accumulator
|
||||
|
||||
**Verified Retrieval**
|
||||
You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To download all artifacts, and the release manifest for future updates, you can use the following command:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:all --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
To retrieve only the `.whl` package for installation, and the release manifest, you can use:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch "latest:rns-*-py3-none-any.whl" --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include a signed `rsm` release manifest and `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rngit` or `rnid`. To perform an offline verification of all release artifacts using a manifest:
|
||||
|
||||
```sh
|
||||
rngit release rns_*.rsm fetch latest:all --offline --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
To verify releases using individual `rsg` files, while also verifying the manifest itself, download the `rsm` and `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns_*.rsm *.rsg
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
|
||||
|
||||
### 2026-05-21: RNS 1.3.0
|
||||
|
||||
This maintenance release fixes a number of bugs.
|
||||
|
||||
**Changes**
|
||||
- Added ability to use wildcards and pattern matches in `rngit` artifact fetch targets
|
||||
- Fixed channel outlet sequence holes and ghost envelopes on dying outlets by **neutral**
|
||||
- Fixed known destination iteration races by **neutral**
|
||||
- Fixed timeout deadlock in `rnsh` by **neutral**
|
||||
- Fixed commit message rendering in `rngit`
|
||||
- Fixed various minor bugs and output inconsistencies in `rngit`
|
||||
- Adjusted timeouts for remote operations in `rngit`
|
||||
- Updated documentation
|
||||
|
||||
**Verified Retrieval**
|
||||
You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To download all artifacts, and the release manifest for future updates, you can use the following command:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:all --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
To retrieve only the `.whl` package for installation, you can use:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:rns-1.3.0-py3-none-any.whl --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include a signed `rsm` release manifest and `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsm` and `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V manifest.rsm *.rsg
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
|
||||
|
||||
### 2026-05-19: RNS 1.2.9
|
||||
|
||||
This release completes the operational functionality of the `rngit` system, which now has full release creation, fetch and verified update support using the `rngit release` command. Additionally, two chapters have been added to the manual should cover all the things that `rngit` is currently capable of.
|
||||
|
||||
**Changes**
|
||||
- Added full `rngit` documentation to the manual
|
||||
- Added offline `.rsm` release manifest verification
|
||||
- Added the ability to fetch release updates directly from `.rsm` manifests
|
||||
- Added canonical `.rsm` release structure validator to `rnid` for import
|
||||
- Added `.rsm` manifest saving when using `rngit release fetch`
|
||||
- Added remote `HEAD` tracking for forks and mirros to `rngit`
|
||||
- Improved known destinations persist reliability
|
||||
- Improved page node ref link handling in `rngit`
|
||||
- Improved logging in various locations
|
||||
|
||||
**Verified Retrieval**
|
||||
You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To download all artifacts, and the release manifest for future updates, you can use the following command:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:all --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
To retrieve only the `.whl` package for installation, you can use:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:rns-1.2.8-py3-none-any.whl --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include a signed `rsm` release manifest and `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsm` and `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V manifest.rsm *.rsg
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
|
||||
|
||||
### 2026-05-18: RNS 1.2.8
|
||||
|
||||
This release improves the `rngit` system with signed release manifest generation and automatic artifact signing. It also includes several additions to `rnid` and various minor fixes and improvements to the `rngit` system.
|
||||
|
||||
**Changes**
|
||||
- Added signed release manifest generation to `rngit release`
|
||||
- Added verified release fetching to `rngit release`
|
||||
- Added automatic artifact signing to `rngit release`
|
||||
- Added signed message creation from file to `rnid`
|
||||
- Added signed message metadata output option to `rnid`
|
||||
- Added `rsm` metadata embedding and spec validation to `rnid`
|
||||
- Added identity and destination aliases to `rngit`
|
||||
- Added blocked identities option to `rngit`
|
||||
- Added ability to render raw micron in markdown files to `rngit`
|
||||
- Added fork and mirror last sync time to repository page in `rngit`
|
||||
- Better handling of silly links in `rngit`
|
||||
- Fixed markdown table cell truncation not closing micron tags
|
||||
- Fixed various minor bugs and inconsistencies in `rngit`
|
||||
- Dropped `note` metadata field requirement from `rsg` structure
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include a signed `rsm` release manifest and `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsm` and `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V manifest.rsm *.rsg
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
|
||||
|
||||
### 2026-05-17: RNS 1.2.7
|
||||
|
||||
This release significantly improves the `rngit` system with fork, mirroring and empty repository creation functionality, a new work document proposals feature, improvements to the transport core reliability and efficiency and various other tweaks and improvements.
|
||||
|
||||
**Changes**
|
||||
- Added work document proposals functionality to `rngit`
|
||||
- Added fork and mirroring support to `rngit`
|
||||
- Added ability to create new repositories remotely to `rngit`
|
||||
- Added latest release management to `rngit`
|
||||
- Added download stats to `rngit`
|
||||
- Improved shared instance RPC error handling
|
||||
- Improved announce cache cleaning
|
||||
- Improved `rngit` page node link handling
|
||||
- Improved stats pages `rngit`
|
||||
- Improved transfer completed feedback in `rncp`, thanks to **neutral**
|
||||
- Improved interface transport insertion and removal
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns*.whl
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether it is valid. If the signature cannot be verified, the file has been tampered with and should be thrown very far away in a jiffy.
|
||||
|
||||
### 2026-05-14: RNS 1.2.6
|
||||
|
||||
This release adds further improvements to the `rnid` and `rngit` utilities, and includes several bugfixes and other improvements.
|
||||
|
||||
**Changes**
|
||||
- Added embedded message signing, validation and viewing to `rnid`
|
||||
- Added file encryption for multiple file path inputs and shell expansions to `rnid`
|
||||
- Added file decryption for multiple file path inputs and shell expansions to `rnid`
|
||||
- Added signature creation for multiple file path inputs and shell expansions to `rnid`
|
||||
- Added signature validation of multiple file path inputs and shell expansions to `rnid`
|
||||
- Added workdoc signing and validation to `rngit`
|
||||
- Added ability to edit workdoc titles to `rngit`
|
||||
- Added ability to download workdocs via the `nomadnet` interface to `rngit`
|
||||
- Added local URL resolution to the `rngit` repository frontpage markdown readme renderer
|
||||
- Improved `rnstatus` remote monitor loop
|
||||
- Improved `rngit` workdoc page handling
|
||||
- Improved `rngit` release page rendering
|
||||
- Fixed missing none check in interface discovery sanitizer thanks to PAzter1101
|
||||
- Fixed potential race condition in interface discovery
|
||||
- Fixed `rngit` remote helper hanging on startup if no client config had been created previously, and RNS loglevel was configured at debug or higher
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns*.whl
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether it is valid. If the signature cannot be verified, the file has been tampered with and should be thrown very far away in a jiffy.
|
||||
|
||||
### 2026-05-09: RNS 1.2.5
|
||||
|
||||
This release brings substantial improvements to path request handling, and should significantly reduce overall network and local transport node processing loads. Path requests are now automatically ingress and egress limited per interface and sub-interface. Although the defaults are effective and sane, and should work right out of the box bring an end to practically all the PR and announce spam going on lately, the backend is fully configurable for both defaults and per interface, if you want to fiddle with the settings.
|
||||
|
||||
People who have written (ahem... *prompted into existence*) strange applications, that believed sending 25 random path requests every 10 seconds to try and punch holes through announce limiting, will now most likely find any potential users of such applications complaining that they are losing the ability to resolve paths alltogether, which is (entirely) by design, of course. Seriously, don't do crap like that.
|
||||
|
||||
You can read more about how the new ingress and egress controls work in the updated manual sections, in the Interfaces chapter.
|
||||
|
||||
For all node ops out there, I'd recomment updating to this at some sort of semi-expedient, but of course not un-leisurely pace, so peace and order on the networks can be restored.
|
||||
|
||||
**Changes**
|
||||
- Added path request ingress and egress control with sane defaults for transport nodes
|
||||
- Added full configurability of ingress and egress controls per interface and for instance-wide defaults
|
||||
- Significantly improved transport logic for path request and announce handling
|
||||
- Added path request frequency display to `rnstatus`
|
||||
- Added AutoInterface per-peer announe rate display to `rnstatus`
|
||||
- Added abilit to filter interfaces by burst state to `rnstatus`
|
||||
- Added hex/base32/base64 ASCII-wrapped output to `rnid` signature generator
|
||||
- Tuned default ingress control parameters
|
||||
- Fixed regression in link close handling in `rnstatus` and `rnpath` remote management handling
|
||||
- Fixed invalid handling of corrupted interface discovery files
|
||||
- Fixed announce processing edge case handling if path was cleaned while waiting for rebroadcast
|
||||
- Improved `rngit` error logging
|
||||
- Improved transport background jobs error handling
|
||||
- Fixed various edge-cases and inconsistencies in markdown rendering in `rngit`
|
||||
- Ensured canonical validation functions in `rngit`
|
||||
- Lots of other small fixes and stability improvements to `rngit`
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.2.5-py3-none-any.whl
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether it is valid. If the signature cannot be verified, the file has been tampered with and should be thrown very far away in a jiffy.
|
||||
|
||||
### 2026-05-07: RNS 1.2.4
|
||||
|
||||
This release brings a complete rewrite and update to the `rnid` utility, which is now a lot more useful, and better at finding and saving identities. It also includes a bunch of other improvements, such as expanded `rngit` functionality, better transport performance and a few bugfixes. Enjoy!
|
||||
|
||||
Unless something really crazy happens, this will probably be the last release that is also published to GitHub, since everything can now run over Reticulum itself. Updates to `pip` will continue at least until `rnpkg` is complete, and RNS is completely self-hosting.
|
||||
|
||||
**Changes**
|
||||
- Completely rewrote the `rnid` utility, **much** better now
|
||||
- Added ability to query network for raw identities to `rnid`
|
||||
- Added new, much more useful `rsg` file signature format
|
||||
- Added auto-retain functionality for used identities to `rnid`
|
||||
- Added outbound announce frequency per-client display to `rnstatus`
|
||||
- Added announce rate control settings display to `rnstatus`
|
||||
- Added announce rate control defaults configuration options
|
||||
- Added saner default announce rate settings for transport nodes
|
||||
- Added detection of Yggdrasil addresses to auto-connect handler
|
||||
- Added work document permissions resolver to `rngit`
|
||||
- Added ability to create updates and comments on `rngit` work documents
|
||||
- Added work document permissions control logic and CLI interaction to `rngit`
|
||||
- Added support for node-local URL-scoping in `rngit` markdown converter
|
||||
- Added API functionality for retaining identity data
|
||||
- Added the manual in markdown format
|
||||
- Improved `rngit` releases page rendering
|
||||
- Improved auto-connect logging
|
||||
- Improved transport performance
|
||||
- Improved logging performance
|
||||
- Improved shutdown handling
|
||||
- Improved workdoc sorting
|
||||
- Fixed time formatting being unintuitive sometimes
|
||||
- Fixed markdown-to-micron formatting and syntax highlighting being weird sometimes
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
e821a0b6a18d6b3263bbcdde880d0388fb4dd0c07c7eb2f83cb0dbc30eda5965 rns-1.2.4-py3-none-any.whl
|
||||
618e823cec0bd368f2f211431dfb78efef75e59132bad93d3101dacbe7deb7a6 rnspure-1.2.4-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.2.4-py3-none-any.whl
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether it is valid. If the signature cannot be verified, the file has been tampered with and should be thrown very far away in a jiffy.
|
||||
|
||||
This is the first release using the new `rsg` signature format, and you will need this latest version of RNS to verify them. Ironic, I know, but that's how it is. Since release file hashes are now embbeded in the `rsg` signatures, this is the last release that will explicitly post the raw release hashes. Verifying with `rnid` is much more effective, since it ensures all data was signed by the release identity.
|
||||
|
||||
### 2026-05-05: RNS 1.2.3
|
||||
|
||||
This release adds Work Document and update/commenting support to `rngit`.
|
||||
|
||||
**Changes**
|
||||
- Added Work Document management to `rngit`.
|
||||
- Added Work pages to the page node of `rngit`.
|
||||
- Added `interact` permission type to `rngit`.
|
||||
- Added `admin` permission type to `rngit`.
|
||||
- Added markdown blockquote support to the `rngit` markdown-to-micron converter.
|
||||
- Improved markdown-to-micron conversion and syntax highlighting accuracy in `rngit`.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
8562130f297a6b33be9d72c449bbe6ae83cad41e1530e0fa112f5fa545a3f364 rns-1.2.3-py3-none-any.whl
|
||||
0862f46a08e610add1bcac0916c6554f3e79590ab2765900178d5e1f1f0c7026 rnspure-1.2.3-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.2.3-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-05-05: RNS 1.2.2
|
||||
|
||||
This release adds release management workflows to the `rngit` utility. Downloading files and release artifacts from `rngit` will require the latest version of Nomad Network. Other nomadnet clients *may* have to update their file download link handling, if they don't already support passing query parameters for file download links.
|
||||
|
||||
**Changes**
|
||||
- Added release management to `rngit`.
|
||||
- Added release pages to the page node of `rngit`.
|
||||
- Added file downloads in the tree browser of `rngit`.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
4bf0a376a9778de8a91b9ec8a5bc4b929be928eede8784b20022c7fe52bbce62 rns-1.2.2-py3-none-any.whl
|
||||
d85f8b765dcf718d284388b249ca0e48e785f250bb41773a83e159e46c5bcf70 rnspure-1.2.2-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.2.2-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-05-04: RNS 1.2.1
|
||||
|
||||
This release adds a nomadnet Git page node to the `rngit` utility.
|
||||
|
||||
**Changes**
|
||||
- Added nomadnet page node to `rngit`.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
5ccbfc31b528133c4dd06c132034c2151e4eed74bc2dcf40af52385094492c9e rns-1.2.1-py3-none-any.whl
|
||||
cda45994a58f18bf25244a1f396c9197240bc012dd85c86bffc2e73dcf0607de rnspure-1.2.1-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.2.1-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-28: RNS 1.2.0
|
||||
|
||||
This release brings the ability to use Git natively over Reticulum networks, adds the `rnsh` program as part of the included utilities, and additionally includes several improvements and performance optimizations.
|
||||
|
||||
**Changes**
|
||||
- Added Reticulum Git Repositories Node utility as part of included utility programs.
|
||||
- Added git remote helper to interact with git repositories over Reticulum.
|
||||
- Added the `rnsh` program to the included utilities.
|
||||
- Added LocalInterface client TX hold on client app sleep on Android.
|
||||
- Added AutoInterface filters for `rmnet` interfaces on Android.
|
||||
- Added inbound packet wait during transport core initialization.
|
||||
- Added the ability to set logfile destination before RNS initialization.
|
||||
- Added automatic active link teardown on instance shutdown.
|
||||
- Improved link teardown on SIGINT/SIGTERM.
|
||||
- Improved ratchet cleaning.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
b58e97332241755ed32e309d46e09615a123490430ae85fcbdec9318c9e26154 rns-1.2.0-py3-none-any.whl
|
||||
9813a6c2236edba18af7d3a072a6226bc65ae384d23b1f41467cb3617d65fdae rnspure-1.2.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.2.0-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-22: RNS 1.1.9
|
||||
|
||||
This maintenance release fixes a critical security issue, that would allow an attacker to craft a BZ2 decompression bomb via Resource transfers or Buffer StreamDataMessage, causing an out-of-memory condition and crashing the receiving process via OOM killer.
|
||||
|
||||
Big thanks to @defidude (github.com/ratspeak) for discovering and reporting this vulnerability!
|
||||
|
||||
**Changes**
|
||||
- Fixed bz2 decompression bomb vulnerability in Resource transfer assembly and Buffer StreamDataMessage unpacking.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
39a131aeb5d76fd73bfc67f68135f49ab0cf8628af154e04096a05c208ce77b6 rns-1.1.9-py3-none-any.whl
|
||||
aab7bfc8c65514c9bdf4c22f00d288faf6c9e1777fc002dbe3eb29c286e67128 rnspure-1.1.9-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.9-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-21: RNS 1.1.8
|
||||
|
||||
This maintenance release fixes a critical bug in path state management, that could result in significant path convergence degradation under certain conditions.
|
||||
|
||||
**Changes**
|
||||
- Fixed path state potentially being applied before path table entry exists, causing worse paths to be selected.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
9cf728e9e9a9fe113e4ac14e6b833f7ee65feedf8468e6ab94a261bf205f2632 rns-1.1.8-py3-none-any.whl
|
||||
407dc3975335e9eabaaddb7ed1dc75fc3a1b8d24a7207e740797440c2ad0b3e5 rnspure-1.1.8-py3-none-any.wh
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.8-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-21: RNS 1.1.7
|
||||
|
||||
**Changes**
|
||||
- Added periodic known destination data cleaning based on local relevance.
|
||||
- Improved resource transfer sequencing timing calculations and reliability.
|
||||
- Improved BackboneInterface error handling on EPOLL errors.
|
||||
- Ensured non-background data persist runs synchronously.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
4d9702c5d9bb8a3c8b94766cb51cccad5afd78d615af9a6b146730347044e6f0 rns-1.1.7-py3-none-any.whl
|
||||
172dede7656b41b85e4319354ed04649b518e58c54586da7e443579c620a0a5b rnspure-1.1.7-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.7-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-18: RNS 1.1.6
|
||||
|
||||
**Changes**
|
||||
- Improved transport memory consumption.
|
||||
- Improved transport tunnel handling.
|
||||
- Improved gracious transport data persist handling.
|
||||
- Added ingress control bypass for pending path requests.
|
||||
- Added local destinations lookup map for better transport efficiency to local destinations.
|
||||
- Fixed disk I/O bound thread execution time starvation on cache management jobs.
|
||||
- Fixed invalid EPOLL modification error handler.
|
||||
- Fixed incorrect default IFAC size for autoconnected, discovered interfaces. Thanks @taprootmx!
|
||||
- Ensure loop-originating closures have variables captured at iteration-time. Thanks @taprootmx!
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
2ce4451668f8c464295cc269188c232e7805ddd618ec0135550a5e6809df5de0 rns-1.1.6-py3-none-any.whl
|
||||
ba3e541e69a2f4892177383c8ec4e7d172d298546317e08270928c0163865aa3 rnspure-1.1.6-py3-none-any.wh
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.6-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-13: RNS 1.1.5
|
||||
|
||||
**Changes**
|
||||
- Initial refactoring work for free-threaded transport I/O.
|
||||
- Improved interface discovery validation.
|
||||
- Fixed invalid ingress control burst activation and subsequent path resolution failure due to incorrect announce frequency calculation.
|
||||
- Fixed missing configuration entry generation for discovered I2P interfaces.
|
||||
- Fixed resource transfer cancellation failing on in-flight split resource transfers.
|
||||
- Fixed ingress control configuration not inheriting down to spawned interfaces on some interface types.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
28f39ad97ef307a1e270b91ef19db07d8e1a7bbc8628c478303725894c64deff rns-1.1.5-py3-none-any.whl
|
||||
1a90db16d2cff4ad909b44baf9b4fd0177da2ed545cdb9cfb2c51423707b49e9 rnspure-1.1.5-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.5-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
#
|
||||
|
||||
### 2026-03-12: RNS 1.1.4
|
||||
|
||||
**Changes**
|
||||
- Fixed invalid application of IP/hostname validation for on non-relevant interfaces. Thanks @joakim!
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
b2a175abd64d1581dd058206832793dbf7053a304c819ff8bc143a79c49cb747 rns-1.1.4-py3-none-any.whl
|
||||
16c4ae6722bbd016e8db046e7bdd60eb24f9ec55966ec5723dc39301265d0186 rnspure-1.1.4-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.4-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-01-17: RNS 1.1.3
|
||||
|
||||
**Changes**
|
||||
- Improved discovered interface auto-connect handling
|
||||
- Improved interface discovery handling
|
||||
- Added `discovered_interfaces` API method
|
||||
- Fixed a potential race condition in request timeout handling
|
||||
- Fixed a regression in resource file transfers
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
1de9b46c8f24931fa41974664ddbf4251d3fdd069be4de03c64b42a7cf4f8fb4 rns-1.1.3-py3-none-any.whl
|
||||
eac8d223fcb6ce94e1bd3f04730d8542675caf4b22286e11988e9402ea9b69c0 rnspure-1.1.3-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.3-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-01-04: RNS 1.1.0
|
||||
|
||||
Enjoy.
|
||||
|
||||
**Changes**
|
||||
- Added on-network global interface discovery. Hello world.
|
||||
- Added discovered interface auto-connection. Robotic.
|
||||
- Added external IP resolution for discovery-enabled interfaces. Snip-snip.
|
||||
- Added encrypted interface discovery announces. Welcome home.
|
||||
- Added bootstrap interface functionality. Decent.
|
||||
- Added blackhole handling and management. Thank the Chinese guy.
|
||||
- Added distributed blackhole list publishing and updating. Spammers go home.
|
||||
- Added foundational network identity implementation. All your base.
|
||||
- Added `await_path` method to API. Tick-tock.
|
||||
- Added reverse-unicast peer discovery packet mechanism to AutoInterface. Ping-pong.
|
||||
- Added custom identity support to `rncp`, thanks MikelCalvo!
|
||||
- Added monitor mode to `rnstatus`, thanks MikelCalvo!
|
||||
- Improved announce processing. Swoosh.
|
||||
- Updated documentation quite a bit. Looky.
|
||||
- Enabled per-peer ingress limiting on Weave and Auto interfaces. Hammertime.
|
||||
- Fixed **the** typo, yes it's the olny one I'm sure.
|
||||
- Fixed bugs. Squish.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
180b8baec2ec7d21abe2cec25ff763e70b2129c012fb02fc23c2fd654f94c1f5 dist/rns-1.1.0-py3-none-any.whl
|
||||
d9e32caf66a9c53199e901d2c173e1de1bf50f1f0c9d5250e5d1b3b07bedcd7c dist/rnspure-1.1.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-11-19: RNS 1.0.4
|
||||
|
||||
This release includes updates to RNode BLE reliability, and adds support for connecting RNodes to a host over WiFi and Ethernet.
|
||||
This maintenance release adds improved handling for RNodes with a PA/LNA combo.
|
||||
|
||||
**Changes**
|
||||
- Improved handling for RNodes with PA/LNA combo
|
||||
|
||||
+18
-11
@@ -8,21 +8,28 @@ Apart from writing code, there are many ways in which you can contribute. Before
|
||||
|
||||
First and foremost, there is one simple requirement for taking part in this community: While we primarily interact virtually, your actions matter and have real consequences. Therefore: **Act like a responsible, civilized person** - especially in the face of disputes and heated disagreements. Speak your mind here; discussions are welcome. Just do so in the spirit of being face-to-face with everyone else. Thank you.
|
||||
|
||||
In order to keep the discussion forums and issue trackers navigable and useful, the following types of posts will be deleted without notice:
|
||||
|
||||
- Spam.
|
||||
- Questions that have already been adequately answered elsewhere. Use the search function.
|
||||
- Low-effort posts or comments that contain no actual information or useful content. This is not a tea-house.
|
||||
- Post or comments solely containing personal opinions or beliefs without adding anything to the discussion. Facebook and X exists.
|
||||
- Content that simply waste the developer's / maintainer's time with completely obvious "ideas", "insights" or "recommendations". Yes, we have *at least* 8 neurons ourselves.
|
||||
- Posts that fail to understand that developing a highly complex software project with a very small amount of resources and people takes time. Imagining perfection on our behalf is useless.
|
||||
|
||||
If you're new to the community and start out your engagement with any of the above transgressions, you will simply be banned without notice or explanation, and your post will be deleted.
|
||||
|
||||
If you find this "harsh", "unfair" or "unwelcoming", go somewhere else. This is not social club, but a work environment for the people contributing to the project.
|
||||
|
||||
## Asking Questions
|
||||
|
||||
If you want to ask a question, **do not open an issue**. The issue tracker is used by people *working on Reticulum* to track bugs, issues and improvements.
|
||||
If you want to ask a question, **do not open an issue**. The issue tracker is used by people *working on Reticulum* to track bugs, issues and improvements. Instead, ask away on the [discussions](https://github.com/markqvist/Reticulum/discussions).
|
||||
|
||||
Instead, ask away on the [discussions](https://github.com/markqvist/Reticulum/discussions) or on the [Reticulum Matrix channel](https://matrix.to/#/#reticulum:matrix.org) at `#reticulum:matrix.org`
|
||||
|
||||
## Providing Feedback & Ideas
|
||||
|
||||
Likewise, feedback, ideas and feature requests are a very welcome way to contribute, and should also be posted on the [discussions](https://github.com/markqvist/Reticulum/discussions), or on the [Reticulum Matrix channel](https://matrix.to/#/#reticulum:matrix.org) at `#reticulum:matrix.org`.
|
||||
|
||||
Please do not post feature requests or general ideas on the issue tracker, or in direct messages to the primary developers. You are much more likely to get a response and start a constructive discussion by posting your ideas in the public channels created for these purposes.
|
||||
Do not post feature requests or general ideas on the issue tracker, or in direct messages to the primary developers. You are much more likely to get a response and start a constructive discussion by posting your ideas in the public channels created for these purposes.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you have found a bug or issue in this project, please report it using the [issue tracker](https://github.com/markqvist/Reticulum/issues). If at all possible, be sure to include details on how to reproduce the bug.
|
||||
If you have found a bug or issue in this project, please report it using the [issue tracker](https://github.com/markqvist/Reticulum/issues). Be sure to include details on how to reproduce the bug.
|
||||
|
||||
Anything submitted to the issue tracker that does not follow these guidelines will be closed and removed without comments or explanation.
|
||||
|
||||
@@ -42,9 +49,9 @@ Even new ideas and proposals that have not been approved by a maintainer, or fal
|
||||
|
||||
## 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.
|
||||
Contributions written using large language models (LLMs) or other generative 'AI' programs are prohibited. LLMs produce errors so frequently and in a way that is so unlike human error that such issues are incredibly time-consuming to spot and fix. This is not a worthwhile tradeoff for Reticulum.
|
||||
|
||||
This applies to all 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).
|
||||
This applies to all Reticulum-related projects and documentation, as well as all submitted issues and discussion in official channels, except in cases where language translation and/or speech recogntion technologies are required for communication.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ def link_established(link):
|
||||
|
||||
# Inform the user that the server is
|
||||
# connected
|
||||
RNS.log("Link established with server, hit enter to sand a resource, or type in \"quit\" to quit")
|
||||
RNS.log("Link established with server, hit enter to send a resource, or type in \"quit\" to quit")
|
||||
|
||||
# When a link is closed, we'll inform the
|
||||
# user, and exit the program
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"drips": {
|
||||
"ethereum": {
|
||||
"ownedBy": "0xae89F3B94fC4AD6563F0864a55F9a697a90261ff"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
Reticulum License
|
||||
|
||||
Copyright (c) 2016-2025 Mark Qvist
|
||||
Copyright (c) 2016-2026 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
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
This repository is a public mirror. All potential future development is happening elsewhere.
|
||||
|
||||
I am stepping back from all public-facing interaction with this project. Reticulum has always been primarily my work, and continuing in the current public, internet-facing model is no longer sustainable.
|
||||
|
||||
The software remains available for use as-is. Occasional updates may appear at unpredictable intervals, but there will be no support, no responses to issues, no discussions, and no community management in this or any other public venue. If it doesn't work for you, it doesn't work. That is the entire extent of available troubleshooting assistance I can offer you.
|
||||
|
||||
If you've followed this project for a while, you already know what this means. You know who designed, wrote and tested this, and you know how many years of my life it took. You'll also know about both my particular challenges and strengths, and how I believe anything worth building needs to be built and maintained with our own hands.
|
||||
|
||||
Seven months ago, I said I needed to step back, that I was exhausted, and that I needed to recover. I believed a public resolve would be enough to effectuate that, but while striving to get just a few more useful features and protocols out, the unproductive requests and demands also ramped up, and I got pulled back into the same patterns and draining interactions that I'd explicitly said I couldn't sustain anymore.
|
||||
|
||||
So here's what you might have already guessed: I'm done playing the game by rules I can't win at.
|
||||
|
||||
Everything you need is right here, and by any sensible measure, it's done. Anyone who wants to invest the time, skill and persistence can build on it, or completely re-imagine it with different priorities. That was always the point.
|
||||
|
||||
The people who actually contributed - you know who you are, and you know I mean it when I say: Thank you. All of you who've used this to build something real - that was the goal, and you did it without needing me to hold your hand.
|
||||
|
||||
The rest of you: You have what you need. Use it or don't. I am not going to be the person who explains it to you anymore.
|
||||
|
||||
This is not a temporary break. It's not "see you after some rest", but a recognition that the current model is fundamentally incompatible with my life, my health, and my reality.
|
||||
|
||||
If you want to support continued work, you can do so at the donation links listed in this repository. But please understand, that this is not purchasing support or guaranteeing updates. It is support for work that happens on my timeline, according to my capacity, which at the moment is not what it was.
|
||||
|
||||
If you want Reticulum to continue evolving, you have the power to make that happen. The protocol is public domain. The code is open source. Everything you need is right here. I've provided the tools, but building what comes next is not my responsibility anymore. It's yours.
|
||||
|
||||
To the small group of people who has actually been here, and understood what this work was and what it cost - you already know where to find me if it actually matters.
|
||||
|
||||
To everyone else: This is where we part ways. No hard feelings. It's just time.
|
||||
|
||||
---
|
||||
|
||||
असतो मा सद्गमय
|
||||
तमसो मा ज्योतिर्गमय
|
||||
मृत्योर्मा अमृतं गमय
|
||||
@@ -27,6 +27,7 @@ clean:
|
||||
purge_docs:
|
||||
@echo Purging documentation build...
|
||||
@-rm -rf ./docs/manual
|
||||
@-rm -rf ./docs/markdown
|
||||
@-rm -rf ./docs/*.pdf
|
||||
@-rm -rf ./docs/*.epub
|
||||
|
||||
@@ -50,18 +51,42 @@ build_pure_wheel:
|
||||
python3 setup.py bdist_wheel --pure
|
||||
|
||||
documentation:
|
||||
make -C docs html
|
||||
make -C docs html markdown
|
||||
|
||||
manual:
|
||||
make -C docs latexpdf epub
|
||||
|
||||
release: test remove_symlinks build_sdist build_wheel build_pure_wheel documentation manual create_symlinks
|
||||
distcollect:
|
||||
cp docs/Reticulum\ Manual.* dist
|
||||
|
||||
build_spkg: remove_symlinks build_sdist create_symlinks
|
||||
|
||||
release: test remove_symlinks build_sdist build_wheel build_pure_wheel documentation manual distcollect create_symlinks
|
||||
|
||||
debug: remove_symlinks build_wheel build_pure_wheel create_symlinks
|
||||
|
||||
local: release sign
|
||||
|
||||
sign:
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum create $$(python setup.py --getversion):dist --name rns --local
|
||||
|
||||
upload:
|
||||
@echo Ready to publish release, hit enter to continue
|
||||
@echo Ready to publish release over Reticulum
|
||||
@read VOID
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum create $$(python setup.py --getversion):dist --name rns
|
||||
|
||||
upload-pip: upload-rns-pip upload-rnspure-pip
|
||||
|
||||
upload-rns-pip:
|
||||
@echo Ready to publish rns release, hit enter to continue
|
||||
@read VOID
|
||||
@echo Uploading to PyPi...
|
||||
twine upload dist/*
|
||||
twine upload dist/rns-*.whl dist/rns-*.tar.gz
|
||||
@echo Release published
|
||||
|
||||
upload-rnspure-pip:
|
||||
@echo Ready to publish rnspure release, hit enter to continue
|
||||
@read VOID
|
||||
@echo Uploading to PyPi...
|
||||
twine upload dist/rnspure-*.whl
|
||||
@echo Release published
|
||||
@@ -3,6 +3,10 @@ Reticulum Network Stack <img align="right" src="https://static.pepy.tech/persona
|
||||
|
||||
<p align="center"><img width="200" src="https://raw.githubusercontent.com/markqvist/Reticulum/master/docs/source/graphics/rns_logo_512.png"></p>
|
||||
|
||||
*This repository is [a public mirror](./MIRROR.md). All development is happening elsewhere.*
|
||||
|
||||
To understand the foundational philosophy and goals of this system, read the [Zen of Reticulum](Zen%20of%20Reticulum.md).
|
||||
|
||||
Reticulum is the cryptography-based networking stack for building local and wide-area
|
||||
networks with readily available hardware. It can operate even with very high latency
|
||||
and extremely low bandwidth. Reticulum allows you to build wide-area networks
|
||||
@@ -74,22 +78,32 @@ For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ
|
||||
- Low cost of keeping links open at only 0.44 bits per second
|
||||
- Reliable sequential delivery with Channel and Buffer mechanisms
|
||||
|
||||
## Roadmap
|
||||
While Reticulum is already a fully featured and functional networking stack,
|
||||
many improvements and additions are actively being worked on, and planned for the future.
|
||||
## Reference Implementation
|
||||
|
||||
To learn more about the direction and future of Reticulum, please see the [Development Roadmap](./Roadmap.md).
|
||||
The Python code in this repository is the Reference Implementation of Reticulum.
|
||||
The Reticulum Protocol is defined entirely and authoritatively by this reference
|
||||
implementation, and its associated manual. It is maintained by Mark Qvist,
|
||||
identified by the Reticulum Identity `<bc7291552be7a58f361522990465165c>`.
|
||||
|
||||
Compatibility with the Reticulum Protocol is defined as having full interoperability,
|
||||
and sufficient functional parity with this reference implementation. Any specific protocol
|
||||
implementation that achieves this is Reticulum. Any that does not is not Reticulum.
|
||||
|
||||
The reference implementation is licensed under the Reticulum License.
|
||||
|
||||
The Reticulum Protocol was dedicated to the Public Domain in 2016.
|
||||
|
||||
## Examples of Reticulum Applications
|
||||
If you want to quickly get an idea of what Reticulum can do, take a look at the
|
||||
following resources.
|
||||
[Programs Using Reticulum](https://reticulum.network/manual/software.html)
|
||||
section of the manual, or the 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 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.
|
||||
- [MeshChatX](https://git.quad4.io/RNS-Things/MeshChatX) is a full-featured LXMF client with many built-in tools and functionalities, that also supports image and voice messages, file transfers and voice calls. It also includes a built-in page browser for browsing Nomad Network nodes.
|
||||
- You can use the included [rnsh](https://reticulum.network/manual/using.html#the-rnsh-utility) program to establish remote shell sessions over Reticulum.
|
||||
|
||||
## Where can Reticulum be used?
|
||||
Over practically any medium that can support at least a half-duplex channel
|
||||
@@ -170,8 +184,10 @@ section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/)
|
||||
- A diagnostics tool called `rnprobe` for checking connectivity to destinations
|
||||
- A simple file transfer program called `rncp` making it easy to transfer files between systems
|
||||
- The identity management and encryption utility `rnid` let's you manage Identities and encrypt/decrypt files
|
||||
- The remote command execution program `rnx` let's you run commands and
|
||||
programs and retrieve output from remote systems
|
||||
- The `rnsh` program allows you to establish fully interactive shell session with remote systems
|
||||
- The remote command execution program `rnx` let's you run simple commands and programs and retrieve output from remote systems
|
||||
- The `rngit` program provides a full multi-repository Git node for serving repositories over Reticulum
|
||||
- The included `git-remote-rns` helper allows you to interact with Git repositories over Reticulum
|
||||
|
||||
All tools, including `rnx` and `rncp`, work reliably and well even over very
|
||||
low-bandwidth links like LoRa or Packet Radio. For full-featured remote shells
|
||||
@@ -216,7 +232,7 @@ probably occur as real-world use is explored and understood. The API and wire-fo
|
||||
can be considered stable.
|
||||
|
||||
## Dependencies
|
||||
The installation of the default `rns` package requires the dependencies listed
|
||||
The installation of the default `rns` package requires only two external dependencies, listed
|
||||
below. Almost all systems and distributions have readily available packages for
|
||||
these dependencies, and when the `rns` package is installed with `pip`, they
|
||||
will be downloaded and installed as well.
|
||||
@@ -244,53 +260,24 @@ that do not support [PyCA/cryptography](https://github.com/pyca/cryptography),
|
||||
it is important that you read and understand the [Cryptographic
|
||||
Primitives](#cryptographic-primitives) section of this document.
|
||||
|
||||
## Bootstrapping Connectivity
|
||||
|
||||
Reticulum is not a service you subscribe to, nor is it a single global network you "join".
|
||||
Reticulum provides functionality for discovering available public interfaces
|
||||
over the network itself, and the broader community has provided various directories
|
||||
of publicly available entrypoints to bootstrap connectivity.
|
||||
|
||||
To learn how to establish initial connectivity over Reticulum, read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual.
|
||||
|
||||
If you already have a general idea of how this works, you can use community-run
|
||||
sites such as [directory.rns.recipes](https://directory.rns.recipes/) and [rmap.world](https://rmap.world)
|
||||
to find interface definitions for initial connectivity to the global distributed Reticulum backbone.
|
||||
|
||||
## Public Testnet
|
||||
If you just want to get started experimenting without building any physical
|
||||
networks, you are welcome to join the RNS Development Testnet.
|
||||
|
||||
The testnet is just that, an informal network for testing and experimenting.
|
||||
It will be up most of the time, and anyone can join, but it also means that
|
||||
there's no guarantees for service availability.
|
||||
|
||||
It probably goes without saying, but *don't use the testnet entry-points as
|
||||
hardcoded or default interfaces in any applications you ship to users*. When
|
||||
shipping applications, the best practice is to provide your own default
|
||||
connectivity solutions, if needed and applicable, or in most cases, simply
|
||||
leave it up to the user which networks to connect to, and how.
|
||||
|
||||
The testnet runs the very latest version of Reticulum (often even a short while
|
||||
before it is publicly released). Sometimes experimental versions of Reticulum
|
||||
might be deployed to nodes on the testnet, which means strange behaviour might
|
||||
occur. If none of that scares you, you can join the testnet via either TCP or
|
||||
I2P. Just add one of the following interfaces to your Reticulum configuration
|
||||
file:
|
||||
|
||||
```
|
||||
# TCP/IP interface to the RNS Amsterdam Hub
|
||||
[[RNS Testnet Amsterdam]]
|
||||
type = TCPClientInterface
|
||||
enabled = yes
|
||||
target_host = amsterdam.connect.reticulum.network
|
||||
target_port = 4965
|
||||
|
||||
# TCP/IP interface to the BetweenTheBorders Hub (community-provided)
|
||||
[[RNS Testnet BetweenTheBorders]]
|
||||
type = TCPClientInterface
|
||||
enabled = yes
|
||||
target_host = reticulum.betweentheborders.com
|
||||
target_port = 4242
|
||||
|
||||
# Interface to Testnet I2P Hub
|
||||
[[RNS Testnet I2P Hub]]
|
||||
type = I2PInterface
|
||||
enabled = yes
|
||||
peers = g3br23bvx3lq5uddcsjii74xgmn6y5q325ovrkq2zw2wbzbqgbuq.b32.i2p
|
||||
```
|
||||
|
||||
The testnet also contains a number of [Nomad Network](https://github.com/markqvist/nomadnet) nodes, and LXMF propagation nodes.
|
||||
***Important!** Historically, a developer-targeted testnet was made available by the Reticulum project itself. As the amount of global Reticulum nodes and entrypoints have grown to a substantial quantity, this public testnet, including the Amsterdam Testnet entrypoint, has now been decommissioned. If your still have instances that relied on this entrypoint for connectivity, transition to using the distributed backbone instead. Reticulum now includes a full on-network interface discovery and connectivity bootstrapping system. Read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual for pointers.*
|
||||
|
||||
## Support Reticulum
|
||||
You can help support the continued development of open, free and private communications systems by donating via one of the following channels:
|
||||
For this to be possible, I need your help. Please support the continued development of open, free and private communications systems by donating via one of the following channels:
|
||||
|
||||
- Monero:
|
||||
```
|
||||
@@ -298,11 +285,11 @@ You can help support the continued development of open, free and private communi
|
||||
```
|
||||
- Bitcoin
|
||||
```
|
||||
bc1p4a6axuvl7n9hpapfj8sv5reqj8kz6uxa67d5en70vzrttj0fmcusgxsfk5
|
||||
bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6
|
||||
```
|
||||
- Ethereum
|
||||
```
|
||||
0xae89F3B94fC4AD6563F0864a55F9a697a90261ff
|
||||
0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E
|
||||
```
|
||||
- Liberapay: https://liberapay.com/Reticulum/
|
||||
|
||||
@@ -393,4 +380,5 @@ projects:
|
||||
- [Configobj](https://github.com/DiffSK/configobj) by Michael Foord, Nicola Larosa, Rob Dennis & Eli Courtwright, *BSD 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)
|
||||
- [rnsh](https://github.com/acehoss/rnsh) by [Aaron Heise](https://github.com/acehoss)
|
||||
- [Python](https://www.python.org)
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
>> Reticulum Network Stack
|
||||
|
||||
To understand the foundational philosophy and goals of this system, read the `_`!`[Zen of Reticulum`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=Zen+of+Reticulum.md]`!`_.
|
||||
|
||||
Reticulum is the cryptography-based networking stack for building local and wide-area networks with readily available hardware. It can operate even with very high latency and extremely low bandwidth. Reticulum allows you to build wide-area networks with off-the-shelf tools, and offers end-to-end encryption and connectivity, initiator anonymity, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, unforgeable delivery acknowledgements and more.
|
||||
|
||||
The vision of Reticulum is to allow anyone to be their own network operator, and to make it cheap and easy to cover vast areas with a myriad of independent, inter-connectable and autonomous networks. Reticulum `!is not`! `*one`* network. It is `!a tool`! for building `*thousands of networks`*. Networks without kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate with each other, and require no central oversight. Networks for human beings. `*Networks for the people`*.
|
||||
|
||||
Reticulum is a complete networking stack, and does not rely on IP or higher layers, but it is possible to use IP as the underlying carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the Internet or private IP networks.
|
||||
|
||||
Having no dependencies on traditional networking stacks frees up overhead that has been used to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality, even in open and trustless networks.
|
||||
|
||||
No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python.
|
||||
|
||||
>> Read The Manual
|
||||
|
||||
The full documentation for Reticulum is available on `_`!`[this node`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
|
||||
|
||||
You can also download the `_`!`[Reticulum manual as a PDF`:/file/download`g=reticulum|r=reticulum|ref=HEAD|path=docs%2FReticulum+Manual.pdf]`!`_ or `_`!`[as an e-book in EPUB format`:/file/download`g=reticulum|r=reticulum|ref=HEAD|path=docs%2FReticulum+Manual.pdf]`!`_.
|
||||
|
||||
>> Notable Features
|
||||
|
||||
• Coordination-less globally unique addressing and identification
|
||||
• Fully self-configuring multi-hop routing over heterogeneous carriers
|
||||
• Flexible scalability over heterogeneous topologies
|
||||
• Reticulum can carry data over any mixture of physical mediums and topologies
|
||||
• Low-bandwidth networks can co-exist and interoperate with large, high-bandwidth networks
|
||||
• Initiator anonymity, communicate without revealing your identity
|
||||
• Reticulum does not include source addresses on any packets
|
||||
• Asymmetric X25519 encryption and Ed25519 signatures as a basis for all communication
|
||||
• The foundational Reticulum Identity Keys are 512-bit Elliptic Curve keysets
|
||||
• 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-256 in CBC mode with PKCS7 padding
|
||||
• HMAC using SHA256 for authentication
|
||||
• IVs are generated through os.urandom()
|
||||
• Unforgeable packet delivery confirmations
|
||||
• Flexible and extensible interface system
|
||||
• Reticulum includes a large variety of built-in interface types
|
||||
• Ability to load and utilise custom user- or community-supplied interface types
|
||||
• Easily create your own custom interfaces for communicating over anything
|
||||
• Authentication and virtual network segmentation on all supported interface types
|
||||
• An intuitive and easy-to-use API
|
||||
• Simpler and easier to use than sockets APIs, but more powerful
|
||||
• Makes building distributed and decentralised applications much simpler
|
||||
• Reliable and efficient transfer of arbitrary amounts of data
|
||||
• Reticulum can handle a few bytes of data or files of many gigabytes
|
||||
• Sequencing, compression, transfer coordination and checksumming are automatic
|
||||
• The API is very easy to use, and provides transfer progress
|
||||
• Lightweight, flexible and expandable Request/Response mechanism
|
||||
• Efficient link establishment
|
||||
• Total cost of setting up an encrypted and verified link is only 3 packets, totalling 297 bytes
|
||||
• Low cost of keeping links open at only 0.44 bits per second
|
||||
• Reliable sequential delivery with Channel and Buffer mechanisms
|
||||
|
||||
>> Reference Implementation
|
||||
|
||||
The Python code in this repository is the Reference Implementation of Reticulum. The Reticulum Protocol is defined entirely and authoritatively by this reference implementation, and its associated manual. It is maintained by Mark Qvist, identified by the Reticulum Identity `B333<bc7291552be7a58f361522990465165c>`b.
|
||||
|
||||
Compatibility with the Reticulum Protocol is defined as having full interoperability, and sufficient functional parity with this reference implementation. Any specific protocol implementation that achieves this is Reticulum. Any that does not is not Reticulum.
|
||||
|
||||
The reference implementation is licensed under the Reticulum License.
|
||||
|
||||
The Reticulum Protocol was dedicated to the Public Domain in 2016.
|
||||
|
||||
>> Examples of Reticulum Applications
|
||||
|
||||
If you want to quickly get an idea of what Reticulum can do, take a look at the `_`!`[Programs Using Reticulum`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/software.md]`!`_ section of the manual, or the following resources:
|
||||
|
||||
• `_`!`[LXMF`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=lxmf]`!`_ is a distributed, delay and disruption tolerant message transfer protocol built on Reticulum
|
||||
|
||||
• The `_`!`[LXST`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=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 `B333rnphone`b program, that can be used to build hardware telephones.
|
||||
|
||||
• For an off-grid, encrypted and resilient mesh communications platform, see `_`!`[Nomad Network`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=nomadnet]`!`_.
|
||||
|
||||
• The Android, Linux, macOS and Windows app `_`!`[Sideband`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=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.
|
||||
|
||||
• `_`!`[MeshChatX`c10d80b1a42fa958c37a6cc30dc04f53]`!`_ (`_`!`[source`5399f5a0212477618821e91e88ce053b:/page/repo.mu`g=quad4|r=MeshChatX]`_`!) is a full-featured LXMF client with many built-in tools and functionalities,
|
||||
that also supports image and voice messages, file transfers and voice calls. It also includes a
|
||||
built-in page browser for browsing Nomad Network nodes.
|
||||
|
||||
• You can use the included `_`!`[rnsh`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/using.md|anchor=the-rnsh-utility]`!`_ program to establish remote shell sessions over Reticulum.
|
||||
|
||||
>> Where can Reticulum be used?
|
||||
|
||||
Over practically any medium that can support at least a half-duplex channel with greater throughput than 5 bits per second, and an MTU of 500 bytes. Data radios, modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, WiFi and Ethernet devices, free-space optical links, and similar systems are all examples of the types of physical devices Reticulum can use.
|
||||
|
||||
An open-source LoRa-based interface called `_`!`[RNode`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/hardware.md|anchor=rnode]`!`_ has been designed specifically for use with Reticulum. It is possible to build yourself, or it can be purchased as a complete transceiver that just needs a USB connection to the host.
|
||||
|
||||
Reticulum can also be encapsulated over existing IP networks, so there's nothing stopping you from using it over wired Ethernet, your local WiFi network or the Internet, where it'll work just as well. In fact, one of the strengths of Reticulum is how easily it allows you to connect different mediums into a self-configuring, resilient and encrypted mesh, using any available mixture of available infrastructure.
|
||||
|
||||
As an example, it's possible to set up a Raspberry Pi connected to both a LoRa radio, a packet radio TNC and a WiFi network. Once the interfaces are configured, Reticulum will take care of the rest, and any device on the WiFi network can communicate with nodes on the LoRa and packet radio sides of the network, and vice versa.
|
||||
|
||||
>> How do I get started?
|
||||
|
||||
The best way to get started with the Reticulum Network Stack depends on what you want to do. For full details and examples, have a look at the `_`!`[Getting Started Fast`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/gettingstartedfast.md]`!`_ section of the `_`!`[Reticulum Manual`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
|
||||
|
||||
To simply install Reticulum and related utilities on your system, the easiest way is via `B333pip`b. You can then start any program that uses Reticulum, or start Reticulum as a system service with `_`!`[the rnsd utility`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/using.md|anchor=the-rnsd-utility]`!`_.
|
||||
|
||||
`B333
|
||||
`=
|
||||
pip install rns
|
||||
`=
|
||||
`b
|
||||
|
||||
If you are using an operating system that blocks normal user package installation via `B333pip`b, you can return `B333pip`b to normal behaviour by editing the `B333~/.config/pip/pip.conf`b file, and adding the following directive in the `B333[global]`b section:
|
||||
|
||||
`B333
|
||||
`=
|
||||
[global]
|
||||
break-system-packages = true
|
||||
`=
|
||||
`b
|
||||
|
||||
Alternatively, you can use the `B333pipx`b tool to install Reticulum in an isolated environment:
|
||||
|
||||
`B333
|
||||
`=
|
||||
pipx install rns
|
||||
`=
|
||||
`b
|
||||
|
||||
When first started, Reticulum will create a default configuration file, providing basic connectivity to other Reticulum peers that might be locally reachable. The default config file contains a few examples, and references for creating a more complex configuration.
|
||||
|
||||
If you have an old version of `B333pip`b on your system, you may need to upgrade it first with `B333pip install pip --upgrade`b. If you no not already have `B333pip`b installed, you can install it using the package manager of your system with `B333sudo apt install python3-pip`b or similar.
|
||||
|
||||
For more detailed examples on how to expand communication over many mediums such as packet radio or LoRa, serial ports, or over fast IP links and the Internet using the UDP and TCP interfaces, take a look at the `_`!`[Supported Interfaces`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/interfaces.md]`!`_ section of the `_`!`[Reticulum Manual`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
|
||||
|
||||
>> Included Utilities
|
||||
|
||||
Reticulum includes a range of useful utilities for managing your networks, viewing status and information, and other tasks. You can read more about these programs in the `_`!`[Included Utility Programs`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/using.md|anchor=included-utility-programs]`!`_ section of the `_`!`[Reticulum Manual`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
|
||||
|
||||
• The system daemon `B333rnsd`b for running Reticulum as an always-available service
|
||||
• An interface status utility called `B333rnstatus`b, that displays information about interfaces
|
||||
• The path lookup and management tool `B333rnpath`b letting you view and modify path tables
|
||||
• A diagnostics tool called `B333rnprobe`b for checking connectivity to destinations
|
||||
• A simple file transfer program called `B333rncp`b making it easy to transfer files between systems
|
||||
• The identity management and encryption utility `B333rnid`b let's you manage Identities and encrypt/decrypt files
|
||||
• The `B333rnsh`b program allows you to establish fully interactive shell session with remote systems
|
||||
• The remote command execution program `B333rnx`b let's you run simple commands and programs and retrieve output from remote systems
|
||||
• The `B333rngit`b program provides a full multi-repository Git node for serving repositories over Reticulum
|
||||
• The included `B333git-remote-rns`b helper allows you to interact with Git repositories over Reticulum
|
||||
|
||||
>> Supported interface types and devices
|
||||
|
||||
Reticulum implements a range of generalised interface types that covers most of the communications hardware that Reticulum can run over. If your hardware is not supported, it's `_`!`[simple to implement a custom interface module`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/interfaces.md|anchor=custom-interfaces]`!`_.
|
||||
|
||||
Currently, the following built-in interfaces are supported:
|
||||
|
||||
• Any Ethernet device
|
||||
• LoRa using `_`!`[RNode`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=rnode_firmware]`!`_
|
||||
• Packet Radio TNCs (with or without AX.25)
|
||||
• KISS-compatible hardware and software modems
|
||||
• Any device with a serial port
|
||||
• TCP over IP networks
|
||||
• UDP over IP networks
|
||||
• External programs via stdio or pipes
|
||||
• Custom hardware via stdio or pipes
|
||||
|
||||
>> Performance
|
||||
|
||||
Reticulum targets a `*very`* wide usable performance envelope, but prioritises functionality and performance on low-bandwidth mediums. The goal is to provide a dynamic performance envelope from 250 bits per second, to 1 gigabit per second on normal hardware.
|
||||
|
||||
Currently, the usable performance envelope is approximately 150 bits per second to 500 megabits per second, with physical mediums faster than that not being saturated. Performance beyond the current level is intended for future upgrades, but not highly prioritised at this point in time.
|
||||
|
||||
>> Current Status
|
||||
|
||||
All core protocol features are implemented and functioning, but additions will probably occur as real-world use is explored and understood. The API and wire-format can be considered stable.
|
||||
|
||||
>> Dependencies
|
||||
|
||||
The installation of the default `B333rns`b package requires only two external dependencies, listed below. Almost all systems and distributions have readily available packages for these dependencies, and when the `B333rns`b package is installed with `B333pip`b, they will be downloaded and installed as well.
|
||||
|
||||
• PyCA/cryptography
|
||||
• pyserial
|
||||
|
||||
On more unusual systems, and in some rare cases, it might not be possible to install or even compile one or more of the above modules. In such situations, you can use the `B333rnspure`b package instead, which require no external dependencies for installation. Please note that the contents of the `B333rns`b and `B333rnspure`b packages are `*identical`*. The only difference is that the `B333rnspure`b package lists no dependencies required for installation.
|
||||
|
||||
No matter how Reticulum is installed and started, it will load external dependencies only if they are `*needed`* and `*available`*. If for example you want to use Reticulum on a system that cannot support `B333pyserial`b, it is perfectly possible to do so using the `B333rnspure`b package, but Reticulum will not be able to use serial-based interfaces. All other available modules will still be loaded when needed.
|
||||
|
||||
`!Please Note!`! If you use the `B333rnspure`b package to run Reticulum on systems that do not support PyCA/cryptography, it is important that you read and understand the `!Cryptographic Primitives`! section of this document.
|
||||
|
||||
>> Bootstrapping Connectivity
|
||||
|
||||
Reticulum is not a service you subscribe to, nor is it a single global network you "join". Reticulum provides functionality for discovering available public interfaces over the network itself, and the broader community has provided various directories of publicly available entrypoints to bootstrap connectivity.
|
||||
|
||||
To learn how to establish initial connectivity over Reticulum, read the `_`!`[Bootstrapping Connectivity`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/gettingstartedfast.md|anchor=bootstrapping-connectivity]`!`_ section of the manual.
|
||||
|
||||
If you already have a general idea of how this works, you can use community-run sites such as `_`!`[rns.recipes`9ce92808be498e9e05590ff27cbfdfe4]`!`_ and `_`!`[rmap.world`a4a5e861626ce97c9aa544d9ecdf6d22]`!`_ to find interface definitions for initial connectivity to the global distributed Reticulum backbone.
|
||||
|
||||
>> Public Testnet
|
||||
|
||||
`!`*Important!`! Historically, a developer-targeted testnet was made available by the Reticulum project itself. As the amount of global Reticulum nodes and entrypoints have grown to a substantial quantity, this public testnet, including the Amsterdam Testnet entrypoint, has now been decommissioned. If you still have instances that relied on this entrypoint for connectivity, transition to using the distributed backbone instead. Reticulum now includes a full on-network interface discovery and connectivity bootstrapping system. Read the `_`[Bootstrapping Connectivity`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/gettingstartedfast.md|anchor=bootstrapping-connectivity]`_ section of the manual for pointers.`*
|
||||
|
||||
>> Support Reticulum
|
||||
|
||||
For this to be possible, I need your help. Please support the continued development of open, free and private communications systems by donating via one of the following channels:
|
||||
|
||||
• `!Monero`!
|
||||
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
|
||||
|
||||
• `!Bitcoin`!
|
||||
bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6
|
||||
|
||||
• `!Ethereum`!
|
||||
0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E
|
||||
|
||||
• `!Liberapay`!
|
||||
`[https://liberapay.com/Reticulum/]
|
||||
|
||||
• `!Ko-Fi`!
|
||||
`[https://ko-fi.com/markqvist]
|
||||
|
||||
>> Cryptographic Primitives
|
||||
|
||||
Reticulum uses a simple suite of efficient, strong and well-tested cryptographic primitives, with widely available implementations that can be used both on general-purpose CPUs and on microcontrollers.
|
||||
|
||||
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 `FTA35050`!is not`!`f 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
|
||||
• A 256-bit X22519 key for ECDH key exchanges
|
||||
• 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
|
||||
• HMAC using SHA256 for message authentication
|
||||
• IVs must be generated through `B333os.urandom()`b or better
|
||||
• AES-256 in CBC mode with PKCS7 padding
|
||||
• No Fernet version and timestamp metadata fields
|
||||
• SHA-256
|
||||
• SHA-512
|
||||
|
||||
In the default installation configuration, the `B333X25519`b, `B333Ed25519`b, and `B333AES-256-CBC`b primitives are provided by `_`!`[OpenSSL`https://www.openssl.org/]`!`_ (via the `_`!`[PyCA/cryptography`https://github.com/pyca/cryptography]`!`_ package). The hashing functions `B333SHA-256`b and `B333SHA-512`b are provided by the standard Python `_`!`[hashlib`https://docs.python.org/3/library/hashlib.html]`!`_. The `B333HKDF`b, `B333HMAC`b, `B333Token`b primitives, and the `B333PKCS7`b padding function are always provided by the following internal implementations:
|
||||
|
||||
• `_`!`[HKDF.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/HKDF.py]`!`_
|
||||
• `_`!`[HMAC.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/HMAC.py]`!`_
|
||||
• `_`!`[Token.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/Token.py]`!`_
|
||||
• `_`!`[PKCS7.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/PKCS7.py]`!`_
|
||||
|
||||
Reticulum also includes a complete implementation of all necessary primitives 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 `B333rnspure`b package instead of the normal `B333rns`b 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.
|
||||
|
||||
Reticulum is relatively young software, and should be considered as such. While it has been built with cryptography best-practices very foremost in mind, it _has not_ been externally security audited, and there could very well be privacy or security breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch.
|
||||
|
||||
>> Acknowledgements & Credits
|
||||
|
||||
Reticulum can only exist because of the mountain of Open Source work it was built on top of, the contributions of everyone involved, and everyone that has supported the project through the years. To everyone who has helped, thank you so much.
|
||||
|
||||
A number of other modules and projects are either part of, or used by Reticulum. Sincere thanks to the authors and contributors of the following 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-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]`!`_ 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`*
|
||||
• `_`!`[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]`!`_
|
||||
• `_`!`[rnsh`https://github.com/acehoss/rnsh]`!`_ by `_`!`[Aaron Heise`https://github.com/acehoss]`!`_
|
||||
• `_`!`[Python`https://www.python.org]`!`_
|
||||
+3
-1
@@ -92,7 +92,9 @@ class StreamDataMessage(MessageBase):
|
||||
self.data = raw[2:]
|
||||
|
||||
if self.compressed:
|
||||
self.data = bz2.decompress(self.data)
|
||||
decompressor = bz2.BZ2Decompressor()
|
||||
self.data = decompressor.decompress(self.data, max_length=RawChannelWriter.MAX_CHUNK_LEN)
|
||||
if not decompressor.eof: raise IOError("Decompressed buffer chunk exceeds maximum legitimate size")
|
||||
|
||||
|
||||
class RawChannelReader(RawIOBase, AbstractContextManager):
|
||||
|
||||
+89
-56
@@ -144,7 +144,7 @@ class MessageBase(abc.ABC):
|
||||
MSGTYPE = None
|
||||
"""
|
||||
Defines a unique identifier for a message class.
|
||||
|
||||
|
||||
* Must be unique within all classes registered with a ``Channel``
|
||||
* Must be less than ``0xf000``. Values greater than or equal to ``0xf000`` are reserved.
|
||||
"""
|
||||
@@ -255,11 +255,11 @@ class Channel(contextlib.AbstractContextManager):
|
||||
|
||||
# The maximum window size for transfers on fast links
|
||||
WINDOW_MAX_FAST = 48
|
||||
|
||||
|
||||
# For calculating maps and guard segments, this
|
||||
# must be set to the global maximum window.
|
||||
WINDOW_MAX = WINDOW_MAX_FAST
|
||||
|
||||
|
||||
# If the fast rate is sustained for this many request
|
||||
# rounds, the fast link window size will be allowed.
|
||||
FAST_RATE_THRESHOLD = 10
|
||||
@@ -285,6 +285,7 @@ class Channel(contextlib.AbstractContextManager):
|
||||
"""
|
||||
self._outlet = outlet
|
||||
self._lock = threading.RLock()
|
||||
self._send_lock = threading.Lock()
|
||||
self._tx_ring: collections.deque[Envelope] = collections.deque()
|
||||
self._rx_ring: collections.deque[Envelope] = collections.deque()
|
||||
self._message_callbacks: [MessageCallbackType] = []
|
||||
@@ -382,27 +383,30 @@ class Channel(contextlib.AbstractContextManager):
|
||||
if envelope.packet is not None:
|
||||
self._outlet.set_packet_timeout_callback(envelope.packet, None)
|
||||
self._outlet.set_packet_delivered_callback(envelope.packet, None)
|
||||
envelope.tracked = False
|
||||
for envelope in self._rx_ring:
|
||||
envelope.tracked = False
|
||||
self._tx_ring.clear()
|
||||
self._rx_ring.clear()
|
||||
|
||||
def _emplace_envelope(self, envelope: Envelope, ring: collections.deque[Envelope]) -> bool:
|
||||
with self._lock:
|
||||
i = 0
|
||||
|
||||
|
||||
for existing in ring:
|
||||
|
||||
if envelope.sequence == existing.sequence:
|
||||
RNS.log(f"Envelope: Emplacement of duplicate envelope with sequence "+str(envelope.sequence), RNS.LOG_EXTREME)
|
||||
return False
|
||||
|
||||
|
||||
if envelope.sequence < existing.sequence and not (self._next_rx_sequence - envelope.sequence) > (Channel.SEQ_MAX//2):
|
||||
ring.insert(i, envelope)
|
||||
|
||||
envelope.tracked = True
|
||||
return True
|
||||
|
||||
|
||||
i += 1
|
||||
|
||||
|
||||
envelope.tracked = True
|
||||
ring.append(envelope)
|
||||
|
||||
@@ -457,7 +461,7 @@ class Channel(contextlib.AbstractContextManager):
|
||||
m = e.unpack(self._message_factories)
|
||||
else:
|
||||
m = e.message
|
||||
|
||||
|
||||
self._rx_ring.remove(e)
|
||||
self._run_callbacks(m)
|
||||
|
||||
@@ -476,7 +480,7 @@ class Channel(contextlib.AbstractContextManager):
|
||||
with self._lock:
|
||||
outstanding = 0
|
||||
for envelope in self._tx_ring:
|
||||
if envelope.outlet == self._outlet:
|
||||
if envelope.outlet == self._outlet:
|
||||
if not envelope.packet or not self._outlet.get_packet_state(envelope.packet) == MessageState.MSGSTATE_DELIVERED:
|
||||
outstanding += 1
|
||||
|
||||
@@ -486,8 +490,10 @@ class Channel(contextlib.AbstractContextManager):
|
||||
return True
|
||||
|
||||
def _packet_tx_op(self, packet: TPacket, op: Callable[[TPacket], bool]):
|
||||
target_id = self._outlet.get_packet_id(packet)
|
||||
with self._lock:
|
||||
envelope = next(filter(lambda e: self._outlet.get_packet_id(e.packet) == self._outlet.get_packet_id(packet),
|
||||
envelope = next(filter(lambda e: e.packet is not None
|
||||
and self._outlet.get_packet_id(e.packet) == target_id,
|
||||
self._tx_ring), None)
|
||||
|
||||
if envelope and op(envelope):
|
||||
@@ -516,7 +522,7 @@ class Channel(contextlib.AbstractContextManager):
|
||||
# TODO: Remove at some point
|
||||
# RNS.log("Increased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
|
||||
# RNS.log("Increased "+str(self)+" min window to "+str(self.window_min), RNS.LOG_DEBUG)
|
||||
|
||||
|
||||
else:
|
||||
self.fast_rate_rounds += 1
|
||||
if self.window_max < Channel.WINDOW_MAX_FAST and self.fast_rate_rounds == Channel.FAST_RATE_THRESHOLD:
|
||||
@@ -547,36 +553,48 @@ class Channel(contextlib.AbstractContextManager):
|
||||
return to
|
||||
|
||||
def _packet_timeout(self, packet: TPacket):
|
||||
def retry_envelope(envelope: Envelope) -> bool:
|
||||
if self._outlet.get_packet_state(packet) == MessageState.MSGSTATE_DELIVERED:
|
||||
return
|
||||
|
||||
target_id = self._outlet.get_packet_id(packet)
|
||||
envelope_to_resend: Envelope | None = None
|
||||
should_teardown = False
|
||||
with self._lock:
|
||||
envelope = next(filter(
|
||||
lambda e: e.packet is not None and self._outlet.get_packet_id(e.packet) == target_id,
|
||||
self._tx_ring), None)
|
||||
if envelope is None:
|
||||
return
|
||||
|
||||
if envelope.tries >= self._max_tries:
|
||||
RNS.log("Retry count exceeded on "+str(self)+", tearing down Link.", RNS.LOG_ERROR)
|
||||
self._shutdown() # start on separate thread?
|
||||
self._outlet.timed_out()
|
||||
return True
|
||||
should_teardown = True
|
||||
else:
|
||||
envelope.tries += 1
|
||||
envelope_to_resend = envelope
|
||||
|
||||
envelope.tries += 1
|
||||
self._outlet.resend(envelope.packet)
|
||||
self._outlet.set_packet_delivered_callback(envelope.packet, self._packet_delivered)
|
||||
self._outlet.set_packet_timeout_callback(envelope.packet, self._packet_timeout, self._get_packet_timeout_time(envelope.tries))
|
||||
self._update_packet_timeouts()
|
||||
if self.window > self.window_min:
|
||||
self.window -= 1
|
||||
if self.window_max > (self.window_min+self.window_flexibility):
|
||||
self.window_max -= 1
|
||||
|
||||
if self.window > self.window_min:
|
||||
self.window -= 1
|
||||
# TODO: Remove at some point
|
||||
# RNS.log("Decreased "+str(self)+" window to "+str(self.window), RNS.LOG_DEBUG)
|
||||
if should_teardown:
|
||||
RNS.log("Retry count exceeded on "+str(self)+", tearing down Link.", RNS.LOG_ERROR)
|
||||
self._shutdown()
|
||||
self._outlet.timed_out()
|
||||
return
|
||||
|
||||
if self.window_max > (self.window_min+self.window_flexibility):
|
||||
self.window_max -= 1
|
||||
# TODO: Remove at some point
|
||||
# RNS.log("Decreased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
|
||||
if envelope_to_resend is not None:
|
||||
self._outlet.resend(envelope_to_resend.packet)
|
||||
with self._lock:
|
||||
self._outlet.set_packet_delivered_callback(envelope_to_resend.packet, self._packet_delivered)
|
||||
self._outlet.set_packet_timeout_callback(
|
||||
envelope_to_resend.packet, self._packet_timeout,
|
||||
self._get_packet_timeout_time(envelope_to_resend.tries))
|
||||
self._update_packet_timeouts()
|
||||
already_delivered = (self._outlet.get_packet_state(envelope_to_resend.packet) == MessageState.MSGSTATE_DELIVERED)
|
||||
|
||||
# TODO: Remove at some point
|
||||
# RNS.log("Decreased "+str(self)+" window to "+str(self.window), RNS.LOG_EXTREME)
|
||||
|
||||
return False
|
||||
|
||||
if self._outlet.get_packet_state(packet) != MessageState.MSGSTATE_DELIVERED:
|
||||
self._packet_tx_op(packet, retry_envelope)
|
||||
if already_delivered:
|
||||
self._packet_delivered(envelope_to_resend.packet)
|
||||
|
||||
def send(self, message: MessageBase) -> Envelope:
|
||||
"""
|
||||
@@ -585,27 +603,39 @@ class Channel(contextlib.AbstractContextManager):
|
||||
|
||||
:param message: an instance of a ``MessageBase`` subclass
|
||||
"""
|
||||
envelope: Envelope | None = None
|
||||
with self._lock:
|
||||
if not self.is_ready_to_send():
|
||||
raise ChannelException(CEType.ME_LINK_NOT_READY, f"Link is not ready")
|
||||
|
||||
envelope = Envelope(self._outlet, message=message, sequence=self._next_sequence)
|
||||
self._next_sequence = (self._next_sequence + 1) % Channel.SEQ_MODULUS
|
||||
self._emplace_envelope(envelope, self._tx_ring)
|
||||
with self._send_lock:
|
||||
with self._lock:
|
||||
if not self.is_ready_to_send():
|
||||
raise ChannelException(CEType.ME_LINK_NOT_READY, f"Link is not ready")
|
||||
|
||||
if envelope is None:
|
||||
raise BlockingIOError()
|
||||
reserved_sequence = self._next_sequence
|
||||
envelope = Envelope(self._outlet, message=message, sequence=reserved_sequence)
|
||||
envelope.pack()
|
||||
if len(envelope.raw) > self._outlet.mdu:
|
||||
raise ChannelException(CEType.ME_TOO_BIG,
|
||||
f"Packed message too big for packet: {len(envelope.raw)} > {self._outlet.mdu}")
|
||||
self._next_sequence = (reserved_sequence + 1) % Channel.SEQ_MODULUS
|
||||
|
||||
envelope.pack()
|
||||
if len(envelope.raw) > self._outlet.mdu:
|
||||
raise ChannelException(CEType.ME_TOO_BIG, f"Packed message too big for packet: {len(envelope.raw)} > {self._outlet.mdu}")
|
||||
|
||||
envelope.packet = self._outlet.send(envelope.raw)
|
||||
envelope.tries += 1
|
||||
self._outlet.set_packet_delivered_callback(envelope.packet, self._packet_delivered)
|
||||
self._outlet.set_packet_timeout_callback(envelope.packet, self._packet_timeout, self._get_packet_timeout_time(envelope.tries))
|
||||
self._update_packet_timeouts()
|
||||
envelope.packet = self._outlet.send(envelope.raw)
|
||||
|
||||
if (envelope.packet is None
|
||||
or getattr(envelope.packet, "raw", None) is None
|
||||
or (hasattr(envelope.packet, "receipt") and envelope.packet.receipt is None)):
|
||||
with self._lock:
|
||||
self._next_sequence = reserved_sequence
|
||||
raise ChannelException(CEType.ME_LINK_NOT_READY, "Outlet did not transmit packet")
|
||||
|
||||
with self._lock:
|
||||
self._emplace_envelope(envelope, self._tx_ring)
|
||||
envelope.tries += 1
|
||||
self._outlet.set_packet_delivered_callback(envelope.packet, self._packet_delivered)
|
||||
self._outlet.set_packet_timeout_callback(envelope.packet, self._packet_timeout, self._get_packet_timeout_time(envelope.tries))
|
||||
self._update_packet_timeouts()
|
||||
already_delivered = (self._outlet.get_packet_state(envelope.packet) == MessageState.MSGSTATE_DELIVERED)
|
||||
|
||||
# prevent _tx_ring envelope leak
|
||||
if already_delivered:
|
||||
self._packet_delivered(envelope.packet)
|
||||
|
||||
return envelope
|
||||
|
||||
@@ -699,7 +729,10 @@ class LinkChannelOutlet(ChannelOutletBase):
|
||||
packet.receipt.set_delivery_callback(inner if callback else None)
|
||||
|
||||
def get_packet_id(self, packet: RNS.Packet) -> any:
|
||||
if packet and hasattr(packet, "get_hash") and callable(packet.get_hash):
|
||||
if (packet
|
||||
and getattr(packet, "raw", None) is not None
|
||||
and hasattr(packet, "get_hash")
|
||||
and callable(packet.get_hash)):
|
||||
return packet.get_hash()
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -35,7 +35,6 @@ class Ed25519PrivateKey:
|
||||
def __init__(self, seed):
|
||||
self.seed = seed
|
||||
self.sk = ed25519.SigningKey(self.seed)
|
||||
#self.vk = self.sk.get_verifying_key()
|
||||
|
||||
@classmethod
|
||||
def generate(cls):
|
||||
|
||||
@@ -62,3 +62,9 @@ def sha512(data):
|
||||
digest.update(data)
|
||||
|
||||
return digest.digest()
|
||||
|
||||
def file_sha256(file):
|
||||
if not hashlib: raise SystemError("The hashlib module is not available on this system")
|
||||
# TODO: Could implement fallback for old snakes here
|
||||
if not hasattr(hashlib, "file_digest"): raise SystemError("The file_digest method is not available on this system. This functionality requires Python 3.11 or later.")
|
||||
else: return hashlib.file_digest(file, "sha256").digest()
|
||||
|
||||
+17
-24
@@ -295,33 +295,26 @@ class Destination:
|
||||
app_data = returned_app_data
|
||||
|
||||
signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash+ratchet
|
||||
if app_data != None:
|
||||
signed_data += app_data
|
||||
if app_data != None: signed_data += app_data
|
||||
|
||||
signature = self.identity.sign(signed_data)
|
||||
announce_data = self.identity.get_public_key()+self.name_hash+random_hash+ratchet+signature
|
||||
|
||||
if app_data != None:
|
||||
announce_data += app_data
|
||||
if app_data != None: announce_data += app_data
|
||||
|
||||
self.path_responses[tag] = [time.time(), announce_data]
|
||||
|
||||
if path_response:
|
||||
announce_context = RNS.Packet.PATH_RESPONSE
|
||||
else:
|
||||
announce_context = RNS.Packet.NONE
|
||||
if path_response: announce_context = RNS.Packet.PATH_RESPONSE
|
||||
else: announce_context = RNS.Packet.NONE
|
||||
|
||||
if ratchet:
|
||||
context_flag = RNS.Packet.FLAG_SET
|
||||
else:
|
||||
context_flag = RNS.Packet.FLAG_UNSET
|
||||
if ratchet: context_flag = RNS.Packet.FLAG_SET
|
||||
else: context_flag = RNS.Packet.FLAG_UNSET
|
||||
|
||||
announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context,
|
||||
attached_interface = attached_interface, context_flag=context_flag)
|
||||
if send:
|
||||
announce_packet.send()
|
||||
else:
|
||||
return announce_packet
|
||||
|
||||
if send: announce_packet.send()
|
||||
else: return announce_packet
|
||||
|
||||
def accepts_links(self, accepts = None):
|
||||
"""
|
||||
@@ -330,13 +323,10 @@ class Destination:
|
||||
:param accepts: If ``True`` or ``False``, this method sets whether the destination accepts incoming link requests. If not provided or ``None``, the method returns whether the destination currently accepts link requests.
|
||||
:returns: ``True`` or ``False`` depending on whether the destination accepts incoming link requests, if the *accepts* parameter is not provided or ``None``.
|
||||
"""
|
||||
if accepts == None:
|
||||
return self.accept_link_requests
|
||||
if accepts == None: return self.accept_link_requests
|
||||
|
||||
if accepts:
|
||||
self.accept_link_requests = True
|
||||
else:
|
||||
self.accept_link_requests = False
|
||||
if accepts: self.accept_link_requests = True
|
||||
else: self.accept_link_requests = False
|
||||
|
||||
def set_link_established_callback(self, callback):
|
||||
"""
|
||||
@@ -421,8 +411,7 @@ class Destination:
|
||||
else:
|
||||
if packet.packet_type == RNS.Packet.DATA:
|
||||
if self.callbacks.packet != None:
|
||||
try:
|
||||
self.callbacks.packet(plaintext, packet)
|
||||
try: self.callbacks.packet(plaintext, packet)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing receive callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
@@ -462,6 +451,10 @@ class Destination:
|
||||
self.ratchets = None
|
||||
self.ratchets_path = None
|
||||
RNS.trace_exception(e)
|
||||
RNS.log(f"The ratchet file located at {ratchets_path} could not be loaded. This could indicate that the ratchet file has become corrupt.", RNS.LOG_CRITICAL)
|
||||
RNS.log(f"You can attempt to manually recover the ratchet file, or simply remove it to have Reticulum recreate it on the next use.", RNS.LOG_CRITICAL)
|
||||
RNS.log(f"If re-initialize this ratchet file, make sure to send an announce for the relevant destination as soon as possible,", RNS.LOG_CRITICAL)
|
||||
RNS.log(f"so that the new ratchet information is synchronized to the network.", RNS.LOG_CRITICAL)
|
||||
raise OSError("Could not read ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,790 @@
|
||||
import os
|
||||
import re
|
||||
import RNS
|
||||
import time
|
||||
import random
|
||||
import threading
|
||||
import ipaddress
|
||||
import subprocess
|
||||
from threading import Lock
|
||||
from .vendor import umsgpack as msgpack
|
||||
|
||||
NAME = 0xFF
|
||||
TRANSPORT_ID = 0xFE
|
||||
INTERFACE_TYPE = 0x00
|
||||
TRANSPORT = 0x01
|
||||
REACHABLE_ON = 0x02
|
||||
LATITUDE = 0x03
|
||||
LONGITUDE = 0x04
|
||||
HEIGHT = 0x05
|
||||
PORT = 0x06
|
||||
IFAC_NETNAME = 0x07
|
||||
IFAC_NETKEY = 0x08
|
||||
FREQUENCY = 0x09
|
||||
BANDWIDTH = 0x0A
|
||||
SPREADINGFACTOR = 0x0B
|
||||
CODINGRATE = 0x0C
|
||||
MODULATION = 0x0D
|
||||
CHANNEL = 0x0E
|
||||
|
||||
APP_NAME = "rnstransport"
|
||||
|
||||
class InterfaceAnnouncer():
|
||||
JOB_INTERVAL = 60
|
||||
DEFAULT_STAMP_VALUE = 14
|
||||
WORKBLOCK_EXPAND_ROUNDS = 20
|
||||
|
||||
DISCOVERABLE_INTERFACE_TYPES = ["BackboneInterface", "TCPServerInterface", "TCPClientInterface",
|
||||
"RNodeInterface", "WeaveInterface", "I2PInterface", "KISSInterface"]
|
||||
|
||||
def __init__(self, owner):
|
||||
import importlib.util
|
||||
if importlib.util.find_spec('LXMF') != None: from LXMF import LXStamper
|
||||
else:
|
||||
RNS.log("Using on-network interface discovery requires the LXMF module to be installed.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install it with the command: pip install lxmf", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.owner = owner
|
||||
self.should_run = False
|
||||
self.job_interval = self.JOB_INTERVAL
|
||||
self.stamper = LXStamper
|
||||
self.stamp_cache = {}
|
||||
|
||||
if self.owner.has_network_identity(): identity = self.owner.network_identity
|
||||
else: identity = self.owner.identity
|
||||
|
||||
self.discovery_destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE,
|
||||
APP_NAME, "discovery", "interface")
|
||||
|
||||
def start(self):
|
||||
if not self.should_run:
|
||||
self.should_run = True
|
||||
threading.Thread(target=self.job, daemon=True).start()
|
||||
|
||||
def stop(self): self.should_run = False
|
||||
|
||||
def job(self):
|
||||
while self.should_run:
|
||||
time.sleep(self.job_interval)
|
||||
try:
|
||||
now = time.time()
|
||||
due_interfaces = [i for i in self.owner.interfaces if i.supports_discovery and i.discoverable and now > (i.last_discovery_announce+i.discovery_announce_interval)]
|
||||
due_interfaces.sort(key=lambda i: now-i.last_discovery_announce, reverse=True)
|
||||
|
||||
if len(due_interfaces) > 0:
|
||||
selected_interface = due_interfaces[0]
|
||||
selected_interface.last_discovery_announce = time.time()
|
||||
RNS.log(f"Preparing interface discovery announce for {selected_interface.name}", RNS.LOG_DEBUG)
|
||||
app_data = self.get_interface_announce_data(selected_interface)
|
||||
if not app_data: RNS.log(f"Could not generate interface discovery announce data for {selected_interface.name}", RNS.LOG_ERROR)
|
||||
else:
|
||||
RNS.log(f"Sending interface discovery announce for {selected_interface.name} with {len(app_data)}B payload", RNS.LOG_DEBUG)
|
||||
self.discovery_destination.announce(app_data=app_data)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while preparing interface discovery announces: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
def sanitize(self, in_str):
|
||||
if in_str == None: return None
|
||||
sanitized = in_str.replace("\n", "")
|
||||
sanitized = sanitized.replace("\r", "")
|
||||
sanitized = sanitized.strip()
|
||||
return sanitized
|
||||
|
||||
def get_interface_announce_data(self, interface):
|
||||
interface_type = type(interface).__name__
|
||||
stamp_value = interface.discovery_stamp_value if interface.discovery_stamp_value else self.DEFAULT_STAMP_VALUE
|
||||
|
||||
if not interface_type in self.DISCOVERABLE_INTERFACE_TYPES: return None
|
||||
else:
|
||||
flags = 0x00
|
||||
info = {INTERFACE_TYPE: interface_type,
|
||||
TRANSPORT: RNS.Reticulum.transport_enabled(),
|
||||
TRANSPORT_ID: RNS.Transport.identity.hash,
|
||||
NAME: self.sanitize(interface.discovery_name),
|
||||
LATITUDE: interface.discovery_latitude,
|
||||
LONGITUDE: interface.discovery_longitude,
|
||||
HEIGHT: interface.discovery_height}
|
||||
|
||||
if interface_type == "TCPClientInterface" and not interface.kiss_framing:
|
||||
RNS.log(f"Invalid interface discovery configuration for {interface}, aborting discovery announce", RNS.LOG_ERROR)
|
||||
return None
|
||||
|
||||
if interface_type in ["BackboneInterface", "TCPServerInterface"]:
|
||||
reachable_on = self.sanitize(interface.reachable_on)
|
||||
|
||||
if not RNS.vendor.platformutils.is_windows():
|
||||
try:
|
||||
exec_path = os.path.expanduser(reachable_on)
|
||||
if os.path.isfile(exec_path) and os.access(exec_path, os.X_OK):
|
||||
RNS.log(f"Evaluating reachable_on from executable at {exec_path}", RNS.LOG_DEBUG)
|
||||
exec_result = subprocess.run([exec_path], stdout=subprocess.PIPE)
|
||||
exec_stdout = exec_result.stdout.decode("utf-8")
|
||||
if exec_result.returncode != 0: raise ValueError("Non-zero exit code from subprocess")
|
||||
reachable_on = self.sanitize(exec_stdout)
|
||||
if not (is_ip_address(reachable_on) or is_hostname(reachable_on)):
|
||||
raise ValueError(f"Valid IP address or hostname was not found in external script output \"{reachable_on}\"")
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while getting reachable_on from executable at {interface.reachable_on}: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"Aborting discovery announce", RNS.LOG_ERROR)
|
||||
return None
|
||||
|
||||
if not (is_ip_address(reachable_on) or is_hostname(reachable_on)):
|
||||
RNS.log(f"The configured reachable_on parameter \"{reachable_on}\" for {interface} is not a valid IP address or hostname", RNS.LOG_ERROR)
|
||||
RNS.log(f"Aborting discovery announce", RNS.LOG_ERROR)
|
||||
return None
|
||||
|
||||
info[REACHABLE_ON] = reachable_on
|
||||
info[PORT] = interface.bind_port
|
||||
|
||||
if interface_type == "I2PInterface" and interface.connectable and interface.b32:
|
||||
info[REACHABLE_ON] = interface.b32
|
||||
|
||||
if interface_type == "RNodeInterface":
|
||||
info[FREQUENCY] = interface.frequency
|
||||
info[BANDWIDTH] = interface.bandwidth
|
||||
info[SPREADINGFACTOR] = interface.sf
|
||||
info[CODINGRATE] = interface.cr
|
||||
|
||||
if interface_type == "WeaveInterface":
|
||||
info[FREQUENCY] = interface.discovery_frequency
|
||||
info[BANDWIDTH] = interface.discovery_bandwidth
|
||||
info[CHANNEL] = interface.discovery_channel
|
||||
info[MODULATION] = interface.discovery_modulation
|
||||
|
||||
if interface_type == "KISSInterface" or (interface_type == "TCPClientInterface" and interface.kiss_framing):
|
||||
info[INTERFACE_TYPE] = "KISSInterface"
|
||||
info[FREQUENCY] = interface.discovery_frequency
|
||||
info[BANDWIDTH] = interface.discovery_bandwidth
|
||||
info[MODULATION] = self.sanitize(interface.discovery_modulation)
|
||||
|
||||
if interface.discovery_publish_ifac == True:
|
||||
info[IFAC_NETNAME] = self.sanitize(interface.ifac_netname)
|
||||
info[IFAC_NETKEY] = self.sanitize(interface.ifac_netkey)
|
||||
|
||||
packed = msgpack.packb(info)
|
||||
infohash = RNS.Identity.full_hash(packed)
|
||||
|
||||
if infohash in self.stamp_cache: stamp = self.stamp_cache[infohash]
|
||||
else: stamp, v = self.stamper.generate_stamp(infohash, stamp_cost=stamp_value, expand_rounds=self.WORKBLOCK_EXPAND_ROUNDS)
|
||||
if not stamp: return None
|
||||
else: self.stamp_cache[infohash] = stamp
|
||||
|
||||
if interface.discovery_encrypt:
|
||||
flags |= InterfaceAnnounceHandler.FLAG_ENCRYPTED
|
||||
if not self.owner.has_network_identity():
|
||||
RNS.log(f"Discovery encryption requested for {interface}, but no network identity configured. Aborting discovery announce.", RNS.LOG_ERROR)
|
||||
return None
|
||||
|
||||
else: payload = self.owner.network_identity.encrypt(packed+stamp)
|
||||
|
||||
else: payload = packed+stamp
|
||||
|
||||
return bytes([flags])+payload
|
||||
|
||||
class InterfaceAnnounceHandler:
|
||||
FLAG_SIGNED = 0b00000001
|
||||
FLAG_ENCRYPTED = 0b00000010
|
||||
|
||||
def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None):
|
||||
import importlib.util
|
||||
if importlib.util.find_spec('LXMF') != None: from LXMF import LXStamper
|
||||
else:
|
||||
RNS.log("Using on-network interface discovery requires the LXMF module to be installed.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install it with the command: pip install lxmf", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.aspect_filter = APP_NAME+".discovery.interface"
|
||||
self.required_value = required_value
|
||||
self.callback = callback
|
||||
self.stamper = LXStamper
|
||||
|
||||
@staticmethod
|
||||
def sanitize_name(name):
|
||||
if not name: return None
|
||||
name = name.encode("ascii", "ignore").decode("ascii").strip()
|
||||
for i in [5,3,2]: name = name.replace(" "*i, " ")
|
||||
while len(name) and name[0] not in san_map: name = name[1:]
|
||||
while len(name) and name[-1] not in san_map+")": name = name[:-1]
|
||||
return name
|
||||
|
||||
def received_announce(self, destination_hash, announced_identity, app_data):
|
||||
try:
|
||||
discovery_sources = RNS.Reticulum.interface_discovery_sources()
|
||||
if discovery_sources and not announced_identity.hash in discovery_sources:
|
||||
RNS.log(f"Interface discovered from non-authorized network identity {RNS.prettyhexrep(announced_identity.hash)}, ignoring", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
if app_data and len(app_data) > self.stamper.STAMP_SIZE+1:
|
||||
flags = app_data[0]
|
||||
app_data = app_data[1:]
|
||||
signed = flags & self.FLAG_SIGNED
|
||||
encrypted = flags & self.FLAG_ENCRYPTED
|
||||
|
||||
if encrypted:
|
||||
if not RNS.Transport.has_network_identity(): return
|
||||
app_data = RNS.Transport.network_identity.decrypt(app_data)
|
||||
if not app_data: return
|
||||
|
||||
stamp = app_data[-self.stamper.STAMP_SIZE:]
|
||||
packed = app_data[:-self.stamper.STAMP_SIZE]
|
||||
infohash = RNS.Identity.full_hash(packed)
|
||||
workblock = self.stamper.stamp_workblock(infohash, expand_rounds=InterfaceAnnouncer.WORKBLOCK_EXPAND_ROUNDS)
|
||||
value = self.stamper.stamp_value(workblock, stamp)
|
||||
valid = self.stamper.stamp_valid(stamp, self.required_value, workblock)
|
||||
|
||||
if not valid:
|
||||
RNS.log(f"Ignored discovered interface with invalid stamp", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
if value < self.required_value: RNS.log(f"Ignored discovered interface with stamp value {value}", RNS.LOG_DEBUG)
|
||||
else:
|
||||
info = None
|
||||
unpacked = msgpack.unpackb(packed)
|
||||
if INTERFACE_TYPE in unpacked:
|
||||
interface_type = unpacked[INTERFACE_TYPE]
|
||||
name = self.sanitize_name(unpacked[NAME])
|
||||
|
||||
if type(unpacked[TRANSPORT]) != bool: raise ValueError("Invalid data in transport field of announce")
|
||||
if type(unpacked[LATITUDE]) not in [type(None), float]: raise ValueError("Invalid data in latitude field of announce")
|
||||
if type(unpacked[LONGITUDE]) not in [type(None), float]: raise ValueError("Invalid data in longitude field of announce")
|
||||
if type(unpacked[HEIGHT]) not in [type(None), float]: raise ValueError("Invalid data in height field of announce")
|
||||
if len(unpacked[TRANSPORT_ID]) != RNS.Identity.TRUNCATED_HASHLENGTH//8: raise ValueError("Invalid data in transport_id field of announce")
|
||||
if not interface_type in InterfaceAnnouncer.DISCOVERABLE_INTERFACE_TYPES:
|
||||
raise ValueError("Invalid interface type in announce data")
|
||||
|
||||
if REACHABLE_ON in unpacked:
|
||||
if not (is_ip_address(unpacked[REACHABLE_ON]) or is_hostname(unpacked[REACHABLE_ON])):
|
||||
raise ValueError("Invalid data in reachable_on field of announce")
|
||||
|
||||
info = {"type": interface_type,
|
||||
"transport": unpacked[TRANSPORT],
|
||||
"name": name or f"Discovered {interface_type}",
|
||||
"received": time.time(),
|
||||
"stamp": stamp,
|
||||
"value": value,
|
||||
"transport_id": RNS.hexrep(unpacked[TRANSPORT_ID], delimit=False),
|
||||
"network_id": RNS.hexrep(announced_identity.hash, delimit=False),
|
||||
"hops": RNS.Transport.hops_to(destination_hash),
|
||||
"latitude": unpacked[LATITUDE],
|
||||
"longitude": unpacked[LONGITUDE],
|
||||
"height": unpacked[HEIGHT]}
|
||||
|
||||
if IFAC_NETNAME in unpacked: info["ifac_netname"] = str(unpacked[IFAC_NETNAME])
|
||||
if IFAC_NETKEY in unpacked: info["ifac_netkey"] = str(unpacked[IFAC_NETKEY])
|
||||
|
||||
if interface_type in ["BackboneInterface", "TCPServerInterface"]:
|
||||
backbone_support = not RNS.vendor.platformutils.is_windows()
|
||||
info["reachable_on"] = unpacked[REACHABLE_ON]
|
||||
info["port"] = unpacked[PORT]
|
||||
connection_interface = "BackboneInterface" if backbone_support else "TCPClientInterface"
|
||||
remote_str = "remote" if backbone_support else "target_host"
|
||||
cfg_name = info["name"]
|
||||
cfg_remote = info["reachable_on"]
|
||||
cfg_port = info["port"]
|
||||
cfg_identity = info["transport_id"]
|
||||
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
|
||||
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
|
||||
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
|
||||
info["config_entry"] = f"[[{cfg_name}]]\n type = {connection_interface}\n enabled = yes\n {remote_str} = {cfg_remote}\n target_port = {cfg_port}{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}"
|
||||
|
||||
if interface_type == "I2PInterface":
|
||||
info["reachable_on"] = unpacked[REACHABLE_ON]
|
||||
cfg_name = info["name"]
|
||||
cfg_remote = info["reachable_on"]
|
||||
cfg_identity = info["transport_id"]
|
||||
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
|
||||
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
|
||||
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
|
||||
info["config_entry"] = f"[[{cfg_name}]]\n type = I2PInterface\n enabled = yes\n peers = {cfg_remote}{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}"
|
||||
|
||||
if interface_type == "RNodeInterface":
|
||||
info["frequency"] = unpacked[FREQUENCY]
|
||||
info["bandwidth"] = unpacked[BANDWIDTH]
|
||||
info["sf"] = unpacked[SPREADINGFACTOR]
|
||||
info["cr"] = unpacked[CODINGRATE]
|
||||
cfg_name = info["name"]
|
||||
cfg_frequency = info["frequency"]
|
||||
cfg_bandwidth = info["bandwidth"]
|
||||
cfg_sf = info["sf"]
|
||||
cfg_cr = info["cr"]
|
||||
cfg_identity = info["transport_id"]
|
||||
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
|
||||
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
|
||||
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
|
||||
info["config_entry"] = f"[[{cfg_name}]]\n type = RNodeInterface\n enabled = yes\n port = \n frequency = {cfg_frequency}\n bandwidth = {cfg_bandwidth}\n spreadingfactor = {cfg_sf}\n codingrate = {cfg_cr}\n txpower = {cfg_netname_str}{cfg_netkey_str}"
|
||||
|
||||
if interface_type == "WeaveInterface":
|
||||
info["frequency"] = unpacked[FREQUENCY]
|
||||
info["bandwidth"] = unpacked[BANDWIDTH]
|
||||
info["channel"] = unpacked[CHANNEL]
|
||||
info["modulation"] = unpacked[MODULATION]
|
||||
cfg_name = info["name"]
|
||||
cfg_identity = info["transport_id"]
|
||||
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
|
||||
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
|
||||
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
|
||||
info["config_entry"] = f"[[{cfg_name}]]\n type = WeaveInterface\n enabled = yes\n port = {cfg_netname_str}{cfg_netkey_str}"
|
||||
|
||||
if interface_type == "KISSInterface":
|
||||
info["frequency"] = unpacked[FREQUENCY]
|
||||
info["bandwidth"] = unpacked[BANDWIDTH]
|
||||
info["modulation"] = unpacked[MODULATION]
|
||||
cfg_name = info["name"]
|
||||
cfg_frequency = info["frequency"]
|
||||
cfg_bandwidth = info["bandwidth"]
|
||||
cfg_modulation = info["modulation"]
|
||||
cfg_identity = info["transport_id"]
|
||||
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
|
||||
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
|
||||
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
|
||||
info["config_entry"] = f"[[{cfg_name}]]\n type = KISSInterface\n enabled = yes\n port = \n # Frequency: {cfg_frequency}\n # Bandwidth: {cfg_bandwidth}\n # Modulation: {cfg_modulation}{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}"
|
||||
|
||||
discovery_hash_material = info["transport_id"]+info["name"]
|
||||
info["discovery_hash"] = RNS.Identity.full_hash(discovery_hash_material.encode("utf-8"))
|
||||
|
||||
if self.callback and callable(self.callback): self.callback(info)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while trying to decode discovered interface. The contained exception was: {e}", RNS.LOG_DEBUG)
|
||||
|
||||
class InterfaceDiscovery():
|
||||
THRESHOLD_UNKNOWN = 24*60*60
|
||||
THRESHOLD_STALE = 3*24*60*60
|
||||
THRESHOLD_REMOVE = 7*24*60*60
|
||||
|
||||
MONITOR_INTERVAL = 5
|
||||
DETACH_THRESHOLD = 12
|
||||
|
||||
STATUS_STALE = 0
|
||||
STATUS_UNKNOWN = 100
|
||||
STATUS_AVAILABLE = 1000
|
||||
STATUS_CODE_MAP = {"available": STATUS_AVAILABLE, "unknown": STATUS_UNKNOWN, "stale": STATUS_STALE}
|
||||
AUTOCONNECT_TYPES = ["BackboneInterface", "TCPServerInterface"]
|
||||
DISCOVERABLE_TYPES = ["BackboneInterface", "TCPServerInterface", "I2PInterface", "RNodeInterface", "WeaveInterface", "KISSInterface"]
|
||||
|
||||
discovery_lock = Lock()
|
||||
|
||||
def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None, discover_interfaces=True):
|
||||
if not required_value: required_value = InterfaceAnnouncer.DEFAULT_STAMP_VALUE
|
||||
|
||||
self.required_value = required_value
|
||||
self.discovery_callback = callback
|
||||
self.rns_instance = RNS.Reticulum.get_instance()
|
||||
self.monitored_interfaces = []
|
||||
self.monitoring_autoconnects = False
|
||||
self.monitor_interval = self.MONITOR_INTERVAL
|
||||
self.detach_threshold = self.DETACH_THRESHOLD
|
||||
self.initial_autoconnect_ran = False
|
||||
|
||||
if not self.rns_instance: raise SystemError("Attempt to start interface discovery listener without an active RNS instance")
|
||||
self.storagepath = os.path.join(RNS.Reticulum.storagepath, "discovery", "interfaces")
|
||||
if not os.path.isdir(self.storagepath): os.makedirs(self.storagepath)
|
||||
|
||||
if discover_interfaces:
|
||||
self.handler = InterfaceAnnounceHandler(callback=self.interface_discovered, required_value=self.required_value)
|
||||
RNS.Transport.register_announce_handler(self.handler)
|
||||
threading.Thread(target=self.connect_discovered, daemon=True).start()
|
||||
|
||||
def list_discovered_interfaces(self, only_available=False, only_transport=False):
|
||||
now = time.time()
|
||||
discovered_interfaces = []
|
||||
discovery_sources = RNS.Reticulum.interface_discovery_sources()
|
||||
for filename in os.listdir(self.storagepath):
|
||||
try:
|
||||
with self.discovery_lock:
|
||||
filepath = os.path.join(self.storagepath, filename)
|
||||
with open(filepath, "rb") as f: info = msgpack.unpackb(f.read())
|
||||
|
||||
should_remove = False
|
||||
heard_delta = now-info["last_heard"]
|
||||
info["name"] = InterfaceAnnounceHandler.sanitize_name(info["name"])
|
||||
|
||||
if heard_delta > self.THRESHOLD_REMOVE: should_remove = True
|
||||
elif discovery_sources and not "network_id" in info: should_remove = True
|
||||
elif discovery_sources and not bytes.fromhex(info["network_id"]) in discovery_sources: should_remove = True
|
||||
elif not "type" in info or ("type" in info and not info["type"] in self.DISCOVERABLE_TYPES): should_remove = True
|
||||
elif "reachable_on" in info:
|
||||
if not (is_ip_address(info["reachable_on"]) or is_hostname(info["reachable_on"])): should_remove = True
|
||||
|
||||
if should_remove:
|
||||
os.unlink(filepath)
|
||||
continue
|
||||
|
||||
else:
|
||||
if heard_delta > self.THRESHOLD_STALE: info["status"] = "stale"
|
||||
elif heard_delta > self.THRESHOLD_UNKNOWN: info["status"] = "unknown"
|
||||
else: info["status"] = "available"
|
||||
|
||||
info["status_code"] = self.STATUS_CODE_MAP[info["status"]]
|
||||
if not only_available and not only_transport: discovered_interfaces.append(info)
|
||||
else:
|
||||
should_append = True
|
||||
status = info["status"]
|
||||
transport = info["transport"]
|
||||
if only_available and status != "available": should_append = False
|
||||
if only_transport and not transport: should_append = False
|
||||
if should_append: discovered_interfaces.append(info)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while loading discovered interface data: {e}", RNS.LOG_WARNING)
|
||||
RNS.log(f"The interface data file {os.path.join(self.storagepath, filename)} may be corrupt", RNS.LOG_WARNING)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
discovered_interfaces.sort(key=lambda info: (info["status_code"], info["value"], info["last_heard"]), reverse=True)
|
||||
return discovered_interfaces
|
||||
|
||||
def interface_discovered(self, info):
|
||||
try:
|
||||
name = info["name"]
|
||||
value = info["value"]
|
||||
interface_type = info["type"]
|
||||
discovery_hash = info["discovery_hash"]
|
||||
discovered_type = info["type"]
|
||||
if not discovered_type in self.DISCOVERABLE_TYPES: return
|
||||
hops = info["hops"]; ms = "" if hops == 1 else "s"
|
||||
filename = RNS.hexrep(discovery_hash, delimit=False)
|
||||
filepath = os.path.join(self.storagepath, filename)
|
||||
RNS.log(f"Discovered {interface_type} {hops} hop{ms} away with stamp value {value}: {name}", RNS.LOG_DEBUG)
|
||||
with self.discovery_lock:
|
||||
if not os.path.isfile(filepath):
|
||||
try:
|
||||
with open(filepath, "wb") as f:
|
||||
info["discovered"] = info["received"]
|
||||
info["last_heard"] = info["received"]
|
||||
info["heard_count"] = 0
|
||||
f.write(msgpack.packb(info))
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
return
|
||||
|
||||
else:
|
||||
discovered = None
|
||||
heard_count = None
|
||||
try:
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
last_info = msgpack.unpackb(f.read())
|
||||
discovered = last_info["discovered"]
|
||||
heard_count = last_info["heard_count"]
|
||||
|
||||
except Exception as e: RNS.log(f"Error while reading existing data for discovered interface, re-creating data", RNS.LOG_ERROR)
|
||||
|
||||
if discovered == None: discovered = info["received"]
|
||||
if heard_count == None: heard_count = 0
|
||||
|
||||
with open(filepath, "wb") as f:
|
||||
info["discovered"] = discovered
|
||||
info["last_heard"] = info["received"]
|
||||
info["heard_count"] = heard_count+1
|
||||
f.write(msgpack.packb(info))
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error processing discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
return
|
||||
|
||||
self.autoconnect(info)
|
||||
|
||||
try:
|
||||
if self.discovery_callback and callable(self.discovery_callback): self.discovery_callback(info)
|
||||
except Exception as e: RNS.log(f"Error while processing external interface discovery callback: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def monitor_interface(self, interface):
|
||||
if not interface in self.monitored_interfaces:
|
||||
self.monitored_interfaces.append(interface)
|
||||
|
||||
if not self.monitoring_autoconnects:
|
||||
self.monitoring_autoconnects = True
|
||||
threading.Thread(target=self.__monitor_job, daemon=True).start()
|
||||
|
||||
def __monitor_job(self):
|
||||
while self.monitoring_autoconnects and RNS.Transport._should_run:
|
||||
time.sleep(self.monitor_interval)
|
||||
detached_interfaces = []
|
||||
online_interfaces = 0
|
||||
autoconnected_interfaces = self.autoconnect_count()
|
||||
for interface in self.monitored_interfaces:
|
||||
try:
|
||||
if interface.online:
|
||||
online_interfaces += 1
|
||||
if hasattr(interface, "autoconnect_down") and interface.autoconnect_down != None:
|
||||
RNS.log(f"Auto-discovered interface {interface} reconnected")
|
||||
interface.autoconnect_down = None
|
||||
|
||||
else:
|
||||
if not hasattr(interface, "autoconnect_down") or interface.autoconnect_down == None:
|
||||
RNS.log(f"Auto-discovered interface {interface} disconnected", RNS.LOG_DEBUG)
|
||||
interface.autoconnect_down = time.time()
|
||||
|
||||
else:
|
||||
down_for = time.time()-interface.autoconnect_down
|
||||
if down_for >= self.detach_threshold:
|
||||
RNS.log(f"Auto-discovered interface {interface} has been down for {RNS.prettytime(down_for)}, detaching", RNS.LOG_DEBUG)
|
||||
detached_interfaces.append(interface)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while checking auto-connected interface state for {interface}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
max_autoconnected_interfaces = RNS.Reticulum.max_autoconnected_interfaces()
|
||||
free_slots = max(0, max_autoconnected_interfaces - autoconnected_interfaces)
|
||||
reserved_slots = max_autoconnected_interfaces//4
|
||||
|
||||
if online_interfaces >= max_autoconnected_interfaces:
|
||||
for interface in RNS.Transport.interfaces:
|
||||
if hasattr(interface, "bootstrap_only") and interface.bootstrap_only == True:
|
||||
RNS.log(f"Tearing down bootstrap-only {interface} since target connected auto-discovered interface count has been reached", RNS.LOG_INFO)
|
||||
if not interface in detached_interfaces: detached_interfaces.append(interface)
|
||||
|
||||
if online_interfaces == 0:
|
||||
if self.bootstrap_interface_count() == 0:
|
||||
RNS.log(f"No auto-discovered interfaces connected, re-enabling bootstrap interfaces", RNS.LOG_NOTICE)
|
||||
for config in RNS.Reticulum.get_instance().bootstrap_configs:
|
||||
RNS.Reticulum.get_instance()._synthesize_interface(config, config["name"])
|
||||
|
||||
if self.initial_autoconnect_ran and free_slots > reserved_slots:
|
||||
candidate_interfaces = self.list_discovered_interfaces(only_available=True, only_transport=True)
|
||||
if len(candidate_interfaces) > 0:
|
||||
random.shuffle(candidate_interfaces)
|
||||
selected_interface = candidate_interfaces[0]
|
||||
if not self.interface_exists(selected_interface): self.autoconnect(selected_interface)
|
||||
|
||||
for interface in detached_interfaces:
|
||||
try: self.teardown_interface(interface)
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while de-registering auto-connected interface from transport: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def teardown_interface(self, interface):
|
||||
interface.detach()
|
||||
RNS.Transport.remove_interface(interface)
|
||||
if interface in self.monitored_interfaces: self.monitored_interfaces.remove(interface)
|
||||
|
||||
def autoconnect_count(self):
|
||||
return len([i for i in RNS.Transport.interfaces if hasattr(i, "autoconnect_hash")])
|
||||
|
||||
def bootstrap_interface_count(self):
|
||||
return len([i for i in RNS.Transport.interfaces if hasattr(i, "bootstrap_only") and i.bootstrap_only == True])
|
||||
|
||||
def connect_discovered(self):
|
||||
if RNS.Reticulum.should_autoconnect_discovered_interfaces():
|
||||
try:
|
||||
discovered_interfaces = self.list_discovered_interfaces(only_transport=True)
|
||||
for info in discovered_interfaces:
|
||||
if self.autoconnect_count() >= RNS.Reticulum.max_autoconnected_interfaces(): break
|
||||
self.autoconnect(info)
|
||||
|
||||
self.initial_autoconnect_ran = True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while reconnecting discovered interfaces: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def endpoint_hash(self, info):
|
||||
endpoint_specifier = ""
|
||||
if "reachable_on" in info: endpoint_specifier += str(info["reachable_on"])
|
||||
if "port" in info: endpoint_specifier += ":"+str(info["port"])
|
||||
endpoint_hash = RNS.Identity.full_hash(endpoint_specifier.encode("utf-8"))
|
||||
return endpoint_hash
|
||||
|
||||
def interface_exists(self, info):
|
||||
exists = False
|
||||
for interface in RNS.Transport.interfaces:
|
||||
if hasattr(interface, "autoconnect_hash") and interface.autoconnect_hash == self.endpoint_hash(info):
|
||||
exists = True
|
||||
break
|
||||
|
||||
else:
|
||||
dest_match = "reachable_on" in info and hasattr(interface, "target_ip") and interface.target_ip == info["reachable_on"]
|
||||
port_match = not "port" in info or (hasattr(interface, "target_port") and "port" in info and interface.target_port == info["port"])
|
||||
b32d_match = "reachable_on" in info and hasattr(interface, "b32") and interface.b32 == info["reachable_on"]
|
||||
|
||||
if (dest_match and port_match) or b32d_match:
|
||||
exists = True
|
||||
break
|
||||
|
||||
return exists
|
||||
|
||||
def autoconnect(self, info):
|
||||
try:
|
||||
if RNS.Reticulum.should_autoconnect_discovered_interfaces():
|
||||
autoconnected_count = self.autoconnect_count()
|
||||
if autoconnected_count < RNS.Reticulum.max_autoconnected_interfaces():
|
||||
interface_type = info["type"]
|
||||
if interface_type in self.AUTOCONNECT_TYPES:
|
||||
endpoint_hash = self.endpoint_hash(info)
|
||||
exists = self.interface_exists(info)
|
||||
|
||||
if exists: RNS.log(f"Discovered {interface_type} already exists, not auto-connecting", RNS.LOG_DEBUG)
|
||||
else:
|
||||
if interface_type == "TCPClientInterface":
|
||||
RNS.log(f"Your operating system does not support the Backbone interface type, and must degrade to using TCPClientInterface instead", RNS.LOG_WARNING)
|
||||
RNS.log(f"Auto-connecting discovered TCPClient interfaces is not yet implemented, aborting auto-connect", RNS.LOG_WARNING)
|
||||
RNS.log(f"You can obtain the configuration entry and add this interface manually instead using rnstatus -D", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
if interface_type == "I2PInterface":
|
||||
RNS.log(f"Auto-connecting discovered I2P interfaces is not yet implemented, aborting auto-connect", RNS.LOG_WARNING)
|
||||
RNS.log(f"You can obtain the configuration entry and add this interface manually instead using rnstatus -D", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
if is_ygg_ipv6(info["reachable_on"]):
|
||||
# TODO: Somehow detect if yggdrasil is enabled on the system
|
||||
return
|
||||
|
||||
interface_name = info["name"]
|
||||
config_entry = info["config_entry"]
|
||||
interface_config = {}
|
||||
interface_config["name"] = f"{interface_name}"
|
||||
ifac_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
ifac_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
interface = None
|
||||
|
||||
if interface_type == "BackboneInterface":
|
||||
from RNS.Interfaces import BackboneInterface
|
||||
interface_config["target_host"] = info["reachable_on"]
|
||||
interface_config["target_port"] = info["port"]
|
||||
interface = BackboneInterface.BackboneClientInterface(RNS.Transport, interface_config)
|
||||
|
||||
if interface:
|
||||
RNS.log(f"Auto-connecting discovered {interface_type} {interface_name}")
|
||||
interface.autoconnect_hash = endpoint_hash
|
||||
interface.autoconnect_source = info["network_id"]
|
||||
mode = RNS.Interfaces.Interface.Interface.MODE_GATEWAY if RNS.Reticulum.transport_enabled() else None
|
||||
ar_target = RNS.Reticulum.get_instance()._default_ar_target() if RNS.Reticulum.transport_enabled() else None
|
||||
ar_penalty = RNS.Reticulum.get_instance()._default_ar_penalty() if RNS.Reticulum.transport_enabled() else None
|
||||
ar_grace = RNS.Reticulum.get_instance()._default_ar_grace() if RNS.Reticulum.transport_enabled() else None
|
||||
RNS.Reticulum.get_instance()._add_interface(interface, mode=mode, ifac_netname=ifac_netname, ifac_netkey=ifac_netkey, configured_bitrate=5E6,
|
||||
announce_rate_target=ar_target, announce_rate_grace=ar_grace, announce_rate_penalty=ar_penalty)
|
||||
self.monitor_interface(interface)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while auto-connecting discovered interface: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
class BlackholeUpdater():
|
||||
INITIAL_WAIT = 20
|
||||
JOB_INTERVAL = 60
|
||||
UPDATE_INTERVAL = 1*60*60
|
||||
SOURCE_TIMEOUT = 25
|
||||
|
||||
def __init__(self):
|
||||
self.last_updates = {}
|
||||
self.should_run = False
|
||||
self.job_interval = self.JOB_INTERVAL
|
||||
self.update_lock = threading.Lock()
|
||||
|
||||
def start(self):
|
||||
if not self.should_run:
|
||||
source_count = len(RNS.Reticulum.blackhole_sources())
|
||||
ms = "" if source_count == 1 else "s"
|
||||
RNS.log(f"Starting blackhole updater with {source_count} source{ms}", RNS.LOG_DEBUG)
|
||||
self.should_run = True
|
||||
threading.Thread(target=self.job, daemon=True).start()
|
||||
|
||||
def stop(self): self.should_run = False
|
||||
|
||||
def update_link_established(self, link):
|
||||
remote_identity = link.get_remote_identity()
|
||||
RNS.log(f"Link established for blackhole list update from {RNS.prettyhexrep(remote_identity.hash)}", RNS.LOG_DEBUG)
|
||||
receipt = link.request("/list")
|
||||
while not receipt.concluded(): time.sleep(0.2)
|
||||
response = receipt.get_response()
|
||||
link.teardown()
|
||||
|
||||
if type(response) == dict: blackhole_list = response
|
||||
else: blackhole_list = None
|
||||
|
||||
if blackhole_list:
|
||||
added = 0
|
||||
for identity_hash in blackhole_list:
|
||||
entry = blackhole_list[identity_hash]
|
||||
if not identity_hash in RNS.Transport.blackholed_identities:
|
||||
RNS.Transport.blackholed_identities[identity_hash] = entry
|
||||
added += 1
|
||||
|
||||
if added > 0:
|
||||
spec = "identity" if added == 1 else "identities"
|
||||
RNS.log(f"Added {added} blackholed {spec} from {RNS.prettyhexrep(remote_identity.hash)}", RNS.LOG_DEBUG)
|
||||
|
||||
try:
|
||||
sourcelistpath = os.path.join(RNS.Reticulum.blackholepath, RNS.hexrep(remote_identity.hash, delimit=False))
|
||||
tmppath = f"{sourcelistpath}.tmp"
|
||||
with open(tmppath, "wb") as f: f.write(msgpack.packb(blackhole_list))
|
||||
if os.path.isfile(sourcelistpath): os.unlink(sourcelistpath)
|
||||
os.rename(tmppath, sourcelistpath)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while persisting blackhole list from {RNS.prettyhexrep(remote_identity.hash)}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
RNS.log(f"Blackhole list update from {RNS.prettyhexrep(remote_identity.hash)} completed", RNS.LOG_DEBUG)
|
||||
|
||||
def job(self):
|
||||
time.sleep(self.INITIAL_WAIT)
|
||||
while self.should_run:
|
||||
try:
|
||||
now = time.time()
|
||||
for identity_hash in RNS.Reticulum.blackhole_sources():
|
||||
if identity_hash in self.last_updates: last_update = self.last_updates[identity_hash]
|
||||
else: last_update = 0
|
||||
|
||||
if now > last_update+RNS.Reticulum.blackhole_update_interval():
|
||||
try:
|
||||
destination_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.info.blackhole", identity_hash)
|
||||
RNS.log(f"Attempting blackhole list update from {RNS.prettyhexrep(identity_hash)}...", RNS.LOG_DEBUG)
|
||||
if not RNS.Transport.await_path(destination_hash): RNS.log(f"No path available for blackhole list update from {RNS.prettyhexrep(identity_hash)}, retrying later", RNS.LOG_VERBOSE)
|
||||
else:
|
||||
remote_identity = RNS.Identity.recall(destination_hash)
|
||||
destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "info", "blackhole")
|
||||
RNS.Link(destination, established_callback=self.update_link_established)
|
||||
self.last_updates[identity_hash] = time.time()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while establishing link for blackhole list update from {RNS.prettyhexrep(identity_hash)}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error in blackhole list updater job: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
time.sleep(self.job_interval)
|
||||
|
||||
def is_ip_address(address_string):
|
||||
try:
|
||||
ipaddress.ip_address(address_string)
|
||||
return True
|
||||
except: return False
|
||||
|
||||
def is_ygg_ipv6(address_string):
|
||||
try: return ipaddress.ip_address(address_string) in ipaddress.IPv6Network("200::/7")
|
||||
except: return False
|
||||
|
||||
def is_hostname(hostname):
|
||||
if hostname[-1] == ".": hostname = hostname[:-1]
|
||||
if len(hostname) > 253: return False
|
||||
components = hostname.split(".")
|
||||
if re.match(r"[0-9]+$", components[-1]): return False
|
||||
allowed = re.compile(r"(?!-)[a-z0-9-]{1,63}(?<!-)$", re.IGNORECASE)
|
||||
return all(allowed.match(label) for label in components)
|
||||
|
||||
san_map = ""
|
||||
for i in range(48, 58): san_map += bytes([i]).decode("ascii")
|
||||
for i in range(65, 91): san_map += bytes([i]).decode("ascii")
|
||||
for i in range(97, 123): san_map += bytes([i]).decode("ascii")
|
||||
+212
-55
@@ -76,6 +76,7 @@ class Identity:
|
||||
# Non-configurable constants
|
||||
TOKEN_OVERHEAD = RNS.Cryptography.Token.TOKEN_OVERHEAD
|
||||
AES128_BLOCKSIZE = 16 # In bytes
|
||||
AES256_BLOCKSIZE = 16 # In bytes
|
||||
HASHLENGTH = 256 # In bits
|
||||
SIGLENGTH = KEYSIZE # In bits
|
||||
|
||||
@@ -94,17 +95,25 @@ class Identity:
|
||||
known_ratchets = {}
|
||||
|
||||
ratchet_persist_lock = threading.Lock()
|
||||
known_destinations_lock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def remember(packet_hash, destination_hash, public_key, app_data = None):
|
||||
if len(public_key) != Identity.KEYSIZE//8:
|
||||
raise TypeError("Can't remember "+RNS.prettyhexrep(destination_hash)+", the public key size of "+str(len(public_key))+" is not valid.", RNS.LOG_ERROR)
|
||||
else:
|
||||
Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data]
|
||||
|
||||
with Identity.known_destinations_lock:
|
||||
if not destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data, 0]
|
||||
else:
|
||||
entry = Identity.known_destinations[destination_hash]
|
||||
entry[0] = time.time()
|
||||
entry[1] = packet_hash
|
||||
entry[2] = public_key
|
||||
entry[3] = app_data
|
||||
|
||||
@staticmethod
|
||||
def recall(target_hash, from_identity_hash=False):
|
||||
def recall(target_hash, from_identity_hash=False, _no_use=False):
|
||||
"""
|
||||
Recall identity for a destination or identity hash. By default, this function
|
||||
will return the identity associated with a given *destination* hash. As an
|
||||
@@ -118,18 +127,22 @@ class Identity:
|
||||
:returns: An :ref:`RNS.Identity<api-identity>` instance that can be used to create an outgoing :ref:`RNS.Destination<api-destination>`, or *None* if the destination is unknown.
|
||||
"""
|
||||
if from_identity_hash:
|
||||
for destination_hash in Identity.known_destinations:
|
||||
if target_hash == Identity.truncated_hash(Identity.known_destinations[destination_hash][2]):
|
||||
identity_data = Identity.known_destinations[destination_hash]
|
||||
with Identity.known_destinations_lock: destination_hashes = list(Identity.known_destinations.keys())
|
||||
for destination_hash in destination_hashes:
|
||||
entry = Identity.known_destinations.get(destination_hash)
|
||||
if not entry: continue
|
||||
if target_hash == Identity.truncated_hash(entry[2]):
|
||||
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(destination_hash)
|
||||
identity = Identity(create_keys=False)
|
||||
identity.load_public_key(identity_data[2])
|
||||
identity.app_data = identity_data[3]
|
||||
identity.load_public_key(entry[2])
|
||||
identity.app_data = entry[3]
|
||||
return identity
|
||||
|
||||
return None
|
||||
|
||||
else:
|
||||
if target_hash in Identity.known_destinations:
|
||||
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(target_hash)
|
||||
identity_data = Identity.known_destinations[target_hash]
|
||||
identity = Identity(create_keys=False)
|
||||
identity.load_public_key(identity_data[2])
|
||||
@@ -146,7 +159,7 @@ class Identity:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def recall_app_data(destination_hash):
|
||||
def recall_app_data(destination_hash, _no_use=False):
|
||||
"""
|
||||
Recall last heard app_data for a destination hash.
|
||||
|
||||
@@ -154,13 +167,14 @@ class Identity:
|
||||
:returns: *Bytes* containing app_data, or *None* if the destination is unknown.
|
||||
"""
|
||||
if destination_hash in Identity.known_destinations:
|
||||
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(destination_hash)
|
||||
app_data = Identity.known_destinations[destination_hash][3]
|
||||
return app_data
|
||||
else:
|
||||
return None
|
||||
|
||||
else: return None
|
||||
|
||||
@staticmethod
|
||||
def save_known_destinations():
|
||||
def save_known_destinations(background=False, recombine=True):
|
||||
# TODO: Improve the storage method so we don't have to
|
||||
# deserialize and serialize the entire table on every
|
||||
# save, but the only changes. It might be possible to
|
||||
@@ -181,34 +195,42 @@ class Identity:
|
||||
Identity.saving_known_destinations = True
|
||||
save_start = time.time()
|
||||
|
||||
storage_known_destinations = {}
|
||||
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
||||
if recombine:
|
||||
storage_known_destinations = {}
|
||||
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
||||
try:
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
|
||||
storage_known_destinations = umsgpack.load(file)
|
||||
|
||||
except: pass
|
||||
|
||||
try:
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
|
||||
storage_known_destinations = umsgpack.load(file)
|
||||
|
||||
except:
|
||||
pass
|
||||
for destination_hash in storage_known_destinations:
|
||||
if not destination_hash in Identity.known_destinations:
|
||||
with Identity.known_destinations_lock:
|
||||
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Skipped recombining known destinations from disk, since an error occurred: "+str(e), RNS.LOG_WARNING)
|
||||
|
||||
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_VERBOSE)
|
||||
temp_file = RNS.Reticulum.storagepath+f"/known_destinations.tmp.{time.time()}"
|
||||
|
||||
try:
|
||||
for destination_hash in storage_known_destinations:
|
||||
if not destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
|
||||
except Exception as e:
|
||||
RNS.log("Skipped recombining known destinations from disk, since an error occurred: "+str(e), RNS.LOG_WARNING)
|
||||
with open(temp_file,"wb") as file: umsgpack.dump(Identity.known_destinations.copy(), file)
|
||||
os.rename(temp_file, RNS.Reticulum.storagepath+f"/known_destinations")
|
||||
|
||||
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_DEBUG)
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","wb") as file:
|
||||
umsgpack.dump(Identity.known_destinations, file)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while serializing and writing known destinations: {e}", RNS.LOG_ERROR)
|
||||
try: os.unlink(temp_file)
|
||||
except Exception as e: RNS.log(f"Could not clean up temporary file {temp_file}: {e}", RNS.LOG_WARNING)
|
||||
raise e
|
||||
|
||||
save_time = time.time() - save_start
|
||||
if save_time < 1:
|
||||
time_str = str(round(save_time*1000,2))+"ms"
|
||||
else:
|
||||
time_str = str(round(save_time,2))+"s"
|
||||
if save_time < 1: time_str = str(round(save_time*1000,2))+"ms"
|
||||
else: time_str = str(round(save_time,2))+"s"
|
||||
|
||||
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_DEBUG)
|
||||
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_VERBOSE)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while saving known destinations to disk, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -219,6 +241,7 @@ class Identity:
|
||||
@staticmethod
|
||||
def load_known_destinations():
|
||||
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
||||
st = time.time()
|
||||
try:
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
|
||||
loaded_known_destinations = umsgpack.load(file)
|
||||
@@ -226,15 +249,119 @@ class Identity:
|
||||
Identity.known_destinations = {}
|
||||
for known_destination in loaded_known_destinations:
|
||||
if len(known_destination) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
|
||||
Identity.known_destinations[known_destination] = loaded_known_destinations[known_destination]
|
||||
if len(loaded_known_destinations[known_destination]) < 5:
|
||||
e = loaded_known_destinations[known_destination]
|
||||
loaded_known_destinations[known_destination] = [e[0], e[1], e[2], e[3], 0]
|
||||
|
||||
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE)
|
||||
with Identity.known_destinations_lock:
|
||||
Identity.known_destinations[known_destination] = loaded_known_destinations[known_destination]
|
||||
|
||||
RNS.log(f"Loaded {len(Identity.known_destinations)} known destination from storage in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_VERBOSE)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
else:
|
||||
RNS.log("Destinations file does not exist, no known destinations loaded", RNS.LOG_VERBOSE)
|
||||
|
||||
@staticmethod
|
||||
def _used_destination_data(destination_hash):
|
||||
with Identity.known_destinations_lock:
|
||||
if destination_hash in Identity.known_destinations:
|
||||
if not Identity.known_destinations[destination_hash][4] < 0:
|
||||
Identity.known_destinations[destination_hash][4] = time.time()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _retain_destination_data(destination_hash):
|
||||
with Identity.known_destinations_lock:
|
||||
if destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations[destination_hash][4] = -1
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _unretain_destination_data(destination_hash):
|
||||
with Identity.known_destinations_lock:
|
||||
if destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations[destination_hash][4] = time.time()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _retain_identity(identity_hash):
|
||||
try:
|
||||
retained = False
|
||||
with Identity.known_destinations_lock: destination_hashes = list(Identity.known_destinations.keys())
|
||||
for destination_hash in destination_hashes:
|
||||
entry = Identity.known_destinations.get(destination_hash)
|
||||
if not entry: continue
|
||||
if identity_hash == Identity.truncated_hash(entry[2]):
|
||||
if Identity._retain_destination_data(destination_hash): retained = True
|
||||
|
||||
return retained
|
||||
|
||||
except Exception as e: RNS.log(f"Error while retaining identity {RNS.prettyhexrep(identity_hash)}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
@staticmethod
|
||||
def clean_known_destinations():
|
||||
now = time.time()
|
||||
st = now
|
||||
total = len(Identity.known_destinations)
|
||||
stale = []
|
||||
no_path = 0
|
||||
retained = 0
|
||||
never_used = 0
|
||||
|
||||
with Identity.known_destinations_lock: destination_hashes = list(Identity.known_destinations.keys())
|
||||
for destination_hash in destination_hashes:
|
||||
try:
|
||||
if RNS.Transport.has_path(destination_hash): has_path = True
|
||||
else:
|
||||
has_path = False
|
||||
no_path += 1
|
||||
|
||||
with Identity.known_destinations_lock:
|
||||
if destination_hash in Identity.known_destinations:
|
||||
last_announce = Identity.known_destinations[destination_hash][0]
|
||||
last_use = 0
|
||||
was_used = False
|
||||
is_retained = False
|
||||
|
||||
if Identity.known_destinations[destination_hash][4] > 0:
|
||||
was_used = True
|
||||
last_use = Identity.known_destinations[destination_hash][4]
|
||||
|
||||
elif Identity.known_destinations[destination_hash][4] == 0:
|
||||
was_used = False
|
||||
never_used += 1
|
||||
|
||||
elif Identity.known_destinations[destination_hash][4] == -1:
|
||||
is_retained = True
|
||||
retained += 1
|
||||
|
||||
unused_for = time.time() - Identity.known_destinations[destination_hash][4]
|
||||
|
||||
if not is_retained and not has_path:
|
||||
if not was_used and now - last_announce > RNS.Transport.UNUSED_DESTINATION_LINGER: stale.append(destination_hash)
|
||||
elif unused_for > RNS.Transport.DESTINATION_TIMEOUT*1.25: stale.append(destination_hash)
|
||||
|
||||
except Exception as e: RNS.log(f"Faulty entry for {RNS.prettyhexrep(destination_hash)} while cleaning known destinations: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
removed = 0
|
||||
for destination_hash in stale:
|
||||
with Identity.known_destinations_lock:
|
||||
if destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations.pop(destination_hash)
|
||||
removed += 1
|
||||
|
||||
# RNS.log(f"Total destinations: {total}, stale: {len(stale)}, removed: {removed}, no path: {no_path}, never used: {never_used}, with path: {total-no_path}, used: {total-never_used}, retained: {retained}. Completed in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_WARNING) # TODO: Remove
|
||||
if not RNS.Transport.owner.is_connected_to_shared_instance: Identity.save_known_destinations(recombine=False)
|
||||
|
||||
@staticmethod
|
||||
def full_hash(data):
|
||||
"""
|
||||
@@ -302,7 +429,7 @@ class Identity:
|
||||
ratchet_exists = False
|
||||
|
||||
if not ratchet_exists:
|
||||
RNS.log(f"Remembering ratchet {RNS.prettyhexrep(Identity._get_ratchet_id(ratchet))} for {RNS.prettyhexrep(destination_hash)}", RNS.LOG_EXTREME)
|
||||
RNS.log(f"Remembering ratchet {RNS.prettyhexrep(Identity._get_ratchet_id(ratchet))} for {RNS.prettyhexrep(destination_hash)}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
Identity.known_ratchets[destination_hash] = ratchet
|
||||
if not RNS.Transport.owner.is_connected_to_shared_instance:
|
||||
def persist_job():
|
||||
@@ -331,35 +458,42 @@ class Identity:
|
||||
|
||||
@staticmethod
|
||||
def _clean_ratchets():
|
||||
RNS.log("Cleaning ratchets...", RNS.LOG_DEBUG)
|
||||
RNS.log("Cleaning ratchets...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
try:
|
||||
count = 0
|
||||
removed = 0
|
||||
not_known = 0
|
||||
now = time.time()
|
||||
ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
|
||||
if os.path.isdir(ratchetdir):
|
||||
for filename in os.listdir(ratchetdir):
|
||||
count += 1
|
||||
try:
|
||||
expired = False
|
||||
corrupted = False
|
||||
with open(f"{ratchetdir}/{filename}", "rb") as rf:
|
||||
# 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 now > ratchet_data["received"]+Identity.RATCHET_EXPIRY: expired = True
|
||||
|
||||
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:
|
||||
destination_hash = bytes.fromhex(filename)
|
||||
if not destination_hash in RNS.Identity.known_destinations: unknown = True; not_known += 1
|
||||
else: unknown = False
|
||||
|
||||
if expired or corrupted or unknown:
|
||||
os.unlink(f"{ratchetdir}/{filename}")
|
||||
removed += 1
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while cleaning ratchets, in the processing of {ratchetdir}/{filename}.", RNS.LOG_ERROR)
|
||||
RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while cleaning ratchets. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log(f"An error occurred while cleaning ratchets. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"Processed {count} ratchets in {RNS.prettytime(time.time()-now)}, not in use {not_known}, removed {removed}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
@staticmethod
|
||||
def get_ratchet(destination_hash):
|
||||
@@ -384,7 +518,7 @@ class Identity:
|
||||
if destination_hash in Identity.known_ratchets:
|
||||
return Identity.known_ratchets[destination_hash]
|
||||
else:
|
||||
RNS.log(f"Could not load ratchet for {RNS.prettyhexrep(destination_hash)}", RNS.LOG_DEBUG)
|
||||
RNS.log(f"Could not load ratchet for {RNS.prettyhexrep(destination_hash)}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@@ -430,6 +564,11 @@ class Identity:
|
||||
announced_identity = Identity(create_keys=False)
|
||||
announced_identity.load_public_key(public_key)
|
||||
|
||||
if len(RNS.Transport.blackholed_identities) > 0:
|
||||
if announced_identity.hash in RNS.Transport.blackholed_identities:
|
||||
RNS.log(f"Invalidated and dropped announce from blackholed identity {RNS.prettyhexrep(announced_identity.hash)}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
return False
|
||||
|
||||
if announced_identity.pub != None and announced_identity.validate(signature, signed_data):
|
||||
if only_validate_signature:
|
||||
del announced_identity
|
||||
@@ -465,9 +604,9 @@ class Identity:
|
||||
signal_str = ""
|
||||
|
||||
if hasattr(packet, "transport_id") and packet.transport_id != None:
|
||||
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received via "+RNS.prettyhexrep(packet.transport_id)+" on "+str(packet.receiving_interface)+signal_str, RNS.LOG_EXTREME)
|
||||
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received via "+RNS.prettyhexrep(packet.transport_id)+" on "+str(packet.receiving_interface)+signal_str, RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
else:
|
||||
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received on "+str(packet.receiving_interface)+signal_str, RNS.LOG_EXTREME)
|
||||
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received on "+str(packet.receiving_interface)+signal_str, RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
|
||||
if ratchet:
|
||||
Identity._remember_ratchet(destination_hash, ratchet)
|
||||
@@ -475,11 +614,11 @@ class Identity:
|
||||
return True
|
||||
|
||||
else:
|
||||
RNS.log("Received invalid announce for "+RNS.prettyhexrep(destination_hash)+": Destination mismatch.", RNS.LOG_DEBUG)
|
||||
RNS.log("Received invalid announce for "+RNS.prettyhexrep(destination_hash)+": Destination mismatch.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return False
|
||||
|
||||
else:
|
||||
RNS.log("Received invalid announce for "+RNS.prettyhexrep(destination_hash)+": Invalid signature.", RNS.LOG_DEBUG)
|
||||
RNS.log("Received invalid announce for "+RNS.prettyhexrep(destination_hash)+": Invalid signature.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
del announced_identity
|
||||
return False
|
||||
|
||||
@@ -488,9 +627,9 @@ class Identity:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def persist_data():
|
||||
def persist_data(background=False):
|
||||
if not RNS.Transport.owner.is_connected_to_shared_instance:
|
||||
Identity.save_known_destinations()
|
||||
Identity.save_known_destinations(background=background)
|
||||
|
||||
@staticmethod
|
||||
def exit_handler():
|
||||
@@ -546,6 +685,22 @@ class Identity:
|
||||
RNS.log("Error while saving identity to "+str(path), RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e))
|
||||
|
||||
def pub_to_file(self, path):
|
||||
"""
|
||||
Saves the public identity to a file.
|
||||
|
||||
:param path: The full path specifying where to save the identity.
|
||||
:returns: True if the file was saved, otherwise False.
|
||||
"""
|
||||
try:
|
||||
with open(path, "wb") as key_file:
|
||||
key_file.write(self.get_public_key())
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
RNS.log("Error while saving identity to "+str(path), RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e))
|
||||
|
||||
def __init__(self,create_keys=True):
|
||||
# Initialize keys to none
|
||||
self.prv = None
|
||||
@@ -585,13 +740,15 @@ class Identity:
|
||||
"""
|
||||
:returns: The private key as *bytes*
|
||||
"""
|
||||
return self.prv_bytes+self.sig_prv_bytes
|
||||
if self.prv_bytes and self.sig_prv_bytes: return self.prv_bytes+self.sig_prv_bytes
|
||||
else: return None
|
||||
|
||||
def get_public_key(self):
|
||||
"""
|
||||
:returns: The public key as *bytes*
|
||||
"""
|
||||
return self.pub_bytes+self.sig_pub_bytes
|
||||
if self.pub_bytes and self.sig_pub_bytes: return self.pub_bytes+self.sig_pub_bytes
|
||||
else: return None
|
||||
|
||||
def load_private_key(self, prv_bytes):
|
||||
"""
|
||||
@@ -738,7 +895,7 @@ class Identity:
|
||||
pass
|
||||
|
||||
if enforce_ratchets and plaintext == None:
|
||||
RNS.log("Decryption with ratchet enforcement by "+RNS.prettyhexrep(self.hash)+" failed. Dropping packet.", RNS.LOG_DEBUG)
|
||||
RNS.log("Decryption with ratchet enforcement by "+RNS.prettyhexrep(self.hash)+" failed. Dropping packet.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
if ratchet_id_receiver:
|
||||
ratchet_id_receiver.latest_ratchet_id = None
|
||||
return None
|
||||
@@ -751,14 +908,14 @@ class Identity:
|
||||
ratchet_id_receiver.latest_ratchet_id = None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed: "+str(e), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
if ratchet_id_receiver:
|
||||
ratchet_id_receiver.latest_ratchet_id = None
|
||||
|
||||
return plaintext
|
||||
|
||||
else:
|
||||
RNS.log("Decryption failed because the token size was invalid.", RNS.LOG_DEBUG)
|
||||
RNS.log("Decryption failed because the token size was invalid.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return None
|
||||
else:
|
||||
raise KeyError("Decryption failed because identity does not hold a private key")
|
||||
|
||||
@@ -65,7 +65,7 @@ class AutoInterface(Interface):
|
||||
|
||||
ALL_IGNORE_IFS = ["lo0"]
|
||||
DARWIN_IGNORE_IFS = ["awdl0", "llw0", "lo0", "en5"]
|
||||
ANDROID_IGNORE_IFS = ["dummy0", "lo", "tun0"]
|
||||
ANDROID_IGNORE_IFS = ["dummy0", "lo", "tun0", "rmnet0", "rmnet1", "rmnet2", "rmnet3", "rmnet4", "rmnet5", "rmnet6", "rmnet7"]
|
||||
|
||||
BITRATE_GUESS = 10*1000*1000
|
||||
|
||||
@@ -138,11 +138,12 @@ class AutoInterface(Interface):
|
||||
|
||||
self.outbound_udp_socket = None
|
||||
|
||||
self.announce_rate_target = None
|
||||
self.announce_interval = AutoInterface.ANNOUNCE_INTERVAL
|
||||
self.peer_job_interval = AutoInterface.PEER_JOB_INTERVAL
|
||||
self.peering_timeout = AutoInterface.PEERING_TIMEOUT
|
||||
self.multicast_echo_timeout = AutoInterface.MCAST_ECHO_TIMEOUT
|
||||
self.announce_rate_target = None
|
||||
self.announce_interval = AutoInterface.ANNOUNCE_INTERVAL
|
||||
self.peer_job_interval = AutoInterface.PEER_JOB_INTERVAL
|
||||
self.peering_timeout = AutoInterface.PEERING_TIMEOUT
|
||||
self.multicast_echo_timeout = AutoInterface.MCAST_ECHO_TIMEOUT
|
||||
self.reverse_peering_interval = self.announce_interval*3.25
|
||||
|
||||
# Increase peering timeout on Android, due to potential
|
||||
# low-power modes implemented on many chipsets.
|
||||
@@ -169,6 +170,8 @@ class AutoInterface(Interface):
|
||||
else:
|
||||
self.discovery_port = discovery_port
|
||||
|
||||
self.unicast_discovery_port = self.discovery_port+1
|
||||
|
||||
if multicast_address_type == None:
|
||||
self.multicast_address_type = AutoInterface.MULTICAST_TEMPORARY_ADDRESS_TYPE
|
||||
elif str(multicast_address_type).lower() == "temporary":
|
||||
@@ -244,33 +247,48 @@ class AutoInterface(Interface):
|
||||
if link_local_addr == None:
|
||||
RNS.log(str(self)+" No link-local IPv6 address configured for "+str(ifname)+", skipping interface", RNS.LOG_EXTREME)
|
||||
else:
|
||||
mcast_addr = self.mcast_discovery_address
|
||||
RNS.log(str(self)+" Creating multicast discovery listener on "+str(ifname)+" with address "+str(mcast_addr), RNS.LOG_EXTREME)
|
||||
RNS.log(str(self)+" Creating unicast discovery listener on "+str(ifname)+" with address "+str(link_local_addr), RNS.LOG_EXTREME)
|
||||
|
||||
# Struct with interface index
|
||||
if_struct = struct.pack("I", self.interface_name_to_index(ifname))
|
||||
|
||||
# Set up multicast socket
|
||||
# Set up unicast discovery socket
|
||||
unicast_discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
unicast_discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
if hasattr(socket, "SO_REUSEPORT"): unicast_discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
|
||||
# Bind unicast discovery socket
|
||||
if RNS.vendor.platformutils.is_windows():
|
||||
# Windows throws "[WinError 10049] The requested address is not valid in its context"
|
||||
# when trying to use the multicast address as host, or when providing interface index
|
||||
# passing an empty host appears to work, but probably not exactly how we want it to...
|
||||
unicast_discovery_socket.bind(('', self.unicast_discovery_port))
|
||||
|
||||
else:
|
||||
addr_info = socket.getaddrinfo(link_local_addr+"%"+ifname, self.unicast_discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
unicast_discovery_socket.bind(addr_info[0][4])
|
||||
|
||||
mcast_addr = self.mcast_discovery_address
|
||||
RNS.log(str(self)+" Creating multicast discovery listener on "+str(ifname)+" with address "+str(mcast_addr), RNS.LOG_EXTREME)
|
||||
|
||||
# Set up multicast discovery socket
|
||||
discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
if hasattr(socket, "SO_REUSEPORT"):
|
||||
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
if hasattr(socket, "SO_REUSEPORT"): discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, if_struct)
|
||||
|
||||
# Join multicast group
|
||||
mcast_group = socket.inet_pton(socket.AF_INET6, mcast_addr) + if_struct
|
||||
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mcast_group)
|
||||
|
||||
# Bind socket
|
||||
# Bind multicast socket
|
||||
if RNS.vendor.platformutils.is_windows():
|
||||
|
||||
# window throws "[WinError 10049] The requested address is not valid in its context"
|
||||
# Windows throws "[WinError 10049] The requested address is not valid in its context"
|
||||
# when trying to use the multicast address as host, or when providing interface index
|
||||
# passing an empty host appears to work, but probably not exactly how we want it to...
|
||||
discovery_socket.bind(('', self.discovery_port))
|
||||
|
||||
else:
|
||||
|
||||
if self.discovery_scope == AutoInterface.SCOPE_LINK:
|
||||
addr_info = socket.getaddrinfo(mcast_addr+"%"+ifname, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
else:
|
||||
@@ -278,12 +296,13 @@ class AutoInterface(Interface):
|
||||
|
||||
discovery_socket.bind(addr_info[0][4])
|
||||
|
||||
# Set up thread for discovery packets
|
||||
# Set up thread for multicast discovery packets
|
||||
def discovery_loop(): self.discovery_handler(discovery_socket, ifname)
|
||||
|
||||
thread = threading.Thread(target=discovery_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
thread = threading.Thread(target=discovery_loop, daemon=True).start()
|
||||
|
||||
# Set up thread for unicast discovery packets
|
||||
def unicast_discovery_loop(): self.discovery_handler(unicast_discovery_socket, ifname, announce=False)
|
||||
thread = threading.Thread(target=unicast_discovery_loop, daemon=True).start()
|
||||
|
||||
suitable_interfaces += 1
|
||||
|
||||
@@ -331,13 +350,13 @@ class AutoInterface(Interface):
|
||||
self.online = True
|
||||
self.final_init_done = True
|
||||
|
||||
def discovery_handler(self, socket, ifname):
|
||||
def announce_loop():
|
||||
self.announce_handler(ifname)
|
||||
|
||||
thread = threading.Thread(target=announce_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
def discovery_handler(self, socket, ifname, announce=True):
|
||||
def announce_loop(): self.announce_handler(ifname)
|
||||
|
||||
if announce:
|
||||
thread = threading.Thread(target=announce_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
while True:
|
||||
data, ipv6_src = socket.recvfrom(1024)
|
||||
@@ -371,6 +390,18 @@ class AutoInterface(Interface):
|
||||
spawned_interface.teardown()
|
||||
RNS.log(str(self)+" removed peer "+str(peer_addr)+" on "+str(removed_peer[0]), RNS.LOG_DEBUG)
|
||||
|
||||
# Send reverse peering packets
|
||||
for peer_addr in self.peers:
|
||||
try:
|
||||
peer = self.peers[peer_addr]
|
||||
ifname = peer[0]
|
||||
last_outbound = peer[2]
|
||||
if now > last_outbound+self.reverse_peering_interval:
|
||||
self.reverse_announce(ifname, peer_addr)
|
||||
peer[2] = time.time()
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while sending reverse peering packet to {peer_addr}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
for ifname in self.adopted_interfaces:
|
||||
# Check that the link-local address has not changed
|
||||
try:
|
||||
@@ -443,6 +474,20 @@ class AutoInterface(Interface):
|
||||
self.peer_announce(ifname)
|
||||
time.sleep(self.announce_interval)
|
||||
|
||||
def reverse_announce(self, ifname, peer_addr):
|
||||
try:
|
||||
link_local_address = self.adopted_interfaces[ifname]
|
||||
discovery_token = RNS.Identity.full_hash(self.group_id+link_local_address.encode("utf-8"))
|
||||
announce_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
addr_info = socket.getaddrinfo(f"{peer_addr}%{ifname}", self.unicast_discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
|
||||
ifis = struct.pack("I", self.interface_name_to_index(ifname))
|
||||
announce_socket.sendto(discovery_token, addr_info[0][4])
|
||||
announce_socket.close()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not send reverse peering packet to {peer_addr} on {ifname}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def peer_announce(self, ifname):
|
||||
try:
|
||||
link_local_address = self.adopted_interfaces[ifname]
|
||||
@@ -480,11 +525,26 @@ class AutoInterface(Interface):
|
||||
|
||||
else:
|
||||
if not addr in self.peers:
|
||||
self.peers[addr] = [ifname, time.time()]
|
||||
self.peers[addr] = [ifname, time.time(), time.time()]
|
||||
|
||||
spawned_interface = AutoInterfacePeer(self, addr, ifname)
|
||||
spawned_interface.OUT = self.OUT
|
||||
spawned_interface.IN = self.IN
|
||||
|
||||
spawned_interface.ingress_control = self.ingress_control
|
||||
spawned_interface.ic_max_held_announces = self.ic_max_held_announces
|
||||
spawned_interface.ic_burst_hold = self.ic_burst_hold
|
||||
spawned_interface.ic_burst_freq = self.ic_burst_freq
|
||||
spawned_interface.ic_burst_freq_new = self.ic_burst_freq_new
|
||||
spawned_interface.ic_new_time = self.ic_new_time
|
||||
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
|
||||
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
|
||||
|
||||
spawned_interface.egress_control = self.egress_control
|
||||
spawned_interface.ec_pr_freq = self.ec_pr_freq
|
||||
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
|
||||
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
|
||||
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
|
||||
@@ -514,11 +574,11 @@ class AutoInterface(Interface):
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
if addr in self.spawned_interfaces:
|
||||
self.spawned_interfaces[addr].detach()
|
||||
self.spawned_interfaces[addr].teardown()
|
||||
self.spawned_interfaces.pop(spawned_interface)
|
||||
if addr in self.spawned_interfaces: self.spawned_interfaces.pop(addr)
|
||||
self.spawned_interfaces[addr] = spawned_interface
|
||||
|
||||
RNS.log(str(self)+" added peer "+str(addr)+" on "+str(ifname), RNS.LOG_DEBUG)
|
||||
@@ -526,28 +586,18 @@ class AutoInterface(Interface):
|
||||
self.refresh_peer(addr)
|
||||
|
||||
def refresh_peer(self, addr):
|
||||
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)
|
||||
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, addr=None):
|
||||
if self.online and addr in self.spawned_interfaces:
|
||||
self.spawned_interfaces[addr].process_incoming(data, addr)
|
||||
|
||||
def process_outgoing(self,data):
|
||||
pass
|
||||
def process_outgoing(self, data): pass
|
||||
|
||||
# Until per-device sub-interfacing is implemented,
|
||||
# ingress limiting should be disabled on AutoInterface
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
def detach(self): self.online = False
|
||||
|
||||
def detach(self):
|
||||
self.online = False
|
||||
|
||||
def __str__(self):
|
||||
return "AutoInterface["+self.name+"]"
|
||||
def __str__(self): return f"AutoInterface[{self.name}]"
|
||||
|
||||
class AutoInterfacePeer(Interface):
|
||||
|
||||
@@ -602,12 +652,10 @@ class AutoInterfacePeer(Interface):
|
||||
|
||||
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()
|
||||
RNS.log(f"The interface {self} experienced an unrecoverable error and is being torn down.", RNS.LOG_ERROR)
|
||||
if RNS.Reticulum.panic_on_interface_error: RNS.panic()
|
||||
|
||||
else:
|
||||
RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_VERBOSE)
|
||||
else: RNS.log(f"The interface {self} is being torn down.", RNS.LOG_VERBOSE)
|
||||
|
||||
self.online = False
|
||||
self.OUT = False
|
||||
@@ -618,13 +666,7 @@ class AutoInterfacePeer(Interface):
|
||||
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
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
class AutoInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
def __init__(self, callback, *args, **keys):
|
||||
|
||||
@@ -127,6 +127,7 @@ class BackboneInterface(Interface):
|
||||
self.detached = False
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
self.spawned_interfaces = []
|
||||
self.supports_discovery = True
|
||||
|
||||
if bindport == None:
|
||||
raise SystemError(f"No TCP port configured for interface \"{name}\"")
|
||||
@@ -155,6 +156,58 @@ class BackboneInterface(Interface):
|
||||
else:
|
||||
raise SystemError("Insufficient parameters to create listener")
|
||||
|
||||
|
||||
__last_ic_burst_check = 0
|
||||
__last_ic_burst_state = False
|
||||
@property
|
||||
def ic_burst_active(self):
|
||||
if time.time() > self.__last_ic_burst_check + 2:
|
||||
self.__last_ic_burst_state = any(i.ic_burst_active for i in self.spawned_interfaces)
|
||||
|
||||
return self.__last_ic_burst_state
|
||||
|
||||
@ic_burst_active.setter
|
||||
def ic_burst_active(self, value): pass
|
||||
|
||||
__ic_burst_activated_check = 0
|
||||
__ic_burst_activated = 0
|
||||
@property
|
||||
def ic_burst_activated(self):
|
||||
if time.time() > self.__ic_burst_activated_check + 2:
|
||||
activated = [i.ic_burst_activated for i in self.spawned_interfaces if i.ic_burst_active]
|
||||
if activated: self.__ic_burst_activated = min(activated)
|
||||
|
||||
return self.__ic_burst_activated
|
||||
|
||||
@ic_burst_activated.setter
|
||||
def ic_burst_activated(self, value): pass
|
||||
|
||||
|
||||
__last_ic_pr_burst_check = 0
|
||||
__last_ic_pr_burst_state = False
|
||||
@property
|
||||
def ic_pr_burst_active(self):
|
||||
if time.time() > self.__last_ic_pr_burst_check + 2:
|
||||
self.__last_ic_pr_burst_state = any(i.ic_pr_burst_active for i in self.spawned_interfaces)
|
||||
|
||||
return self.__last_ic_pr_burst_state
|
||||
|
||||
@ic_pr_burst_active.setter
|
||||
def ic_pr_burst_active(self, value): pass
|
||||
|
||||
__ic_pr_burst_activated_check = 0
|
||||
__ic_pr_burst_activated = 0
|
||||
@property
|
||||
def ic_pr_burst_activated(self):
|
||||
if time.time() > self.__ic_pr_burst_activated_check + 2:
|
||||
activated = [i.ic_pr_burst_activated for i in self.spawned_interfaces if i.ic_pr_burst_active]
|
||||
if activated: self.__ic_pr_burst_activated = min(activated)
|
||||
|
||||
return self.__ic_pr_burst_activated
|
||||
|
||||
@ic_pr_burst_activated.setter
|
||||
def ic_pr_burst_activated(self, value): pass
|
||||
|
||||
@staticmethod
|
||||
def start():
|
||||
if not BackboneInterface._job_active: threading.Thread(target=BackboneInterface.__job, daemon=True).start()
|
||||
@@ -195,17 +248,17 @@ class BackboneInterface(Interface):
|
||||
@staticmethod
|
||||
def register_in(fileno):
|
||||
if fileno < 0:
|
||||
RNS.log(f"Attempt to register invalid file descriptor {fileno}", RNS.LOG_ERROR)
|
||||
RNS.log(f"Attempt to register invalid file descriptor {fileno}", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
try: BackboneInterface.epoll.register(fileno, select.EPOLLIN)
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while registering EPOLL_IN for file descriptor {fileno}: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"An error occurred while registering EPOLL_IN for file descriptor {fileno}: {e}", RNS.LOG_WARNING)
|
||||
|
||||
@staticmethod
|
||||
def deregister_fileno(fileno):
|
||||
if fileno < 0:
|
||||
RNS.log(f"Attempt to deregister invalid file descriptor {fileno}", RNS.LOG_ERROR)
|
||||
RNS.log(f"Attempt to deregister invalid file descriptor {fileno}", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
try: BackboneInterface.epoll.unregister(fileno)
|
||||
@@ -227,10 +280,10 @@ class BackboneInterface(Interface):
|
||||
if interface.socket:
|
||||
fileno = interface.socket.fileno()
|
||||
if fileno in BackboneInterface.spawned_interface_filenos:
|
||||
try:
|
||||
BackboneInterface.epoll.modify(interface.socket.fileno(), select.EPOLLOUT)
|
||||
try: BackboneInterface.epoll.modify(fileno, select.EPOLLOUT)
|
||||
except Exception as e:
|
||||
RNS.trace_exception(e)
|
||||
RNS.log(f"Error occurred on {interface} while modifying socket EPOLL state: {e}", RNS.LOG_WARNING)
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def __job():
|
||||
@@ -269,8 +322,7 @@ class BackboneInterface(Interface):
|
||||
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)
|
||||
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)
|
||||
@@ -288,11 +340,15 @@ class BackboneInterface(Interface):
|
||||
except Exception as e: RNS.log(f"Error while removing spawned interface from {pif}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
try: client_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing socket for {spawned_interface}: {e}", RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log(f"Error while closing socket for {spawned_interface}: {e}", RNS.LOG_WARNING)
|
||||
spawned_interface.receive(b"")
|
||||
|
||||
spawned_interface.transmit_buffer = spawned_interface.transmit_buffer[written:]
|
||||
if len(spawned_interface.transmit_buffer) == 0: BackboneInterface.epoll.modify(fileno, select.EPOLLIN)
|
||||
try:
|
||||
if len(spawned_interface.transmit_buffer) == 0: BackboneInterface.epoll.modify(fileno, select.EPOLLIN)
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while setting EPOLLIN on {spawned_interface}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
spawned_interface.txb += written
|
||||
if spawned_interface.parent_interface: spawned_interface.parent_interface.txb += written
|
||||
|
||||
@@ -316,18 +372,24 @@ class BackboneInterface(Interface):
|
||||
elif fileno in BackboneInterface.listener_filenos:
|
||||
owner_interface, server_socket = BackboneInterface.listener_filenos[fileno]
|
||||
if fileno == server_socket.fileno() and (event & select.EPOLLIN):
|
||||
client_socket, address = server_socket.accept()
|
||||
client_socket.setblocking(0)
|
||||
if not owner_interface.incoming_connection(client_socket):
|
||||
try:
|
||||
client_socket, address = server_socket.accept()
|
||||
client_socket.setblocking(0)
|
||||
if not owner_interface.incoming_connection(client_socket):
|
||||
try: client_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing socket for failed incoming connection: {e}", RNS.LOG_WARNING)
|
||||
|
||||
except:
|
||||
RNS.log(f"Accepting socket failed for incoming connection: {e}", RNS.LOG_WARNING)
|
||||
try: client_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing socket for failed incoming connection: {e}", RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log(f"Error while closing socket for failed incoming socket accept: {e}", RNS.LOG_WARNING)
|
||||
|
||||
elif fileno == server_socket.fileno() and (event & select.EPOLLHUP):
|
||||
try: BackboneInterface.deregister_fileno(fileno)
|
||||
except Exception as e: RNS.log(f"Error while deregistering listener file descriptor {fileno}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
try: server_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing listener socket for {server_socket}: {e}", RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log(f"Error while closing listener socket for {server_socket}: {e}", RNS.LOG_WARNING)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"BackboneInterface error: {e}", RNS.LOG_ERROR)
|
||||
@@ -343,6 +405,21 @@ class BackboneInterface(Interface):
|
||||
spawned_interface = BackboneClientInterface(self.owner, spawned_configuration, connected_socket=socket)
|
||||
spawned_interface.OUT = self.OUT
|
||||
spawned_interface.IN = self.IN
|
||||
|
||||
spawned_interface.ingress_control = self.ingress_control
|
||||
spawned_interface.ic_max_held_announces = self.ic_max_held_announces
|
||||
spawned_interface.ic_burst_hold = self.ic_burst_hold
|
||||
spawned_interface.ic_burst_freq = self.ic_burst_freq
|
||||
spawned_interface.ic_burst_freq_new = self.ic_burst_freq_new
|
||||
spawned_interface.ic_new_time = self.ic_new_time
|
||||
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
|
||||
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
|
||||
|
||||
spawned_interface.egress_control = self.egress_control
|
||||
spawned_interface.ec_pr_freq = self.ec_pr_freq
|
||||
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
|
||||
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
|
||||
|
||||
spawned_interface.socket = socket
|
||||
spawned_interface.target_ip = socket.getpeername()[0]
|
||||
spawned_interface.target_port = str(socket.getpeername()[1])
|
||||
@@ -377,7 +454,7 @@ class BackboneInterface(Interface):
|
||||
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)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
while spawned_interface in self.spawned_interfaces: self.spawned_interfaces.remove(spawned_interface)
|
||||
self.spawned_interfaces.append(spawned_interface)
|
||||
BackboneInterface.add_client_socket(socket, spawned_interface)
|
||||
@@ -394,6 +471,12 @@ class BackboneInterface(Interface):
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def received_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.ip_freq_deque.append(time.time())
|
||||
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.op_freq_deque.append(time.time())
|
||||
|
||||
def process_outgoing(self, data):
|
||||
pass
|
||||
|
||||
@@ -407,7 +490,9 @@ class BackboneInterface(Interface):
|
||||
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)
|
||||
except Exception as e:
|
||||
if str(e).endswith("Transport endpoint is not connected"): pass
|
||||
else: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def __str__(self):
|
||||
if ":" in self.bind_ip:
|
||||
@@ -522,7 +607,9 @@ class BackboneClientInterface(Interface):
|
||||
|
||||
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)
|
||||
except Exception as e:
|
||||
if str(e).endswith("Transport endpoint is not connected"): pass
|
||||
else: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
try:
|
||||
if self.socket != None: self.socket.close()
|
||||
@@ -560,8 +647,8 @@ class BackboneClientInterface(Interface):
|
||||
|
||||
except Exception as e:
|
||||
if initial:
|
||||
RNS.log("Initial connection for "+str(self)+" could not be established: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("Leaving unconnected and retrying connection in "+str(BackboneClientInterface.RECONNECT_WAIT)+" seconds.", RNS.LOG_ERROR)
|
||||
RNS.log("Initial connection for "+str(self)+" could not be established: "+str(e), RNS.LOG_WARNING)
|
||||
RNS.log("Leaving unconnected and retrying connection in "+str(BackboneClientInterface.RECONNECT_WAIT)+" seconds.", RNS.LOG_WARNING)
|
||||
return False
|
||||
|
||||
else:
|
||||
@@ -579,12 +666,12 @@ class BackboneClientInterface(Interface):
|
||||
if not self.reconnecting:
|
||||
self.reconnecting = True
|
||||
attempts = 0
|
||||
while not self.online:
|
||||
while not self.online and not self.detached:
|
||||
time.sleep(BackboneClientInterface.RECONNECT_WAIT)
|
||||
attempts += 1
|
||||
|
||||
if self.max_reconnect_tries != None and attempts > self.max_reconnect_tries:
|
||||
RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_ERROR)
|
||||
RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_WARNING)
|
||||
self.teardown()
|
||||
break
|
||||
|
||||
@@ -592,6 +679,8 @@ class BackboneClientInterface(Interface):
|
||||
except Exception as e:
|
||||
RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
|
||||
if not self.online: return
|
||||
|
||||
if not self.never_connected:
|
||||
RNS.log("Reconnected socket for "+str(self)+".", RNS.LOG_INFO)
|
||||
|
||||
@@ -649,7 +738,7 @@ class BackboneClientInterface(Interface):
|
||||
def job(): self.reconnect()
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
else:
|
||||
RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_VERBOSE)
|
||||
RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_DEBUG)
|
||||
self.teardown()
|
||||
|
||||
except Exception as e:
|
||||
@@ -680,9 +769,8 @@ class BackboneClientInterface(Interface):
|
||||
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)
|
||||
if not self.initiator:
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
|
||||
@@ -826,9 +826,8 @@ class I2PInterfacePeer(Interface):
|
||||
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)
|
||||
if not self.initiator:
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
@@ -880,6 +879,7 @@ class I2PInterface(Interface):
|
||||
self.ifac_size = ifac_size
|
||||
self.ifac_netname = ifac_netname
|
||||
self.ifac_netkey = ifac_netkey
|
||||
self.supports_discovery = True
|
||||
|
||||
self.online = False
|
||||
|
||||
@@ -939,7 +939,7 @@ class I2PInterface(Interface):
|
||||
peer_interface.IN = True
|
||||
peer_interface.parent_interface = self
|
||||
peer_interface.parent_count = False
|
||||
RNS.Transport.interfaces.append(peer_interface)
|
||||
RNS.Transport.add_interface(peer_interface)
|
||||
|
||||
def incoming_connection(self, handler):
|
||||
RNS.log("Accepting incoming I2P connection", RNS.LOG_VERBOSE)
|
||||
@@ -947,6 +947,21 @@ class I2PInterface(Interface):
|
||||
spawned_interface = I2PInterfacePeer(self, self.owner, interface_name, connected_socket=handler.request)
|
||||
spawned_interface.OUT = True
|
||||
spawned_interface.IN = True
|
||||
|
||||
spawned_interface.ingress_control = self.ingress_control
|
||||
spawned_interface.ic_max_held_announces = self.ic_max_held_announces
|
||||
spawned_interface.ic_burst_hold = self.ic_burst_hold
|
||||
spawned_interface.ic_burst_freq = self.ic_burst_freq
|
||||
spawned_interface.ic_burst_freq_new = self.ic_burst_freq_new
|
||||
spawned_interface.ic_new_time = self.ic_new_time
|
||||
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
|
||||
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
|
||||
|
||||
spawned_interface.egress_control = self.egress_control
|
||||
spawned_interface.ec_pr_freq = self.ec_pr_freq
|
||||
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
|
||||
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
|
||||
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.online = True
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
@@ -977,7 +992,7 @@ class I2PInterface(Interface):
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
RNS.log("Spawned new I2PInterface Peer: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
while spawned_interface in self.spawned_interfaces:
|
||||
self.spawned_interfaces.remove(spawned_interface)
|
||||
self.spawned_interfaces.append(spawned_interface)
|
||||
@@ -992,6 +1007,12 @@ class I2PInterface(Interface):
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def received_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.ip_freq_deque.append(time.time())
|
||||
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.op_freq_deque.append(time.time())
|
||||
|
||||
def detach(self):
|
||||
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
|
||||
self.i2p.stop()
|
||||
|
||||
+141
-60
@@ -55,8 +55,15 @@ class Interface:
|
||||
|
||||
# How many samples to use for announce
|
||||
# frequency calculations
|
||||
IA_FREQ_SAMPLES = 6
|
||||
OA_FREQ_SAMPLES = 6
|
||||
IA_FREQ_SAMPLES = 48
|
||||
OA_FREQ_SAMPLES = 48
|
||||
IP_FREQ_SAMPLES = 48
|
||||
OP_FREQ_SAMPLES = 48
|
||||
|
||||
AR_MINFREQ_HZ = 0.1
|
||||
PR_MINFREQ_HZ = 0.1
|
||||
AR_FREQ_DECAY = 1/AR_MINFREQ_HZ
|
||||
PR_FREQ_DECAY = 1/PR_MINFREQ_HZ
|
||||
|
||||
# Maximum amount of ingress limited announces
|
||||
# to hold at any given time.
|
||||
@@ -66,11 +73,22 @@ class Interface:
|
||||
# considered to be newly created. Two
|
||||
# hours by default.
|
||||
IC_NEW_TIME = 2*60*60
|
||||
IC_BURST_FREQ_NEW = 3.5
|
||||
IC_BURST_FREQ = 12
|
||||
IC_BURST_HOLD = 1*60
|
||||
IC_BURST_PENALTY = 5*60
|
||||
IC_HELD_RELEASE_INTERVAL = 30
|
||||
IC_BURST_FREQ_NEW = 3
|
||||
IC_BURST_FREQ = 10
|
||||
IC_PR_BURST_FREQ_NEW = 3
|
||||
IC_PR_BURST_FREQ = 8
|
||||
IC_BURST_HOLD = 15
|
||||
IC_BURST_PENALTY = 15
|
||||
IC_HELD_RELEASE_INTERVAL = 5
|
||||
IC_DEQUE_MIN_SAMPLE = 2
|
||||
IC_BURST_MIN_SAMPLES = 6
|
||||
EC_PR_FREQ = 5
|
||||
EGRESS_CONTROL = False
|
||||
|
||||
# Default announce rate targets
|
||||
DEFAULT_AR_TARGET = 3600
|
||||
DEFAULT_AR_PENALTY = 0
|
||||
DEFAULT_AR_GRACE = 5
|
||||
|
||||
AUTOCONFIGURE_MTU = False
|
||||
FIXED_MTU = False
|
||||
@@ -84,24 +102,38 @@ 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
|
||||
self.ic_burst_active = False
|
||||
self.ic_burst_activated = 0
|
||||
self.ic_held_release = 0
|
||||
self.ic_burst_freq_new = Interface.IC_BURST_FREQ_NEW
|
||||
self.ic_burst_freq = Interface.IC_BURST_FREQ
|
||||
self.ic_new_time = Interface.IC_NEW_TIME
|
||||
self.ic_burst_penalty = Interface.IC_BURST_PENALTY
|
||||
self.ic_held_release_interval = Interface.IC_HELD_RELEASE_INTERVAL
|
||||
self.held_announces = {}
|
||||
self.supports_discovery = False
|
||||
self.discoverable = False
|
||||
self.last_discovery_announce = 0
|
||||
self.bootstrap_only = False
|
||||
self.parent_interface = None
|
||||
self.spawned_interfaces = None
|
||||
self.tunnel_id = None
|
||||
self.ingress_control = True
|
||||
self.phy_keepalive = False
|
||||
|
||||
self.ic_burst_active = False
|
||||
self.ic_burst_activated = 0
|
||||
self.ic_pr_burst_active = False
|
||||
self.ic_pr_burst_activated = 0
|
||||
self.ic_held_release = 0
|
||||
self.ic_max_held_announces = RNS.Reticulum.get_instance()._default_ic_max_held_announces()
|
||||
self.ic_burst_hold = RNS.Reticulum.get_instance()._default_ic_burst_hold()
|
||||
self.ic_burst_freq_new = RNS.Reticulum.get_instance()._default_ic_burst_freq_new()
|
||||
self.ic_burst_freq = RNS.Reticulum.get_instance()._default_ic_burst_freq()
|
||||
self.ic_pr_burst_freq_new = RNS.Reticulum.get_instance()._default_ic_pr_burst_freq_new()
|
||||
self.ic_pr_burst_freq = RNS.Reticulum.get_instance()._default_ic_pr_burst_freq()
|
||||
self.ic_new_time = RNS.Reticulum.get_instance()._default_ic_new_time()
|
||||
self.ic_burst_penalty = RNS.Reticulum.get_instance()._default_ic_burst_penalty()
|
||||
self.ic_held_release_interval = RNS.Reticulum.get_instance()._default_ic_held_release_interval()
|
||||
self.ec_pr_freq = RNS.Reticulum.get_instance()._default_ec_pr_freq()
|
||||
self.egress_control = RNS.Reticulum.get_instance()._default_egress_control()
|
||||
self.held_announces = {}
|
||||
|
||||
self.ia_freq_deque = deque(maxlen=Interface.IA_FREQ_SAMPLES)
|
||||
self.oa_freq_deque = deque(maxlen=Interface.OA_FREQ_SAMPLES)
|
||||
self.ip_freq_deque = deque(maxlen=Interface.IA_FREQ_SAMPLES)
|
||||
self.op_freq_deque = deque(maxlen=Interface.OA_FREQ_SAMPLES)
|
||||
|
||||
def get_hash(self):
|
||||
return RNS.Identity.full_hash(str(self).encode("utf-8"))
|
||||
@@ -117,21 +149,51 @@ class Interface:
|
||||
|
||||
if self.ic_burst_active:
|
||||
if ia_freq < freq_threshold and time.time() > self.ic_burst_activated+self.ic_burst_hold:
|
||||
self.ic_burst_active = False
|
||||
self.ic_held_release = time.time() + self.ic_burst_penalty
|
||||
if len(self.ia_freq_deque) >= self.IC_BURST_MIN_SAMPLES: self.ic_burst_active = False
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
if ia_freq > freq_threshold:
|
||||
self.ic_burst_active = True
|
||||
self.ic_burst_activated = time.time()
|
||||
self.ic_held_release = time.time() + self.ic_burst_penalty
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
else: return False
|
||||
|
||||
else:
|
||||
return False
|
||||
else: return False
|
||||
|
||||
def should_ingress_limit_pr(self):
|
||||
if self.ingress_control:
|
||||
freq_threshold = self.ic_pr_burst_freq_new if self.age() < self.ic_new_time else self.ic_pr_burst_freq
|
||||
ip_freq = self.incoming_pr_frequency()
|
||||
|
||||
if self.ic_pr_burst_active:
|
||||
if ip_freq < freq_threshold and time.time() > self.ic_pr_burst_activated+self.ic_burst_hold:
|
||||
self.ic_pr_burst_active = False
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
if ip_freq > freq_threshold:
|
||||
self.ic_pr_burst_active = True
|
||||
self.ic_pr_burst_activated = time.time()
|
||||
return True
|
||||
|
||||
else: return False
|
||||
|
||||
else: return False
|
||||
|
||||
def should_egress_limit_pr(self):
|
||||
if self.egress_control:
|
||||
freq_threshold = self.ec_pr_freq
|
||||
op_freq = self.outgoing_pr_frequency()
|
||||
|
||||
if op_freq > freq_threshold:
|
||||
if len(self.op_freq_deque) >= self.IC_BURST_MIN_SAMPLES: return True
|
||||
|
||||
return False
|
||||
|
||||
def optimise_mtu(self):
|
||||
if self.AUTOCONFIGURE_MTU:
|
||||
@@ -158,7 +220,7 @@ class Interface:
|
||||
else:
|
||||
self.HW_MTU = None
|
||||
|
||||
RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_DEBUG) # TODO: Remove debug
|
||||
RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_DEBUG)
|
||||
|
||||
def age(self):
|
||||
return time.time()-self.created
|
||||
@@ -171,7 +233,7 @@ class Interface:
|
||||
|
||||
def process_held_announces(self):
|
||||
try:
|
||||
if not self.should_ingress_limit() and len(self.held_announces) > 0 and time.time() > self.ic_held_release:
|
||||
if len(self.held_announces) > 0 and time.time() > self.ic_held_release:
|
||||
freq_threshold = self.ic_burst_freq_new if self.age() < self.ic_new_time else self.ic_burst_freq
|
||||
ia_freq = self.incoming_announce_frequency()
|
||||
if ia_freq < freq_threshold:
|
||||
@@ -187,8 +249,7 @@ class Interface:
|
||||
RNS.log("Releasing held announce packet "+str(selected_announce_packet)+" from "+str(self), RNS.LOG_EXTREME)
|
||||
self.ic_held_release = time.time() + self.ic_held_release_interval
|
||||
self.held_announces.pop(selected_announce_packet.destination_hash)
|
||||
def release():
|
||||
RNS.Transport.inbound(selected_announce_packet.raw, selected_announce_packet.receiving_interface)
|
||||
def release(): RNS.Transport.inbound(selected_announce_packet.raw, selected_announce_packet.receiving_interface)
|
||||
threading.Thread(target=release, daemon=True).start()
|
||||
|
||||
except Exception as e:
|
||||
@@ -205,39 +266,59 @@ class Interface:
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.sent_announce(from_spawned=True)
|
||||
|
||||
def incoming_announce_frequency(self):
|
||||
if not len(self.ia_freq_deque) > 1:
|
||||
return 0
|
||||
else:
|
||||
dq_len = len(self.ia_freq_deque)
|
||||
delta_sum = 0
|
||||
for i in range(1,dq_len):
|
||||
delta_sum += self.ia_freq_deque[i]-self.ia_freq_deque[i-1]
|
||||
delta_sum += time.time() - self.ia_freq_deque[dq_len-1]
|
||||
|
||||
if delta_sum == 0:
|
||||
avg = 0
|
||||
else:
|
||||
avg = 1/(delta_sum/(dq_len))
|
||||
def received_path_request(self, from_spawned=False):
|
||||
self.ip_freq_deque.append(time.time())
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.received_path_request(from_spawned=True)
|
||||
|
||||
return avg
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
self.op_freq_deque.append(time.time())
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.sent_path_request(from_spawned=True)
|
||||
|
||||
def incoming_announce_frequency(self):
|
||||
n = len(self.ia_freq_deque)
|
||||
if not n > self.IC_DEQUE_MIN_SAMPLE: return 0
|
||||
else:
|
||||
oldest = self.ia_freq_deque[0]
|
||||
span = time.time() - oldest
|
||||
if span > self.AR_FREQ_DECAY: self.ia_freq_deque.popleft()
|
||||
if span <= 0: return 0
|
||||
hz = n / span
|
||||
return hz
|
||||
|
||||
def outgoing_announce_frequency(self):
|
||||
if not len(self.oa_freq_deque) > 1:
|
||||
return 0
|
||||
n = len(self.oa_freq_deque)
|
||||
if not len(self.oa_freq_deque) > 1: return 0
|
||||
else:
|
||||
dq_len = len(self.oa_freq_deque)
|
||||
delta_sum = 0
|
||||
for i in range(1,dq_len):
|
||||
delta_sum += self.oa_freq_deque[i]-self.oa_freq_deque[i-1]
|
||||
delta_sum += time.time() - self.oa_freq_deque[dq_len-1]
|
||||
|
||||
if delta_sum == 0:
|
||||
avg = 0
|
||||
else:
|
||||
avg = 1/(delta_sum/(dq_len))
|
||||
oldest = self.oa_freq_deque[0]
|
||||
span = time.time() - oldest
|
||||
if span > self.AR_FREQ_DECAY: self.oa_freq_deque.popleft()
|
||||
if span <= 0: return 0
|
||||
hz = n / span
|
||||
return hz
|
||||
|
||||
return avg
|
||||
def incoming_pr_frequency(self):
|
||||
n = len(self.ip_freq_deque)
|
||||
if not n > self.IC_DEQUE_MIN_SAMPLE: return 0
|
||||
else:
|
||||
oldest = self.ip_freq_deque[0]
|
||||
span = time.time() - oldest
|
||||
if span > self.PR_FREQ_DECAY: self.ip_freq_deque.popleft()
|
||||
if span <= 0: return 0
|
||||
hz = n / span
|
||||
return hz
|
||||
|
||||
def outgoing_pr_frequency(self):
|
||||
n = len(self.op_freq_deque)
|
||||
if not len(self.op_freq_deque) > 1: return 0
|
||||
else:
|
||||
oldest = self.op_freq_deque[0]
|
||||
span = time.time() - oldest
|
||||
if span > self.PR_FREQ_DECAY: self.op_freq_deque.popleft()
|
||||
if span <= 0: return 0
|
||||
hz = n / span
|
||||
return hz
|
||||
|
||||
def process_announce_queue(self):
|
||||
if not hasattr(self, "announce_cap"):
|
||||
|
||||
@@ -62,6 +62,7 @@ class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
class LocalClientInterface(Interface):
|
||||
RECONNECT_WAIT = 8
|
||||
AUTOCONFIGURE_MTU = True
|
||||
CLIENT_SLEEP_PAUSE_TIMEOUT = 12
|
||||
|
||||
def __init__(self, owner, name, target_port = None, connected_socket=None, socket_path=None):
|
||||
super().__init__()
|
||||
@@ -85,8 +86,9 @@ class LocalClientInterface(Interface):
|
||||
self.frame_buffer = b""
|
||||
self.transmit_buffer = b""
|
||||
|
||||
if RNS.vendor.platformutils.use_epoll():
|
||||
self.epoll_backend = True
|
||||
if RNS.vendor.platformutils.use_epoll(): self.epoll_backend = True
|
||||
|
||||
self.pause_on_client_sleep = False
|
||||
|
||||
if connected_socket != None:
|
||||
self.receives = True
|
||||
@@ -99,6 +101,10 @@ class LocalClientInterface(Interface):
|
||||
|
||||
self.is_connected_to_shared_instance = False
|
||||
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
self.pause_on_client_sleep = True
|
||||
self.pause_timeout = time.time() + self.CLIENT_SLEEP_PAUSE_TIMEOUT
|
||||
|
||||
elif self.socket_path != None:
|
||||
self.receives = True
|
||||
self.target_ip = None
|
||||
@@ -145,6 +151,7 @@ class LocalClientInterface(Interface):
|
||||
self.is_connected_to_shared_instance = True
|
||||
self.never_connected = False
|
||||
|
||||
if RNS.vendor.platformutils.is_android(): self.phy_keepalive = True
|
||||
if self.epoll_backend: BackboneInterface.add_client_socket(self.socket, self)
|
||||
|
||||
return True
|
||||
@@ -185,17 +192,36 @@ class LocalClientInterface(Interface):
|
||||
raise IOError("Attempt to reconnect on a non-initiator local interface")
|
||||
|
||||
|
||||
def send_keepalive(self):
|
||||
if self.online:
|
||||
RNS.log(f"Sending keepalive on {self}", RNS.LOG_DEBUG) # TODO: Remove
|
||||
try:
|
||||
if self.epoll_backend:
|
||||
self.transmit_buffer += bytes([HDLC.FLAG])+bytes([HDLC.FLAG])
|
||||
BackboneInterface.tx_ready(self)
|
||||
|
||||
else:
|
||||
self.writing = True
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
self.socket.sendall(data)
|
||||
self.writing = False
|
||||
|
||||
except Exception as e: RNS.log(f"Exception occurred while sending keepalive on {self}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
if self.parent_interface != None: self.parent_interface.rxb += len(data)
|
||||
|
||||
try:
|
||||
self.owner.inbound(data, self)
|
||||
try: self.owner.inbound(data, self)
|
||||
except Exception as e:
|
||||
RNS.log(f"An error in the processing of an incoming frame for {self}: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"An error occurred in the processing of an incoming frame for {self}: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
def process_outgoing(self, data):
|
||||
if self.pause_on_client_sleep and time.time() > self.pause_timeout:
|
||||
RNS.log(f"TX paused for LocalInterface client, dropping outbound packet", RNS.LOG_DEBUG) # TODO: Remove
|
||||
return
|
||||
|
||||
if self.online:
|
||||
try:
|
||||
if self.epoll_backend:
|
||||
@@ -238,13 +264,12 @@ class LocalClientInterface(Interface):
|
||||
frame = self.frame_buffer[frame_start+1:frame_end]
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG]))
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
|
||||
if len(frame) > RNS.Reticulum.HEADER_MINSIZE:
|
||||
self.process_incoming(frame)
|
||||
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: flags_remaining = False
|
||||
|
||||
else: flags_remaining = False
|
||||
|
||||
def receive(self, data_in):
|
||||
try:
|
||||
@@ -267,6 +292,8 @@ class LocalClientInterface(Interface):
|
||||
RNS.log("Tearing down "+str(self), RNS.LOG_ERROR)
|
||||
self.teardown()
|
||||
|
||||
if self.pause_on_client_sleep: self.pause_timeout = time.time() + self.CLIENT_SLEEP_PAUSE_TIMEOUT
|
||||
|
||||
def read_loop(self):
|
||||
try:
|
||||
self.frame_buffer = b""
|
||||
@@ -320,15 +347,15 @@ class LocalClientInterface(Interface):
|
||||
self.OUT = False
|
||||
self.IN = False
|
||||
|
||||
if self in RNS.Transport.interfaces:
|
||||
RNS.Transport.interfaces.remove(self)
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
if self in RNS.Transport.local_client_interfaces:
|
||||
RNS.Transport.local_client_interfaces.remove(self)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.clients -= 1
|
||||
if hasattr(RNS.Transport, "owner") and RNS.Transport.owner != None:
|
||||
RNS.Transport.owner._should_persist_data()
|
||||
background = not self.detached
|
||||
RNS.Transport.owner._should_persist_data(background=background)
|
||||
|
||||
if nowarning == False:
|
||||
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)
|
||||
@@ -430,7 +457,7 @@ class LocalServerInterface(Interface):
|
||||
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.add_interface(spawned_interface)
|
||||
RNS.Transport.local_client_interfaces.append(spawned_interface)
|
||||
BackboneInterface.add_client_socket(client_socket, spawned_interface)
|
||||
self.clients += 1
|
||||
@@ -446,7 +473,7 @@ class LocalServerInterface(Interface):
|
||||
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.add_interface(spawned_interface)
|
||||
RNS.Transport.local_client_interfaces.append(spawned_interface)
|
||||
self.clients += 1
|
||||
spawned_interface.read_loop()
|
||||
@@ -460,6 +487,12 @@ class LocalServerInterface(Interface):
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def received_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.ip_freq_deque.append(time.time())
|
||||
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.op_freq_deque.append(time.time())
|
||||
|
||||
def __str__(self):
|
||||
if self.socket_path: return "Shared Instance["+str(self.socket_path.replace("\0", ""))+"]"
|
||||
else: return "Shared Instance["+str(self.bind_port)+"]"
|
||||
|
||||
@@ -185,7 +185,7 @@ class RNodeInterface(Interface):
|
||||
else:
|
||||
ble_name = ble_string
|
||||
|
||||
if port.lower().startswith(tcp_uri_scheme):
|
||||
elif port.lower().startswith(tcp_uri_scheme):
|
||||
force_tcp = True
|
||||
tcp_string = port[len(tcp_uri_scheme):]
|
||||
port = None
|
||||
@@ -296,6 +296,7 @@ class RNodeInterface(Interface):
|
||||
self.flow_control = flow_control
|
||||
self.interface_ready = False
|
||||
self.announce_rate_target = None
|
||||
self.supports_discovery = True
|
||||
|
||||
if force_ble or self.ble_addr != None or self.ble_name != None: self.use_ble = True
|
||||
if force_tcp or self.tcp_host != None: self.use_tcp = True
|
||||
|
||||
@@ -375,7 +375,7 @@ class RNodeMultiInterface(Interface):
|
||||
interface.mode = self.mode
|
||||
interface.HW_MTU = self.HW_MTU
|
||||
interface.detected = True
|
||||
RNS.Transport.interfaces.append(interface)
|
||||
RNS.Transport.add_interface(interface)
|
||||
RNS.log("Spawned new RNode subinterface: "+str(interface), RNS.LOG_VERBOSE)
|
||||
|
||||
self.clients += 1
|
||||
@@ -549,6 +549,12 @@ class RNodeMultiInterface(Interface):
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def received_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.ip_freq_deque.append(time.time())
|
||||
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.op_freq_deque.append(time.time())
|
||||
|
||||
def readLoop(self):
|
||||
try:
|
||||
in_frame = False
|
||||
@@ -903,8 +909,7 @@ class RNodeMultiInterface(Interface):
|
||||
def teardown_subinterfaces(self):
|
||||
for interface in self.subinterfaces:
|
||||
if interface != 0:
|
||||
if interface in RNS.Transport.interfaces:
|
||||
RNS.Transport.interfaces.remove(interface)
|
||||
RNS.Transport.remove_interface(interface)
|
||||
self.subinterfaces[interface.index] = 0
|
||||
|
||||
def should_ingress_limit(self):
|
||||
|
||||
@@ -107,8 +107,13 @@ class TCPClientInterface(Interface):
|
||||
i2p_tunneled = c.as_bool("i2p_tunneled") if "i2p_tunneled" in c else False
|
||||
connect_timeout = c.as_int("connect_timeout") if "connect_timeout" in c else None
|
||||
max_reconnect_tries = c.as_int("max_reconnect_tries") if "max_reconnect_tries" in c else None
|
||||
fixed_mtu = c.as_int("fixed_mtu") if "fixed_mtu" in c else None
|
||||
if fixed_mtu:
|
||||
if fixed_mtu < RNS.Reticulum.MTU: raise ValueError(f"Configured MTU of {fixed_mtu} bytes is too small")
|
||||
self.AUTOCONFIGURE_MTU = False
|
||||
self.FIXED_MTU = True
|
||||
|
||||
self.HW_MTU = TCPInterface.HW_MTU
|
||||
self.HW_MTU = TCPInterface.HW_MTU if not fixed_mtu else fixed_mtu
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.socket = None
|
||||
@@ -126,10 +131,9 @@ class TCPClientInterface(Interface):
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
self.bitrate = TCPClientInterface.BITRATE_GUESS
|
||||
|
||||
if max_reconnect_tries == None:
|
||||
self.max_reconnect_tries = TCPClientInterface.RECONNECT_MAX_TRIES
|
||||
else:
|
||||
self.max_reconnect_tries = max_reconnect_tries
|
||||
self.supports_discovery = True
|
||||
if max_reconnect_tries == None: self.max_reconnect_tries = TCPClientInterface.RECONNECT_MAX_TRIES
|
||||
else: self.max_reconnect_tries = max_reconnect_tries
|
||||
|
||||
if connected_socket != None:
|
||||
self.receives = True
|
||||
@@ -399,7 +403,7 @@ class TCPClientInterface(Interface):
|
||||
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)
|
||||
RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_DEBUG)
|
||||
self.teardown()
|
||||
|
||||
break
|
||||
@@ -432,9 +436,8 @@ class TCPClientInterface(Interface):
|
||||
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)
|
||||
if not self.initiator:
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
@@ -508,6 +511,7 @@ class TCPServerInterface(Interface):
|
||||
if port != None:
|
||||
bindport = port
|
||||
|
||||
self.supports_discovery = True
|
||||
self.HW_MTU = TCPInterface.HW_MTU
|
||||
|
||||
self.online = False
|
||||
@@ -574,6 +578,21 @@ class TCPServerInterface(Interface):
|
||||
spawned_interface = TCPClientInterface(self.owner, spawned_configuration, connected_socket=handler.request)
|
||||
spawned_interface.OUT = self.OUT
|
||||
spawned_interface.IN = self.IN
|
||||
|
||||
spawned_interface.ingress_control = self.ingress_control
|
||||
spawned_interface.ic_max_held_announces = self.ic_max_held_announces
|
||||
spawned_interface.ic_burst_hold = self.ic_burst_hold
|
||||
spawned_interface.ic_burst_freq = self.ic_burst_freq
|
||||
spawned_interface.ic_burst_freq_new = self.ic_burst_freq_new
|
||||
spawned_interface.ic_new_time = self.ic_new_time
|
||||
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
|
||||
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
|
||||
|
||||
spawned_interface.egress_control = self.egress_control
|
||||
spawned_interface.ec_pr_freq = self.ec_pr_freq
|
||||
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
|
||||
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
|
||||
|
||||
spawned_interface.target_ip = handler.client_address[0]
|
||||
spawned_interface.target_port = str(handler.client_address[1])
|
||||
spawned_interface.parent_interface = self
|
||||
@@ -607,7 +626,7 @@ class TCPServerInterface(Interface):
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.log("Spawned new TCPClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
while spawned_interface in self.spawned_interfaces:
|
||||
self.spawned_interfaces.remove(spawned_interface)
|
||||
self.spawned_interfaces.append(spawned_interface)
|
||||
@@ -619,6 +638,12 @@ class TCPServerInterface(Interface):
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def received_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.ip_freq_deque.append(time.time())
|
||||
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.op_freq_deque.append(time.time())
|
||||
|
||||
def process_outgoing(self, data):
|
||||
pass
|
||||
|
||||
|
||||
@@ -99,6 +99,12 @@ class WDCL():
|
||||
if not RNS.vendor.platformutils.is_android():
|
||||
if port == None: raise ValueError("No port specified")
|
||||
|
||||
self.supports_discovery = True
|
||||
self.discovery_frequency = None
|
||||
self.discovery_bandwidth = None
|
||||
self.discovery_channel = None
|
||||
self.discovery_modulation = None
|
||||
|
||||
self.switch_identity = owner.switch_identity
|
||||
self.switch_id = self.switch_identity.sig_pub_bytes[-4:]
|
||||
self.switch_pub_bytes = self.switch_identity.sig_pub_bytes
|
||||
@@ -769,8 +775,8 @@ class WeaveDevice():
|
||||
self.cpu_load = frame.data[0]
|
||||
self.capture_stats_cpu()
|
||||
elif frame.event == Evt.ET_STAT_MEMORY:
|
||||
self.memory_free = int.from_bytes(frame.data[:4])
|
||||
self.memory_total = int.from_bytes(frame.data[4:])
|
||||
self.memory_free = int.from_bytes(frame.data[:4], "big")
|
||||
self.memory_total = int.from_bytes(frame.data[4:], "big")
|
||||
self.memory_used = self.memory_total-self.memory_free
|
||||
self.memory_used_pct = round((self.memory_used/self.memory_total)*100, 2)
|
||||
self.capture_stats_memory()
|
||||
@@ -936,6 +942,16 @@ class WeaveInterface(Interface):
|
||||
spawned_interface = WeaveInterfacePeer(self, endpoint_addr)
|
||||
spawned_interface.OUT = self.OUT
|
||||
spawned_interface.IN = self.IN
|
||||
|
||||
spawned_interface.ingress_control = self.ingress_control
|
||||
spawned_interface.ic_max_held_announces = self.ic_max_held_announces
|
||||
spawned_interface.ic_burst_hold = self.ic_burst_hold
|
||||
spawned_interface.ic_burst_freq = self.ic_burst_freq
|
||||
spawned_interface.ic_burst_freq_new = self.ic_burst_freq_new
|
||||
spawned_interface.ic_new_time = self.ic_new_time
|
||||
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
|
||||
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
|
||||
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
|
||||
@@ -965,7 +981,7 @@ class WeaveInterface(Interface):
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface._online = True
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
if endpoint_addr in self.spawned_interfaces:
|
||||
self.spawned_interfaces[endpoint_addr].detach()
|
||||
self.spawned_interfaces[endpoint_addr].teardown()
|
||||
@@ -991,9 +1007,6 @@ class WeaveInterface(Interface):
|
||||
def process_outgoing(self,data):
|
||||
pass
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
def detach(self):
|
||||
self._online = False
|
||||
|
||||
@@ -1084,8 +1097,4 @@ class WeaveInterfacePeer(Interface):
|
||||
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)
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
+53
-64
@@ -722,12 +722,9 @@ class Link:
|
||||
pass
|
||||
|
||||
def link_closed(self):
|
||||
for resource in self.incoming_resources:
|
||||
resource.cancel()
|
||||
for resource in self.outgoing_resources:
|
||||
resource.cancel()
|
||||
if self._channel:
|
||||
self._channel._shutdown()
|
||||
for resource in self.incoming_resources: resource.cancel()
|
||||
for resource in self.outgoing_resources: resource.cancel()
|
||||
if self._channel: self._channel._shutdown()
|
||||
|
||||
self.prv = None
|
||||
self.pub = None
|
||||
@@ -741,8 +738,7 @@ class Link:
|
||||
self.destination.links.remove(self)
|
||||
|
||||
if self.callbacks.link_closed != None:
|
||||
try:
|
||||
self.callbacks.link_closed(self)
|
||||
try: self.callbacks.link_closed(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing link closed callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
@@ -907,21 +903,21 @@ class Link:
|
||||
identity_string = str(self.get_remote_identity()) if self.get_remote_identity() != None else "<Unknown>"
|
||||
RNS.log("Request "+RNS.prettyhexrep(request_id)+" from "+identity_string+" not allowed for: "+str(path), RNS.LOG_DEBUG)
|
||||
|
||||
def handle_response(self, request_id, response_data, response_size, response_transfer_size, metadata=None):
|
||||
def handle_response(self, request_id, response_data, response_size, response_transfer_size, metadata=None, update_sizes=False):
|
||||
if self.status == Link.ACTIVE:
|
||||
remove = None
|
||||
for pending_request in self.pending_requests:
|
||||
if pending_request.request_id == request_id:
|
||||
remove = pending_request
|
||||
try:
|
||||
pending_request.response_size = response_size
|
||||
if pending_request.response_transfer_size == None:
|
||||
pending_request.response_transfer_size = 0
|
||||
pending_request.response_transfer_size += response_transfer_size
|
||||
pending_request.response_received(response_data, metadata)
|
||||
except Exception as e:
|
||||
RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
if update_sizes:
|
||||
pending_request.response_size = response_size
|
||||
if pending_request.response_transfer_size == None: pending_request.response_transfer_size = 0
|
||||
pending_request.response_transfer_size += response_transfer_size
|
||||
|
||||
pending_request.response_received(response_data, metadata)
|
||||
|
||||
except Exception as e: RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
break
|
||||
|
||||
if remove != None:
|
||||
@@ -935,7 +931,8 @@ class Link:
|
||||
request_id = RNS.Identity.truncated_hash(packed_request)
|
||||
request_data = unpacked_request
|
||||
|
||||
self.handle_request(request_id, request_data)
|
||||
def job(): self.handle_request(request_id, request_data)
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
else:
|
||||
RNS.log("Incoming request resource failed with status: "+RNS.hexrep([resource.status]), RNS.LOG_DEBUG)
|
||||
|
||||
@@ -1021,12 +1018,15 @@ class Link:
|
||||
identity.load_public_key(public_key)
|
||||
|
||||
if identity.validate(signature, signed_data):
|
||||
self.__remote_identity = identity
|
||||
if self.callbacks.remote_identified != None:
|
||||
try:
|
||||
self.callbacks.remote_identified(self, self.__remote_identity)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing remote identified callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
if RNS.Reticulum.get_instance().is_blackholed(identity.hash):
|
||||
RNS.log(f"Terminating incoming link from blackholed identity {RNS.prettyhexrep(identity.hash)}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.teardown()
|
||||
|
||||
else:
|
||||
self.__remote_identity = identity
|
||||
if self.callbacks.remote_identified != None:
|
||||
try: self.callbacks.remote_identified(self, self.__remote_identity)
|
||||
except Exception as e: RNS.log(f"Error while executing remote identified callback from {self}. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
self.__update_phy_stats(packet, query_shared=True)
|
||||
|
||||
@@ -1036,7 +1036,8 @@ class Link:
|
||||
packed_request = self.decrypt(packet.data)
|
||||
if packed_request != None:
|
||||
unpacked_request = umsgpack.unpackb(packed_request)
|
||||
self.handle_request(request_id, unpacked_request)
|
||||
def job(): self.handle_request(request_id, unpacked_request)
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
self.__update_phy_stats(packet, query_shared=True)
|
||||
except Exception as e:
|
||||
RNS.log("Error occurred while handling request. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -1049,7 +1050,8 @@ class Link:
|
||||
request_id = unpacked_response[0]
|
||||
response_data = unpacked_response[1]
|
||||
transfer_size = len(umsgpack.packb(response_data))-2
|
||||
self.handle_response(request_id, response_data, transfer_size, transfer_size)
|
||||
def job(): self.handle_response(request_id, response_data, transfer_size, transfer_size, update_sizes=True)
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
self.__update_phy_stats(packet, query_shared=True)
|
||||
except Exception as e:
|
||||
RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -1085,17 +1087,14 @@ class Link:
|
||||
pending_request.started_at = time.time()
|
||||
pending_request.response_resource_progress(response_resource)
|
||||
|
||||
elif self.resource_strategy == Link.ACCEPT_NONE:
|
||||
pass
|
||||
elif self.resource_strategy == Link.ACCEPT_NONE: pass
|
||||
elif self.resource_strategy == Link.ACCEPT_APP:
|
||||
if self.callbacks.resource != None:
|
||||
try:
|
||||
resource_advertisement = RNS.ResourceAdvertisement.unpack(packet.plaintext)
|
||||
resource_advertisement.link = self
|
||||
if self.callbacks.resource(resource_advertisement):
|
||||
RNS.Resource.accept(packet, self.callbacks.resource_concluded)
|
||||
else:
|
||||
RNS.Resource.reject(packet)
|
||||
if self.callbacks.resource(resource_advertisement): RNS.Resource.accept(packet, self.callbacks.resource_concluded)
|
||||
else: RNS.Resource.reject(packet)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing resource accept callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
elif self.resource_strategy == Link.ACCEPT_ALL:
|
||||
@@ -1181,7 +1180,8 @@ class Link:
|
||||
resource_hash = packet.data[0:RNS.Identity.HASHLENGTH//8]
|
||||
for resource in self.outgoing_resources:
|
||||
if resource_hash == resource.hash:
|
||||
resource.validate_proof(packet.data)
|
||||
def job(resource=resource): resource.validate_proof(packet.data)
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
self.__update_phy_stats(packet, query_shared=True)
|
||||
|
||||
self.watchdog_lock = False
|
||||
@@ -1299,10 +1299,8 @@ class Link:
|
||||
:param resource_strategy: One of ``RNS.Link.ACCEPT_NONE``, ``RNS.Link.ACCEPT_ALL`` or ``RNS.Link.ACCEPT_APP``. If ``RNS.Link.ACCEPT_APP`` is set, the `resource_callback` will be called to determine whether the resource should be accepted or not.
|
||||
:raises: *TypeError* if the resource strategy is unsupported.
|
||||
"""
|
||||
if not resource_strategy in Link.resource_strategies:
|
||||
raise TypeError("Unsupported resource strategy")
|
||||
else:
|
||||
self.resource_strategy = resource_strategy
|
||||
if not resource_strategy in Link.resource_strategies: raise TypeError("Unsupported resource strategy")
|
||||
else: self.resource_strategy = resource_strategy
|
||||
|
||||
def register_outgoing_resource(self, resource):
|
||||
self.outgoing_resources.append(resource)
|
||||
@@ -1312,8 +1310,7 @@ class Link:
|
||||
|
||||
def has_incoming_resource(self, resource):
|
||||
for incoming_resource in self.incoming_resources:
|
||||
if incoming_resource.hash == resource.hash:
|
||||
return True
|
||||
if incoming_resource.hash == resource.hash: return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -1324,25 +1321,18 @@ class Link:
|
||||
return self.last_resource_eifr
|
||||
|
||||
def cancel_outgoing_resource(self, resource):
|
||||
if resource in self.outgoing_resources:
|
||||
self.outgoing_resources.remove(resource)
|
||||
else:
|
||||
RNS.log("Attempt to cancel a non-existing outgoing resource", RNS.LOG_ERROR)
|
||||
if resource in self.outgoing_resources: self.outgoing_resources.remove(resource)
|
||||
else: RNS.log("Attempt to cancel a non-existing outgoing resource", RNS.LOG_WARNING)
|
||||
|
||||
def cancel_incoming_resource(self, resource):
|
||||
if resource in self.incoming_resources:
|
||||
self.incoming_resources.remove(resource)
|
||||
else:
|
||||
RNS.log("Attempt to cancel a non-existing incoming resource", RNS.LOG_ERROR)
|
||||
if resource in self.incoming_resources: self.incoming_resources.remove(resource)
|
||||
else: RNS.log("Attempt to cancel a non-existing incoming resource", RNS.LOG_WARNING)
|
||||
|
||||
def ready_for_new_resource(self):
|
||||
if len(self.outgoing_resources) > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
if len(self.outgoing_resources) > 0: return False
|
||||
else: return True
|
||||
|
||||
def __str__(self):
|
||||
return RNS.prettyhexrep(self.link_id)
|
||||
def __str__(self): return RNS.prettyhexrep(self.link_id)
|
||||
|
||||
|
||||
class RequestReceipt():
|
||||
@@ -1427,20 +1417,21 @@ class RequestReceipt():
|
||||
now = time.time()
|
||||
if now > self.__resource_response_timeout:
|
||||
self.request_timed_out(None)
|
||||
break
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def request_timed_out(self, packet_receipt):
|
||||
self.status = RequestReceipt.FAILED
|
||||
self.concluded_at = time.time()
|
||||
self.link.pending_requests.remove(self)
|
||||
if self in self.link.pending_requests and self.status == RequestReceipt.DELIVERED:
|
||||
self.status = RequestReceipt.FAILED
|
||||
self.concluded_at = time.time()
|
||||
self.link.pending_requests.remove(self)
|
||||
|
||||
if self.callbacks.failed != None:
|
||||
try:
|
||||
self.callbacks.failed(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing request timed out callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
if self.callbacks.failed != None:
|
||||
try: self.callbacks.failed(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing request timed out callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
def response_resource_progress(self, resource):
|
||||
@@ -1482,14 +1473,12 @@ class RequestReceipt():
|
||||
self.packet_receipt.callbacks.delivery(self.packet_receipt)
|
||||
|
||||
if self.callbacks.progress != None:
|
||||
try:
|
||||
self.callbacks.progress(self)
|
||||
try: self.callbacks.progress(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing response progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
if self.callbacks.response != None:
|
||||
try:
|
||||
self.callbacks.response(self)
|
||||
try: self.callbacks.response(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing response received callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
+8
-9
@@ -117,7 +117,7 @@ class Packet:
|
||||
__slots__ = "hops", "header", "header_type", "packet_type", "transport_type", "context", "context_flag", "destination"
|
||||
__slots__ += "transport_id", "data", "flags", "raw", "packed", "sent", "create_receipt", "receipt", "fromPacked", "MTU"
|
||||
__slots__ += "sent_at", "packet_hash", "ratchet_id", "attached_interface", "receiving_interface", "rssi", "snr", "q"
|
||||
__slots__ += "ciphertext", "plaintext", "destination_hash", "destination_type", "link", "map_hash"
|
||||
__slots__ += "ciphertext", "plaintext", "destination_hash", "destination_type", "link", "map_hash", "is_outbound_pr"
|
||||
|
||||
def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST,
|
||||
header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True, context_flag=FLAG_UNSET):
|
||||
@@ -161,6 +161,7 @@ class Packet:
|
||||
|
||||
self.attached_interface = attached_interface
|
||||
self.receiving_interface = None
|
||||
self.is_outbound_pr = False
|
||||
self.rssi = None
|
||||
self.snr = None
|
||||
self.q = None
|
||||
@@ -267,7 +268,7 @@ class Packet:
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Received malformed packet, dropping it. The contained exception was: "+str(e), RNS.LOG_EXTREME)
|
||||
RNS.log("Received malformed packet, dropping it. The contained exception was: "+str(e), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
return False
|
||||
|
||||
def send(self):
|
||||
@@ -279,7 +280,7 @@ class Packet:
|
||||
if not self.sent:
|
||||
if self.destination.type == RNS.Destination.LINK:
|
||||
if self.destination.status == RNS.Link.CLOSED:
|
||||
RNS.log("Attempt to transmit over a closed link, dropping packet", RNS.LOG_DEBUG)
|
||||
RNS.log("Attempt to transmit over a closed link, dropping packet", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.sent = False
|
||||
self.receipt = None
|
||||
return False
|
||||
@@ -289,13 +290,11 @@ class Packet:
|
||||
self.destination.tx += 1
|
||||
self.destination.txbytes += len(self.data)
|
||||
|
||||
if not self.packed:
|
||||
self.pack()
|
||||
if not self.packed: self.pack()
|
||||
|
||||
if RNS.Transport.outbound(self):
|
||||
return self.receipt
|
||||
if RNS.Transport.outbound(self): return self.receipt
|
||||
else:
|
||||
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
|
||||
RNS.log("No interfaces could process the outbound packet", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.sent = False
|
||||
self.receipt = None
|
||||
return False
|
||||
@@ -317,7 +316,7 @@ class Packet:
|
||||
if RNS.Transport.outbound(self):
|
||||
return self.receipt
|
||||
else:
|
||||
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
|
||||
RNS.log("Re-send failed. No interfaces could process the outbound packet", RNS.LOG_WARNING)
|
||||
self.sent = False
|
||||
self.receipt = None
|
||||
return False
|
||||
|
||||
+70
-50
@@ -126,6 +126,7 @@ class Resource:
|
||||
PART_TIMEOUT_FACTOR = 4
|
||||
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
|
||||
PROOF_TIMEOUT_FACTOR = 3
|
||||
HMU_WAIT_FACTOR = 3.5
|
||||
MAX_RETRIES = 16
|
||||
MAX_ADV_RETRIES = 4
|
||||
SENDER_GRACE_TIME = 10.0
|
||||
@@ -193,6 +194,7 @@ class Resource:
|
||||
resource.window_flexibility = Resource.WINDOW_FLEXIBILITY
|
||||
resource.last_activity = time.time()
|
||||
resource.started_transferring = resource.last_activity
|
||||
resource.advertisement_packet = advertisement_packet
|
||||
|
||||
resource.storagepath = RNS.Reticulum.resourcepath+"/"+resource.original_hash.hex()
|
||||
resource.meta_storagepath = resource.storagepath+".meta"
|
||||
@@ -221,7 +223,7 @@ class Resource:
|
||||
if not resource.link.has_incoming_resource(resource):
|
||||
resource.link.register_incoming_resource(resource)
|
||||
|
||||
RNS.log(f"Accepting resource advertisement for {RNS.prettyhexrep(resource.hash)}. Transfer size is {RNS.prettysize(resource.size)} in {resource.total_parts} parts.", RNS.LOG_DEBUG)
|
||||
RNS.log(f"Accepting resource advertisement for {RNS.prettyhexrep(resource.hash)}. Transfer size is {RNS.prettysize(resource.size)} in {resource.total_parts} parts.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
if resource.link.callbacks.resource_started != None:
|
||||
try:
|
||||
resource.link.callbacks.resource_started(resource)
|
||||
@@ -233,11 +235,11 @@ class Resource:
|
||||
return resource
|
||||
|
||||
else:
|
||||
RNS.log("Ignoring resource advertisement for "+RNS.prettyhexrep(resource.hash)+", resource already transferring", RNS.LOG_DEBUG)
|
||||
RNS.log("Ignoring resource advertisement for "+RNS.prettyhexrep(resource.hash)+", resource already transferring", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not decode resource advertisement, dropping resource", RNS.LOG_DEBUG)
|
||||
RNS.log("Could not decode resource advertisement, dropping resource", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return None
|
||||
|
||||
# Create a resource for transmission to a remote destination
|
||||
@@ -359,6 +361,7 @@ class Resource:
|
||||
self.request_id = request_id
|
||||
self.started_transferring = None
|
||||
self.is_response = is_response
|
||||
self.max_decompressed_size = Resource.AUTO_COMPRESS_MAX_SIZE
|
||||
self.auto_compress_limit = Resource.AUTO_COMPRESS_MAX_SIZE
|
||||
self.auto_compress_option = auto_compress
|
||||
|
||||
@@ -385,9 +388,9 @@ class Resource:
|
||||
|
||||
compression_began = time.time()
|
||||
if self.auto_compress and data_size <= self.auto_compress_limit:
|
||||
RNS.log("Compressing resource data...", RNS.LOG_EXTREME)
|
||||
RNS.log("Compressing resource data...", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
self.compressed_data = bz2.compress(self.uncompressed_data)
|
||||
RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_EXTREME)
|
||||
RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
else:
|
||||
self.compressed_data = self.uncompressed_data
|
||||
|
||||
@@ -396,7 +399,7 @@ class Resource:
|
||||
|
||||
if (self.compressed_size < self.uncompressed_size and auto_compress):
|
||||
saved_bytes = len(self.uncompressed_data) - len(self.compressed_data)
|
||||
RNS.log("Compression saved "+str(saved_bytes)+" bytes, sending compressed", RNS.LOG_EXTREME)
|
||||
RNS.log("Compression saved "+str(saved_bytes)+" bytes, sending compressed", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
|
||||
self.data = b""
|
||||
self.data += RNS.Identity.get_random_hash()[:Resource.RANDOM_HASH_SIZE]
|
||||
@@ -412,7 +415,7 @@ class Resource:
|
||||
self.compressed = False
|
||||
self.compressed_data = None
|
||||
if self.auto_compress and data_size <= self.auto_compress_limit:
|
||||
RNS.log("Compression did not decrease size, sending uncompressed", RNS.LOG_EXTREME)
|
||||
RNS.log("Compression did not decrease size, sending uncompressed", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
|
||||
self.compressed_data = None
|
||||
self.uncompressed_data = None
|
||||
@@ -432,7 +435,7 @@ class Resource:
|
||||
hashmap_ok = False
|
||||
while not hashmap_ok:
|
||||
hashmap_computation_began = time.time()
|
||||
RNS.log("Starting resource hashmap computation with "+str(hashmap_entries)+" entries...", RNS.LOG_EXTREME)
|
||||
RNS.log("Starting resource hashmap computation with "+str(hashmap_entries)+" entries...", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
|
||||
self.random_hash = RNS.Identity.get_random_hash()[:Resource.RANDOM_HASH_SIZE]
|
||||
self.hash = RNS.Identity.full_hash(data+self.random_hash)
|
||||
@@ -452,7 +455,7 @@ class Resource:
|
||||
map_hash = self.get_map_hash(data)
|
||||
|
||||
if map_hash in collision_guard_list:
|
||||
RNS.log("Found hash collision in resource map, remapping...", RNS.LOG_DEBUG)
|
||||
RNS.log("Found hash collision in resource map, remapping...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
hashmap_ok = False
|
||||
break
|
||||
else:
|
||||
@@ -468,7 +471,7 @@ class Resource:
|
||||
self.hashmap += part.map_hash
|
||||
self.parts.append(part)
|
||||
|
||||
RNS.log("Hashmap computation concluded in "+str(round(time.time()-hashmap_computation_began, 3))+" seconds", RNS.LOG_EXTREME)
|
||||
RNS.log("Hashmap computation concluded in "+str(round(time.time()-hashmap_computation_began, 3))+" seconds", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
|
||||
self.data = None
|
||||
if advertise:
|
||||
@@ -529,7 +532,7 @@ class Resource:
|
||||
self.status = Resource.ADVERTISED
|
||||
self.retries_left = self.max_adv_retries
|
||||
self.link.register_outgoing_resource(self)
|
||||
RNS.log("Sent resource advertisement for "+RNS.prettyhexrep(self.hash), RNS.LOG_EXTREME)
|
||||
RNS.log("Sent resource advertisement for "+RNS.prettyhexrep(self.hash), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
except Exception as e:
|
||||
RNS.log("Could not advertise resource, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
self.cancel()
|
||||
@@ -571,12 +574,12 @@ class Resource:
|
||||
sleep_time = (self.adv_sent+self.timeout+Resource.PROCESSING_GRACE)-time.time()
|
||||
if sleep_time < 0:
|
||||
if self.retries_left <= 0:
|
||||
RNS.log("Resource transfer timeout after sending advertisement", RNS.LOG_DEBUG)
|
||||
RNS.log("Resource transfer timeout after sending advertisement", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.cancel()
|
||||
sleep_time = 0.001
|
||||
else:
|
||||
try:
|
||||
RNS.log("No part requests received, retrying resource advertisement...", RNS.LOG_DEBUG)
|
||||
RNS.log("No part requests received, retrying resource advertisement...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.retries_left -= 1
|
||||
self.advertisement_packet = RNS.Packet(self.link, ResourceAdvertisement(self).pack(), context=RNS.Packet.RESOURCE_ADV)
|
||||
self.advertisement_packet.send()
|
||||
@@ -584,7 +587,7 @@ class Resource:
|
||||
self.adv_sent = self.last_activity
|
||||
sleep_time = 0.001
|
||||
except Exception as e:
|
||||
RNS.log("Could not resend advertisement packet, cancelling resource. The contained exception was: "+str(e), RNS.LOG_VERBOSE)
|
||||
RNS.log("Could not resend advertisement packet, cancelling resource. The contained exception was: "+str(e), RNS.LOG_VERBOSE) if RNS.sl(RNS.LOG_VERBOSE) else None
|
||||
self.cancel()
|
||||
|
||||
|
||||
@@ -594,21 +597,22 @@ class Resource:
|
||||
extra_wait = retries_used * Resource.PER_RETRY_DELAY
|
||||
|
||||
self.update_eifr()
|
||||
expected_hmu_wait_remaining = (self.sdu*8*self.HMU_WAIT_FACTOR)/self.eifr if self.waiting_for_hmu or self.outstanding_parts == 0 else 0
|
||||
expected_tof_remaining = (self.outstanding_parts*self.sdu*8)/self.eifr
|
||||
|
||||
if self.req_resp_rtt_rate != 0:
|
||||
sleep_time = self.last_activity + self.part_timeout_factor*expected_tof_remaining + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
|
||||
sleep_time = self.last_activity + self.part_timeout_factor*expected_tof_remaining + expected_hmu_wait_remaining + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
|
||||
else:
|
||||
sleep_time = self.last_activity + self.part_timeout_factor*((3*self.sdu)/self.eifr) + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
|
||||
|
||||
# TODO: Remove debug at some point
|
||||
# RNS.log(f"EIFR {RNS.prettyspeed(self.eifr)}, ETOF {RNS.prettyshorttime(expected_tof_remaining)} ", RNS.LOG_DEBUG, pt=True)
|
||||
# RNS.log(f"EIFR {RNS.prettyspeed(self.eifr)}, ETOF {RNS.prettyshorttime(expected_tof_remaining)}, EHWR {RNS.prettyshorttime(expected_hmu_wait_remaining)}", RNS.LOG_DEBUG, pt=True)
|
||||
# RNS.log(f"Resource ST {RNS.prettyshorttime(sleep_time)}, RTT {RNS.prettyshorttime(self.rtt or self.link.rtt)}, {self.outstanding_parts} left", RNS.LOG_DEBUG, pt=True)
|
||||
|
||||
if sleep_time < 0:
|
||||
if self.retries_left > 0:
|
||||
ms = "" if self.outstanding_parts == 1 else "s"
|
||||
RNS.log(f"Timed out waiting for {self.outstanding_parts} part{ms}, requesting retry on {self}", RNS.LOG_DEBUG)
|
||||
RNS.log(f"Timed out waiting for {self.outstanding_parts} part{ms}, requesting retry on {self}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
if self.window > self.window_min:
|
||||
self.window -= 1
|
||||
if self.window_max > self.window_min:
|
||||
@@ -628,7 +632,7 @@ class Resource:
|
||||
max_wait = self.rtt * self.timeout_factor * self.max_retries + self.sender_grace_time + max_extra_wait
|
||||
sleep_time = self.last_activity + max_wait - time.time()
|
||||
if sleep_time < 0:
|
||||
RNS.log("Resource timed out waiting for part requests", RNS.LOG_DEBUG)
|
||||
RNS.log("Resource timed out waiting for part requests", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.cancel()
|
||||
sleep_time = 0.001
|
||||
|
||||
@@ -640,11 +644,11 @@ class Resource:
|
||||
sleep_time = self.last_part_sent + (self.rtt*self.timeout_factor+self.sender_grace_time) - time.time()
|
||||
if sleep_time < 0:
|
||||
if self.retries_left <= 0:
|
||||
RNS.log("Resource timed out waiting for proof", RNS.LOG_DEBUG)
|
||||
RNS.log("Resource timed out waiting for proof", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.cancel()
|
||||
sleep_time = 0.001
|
||||
else:
|
||||
RNS.log("All parts sent, but no resource proof received, querying network cache...", RNS.LOG_DEBUG)
|
||||
RNS.log("All parts sent, but no resource proof received, querying network cache...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.retries_left -= 1
|
||||
expected_data = self.hash + self.expected_proof
|
||||
expected_proof_packet = RNS.Packet(self.link, expected_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.RESOURCE_PRF)
|
||||
@@ -657,7 +661,7 @@ class Resource:
|
||||
sleep_time = 0.001
|
||||
|
||||
if sleep_time == 0:
|
||||
RNS.log("Warning! Link watchdog sleep time of 0!", RNS.LOG_DEBUG)
|
||||
RNS.log("Warning! Link watchdog sleep time of 0!", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
if sleep_time == None or sleep_time < 0:
|
||||
RNS.log("Timing error, cancelling resource transfer.", RNS.LOG_ERROR)
|
||||
self.cancel()
|
||||
@@ -677,8 +681,15 @@ class Resource:
|
||||
# Strip off random hash
|
||||
data = data[Resource.RANDOM_HASH_SIZE:]
|
||||
|
||||
if self.compressed: self.data = bz2.decompress(data)
|
||||
else: self.data = data
|
||||
if not self.compressed: self.data = data
|
||||
else:
|
||||
decompressor = bz2.BZ2Decompressor()
|
||||
self.data = decompressor.decompress(data, max_length=self.max_decompressed_size)
|
||||
if not decompressor.eof:
|
||||
self.status = Resource.CORRUPT
|
||||
self.cancel()
|
||||
RNS.log(f"Decompressed resource exceeded maximum decompressed size. The resource was rejected.", RNS.LOG_ERROR)
|
||||
return
|
||||
|
||||
calculated_hash = RNS.Identity.full_hash(self.data+self.random_hash)
|
||||
if calculated_hash == self.hash:
|
||||
@@ -735,7 +746,7 @@ class Resource:
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while cleaning up resource files, the contained exception was: {e}", RNS.LOG_ERROR)
|
||||
else:
|
||||
RNS.log("Resource segment "+str(self.segment_index)+" of "+str(self.total_segments)+" received, waiting for next segment to be announced", RNS.LOG_DEBUG)
|
||||
RNS.log("Resource segment "+str(self.segment_index)+" of "+str(self.total_segments)+" received, waiting for next segment to be announced", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
|
||||
def prove(self):
|
||||
@@ -747,26 +758,26 @@ class Resource:
|
||||
proof_packet.send()
|
||||
RNS.Transport.cache(proof_packet, force_cache=True)
|
||||
except Exception as e:
|
||||
RNS.log("Could not send proof packet, cancelling resource", RNS.LOG_DEBUG)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
|
||||
RNS.log("Could not send proof packet, cancelling resource", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.cancel()
|
||||
|
||||
def __prepare_next_segment(self):
|
||||
# Prepare the next segment for advertisement
|
||||
RNS.log(f"Preparing segment {self.segment_index+1} of {self.total_segments} for resource {self}", RNS.LOG_DEBUG)
|
||||
RNS.log(f"Preparing segment {self.segment_index+1} of {self.total_segments} for resource {self}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.preparing_next_segment = True
|
||||
self.next_segment = Resource(
|
||||
self.input_file, self.link,
|
||||
callback = self.callback,
|
||||
segment_index = self.segment_index+1,
|
||||
original_hash=self.original_hash,
|
||||
progress_callback = self.__progress_callback,
|
||||
request_id = self.request_id,
|
||||
is_response = self.is_response,
|
||||
advertise = False,
|
||||
auto_compress = self.auto_compress_option,
|
||||
sent_metadata_size = self.metadata_size,
|
||||
)
|
||||
self.next_segment = Resource(self.input_file, self.link,
|
||||
callback = self.callback,
|
||||
segment_index = self.segment_index+1,
|
||||
original_hash=self.original_hash,
|
||||
progress_callback = self.__progress_callback,
|
||||
request_id = self.request_id,
|
||||
is_response = self.is_response,
|
||||
advertise = False,
|
||||
auto_compress = self.auto_compress_option,
|
||||
sent_metadata_size = self.metadata_size)
|
||||
if self.__progress_callback:
|
||||
self.next_segment.progress_callback(self.__progress_callback)
|
||||
|
||||
def validate_proof(self, proof_data):
|
||||
if not self.status == Resource.FAILED:
|
||||
@@ -882,7 +893,7 @@ class Resource:
|
||||
|
||||
if self.received_count == self.total_parts and not self.assembly_lock:
|
||||
self.assembly_lock = True
|
||||
self.assemble()
|
||||
threading.Thread(target=self.assemble, daemon=True).start()
|
||||
elif self.outstanding_parts == 0:
|
||||
# TODO: Figure out if there is a mathematically
|
||||
# optimal way to adjust windows
|
||||
@@ -959,11 +970,12 @@ class Resource:
|
||||
self.last_activity = time.time()
|
||||
self.req_sent = self.last_activity
|
||||
self.req_sent_bytes = len(request_packet.raw)
|
||||
self.rtt_rxd_bytes_at_part_req = self.rtt_rxd_bytes
|
||||
self.req_resp = None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not send resource request packet, cancelling resource", RNS.LOG_DEBUG)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
|
||||
RNS.log("Could not send resource request packet, cancelling resource", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.cancel()
|
||||
|
||||
# Called on outgoing resource to make it send more data
|
||||
@@ -1008,8 +1020,8 @@ class Resource:
|
||||
self.last_part_sent = self.last_activity
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Resource could not send parts, cancelling transfer!", RNS.LOG_DEBUG)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
|
||||
RNS.log("Resource could not send parts, cancelling transfer!", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.cancel()
|
||||
|
||||
if wants_more_hashmap:
|
||||
@@ -1047,8 +1059,8 @@ class Resource:
|
||||
hmu_packet.send()
|
||||
self.last_activity = time.time()
|
||||
except Exception as e:
|
||||
RNS.log("Could not send resource HMU packet, cancelling resource", RNS.LOG_DEBUG)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
|
||||
RNS.log("Could not send resource HMU packet, cancelling resource", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.cancel()
|
||||
|
||||
if self.sent_parts == len(self.parts):
|
||||
@@ -1056,8 +1068,7 @@ class Resource:
|
||||
self.retries_left = 3
|
||||
|
||||
if self.__progress_callback != None:
|
||||
try:
|
||||
self.__progress_callback(self)
|
||||
try: self.__progress_callback(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
@@ -1065,7 +1076,14 @@ class Resource:
|
||||
"""
|
||||
Cancels transferring the resource.
|
||||
"""
|
||||
if self.status < Resource.COMPLETE:
|
||||
if self.next_segment: self.next_segment.cancel()
|
||||
|
||||
if self.status == Resource.CORRUPT:
|
||||
self.link.cancel_incoming_resource(self)
|
||||
self.reject(self.advertisement_packet)
|
||||
self.link.teardown()
|
||||
|
||||
elif self.status < Resource.COMPLETE:
|
||||
self.status = Resource.FAILED
|
||||
if self.initiator:
|
||||
if self.link.status == RNS.Link.ACTIVE:
|
||||
@@ -1093,7 +1111,8 @@ class Resource:
|
||||
if self.callback != None:
|
||||
try:
|
||||
self.link.resource_concluded(self)
|
||||
self.callback(self)
|
||||
def job(): self.callback(self)
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing callbacks on resource reject from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
@@ -1102,6 +1121,7 @@ class Resource:
|
||||
|
||||
def progress_callback(self, callback):
|
||||
self.__progress_callback = callback
|
||||
if self.next_segment: self.next_segment.progress_callback(callback)
|
||||
|
||||
def get_progress(self):
|
||||
"""
|
||||
|
||||
+868
-397
File diff suppressed because it is too large
Load Diff
+1646
-1138
File diff suppressed because it is too large
Load Diff
+38
-45
@@ -49,17 +49,34 @@ fetch_jail = None
|
||||
save_path = None
|
||||
show_phy_rates = False
|
||||
allowed_identity_hashes = []
|
||||
identity = None
|
||||
|
||||
def prepare_identity(identity_path):
|
||||
global identity
|
||||
if identity_path == None:
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
if identity == None:
|
||||
RNS.log(f"Could not load identity for rncp. The identity file at \"{identity_path}\" may be corrupt or unreadable.", RNS.LOG_ERROR)
|
||||
RNS.exit(2)
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
|
||||
REQ_FETCH_NOT_ALLOWED = 0xF0
|
||||
|
||||
es = " "
|
||||
erase_str = "\33[2K\r"
|
||||
|
||||
def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identity = False,
|
||||
def listen(configdir, identitypath = None, verbosity = 0, quietness = 0, allowed = [], display_identity = False,
|
||||
limit = None, disable_auth = None, fetch_allowed = False, no_compress=False,
|
||||
jail = None, save = None, announce = False, allow_overwrite=False):
|
||||
|
||||
global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path
|
||||
global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path, identity
|
||||
global fetch_auto_compress, allow_overwrite_on_receive
|
||||
|
||||
allow_fetch = fetch_allowed
|
||||
@@ -90,14 +107,7 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi
|
||||
|
||||
RNS.log("Saving received files in \""+save_path+"\"", RNS.LOG_VERBOSE)
|
||||
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
prepare_identity(identitypath)
|
||||
|
||||
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "receive")
|
||||
|
||||
@@ -153,11 +163,11 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
RNS.log(f"Could not apply allowed identity: {e}", RNS.LOG_ERROR)
|
||||
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!")
|
||||
RNS.log("No allowed identities configured, rncp will not accept any files!", RNS.LOG_WARNING)
|
||||
|
||||
def fetch_request(path, data, request_id, link_id, remote_identity, requested_at):
|
||||
global allow_fetch, fetch_jail, fetch_auto_compress
|
||||
@@ -207,7 +217,7 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi
|
||||
else:
|
||||
destination.register_request_handler("fetch_file", response_generator=fetch_request, allow=RNS.Destination.ALLOW_LIST, allowed_list=allowed_identity_hashes)
|
||||
|
||||
print("rncp listening on "+RNS.prettyhexrep(destination.hash))
|
||||
RNS.log("rncp listening on "+RNS.prettyhexrep(destination.hash), RNS.LOG_INFO)
|
||||
|
||||
if announce >= 0:
|
||||
def job():
|
||||
@@ -261,15 +271,15 @@ def receive_resource_started(resource):
|
||||
else:
|
||||
id_str = ""
|
||||
|
||||
print("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str)
|
||||
RNS.log("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str, RNS.LOG_INFO)
|
||||
|
||||
def receive_resource_concluded(resource):
|
||||
global save_path, allow_overwrite_on_receive
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
print(str(resource)+" completed")
|
||||
RNS.log(f"Incoming resource {resource} completed", RNS.LOG_INFO)
|
||||
|
||||
if resource.metadata == None:
|
||||
print("Invalid data received, ignoring resource")
|
||||
RNS.log("Invalid data received, ignoring resource", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
else:
|
||||
@@ -296,13 +306,14 @@ def receive_resource_concluded(resource):
|
||||
full_save_path = saved_filename+"."+str(counter)
|
||||
|
||||
shutil.move(resource.data.name, full_save_path)
|
||||
RNS.log("Saved received file to "+full_save_path, RNS.LOG_NOTICE)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while saving received resource: {e}", RNS.LOG_ERROR)
|
||||
return
|
||||
|
||||
else:
|
||||
print("Resource failed")
|
||||
RNS.log("Resource failed", RNS.LOG_INFO)
|
||||
|
||||
resource_done = False
|
||||
current_resource = None
|
||||
@@ -345,8 +356,8 @@ def sender_progress(resource):
|
||||
resource_done = True
|
||||
|
||||
link = None
|
||||
def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, save=None, allow_overwrite=False):
|
||||
global current_resource, resource_done, link, speed, show_phy_rates, save_path, allow_overwrite_on_receive
|
||||
def fetch(configdir, identitypath = None, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, save=None, allow_overwrite=False):
|
||||
global current_resource, resource_done, link, speed, show_phy_rates, save_path, allow_overwrite_on_receive, identity
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
show_phy_rates = phy_rates
|
||||
allow_overwrite_on_receive = allow_overwrite
|
||||
@@ -377,19 +388,8 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
|
||||
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
|
||||
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
if identity == None:
|
||||
RNS.log("Could not load identity for rncp. The identity file at \""+str(identity_path)+"\" may be corrupt or unreadable.", RNS.LOG_ERROR)
|
||||
RNS.exit(2)
|
||||
else:
|
||||
identity = None
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
prepare_identity(identitypath)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
@@ -614,8 +614,8 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
|
||||
RNS.exit(0)
|
||||
|
||||
|
||||
def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, no_compress=False):
|
||||
global current_resource, resource_done, link, speed, show_phy_rates, phy_got_total, phy_speed
|
||||
def send(configdir, identitypath = None, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, no_compress=False):
|
||||
global current_resource, resource_done, link, speed, show_phy_rates, phy_got_total, phy_speed, identity
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
show_phy_rates = phy_rates
|
||||
|
||||
@@ -643,19 +643,8 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
|
||||
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
if identity == None:
|
||||
RNS.log("Could not load identity for rncp. The identity file at \""+str(identity_path)+"\" may be corrupt or unreadable.", RNS.LOG_ERROR)
|
||||
RNS.exit(2)
|
||||
else:
|
||||
identity = None
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
prepare_identity(identitypath)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
@@ -822,6 +811,7 @@ def main():
|
||||
parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="allow this identity (or add in ~/.rncp/allowed_identities)", type=str)
|
||||
parser.add_argument('-n', '--no-auth', action='store_true', default=False, help="accept requests from anyone")
|
||||
parser.add_argument('-p', '--print-identity', action='store_true', default=False, help="print identity and destination info and exit")
|
||||
parser.add_argument('-i', metavar="identity", action='store', dest="identity", default=None, help="path to identity to use", type=str)
|
||||
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="sender timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
|
||||
parser.add_argument('-P', '--phy-rates', action='store_true', default=False, help="display physical layer transfer rates")
|
||||
# parser.add_argument("--limit", action="store", metavar="files", type=float, help="maximum number of files to accept", default=None)
|
||||
@@ -832,6 +822,7 @@ def main():
|
||||
if args.listen or args.print_identity:
|
||||
listen(
|
||||
configdir = args.config,
|
||||
identitypath = args.identity,
|
||||
verbosity=args.verbose,
|
||||
quietness=args.quiet,
|
||||
allowed = args.allowed,
|
||||
@@ -850,6 +841,7 @@ def main():
|
||||
if args.destination != None and args.file != None:
|
||||
fetch(
|
||||
configdir = args.config,
|
||||
identitypath = args.identity,
|
||||
verbosity = args.verbose,
|
||||
quietness = args.quiet,
|
||||
destination = args.destination,
|
||||
@@ -868,6 +860,7 @@ def main():
|
||||
elif args.destination != None and args.file != None:
|
||||
send(
|
||||
configdir = args.config,
|
||||
identitypath = args.identity,
|
||||
verbosity = args.verbose,
|
||||
quietness = args.quiet,
|
||||
destination = args.destination,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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.
|
||||
|
||||
APP_NAME = "git"
|
||||
|
||||
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,715 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 RNS
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
from RNS._version import __version__
|
||||
from RNS.Utilities.rngit import APP_NAME
|
||||
|
||||
from RNS.vendor.configobj import ConfigObj
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
def program_setup(configdir, rnsconfigdir, destination_hexhash, group_name, repo_name):
|
||||
git_client = ReticulumGitClient(configdir=configdir, rnsconfigdir=rnsconfigdir, destination_hexhash=destination_hexhash,
|
||||
group_name=group_name, repo_name=repo_name)
|
||||
|
||||
if not git_client.ready: sys.exit(1)
|
||||
else: git_client.run()
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: git-remote-rns <remote-name> <url>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
url = sys.argv[2]
|
||||
if not url.startswith("rns://"):
|
||||
print("Invalid URL scheme. Must be rns://", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
parts = url[6:].split("/", 2)
|
||||
destination_hexhash = parts[0]
|
||||
group_name = parts[1]
|
||||
repo_name = parts[2]
|
||||
|
||||
except IndexError: print("Invalid URL format. Use rns://<hash>/<group>/<repo>", file=sys.stderr); sys.exit(1)
|
||||
|
||||
configdir = os.environ.get("RNGIT_CONFIG", None)
|
||||
rnsconfigdir = os.environ.get("RNS_CONFIG", None)
|
||||
|
||||
program_setup(configdir, rnsconfigdir, destination_hexhash, group_name, repo_name)
|
||||
exit(0)
|
||||
|
||||
|
||||
class ReticulumGitClient():
|
||||
PATH_LIST = "/git/list"
|
||||
PATH_FETCH = "/git/fetch"
|
||||
PATH_PUSH = "/git/push"
|
||||
PATH_DELETE = "/git/delete"
|
||||
|
||||
RES_DISALLOWED = 0x01
|
||||
RES_INVALID_REQ = 0x02
|
||||
RES_NOT_FOUND = 0x03
|
||||
RES_REMOTE_FAIL = 0xFF
|
||||
|
||||
IDX_REPOSITORY = 0x00
|
||||
IDX_RESULT_CODE = 0x01
|
||||
|
||||
REF_BATCH_SIZE = 25
|
||||
PATH_TIMEOUT = 15
|
||||
LINK_TIMEOUT = 15
|
||||
|
||||
def __init__(self, configdir, rnsconfigdir, destination_hexhash, group_name, repo_name):
|
||||
# Client state and configuration
|
||||
self.identity = None
|
||||
self.userdir = os.path.expanduser("~")
|
||||
self.config = None
|
||||
self.ready = False
|
||||
|
||||
self.destination_aliases = {}
|
||||
self.remote_identity = None
|
||||
self.destination = None
|
||||
self.link = None
|
||||
self.link_ready = False
|
||||
self.link_failed = False
|
||||
self.link_timeout = self.LINK_TIMEOUT
|
||||
self.path_timeout = self.PATH_TIMEOUT
|
||||
|
||||
self.destination_hexhash = destination_hexhash
|
||||
self.group_name = group_name
|
||||
self.repo_name = repo_name
|
||||
self.repo_path = f"{group_name}/{repo_name}"
|
||||
|
||||
self.tmp_dir = TemporaryDirectory()
|
||||
self.request_event = threading.Event()
|
||||
self.request_response = None
|
||||
self.response_metadata = None
|
||||
|
||||
self.ref_batch_size = self.REF_BATCH_SIZE
|
||||
self.remote_refs = {}
|
||||
|
||||
self.response_progress = 0
|
||||
self.previous_progress = 0
|
||||
self.response_size = None
|
||||
self.response_transfer_size = None
|
||||
self.progress_updated_at = None
|
||||
self.progress_enabled = False
|
||||
|
||||
if configdir != None: self.configdir = configdir
|
||||
else:
|
||||
if os.path.isdir(self.userdir+"/.config/rngit") and os.path.isfile(self.userdir+"/.config/rngit/config"): self.configdir = self.userdir+"/.rngit/reticulum"
|
||||
else: self.configdir = self.userdir+"/.rngit"
|
||||
|
||||
self.logfile = self.configdir+"/client_log"
|
||||
self.configpath = self.configdir+"/client_config"
|
||||
self.identitypath = self.configdir+"/client_identity"
|
||||
|
||||
if os.path.isfile(self.configpath):
|
||||
try: self.config = ConfigObj(self.configpath)
|
||||
except Exception as e:
|
||||
RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR)
|
||||
return
|
||||
|
||||
else: self.__create_default_config()
|
||||
|
||||
RNS.logfile = self.logfile
|
||||
try: self.reticulum = RNS.Reticulum(configdir=rnsconfigdir, logdest=RNS.LOG_FILE)
|
||||
except Exception as e:
|
||||
print(f"Failed to initialize Reticulum: {e}", file=sys.stderr)
|
||||
return
|
||||
|
||||
self.__apply_config()
|
||||
self.ready = True
|
||||
|
||||
def __create_default_config(self):
|
||||
self.config = ConfigObj(__default_rngit_config__)
|
||||
self.config.filename = self.configpath
|
||||
if not os.path.isdir(self.configdir): os.makedirs(self.configdir)
|
||||
self.config.write()
|
||||
|
||||
def __apply_config(self):
|
||||
if "logging" in self.config:
|
||||
section = self.config["logging"]
|
||||
if "loglevel" in section: RNS.loglevel = max(RNS.LOG_NONE, min(RNS.LOG_EXTREME, section.as_int("loglevel")))
|
||||
|
||||
if "client" in self.config:
|
||||
section = self.config["client"]
|
||||
if "ref_batch_size" in section: self.ref_batch_size = max(0, min(1024, section.as_int("ref_batch_size")))
|
||||
|
||||
if "aliases" in self.config:
|
||||
section = self.config["aliases"]
|
||||
for alias in section:
|
||||
alias_hexhash = section[alias]
|
||||
len_ok = len(alias_hexhash) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2
|
||||
try: alias_hash = bytes.fromhex(alias_hexhash)
|
||||
except: alias_hash = None
|
||||
alias_exists = alias in self.destination_aliases
|
||||
if not len_ok or not alias_hash: continue
|
||||
if alias_exists: continue
|
||||
self.destination_aliases[alias] = RNS.hexrep(alias_hash, delimit=False)
|
||||
|
||||
if not os.path.isfile(self.identitypath):
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(self.identitypath)
|
||||
RNS.log(f"Client identity generated and persisted to {self.identitypath}", RNS.LOG_VERBOSE)
|
||||
|
||||
else:
|
||||
identity = RNS.Identity.from_file(self.identitypath)
|
||||
RNS.log(f"Client identity loaded from {self.identitypath}", RNS.LOG_VERBOSE)
|
||||
|
||||
if not identity:
|
||||
RNS.log("Could not initialize client identity.", RNS.LOG_ERROR)
|
||||
self.ready = False
|
||||
|
||||
else: self.identity = identity
|
||||
|
||||
self.destination_hexhash = self.__resolve_destination_alias(self.destination_hexhash)
|
||||
|
||||
def __resolve_destination_alias(self, alias):
|
||||
def resolve(alias):
|
||||
len_match = len(alias) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2
|
||||
try: hash_bytes = bytes.fromhex(alias)
|
||||
except: hash_bytes = None
|
||||
if len_match and hash_bytes: return alias
|
||||
else: return self.destination_aliases[alias] if alias in self.destination_aliases else alias
|
||||
|
||||
resolved = resolve(alias)
|
||||
return resolved
|
||||
|
||||
def abort(self, reason=None, code=255):
|
||||
if not reason: reason = "Unknown reason"
|
||||
print(f"git-remote-rns failed: {reason}", file=sys.stderr)
|
||||
if self.link: self.link.teardown()
|
||||
sys.exit(code)
|
||||
|
||||
def connect_server(self):
|
||||
try: destination_hash = bytes.fromhex(self.destination_hexhash)
|
||||
except Exception as e: self.abort(f"Invalid destination hash: {e}")
|
||||
|
||||
RNS.log(f"Requesting path to {RNS.prettyhexrep(destination_hash)}", RNS.LOG_DEBUG)
|
||||
sys.stderr.write(f"Requesting path..."); sys.stderr.flush()
|
||||
if not RNS.Transport.await_path(destination_hash, timeout=self.path_timeout):
|
||||
sys.stderr.write(f"\n"); sys.stderr.flush()
|
||||
self.abort(f"Could not resolve path to {RNS.prettyhexrep(destination_hash)}")
|
||||
|
||||
else:
|
||||
RNS.log(f"Path to {RNS.prettyhexrep(destination_hash)} resolved", RNS.LOG_DEBUG);
|
||||
sys.stderr.write(f"\rPath resolved "); sys.stderr.flush()
|
||||
|
||||
self.remote_identity = RNS.Identity.recall(destination_hash)
|
||||
if not self.remote_identity: self.abort("Could not recall remote identity. Is the server announcing?")
|
||||
|
||||
sys.stderr.write(f"\rEstablishing link..."); sys.stderr.flush()
|
||||
self.destination = RNS.Destination(self.remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "repositories")
|
||||
self.link = RNS.Link(self.destination)
|
||||
self.link.set_link_established_callback(self.link_established)
|
||||
self.link.set_link_closed_callback(self.link_closed)
|
||||
|
||||
def link_established(self, link):
|
||||
RNS.log(f"Link established, identifying...", RNS.LOG_DEBUG)
|
||||
sys.stderr.write(f"\rLink established with remote\n"); sys.stderr.flush()
|
||||
link.identify(self.identity)
|
||||
self.link_ready = True
|
||||
|
||||
def link_closed(self, link):
|
||||
RNS.log(f"Link was closed", RNS.LOG_DEBUG)
|
||||
if not self.link_ready: self.link_failed = True
|
||||
|
||||
def _on_progress(self, transfer_instance):
|
||||
if hasattr(transfer_instance, "progress"):
|
||||
self.response_progress = transfer_instance.progress
|
||||
self.response_size = transfer_instance.response_size
|
||||
self.response_transfer_size = transfer_instance.response_transfer_size
|
||||
|
||||
elif hasattr(transfer_instance, "get_progress") and callable(transfer_instance.get_progress):
|
||||
self.response_progress = transfer_instance.get_progress()
|
||||
self.response_size = transfer_instance.total_size
|
||||
self.response_transfer_size = transfer_instance.size
|
||||
|
||||
now = time.time()
|
||||
if self.progress_updated_at == None: self.progress_updated_at = now
|
||||
|
||||
if now > self.progress_updated_at+1:
|
||||
td = now - self.progress_updated_at
|
||||
pd = self.response_progress - self.previous_progress
|
||||
bd = pd*self.response_size if self.response_size else 0
|
||||
self.response_speed = (bd/td)*8 if td > 0 else 0
|
||||
self.previous_progress = self.response_progress
|
||||
self.progress_updated_at = now
|
||||
|
||||
# Report progress to git via stderr
|
||||
if self.progress_enabled and self.response_size:
|
||||
percent = round(self.response_progress * 100, 1)
|
||||
size = self.response_size
|
||||
rxd = size*self.response_progress
|
||||
speed_kbps = (self.response_speed / 1000) if hasattr(self, 'response_speed') else 0
|
||||
sys.stderr.write(f"Transferring: {percent}% ({RNS.prettysize(rxd)}/{RNS.prettysize(size)}) {RNS.prettyspeed(self.response_speed)} \r")
|
||||
sys.stderr.flush()
|
||||
|
||||
################################
|
||||
# Synchronous Request Wrappers #
|
||||
################################
|
||||
|
||||
def _response_ready(self, request_receipt):
|
||||
self.request_response = request_receipt.response
|
||||
self.response_metadata = request_receipt.metadata
|
||||
|
||||
if hasattr(self.request_response, "read") and callable(self.request_response.read):
|
||||
response_path = self.request_response.name
|
||||
base_name = os.path.basename(response_path)
|
||||
retained_path = os.path.join(self.tmp_dir.name, base_name)
|
||||
shutil.move(response_path, retained_path)
|
||||
self.request_response = open(retained_path, "rb")
|
||||
|
||||
self.request_event.set()
|
||||
|
||||
def _response_failed(self, request_receipt=None):
|
||||
self.request_response = None
|
||||
self.request_event.set()
|
||||
|
||||
def send_request(self, path, data, timeout=7200):
|
||||
if not self.link_ready: self.abort("Link not ready for request")
|
||||
|
||||
self.request_event.clear()
|
||||
self.request_response = None
|
||||
self.response_metadata = None
|
||||
self.previous_progress = 0
|
||||
self.progress_updated_at = None
|
||||
|
||||
RNS.log(f"Sending request: {path}", RNS.LOG_DEBUG)
|
||||
request_receipt = self.link.request(path, data, progress_callback=self._on_progress, response_callback=self._response_ready, failed_callback=self._response_failed, timeout=timeout)
|
||||
if request_receipt.resource: request_receipt.resource.progress_callback(self._on_progress)
|
||||
self.request_event.wait(timeout=timeout)
|
||||
|
||||
if self.request_response is None: self.abort("Request failed or timed out")
|
||||
RNS.log(f"Got response for: {path}", RNS.LOG_DEBUG)
|
||||
|
||||
return self.request_response, self.response_metadata
|
||||
|
||||
#############################
|
||||
# Git Helper Protocol Logic #
|
||||
#############################
|
||||
|
||||
def _detach_stdout(self):
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
sys.stderr = open(os.devnull, "w")
|
||||
|
||||
def run(self):
|
||||
try: self.connect_server()
|
||||
except Exception as e: self.abort(str(e))
|
||||
|
||||
timeout = self.link_timeout
|
||||
while not self.link_ready and not self.link_failed and timeout > 0:
|
||||
time.sleep(0.5)
|
||||
timeout -= 1
|
||||
|
||||
if not self.link_ready: self.abort("Failed to establish link")
|
||||
|
||||
self.progress_enabled = False
|
||||
|
||||
git_stdin = sys.stdin
|
||||
git_stdout = sys.stdout
|
||||
git_stderr = sys.stderr
|
||||
|
||||
fetch_queue = []
|
||||
push_queue = []
|
||||
|
||||
while True:
|
||||
line = git_stdin.readline()
|
||||
if not line: break
|
||||
|
||||
line = line.strip()
|
||||
if line == "capabilities":
|
||||
git_stdout.write("list\n")
|
||||
git_stdout.write("fetch\n")
|
||||
git_stdout.write("push\n")
|
||||
git_stdout.write("option\n")
|
||||
git_stdout.write("\n")
|
||||
git_stdout.flush()
|
||||
|
||||
elif line == "list": self.handle_git_list(git_stdout)
|
||||
|
||||
elif line.startswith("list "): self.handle_git_list(git_stdout, for_push=True) # List for push
|
||||
|
||||
elif line.startswith("option"):
|
||||
# Line format: option <name> <value>
|
||||
parts = line.split(maxsplit=2)
|
||||
opt_name = parts[1] if len(parts) > 1 else ""
|
||||
opt_value = parts[2] if len(parts) > 2 else ""
|
||||
|
||||
if opt_name == "progress": self.progress_enabled = opt_value.lower() in ("true", "1", "yes"); git_stdout.write("ok\n")
|
||||
else: git_stdout.write("unsupported\n")
|
||||
|
||||
git_stdout.flush()
|
||||
|
||||
elif line.startswith("fetch"):
|
||||
# Line format: fetch <sha> <ref>
|
||||
parts = line.split()
|
||||
sha = parts[1]
|
||||
ref = parts[2]
|
||||
# Avoid duplicates in the same batch - TODO: Re-evaluate this
|
||||
if (sha, ref) not in fetch_queue: fetch_queue.append((sha, ref))
|
||||
push_queue = []
|
||||
|
||||
elif line.startswith("push"):
|
||||
# Line format: push <local_ref>:<remote_ref>
|
||||
parts = line.split()
|
||||
refspec = parts[1]
|
||||
local_ref, remote_ref = refspec.split(":", 1)
|
||||
push_queue.append((local_ref, remote_ref))
|
||||
fetch_queue = []
|
||||
|
||||
elif line == "": # End of batch
|
||||
try:
|
||||
self.process_fetch_queue(fetch_queue, git_stdout, self.progress_enabled, self.ref_batch_size)
|
||||
self.process_push_queue(push_queue, git_stdout, git_stderr, self.progress_enabled)
|
||||
fetch_queue = []
|
||||
push_queue = []
|
||||
git_stdout.write("\n")
|
||||
git_stdout.flush()
|
||||
|
||||
except BrokenPipeError:
|
||||
self._detach_stdout()
|
||||
RNS.log("Git closed connection, exiting", RNS.LOG_DEBUG)
|
||||
break
|
||||
|
||||
else: self.abort(f"Unknown Git command: {line}")
|
||||
|
||||
try: sys.stdout.flush()
|
||||
except BrokenPipeError: pass
|
||||
|
||||
if self.link: self.link.teardown()
|
||||
|
||||
def handle_git_list(self, git_stdout, for_push=False):
|
||||
RNS.log("Handle git list" + (" for-push" if for_push else ""), RNS.LOG_DEBUG)
|
||||
request_data = {self.IDX_REPOSITORY: self.repo_path, "for_push": for_push}
|
||||
response, metadata = self.send_request(self.PATH_LIST, request_data)
|
||||
|
||||
if not response or not isinstance(response, bytes): self.abort("Invalid list response from server")
|
||||
|
||||
status_byte = response[0]
|
||||
payload = response[1:]
|
||||
|
||||
if status_byte != 0: self.abort(f"Server refused list: {payload.decode('utf-8', errors='ignore')}")
|
||||
|
||||
response_text = payload.decode("utf-8")
|
||||
|
||||
self.remote_refs = {}
|
||||
for line in response_text.split("\n"):
|
||||
line = line.strip()
|
||||
if not line: continue
|
||||
parts = line.split(" ", 1)
|
||||
if len(parts) == 2:
|
||||
sha, ref_name = parts
|
||||
if ref_name == "HEAD": continue
|
||||
self.remote_refs[ref_name] = sha
|
||||
|
||||
git_stdout.write(response_text)
|
||||
git_stdout.write("\n") # Required to terminate list
|
||||
git_stdout.flush()
|
||||
|
||||
def escape_for_stdout(self, value):
|
||||
if isinstance(value, bytes): value = value.decode('utf-8', errors='replace')
|
||||
|
||||
escaped = '"'
|
||||
for char in value:
|
||||
if char == '\\': escaped += '\\\\'
|
||||
elif char == '"': escaped += '\\"'
|
||||
elif char == '\n': escaped += '\\n'
|
||||
elif char == '\t': escaped += '\\t'
|
||||
elif char == '\r': escaped += '\\r'
|
||||
elif ord(char) < 32 or ord(char) > 126: escaped += f'\\x{ord(char):02x}'
|
||||
else: escaped += char
|
||||
|
||||
return escaped + '"'
|
||||
|
||||
def process_fetch_queue(self, fetch_queue, git_stdout, progress_enabled=False, ref_batch_size=REF_BATCH_SIZE):
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
if not fetch_queue: return
|
||||
|
||||
# Build a global have list from all remote refs that the client already has objects for
|
||||
have_shas = []
|
||||
for sha in self.remote_refs.values():
|
||||
try:
|
||||
result = subprocess.run(["git", "cat-file", "-t", sha], capture_output=True, check=False)
|
||||
if result.returncode == 0: have_shas.append(sha)
|
||||
|
||||
except Exception as e: RNS.log(f"Could not verify remote SHA {sha} locally: {e}", RNS.LOG_WARNING)
|
||||
|
||||
while fetch_queue:
|
||||
batch = fetch_queue[:ref_batch_size]
|
||||
fetch_queue = fetch_queue[ref_batch_size:]
|
||||
|
||||
refs_list = []
|
||||
for sha, ref in batch:
|
||||
ref_entry = {"sha": sha, "ref": ref}
|
||||
try:
|
||||
# Attempt to get local ref SHA for incremental bundle generation on remote
|
||||
result = subprocess.run(["git", "rev-parse", ref], capture_output=True, text=True, check=False)
|
||||
if result.returncode == 0:
|
||||
local_sha = result.stdout.strip()
|
||||
if local_sha != sha: ref_entry["have"] = local_sha
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not resolve local SHA for {ref} during fetch enumeration, getting full history for this ref: {e}", RNS.LOG_WARNING)
|
||||
|
||||
refs_list.append(ref_entry)
|
||||
|
||||
ref_names = [ref for _, ref in batch]
|
||||
RNS.log(f"Fetching batch of {len(refs_list)} refs: {ref_names} (have {len(have_shas)} common objects)", RNS.LOG_DEBUG)
|
||||
|
||||
request_data = { self.IDX_REPOSITORY: self.repo_path, "refs": refs_list }
|
||||
if have_shas: request_data["have"] = have_shas
|
||||
|
||||
response, metadata = self.send_request(self.PATH_FETCH, request_data)
|
||||
|
||||
if not response: self.abort(f"No data in fetch response for batch")
|
||||
if not metadata:
|
||||
if not isinstance(response, bytes): self.abort(f"Invalid fetch response for batch")
|
||||
status_byte = response[0]
|
||||
|
||||
if status_byte == 0:
|
||||
RNS.log(f"Server returned empty bundle, all objects already exist locally", RNS.LOG_DEBUG)
|
||||
continue
|
||||
|
||||
else:
|
||||
error_msg = response[1:].decode('utf-8', errors='ignore')
|
||||
self.abort(f"Fetch failed for batch: {error_msg}")
|
||||
|
||||
else:
|
||||
if not self.IDX_RESULT_CODE in metadata: self.abort(f"No result metadata on bundle response")
|
||||
status_byte = metadata[self.IDX_RESULT_CODE]
|
||||
if status_byte == 0: bundle_path = response.name
|
||||
else: self.abort(f"Unknown remote state for batch ref fetch")
|
||||
|
||||
if progress_enabled:
|
||||
size = os.stat(bundle_path).st_size
|
||||
sys.stderr.write(f"Transferring: 100% ({RNS.prettysize(size)}). \n")
|
||||
sys.stderr.flush()
|
||||
|
||||
stderr_arg = sys.stderr if progress_enabled else subprocess.DEVNULL
|
||||
|
||||
verify_cmd = ["git", "bundle", "verify", "-q", bundle_path]
|
||||
verify_result = subprocess.run(verify_cmd, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
|
||||
|
||||
if verify_result.returncode != 0: self.abort(f"Bundle verification failed for batch")
|
||||
|
||||
unbundle_cmd = ["git", "bundle", "unbundle"]
|
||||
if progress_enabled: unbundle_cmd.append("--progress")
|
||||
unbundle_cmd.append(bundle_path)
|
||||
|
||||
unbundle_result = subprocess.run(unbundle_cmd, stderr=stderr_arg, stdout=subprocess.DEVNULL)
|
||||
|
||||
if unbundle_result.returncode != 0: self.abort(f"Bundle unbundle failed for batch: Non-zero return code")
|
||||
|
||||
def process_push_queue(self, push_queue, git_stdout, git_stderr, progress_enabled=False):
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
for local_ref, remote_ref in push_queue:
|
||||
RNS.log(f"Pushing {local_ref} to {remote_ref}", RNS.LOG_DEBUG)
|
||||
|
||||
# Handle potential deletions
|
||||
if not local_ref or local_ref == "":
|
||||
request_data = { self.IDX_REPOSITORY: self.repo_path, "ref": remote_ref }
|
||||
response, metadata = self.send_request(self.PATH_DELETE, request_data)
|
||||
|
||||
if not response or not isinstance(response, bytes):
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout('No response from server')}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
status_byte = response[0]
|
||||
if status_byte != 0:
|
||||
error_msg = response[1:].decode("utf-8", errors="ignore")
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout(error_msg)}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
git_stdout.write(f"ok {remote_ref}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
force = local_ref.startswith("+")
|
||||
if force: local_ref = local_ref[1:]
|
||||
|
||||
stderr_arg = sys.stderr if progress_enabled else subprocess.DEVNULL
|
||||
|
||||
# Resolve the SHA that local_ref points to
|
||||
sha_result = subprocess.run(["git", "rev-parse", local_ref], capture_output=True, text=True, check=False)
|
||||
if sha_result.returncode != 0:
|
||||
error_msg = f"Could not resolve local ref {local_ref}"
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout(error_msg)}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
local_sha = sha_result.stdout.strip()
|
||||
|
||||
bundle_empty = False
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
bundle_path = tmpdir + "/push.bundle"
|
||||
|
||||
create_cmd = ["git", "bundle", "create", bundle_path, local_ref]
|
||||
|
||||
# Exclude all remote ref SHAs that exist locally, so the
|
||||
# bundle only contains objects the remote doesn't already have
|
||||
exclude_count = 0
|
||||
for sha in self.remote_refs.values():
|
||||
try:
|
||||
# We need to verify each SHA actually exists locally, since git
|
||||
# bundle create will fail if a ^<sha> argument references an object
|
||||
# not present in the local repository.
|
||||
result = subprocess.run(["git", "cat-file", "-t", sha], capture_output=True, check=False)
|
||||
if result.returncode == 0:
|
||||
create_cmd.append(f"^{sha}")
|
||||
exclude_count += 1
|
||||
|
||||
except Exception as e: RNS.log(f"Could not verify remote SHA {sha} locally: {e}", RNS.LOG_WARNING)
|
||||
|
||||
RNS.log(f"Excluding {exclude_count}/{len(self.remote_refs)} remote refs for {local_ref}", RNS.LOG_DEBUG)
|
||||
|
||||
if progress_enabled: create_cmd.insert(3, "--progress")
|
||||
|
||||
create_result = subprocess.run(create_cmd, capture_output=True, text=True, check=False)
|
||||
|
||||
if create_result.returncode == 0:
|
||||
if create_result.stderr:
|
||||
# git_stderr.write(create_result.stderr)
|
||||
pass
|
||||
else:
|
||||
if "empty bundle" in create_result.stderr.lower():
|
||||
# All objects reachable from local_ref already exist on
|
||||
# the remote. In this case, no bundle is needed and we can
|
||||
# update the ref directly via the operations path instead.
|
||||
bundle_empty = True
|
||||
RNS.log(f"Empty bundle for {local_ref}, all objects already on remote", RNS.LOG_DEBUG)
|
||||
|
||||
else:
|
||||
if progress_enabled and create_result.stderr: git_stderr.write(create_result.stderr)
|
||||
error_msg = "Bundle creation failed"
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout(error_msg)}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
if not bundle_empty:
|
||||
with open(bundle_path, "rb") as f: bundle_data = f.read()
|
||||
|
||||
request_data = { self.IDX_REPOSITORY: self.repo_path, "local_ref": local_ref, "remote_ref": remote_ref,
|
||||
"force": force, "bundle": bundle_data }
|
||||
|
||||
response, metadata = self.send_request(self.PATH_PUSH, request_data)
|
||||
|
||||
if not response or not isinstance(response, bytes):
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout('No response from server')}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
status_byte = response[0]
|
||||
if status_byte != 0:
|
||||
error_msg = response[1:].decode('utf-8', errors='ignore')
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout(error_msg)}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
# When all reachable objects already exist on the remote, send a
|
||||
# direct ref update operation instead of a bundle.
|
||||
if bundle_empty:
|
||||
operation = {"action": "update_ref", "ref": remote_ref, "sha": local_sha, "force": force}
|
||||
request_data = { self.IDX_REPOSITORY: self.repo_path,
|
||||
"operations": [operation] }
|
||||
|
||||
response, metadata = self.send_request(self.PATH_PUSH, request_data)
|
||||
|
||||
if not response or not isinstance(response, bytes):
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout('No response from server')}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
status_byte = response[0]
|
||||
if status_byte != 0:
|
||||
error_msg = response[1:].decode('utf-8', errors='ignore')
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout(error_msg)}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
git_stdout.write(f"ok {remote_ref}\n")
|
||||
git_stdout.flush()
|
||||
|
||||
|
||||
__default_rngit_config__ = '''# This is the default rngit client config file.
|
||||
|
||||
[client]
|
||||
|
||||
# You can control the batch size of ref transfers
|
||||
# using the ref_batch_size directive:
|
||||
|
||||
ref_batch_size = 25
|
||||
|
||||
|
||||
[aliases]
|
||||
|
||||
# You can define aliases for commonly used destination
|
||||
# hashes in this section. Each line must be in the format
|
||||
# aliased_name = DESTINATION_HASH
|
||||
#
|
||||
# These hashes are used for resolving remote destinations.
|
||||
# For rngit node permissions and identity resolution,
|
||||
# aliases must be defined in ~/.rngit/config.
|
||||
|
||||
# my_node = 063d38912bffc850af4a1b8a270a9d85
|
||||
# bobs_node = 714981d03e41deda0e4468cb274414cc
|
||||
|
||||
|
||||
[logging]
|
||||
# Valid log levels are 0 through 7:
|
||||
# 0: Log only critical information
|
||||
# 1: Log errors and lower log levels
|
||||
# 2: Log warnings and lower log levels
|
||||
# 3: Log notices and lower log levels
|
||||
# 4: Log info and lower (this is the default)
|
||||
# 5: Verbose logging
|
||||
# 6: Debug logging
|
||||
# 7: Extreme logging
|
||||
|
||||
loglevel = 4
|
||||
|
||||
'''.splitlines()
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
Executable
+324
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 sys
|
||||
import RNS
|
||||
import struct
|
||||
import base64
|
||||
import argparse
|
||||
|
||||
from RNS.Utilities.rnid import validate_rsg, create_rsg, extract_signed_rsg_data
|
||||
|
||||
SSHSIG_MAGIC = b"SSHSIG"
|
||||
SSHSIG_VERSION = 1
|
||||
NAMESPACE_GIT = b"git"
|
||||
RESERVED_EMPTY = b""
|
||||
HASH_ALGORITHM = b"sha256"
|
||||
|
||||
def ssh_string(data): return struct.pack(">I", len(data)) + data
|
||||
|
||||
def read_ssh_string(data, offset):
|
||||
if offset + 4 > len(data): raise ValueError("Not enough data for string length")
|
||||
length = struct.unpack(">I", data[offset:offset+4])[0]
|
||||
if offset + 4 + length > len(data): raise ValueError("Not enough data for string content")
|
||||
return data[offset+4:offset+4+length], offset + 4 + length
|
||||
|
||||
def create_ssh_signature(public_key_wire, namespace, reserved, hash_algorithm, signature_data):
|
||||
# SSHSIG (6 bytes) || version (uint32) || pubkey (ssh-string) || namespace (ssh-string) ||
|
||||
# reserved (ssh-string) || hash_algorithm (ssh-string) || signature (ssh-string)
|
||||
sig_blob = SSHSIG_MAGIC
|
||||
sig_blob += struct.pack(">I", SSHSIG_VERSION)
|
||||
sig_blob += ssh_string(public_key_wire)
|
||||
sig_blob += ssh_string(namespace)
|
||||
sig_blob += ssh_string(reserved)
|
||||
sig_blob += ssh_string(hash_algorithm)
|
||||
sig_blob += ssh_string(signature_data)
|
||||
return sig_blob
|
||||
|
||||
def parse_ssh_signature(sig_data):
|
||||
offset = 0
|
||||
|
||||
if not sig_data.startswith(SSHSIG_MAGIC): raise ValueError("Invalid SSH signature: missing SSHSIG magic")
|
||||
offset += len(SSHSIG_MAGIC)
|
||||
|
||||
if offset + 4 > len(sig_data): raise ValueError("Invalid SSH signature: truncated")
|
||||
version = struct.unpack(">I", sig_data[offset:offset+4])[0]
|
||||
if version != SSHSIG_VERSION: raise ValueError(f"Unsupported SSH signature version: {version}")
|
||||
offset += 4
|
||||
|
||||
public_key, offset = read_ssh_string(sig_data, offset)
|
||||
namespace, offset = read_ssh_string(sig_data, offset)
|
||||
reserved, offset = read_ssh_string(sig_data, offset)
|
||||
hash_algorithm, offset = read_ssh_string(sig_data, offset)
|
||||
signature_data, offset = read_ssh_string(sig_data, offset)
|
||||
|
||||
return { "version": version,
|
||||
"public_key": public_key,
|
||||
"namespace": namespace,
|
||||
"reserved": reserved,
|
||||
"hash_algorithm": hash_algorithm,
|
||||
"signature_data": signature_data }
|
||||
|
||||
def armor_ssh_signature(sig_blob):
|
||||
b64_data = base64.b64encode(sig_blob).decode('ascii')
|
||||
lines = [b64_data[i:i+70] for i in range(0, len(b64_data), 70)]
|
||||
|
||||
result = "-----BEGIN SSH SIGNATURE-----\n"
|
||||
result += "\n".join(lines) + "\n"
|
||||
result += "-----END SSH SIGNATURE-----\n"
|
||||
return result
|
||||
|
||||
def unarmor_ssh_signature(armored_data):
|
||||
lines = armored_data.strip().split('\n')
|
||||
b64_data = ""
|
||||
in_sig = False
|
||||
|
||||
for line in lines:
|
||||
if 'BEGIN SSH SIGNATURE' in line: in_sig = True; continue
|
||||
if 'END SSH SIGNATURE' in line: break
|
||||
if in_sig: b64_data += line.strip()
|
||||
|
||||
if not b64_data: raise ValueError("No signature data found in armored input")
|
||||
|
||||
return base64.b64decode(b64_data)
|
||||
|
||||
def get_pubkey_wire_format(identity):
|
||||
return ssh_string(b"ssh-ed25519")+ssh_string(identity.sig_pub_bytes)
|
||||
|
||||
def sign(args):
|
||||
keyfile = args.keyfile
|
||||
if not keyfile or not os.path.isfile(keyfile):
|
||||
print(f"Identity file not found: {keyfile}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
identity = RNS.Identity.from_file(keyfile)
|
||||
if not identity or not identity.get_private_key():
|
||||
print("Error: Could not load identity or identity has no private key", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error loading identity: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.file and os.path.isfile(args.file):
|
||||
with open(args.file, 'rb') as f: message = f.read()
|
||||
sig_file = args.file + ".sig"
|
||||
else:
|
||||
message = sys.stdin.buffer.read()
|
||||
sig_file = None
|
||||
|
||||
try: rsg = create_rsg(identity, message)
|
||||
except Exception as e:
|
||||
print(f"Error creating signature: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try: ssh_pubkey = get_pubkey_wire_format(identity)
|
||||
except Exception as e:
|
||||
print(f"Error converting public key: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
ssh_sig = create_ssh_signature(public_key_wire=ssh_pubkey, namespace=NAMESPACE_GIT, reserved=RESERVED_EMPTY,
|
||||
hash_algorithm=HASH_ALGORITHM, signature_data=rsg)
|
||||
except Exception as e:
|
||||
print(f"Error creating SSH signature: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try: armored = armor_ssh_signature(ssh_sig)
|
||||
except Exception as e:
|
||||
print(f"Error armoring signature: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if sig_file:
|
||||
try:
|
||||
with open(sig_file, 'w') as f: f.write(armored)
|
||||
except Exception as e:
|
||||
print(f"Error writing signature file: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
else: print(armored, end="")
|
||||
|
||||
return 0
|
||||
|
||||
def find_principals(args):
|
||||
sigfile = args.sigfile
|
||||
if not sigfile or not os.path.isfile(sigfile): print("Error: Signature file not found", file=sys.stderr); return 1
|
||||
|
||||
try:
|
||||
with open(sigfile, 'r') as f: armored_sig = f.read()
|
||||
except Exception as e: print(f"Error reading signature file: {e}", file=sys.stderr); return 1
|
||||
|
||||
try: ssh_sig = parse_ssh_signature(unarmor_ssh_signature(armored_sig))
|
||||
except Exception as e: print(f"Error parsing SSH signature: {e}", file=sys.stderr); return 1
|
||||
|
||||
if ssh_sig["namespace"] != NAMESPACE_GIT:
|
||||
print(f"Error: Namespace mismatch: {ssh_sig['namespace']}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
rsg = ssh_sig["signature_data"]
|
||||
try: identity_hash = extract_signed_rsg_data(rsg)["meta"]["signer"]
|
||||
except Exception as e: print(f"Could not determine signer identity: {e}", file=sys.stderr); return 1
|
||||
|
||||
print(RNS.hexrep(identity_hash, delimit=False))
|
||||
return 0
|
||||
|
||||
def check_novalidate(args):
|
||||
sigfile = args.sigfile
|
||||
if not sigfile or not os.path.isfile(sigfile): return 1
|
||||
|
||||
try:
|
||||
with open(sigfile, 'r') as f: armored_sig = f.read()
|
||||
ssh_sig = parse_ssh_signature(unarmor_ssh_signature(armored_sig))
|
||||
|
||||
if ssh_sig["namespace"] != NAMESPACE_GIT: return 1
|
||||
|
||||
rsg = ssh_sig["signature_data"]
|
||||
signed_data = extract_signed_rsg_data(rsg)
|
||||
if not signed_data: return 1
|
||||
else: return 0
|
||||
|
||||
except Exception: return 1
|
||||
|
||||
def extract_commit_author(message):
|
||||
message_lines = message.splitlines()
|
||||
author = ""
|
||||
AUTHOR_TARGET = b"author "
|
||||
for line in message_lines:
|
||||
if not line.strip(b""): break
|
||||
elif line.startswith(AUTHOR_TARGET):
|
||||
try:
|
||||
spos = line.find(b"<"); epos = line.find(b">")
|
||||
if spos > len(AUTHOR_TARGET) and epos > spos and epos < len(line)-1:
|
||||
author = line[spos+1:epos].decode("utf-8")
|
||||
break
|
||||
except Exception as e: print(f"Error while determining author from signed commit"); return 1
|
||||
|
||||
return author
|
||||
|
||||
def extract_commit_committer(message):
|
||||
message_lines = message.splitlines()
|
||||
committer = ""
|
||||
COMMITTER_TARGET = b"committer "
|
||||
for line in message_lines:
|
||||
if not line.strip(b""): break
|
||||
elif line.startswith(COMMITTER_TARGET):
|
||||
try:
|
||||
spos = line.find(b"<"); epos = line.find(b">")
|
||||
if spos > len(COMMITTER_TARGET) and epos > spos and epos < len(line)-1:
|
||||
committer = line[spos+1:epos].decode("utf-8")
|
||||
break
|
||||
except Exception as e: print(f"Error while determining committer from signed commit"); return 1
|
||||
|
||||
return committer
|
||||
|
||||
def extract_commit_tagger(message):
|
||||
message_lines = message.splitlines()
|
||||
tagger = ""
|
||||
is_tag = False
|
||||
for line in message_lines:
|
||||
TAG_TARGET = b"tag "
|
||||
TAGGER_TARGET = b"tagger "
|
||||
if not line.strip(b""): break
|
||||
elif line.startswith(TAG_TARGET): is_tag = True
|
||||
elif line.startswith(TAGGER_TARGET) and is_tag:
|
||||
try:
|
||||
spos = line.find(b"<"); epos = line.find(b">")
|
||||
if spos > len(TAGGER_TARGET) and epos > spos and epos < len(line)-1:
|
||||
tagger = line[spos+1:epos].decode("utf-8")
|
||||
break
|
||||
except Exception as e: print(f"Error while determining tagger from signed commit"); return 1
|
||||
|
||||
return tagger, is_tag
|
||||
|
||||
def verify(args):
|
||||
sigfile = args.sigfile
|
||||
principal = args.principal
|
||||
if not sigfile or not os.path.isfile(sigfile): print("Error: Signature file not found", file=sys.stderr); return 1
|
||||
|
||||
message = sys.stdin.buffer.read()
|
||||
|
||||
try:
|
||||
with open(sigfile, 'r') as f: armored_sig = f.read()
|
||||
raw_sig = unarmor_ssh_signature(armored_sig)
|
||||
ssh_sig = parse_ssh_signature(raw_sig)
|
||||
|
||||
except Exception as e: print(f"Error parsing signature: {e}", file=sys.stderr); return 1
|
||||
|
||||
author = extract_commit_author(message)
|
||||
committer = extract_commit_committer(message)
|
||||
tagger, is_tag = extract_commit_tagger(message)
|
||||
|
||||
if ssh_sig["namespace"] != NAMESPACE_GIT: print(f"Invalid commit signature namespace", file=sys.stderr); return 1
|
||||
|
||||
rsg = ssh_sig["signature_data"]
|
||||
valid, signed_data, signing_identity = validate_rsg(rsg, message)
|
||||
|
||||
if not valid: print(f"Invalid signature", file=sys.stderr); return 1
|
||||
|
||||
if is_tag: author = tagger
|
||||
|
||||
signer_hash = RNS.hexrep(signing_identity.hash, delimit=False)
|
||||
if not author == signer_hash: print(f"Commit not signed by author <{author}>"); return 1
|
||||
|
||||
if principal:
|
||||
if principal != signer_hash: print(f"Principal mismatch", file=sys.stderr); return 1
|
||||
|
||||
print(f"Good \"git\" signature for commit, signed with Reticulum Identity key <{signer_hash}>")
|
||||
return 0
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Git commit signer and validator")
|
||||
parser.add_argument("-Y", dest="op", required=True, choices=["sign", "find-principals", "check-novalidate", "verify"], help="Operation to perform")
|
||||
parser.add_argument("-n", dest="namespace", default="git", help="Namespace")
|
||||
parser.add_argument("-f", dest="keyfile", help="Key file (for signing) or allowed signers file (for verification)")
|
||||
parser.add_argument("-I", dest="principal", help="Principal identity (for verification)")
|
||||
parser.add_argument("-s", dest="sigfile", help="Signature file")
|
||||
parser.add_argument("file", nargs="?", help="File to sign (for signing)")
|
||||
parser.add_argument("-O", dest="ssh_options", action="append", default=[], help="SSH options (for git compatibility, ignored)")
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
for arg in unknown:
|
||||
if arg.startswith('-O'): continue # TODO: Add options for time validation
|
||||
else:
|
||||
print(f"Error: Unknown argument: {arg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.op == "sign": return sign(args)
|
||||
elif args.op == "find-principals": return find_principals(args)
|
||||
elif args.op == "check-novalidate": return check_novalidate(args)
|
||||
elif args.op == "verify": return verify(args)
|
||||
else:
|
||||
print(f"Error: Unknown operation: {args.op}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__": sys.exit(main())
|
||||
@@ -0,0 +1,412 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 io
|
||||
import RNS
|
||||
|
||||
class SyntaxHighlighter:
|
||||
|
||||
def __init__(self, theme=None):
|
||||
self.pygments_available = False
|
||||
self.pygments = None
|
||||
self._lexer_cache = {}
|
||||
self._check_pygments()
|
||||
self.theme = theme or self._get_default_theme()
|
||||
|
||||
def _get_default_theme(self):
|
||||
return {
|
||||
# Control flow - warm coral-red
|
||||
"keyword": "ff7b72",
|
||||
"keyword_constant": "ff7b72",
|
||||
"keyword_control": "ff7b72",
|
||||
"keyword_declaration": "ff7b72",
|
||||
|
||||
# Function definitions - bright sky blue
|
||||
"function_def": "79c0ff",
|
||||
"function_magic": "ff7b72",
|
||||
|
||||
# Function calls - soft lavender
|
||||
"function_call": "d2a8ff",
|
||||
"function_builtin": "ffa657", # amber
|
||||
|
||||
# Class definitions - fresh mint green
|
||||
"class_def": "7ee787",
|
||||
"class_ref": "56d364", # muted when referenced
|
||||
|
||||
# Instance context - soft pink
|
||||
"self": "ff9bce",
|
||||
"cls": "ff9bce",
|
||||
|
||||
# Data literals - cool, calm ice blue
|
||||
"string": "a5d6ff",
|
||||
"string_quoted": "a5d6ff",
|
||||
"string_doc": "8b949e", # docstrings - like comments
|
||||
"string_interpol": "ffd700", # f-string braces - gold
|
||||
"string_escape": "ffea00", # escape sequences - bright yellow
|
||||
|
||||
# Numbers - same as function def
|
||||
"number": "79c0ff",
|
||||
"number_float": "79c0ff",
|
||||
"number_integer": "79c0ff",
|
||||
"number_hex": "79c0ff",
|
||||
|
||||
# Comments - muted gray
|
||||
"comment": "8b949e",
|
||||
"comment_doc": "8b949e",
|
||||
"comment_preproc": "ff7b72", # preprocessor directives
|
||||
|
||||
# Operators - distinct pink/red for visibility
|
||||
"operator": "ff7b72", # General operators - coral
|
||||
"operator_arithmetic": "ff7b72", # +, -, *, /, etc.
|
||||
"operator_comparison": "ff7b72", # ==, !=, <, >, etc.
|
||||
"operator_assignment": "ff7b72", # =, +=, -=, etc.
|
||||
"operator_word": "ff7b72", # and, or, not, in, is
|
||||
"operator_dot": "c9d1d9", # . - subtle for attribute access
|
||||
|
||||
# Punctuation - neutral
|
||||
"punctuation": "b4b4b4",
|
||||
"punctuation_brace": "b4b4b4", # [, ], {, }
|
||||
"punctuation_paren": "b4b4b4", # (, )
|
||||
"punctuation_colon": "b4b4b4", # :, ;
|
||||
"punctuation_comma": "8b949e", # , - slightly dimmed
|
||||
|
||||
# Decorators - burnt orange
|
||||
"decorator": "f0883e",
|
||||
|
||||
# Constants - same as keywords
|
||||
"constant": "ff7b72",
|
||||
"constant_builtin": "ff7b72", # True, False, None
|
||||
|
||||
# Type hints and annotations - amber
|
||||
"type_hint": "ffa657",
|
||||
"type_builtin": "ffa657",
|
||||
|
||||
# Exception handling - alert red
|
||||
"exception": "f85149",
|
||||
"exception_builtin": "f85149",
|
||||
|
||||
# Names and attributes - near-white for readability
|
||||
"name": "e6edf3",
|
||||
"attribute": "e6edf3",
|
||||
"attribute_call": "d2a8ff", # Function/method calls after dot - lavender
|
||||
"variable": "e6edf3",
|
||||
"parameter": "e6edf3",
|
||||
|
||||
# Namespaces and modules
|
||||
"namespace": "7ee787",
|
||||
"module": "a5d6ff",
|
||||
|
||||
# Generic tokens
|
||||
"generic_heading": "c9d1d9",
|
||||
"generic_subheading": "c9d1d9",
|
||||
"generic_prompt": "8b949e",
|
||||
"generic_error": "f85149",
|
||||
"generic_deleted": "f85149",
|
||||
"generic_inserted": "7ee787",
|
||||
"generic_output": "e6edf3",
|
||||
|
||||
# Text and whitespace - no color (None means no color tag)
|
||||
"text": None,
|
||||
"whitespace": None,
|
||||
}
|
||||
|
||||
def _check_pygments(self):
|
||||
try:
|
||||
import pygments
|
||||
from pygments.lexers import get_lexer_for_filename, guess_lexer, get_lexer_by_name
|
||||
from pygments.formatter import Formatter
|
||||
from pygments.token import Token
|
||||
|
||||
self.pygments = pygments
|
||||
self.pygments_available = True
|
||||
RNS.log("Pygments syntax highlighting available", RNS.LOG_DEBUG)
|
||||
|
||||
except ImportError:
|
||||
self.pygments_available = False
|
||||
RNS.log("Pygments not available, using plain text rendering", RNS.LOG_DEBUG)
|
||||
|
||||
def highlight(self, content, filename=None, language=None):
|
||||
if not content: return self._plain_text(content)
|
||||
|
||||
if self.pygments_available:
|
||||
try:
|
||||
highlighted = self._highlight_pygments(content, filename, language)
|
||||
# Fix pygments insisting on trailing newlines
|
||||
if highlighted.endswith("\n") and not content.endswith("\n"): highlighted = highlighted[:-1]
|
||||
return highlighted
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Pygments highlighting failed, falling back: {e}", RNS.LOG_WARNING)
|
||||
return self._plain_text(content).replace("\\", "\\\\")
|
||||
|
||||
# TODO: Implement Python tokenize fallback for .py files.
|
||||
# For now, route to plain text
|
||||
if filename and filename.endswith(".py"):
|
||||
return self._plain_text(content).replace("\\", "\\\\")
|
||||
|
||||
# Universal fallback
|
||||
return self._plain_text(content).replace("\\", "\\\\")
|
||||
|
||||
def _highlight_pygments(self, content, filename=None, language=None):
|
||||
from pygments.lexers import get_lexer_for_filename, guess_lexer, get_lexer_by_name
|
||||
from pygments.util import ClassNotFound
|
||||
|
||||
lexer = None
|
||||
if language:
|
||||
if language == "env": language = "bash"
|
||||
if language == "environment": language = "bash"
|
||||
try: lexer = get_lexer_by_name(language)
|
||||
except ClassNotFound: pass
|
||||
|
||||
if lexer is None and filename:
|
||||
try: lexer = get_lexer_for_filename(filename)
|
||||
except ClassNotFound: pass
|
||||
|
||||
if lexer is None:
|
||||
try:
|
||||
if len(content) > 20: lexer = guess_lexer(content)
|
||||
except ClassNotFound: pass
|
||||
|
||||
if lexer is None: return self._plain_text(content)
|
||||
|
||||
formatter = MicronFormatter(theme=self.theme)
|
||||
result = self.pygments.highlight(content, lexer, formatter)
|
||||
return result
|
||||
|
||||
def _plain_text(self, content):
|
||||
escaped = self._escape_micron(content)
|
||||
return f"`=\n{escaped}\n`="
|
||||
|
||||
@staticmethod
|
||||
def _escape_micron(text): return text.replace("`", "\\`")
|
||||
|
||||
|
||||
class MicronFormatter:
|
||||
def __init__(self, theme, **options):
|
||||
self.theme = theme
|
||||
self.options = options
|
||||
|
||||
def format(self, tokensource, outfile):
|
||||
output_parts = []
|
||||
prev_was_dot = False
|
||||
|
||||
last_ended_with_break = True
|
||||
for ttype, value in tokensource:
|
||||
is_dot = (str(ttype) == "Token.Operator" and value == ".")
|
||||
ends_with_break = value.endswith("\n")
|
||||
|
||||
# If previous token was a dot and this is a Name, treat as attribute/function call
|
||||
# TODO: Improve this if we can check next token as parantheses or something.
|
||||
if prev_was_dot and str(ttype).startswith("Token.Name") and value:
|
||||
color = self._get_color_from_key("attribute_call")
|
||||
if color:
|
||||
escaped = self._escape_value(value)
|
||||
output_parts.append(f"`FT{color}{escaped}`f")
|
||||
else:
|
||||
output_parts.append(self._escape_value(value))
|
||||
|
||||
else:
|
||||
color_key = self._get_color_key_for_token(ttype)
|
||||
color = self._get_color_from_key(color_key)
|
||||
|
||||
if color and value:
|
||||
escaped = self._escape_value(value)
|
||||
if escaped.startswith("\n"): ilb = "\n"; escaped = escaped[1:]
|
||||
else: ilb = ""
|
||||
if escaped.endswith("\n"): tlb = "\n"; escaped = escaped[:-1]
|
||||
else: tlb = ""
|
||||
|
||||
if len(escaped): output = f"{ilb}`FT{color}{escaped}`f{tlb}"
|
||||
else: output = f"{ilb}{tlb}"
|
||||
|
||||
output_parts.append(output)
|
||||
|
||||
else:
|
||||
escaped = self._escape_value(value)
|
||||
if "\n" in escaped:
|
||||
parts = []
|
||||
splitl = escaped.splitlines()
|
||||
if len(splitl) > 1:
|
||||
for line in splitl:
|
||||
if line.startswith("-"): l = f"\\{line}"
|
||||
elif line.startswith(">"): l = f"\\{line}"
|
||||
elif line.startswith("<"): l = f"\\{line}"
|
||||
else: l = line
|
||||
parts.append(l)
|
||||
trmpart = "\n" if escaped.endswith("\n") else ""
|
||||
escaped = "\n".join(parts)+trmpart
|
||||
|
||||
elif last_ended_with_break:
|
||||
if escaped.startswith("-"): escaped = f"\\{escaped}"
|
||||
elif escaped.startswith(">"): escaped = f"\\{escaped}"
|
||||
elif escaped.startswith("<"): escaped = f"\\{escaped}"
|
||||
|
||||
output_parts.append(escaped)
|
||||
|
||||
prev_was_dot = is_dot
|
||||
last_ended_with_break = ends_with_break
|
||||
|
||||
output = "".join(output_parts)
|
||||
outfile.write(output)
|
||||
|
||||
def _get_color_key_for_token(self, ttype):
|
||||
token_parts = []
|
||||
current = ttype
|
||||
while current:
|
||||
token_parts.insert(0, current[0] if isinstance(current, tuple) else str(current).split(".")[-1])
|
||||
current = current.parent if hasattr(current, "parent") else None
|
||||
|
||||
token_str = ".".join(["Token"] + token_parts[1:] if len(token_parts) > 1 else token_parts)
|
||||
|
||||
current_type = ttype
|
||||
while current_type:
|
||||
token_key = str(current_type)
|
||||
if token_key in granular_token_map: return granular_token_map[token_key]
|
||||
|
||||
# Move to parent
|
||||
current_type = current_type.parent if hasattr(current_type, "parent") else None
|
||||
|
||||
return None
|
||||
|
||||
def _get_color_from_key(self, color_key):
|
||||
if color_key and color_key in self.theme: return self.theme[color_key]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _escape_value(value):
|
||||
return value.replace("\\", "\\\\").replace("`", "\\`")
|
||||
|
||||
# Required by Pygments formatter API, returns None for Micron
|
||||
def get_style_defs(self, arg=None): return None
|
||||
|
||||
|
||||
# Convenience function for direct use
|
||||
def highlight_code(content: str, filename: str = None, language: str = None, theme=None) -> str:
|
||||
highlighter = SyntaxHighlighter(theme=theme)
|
||||
return highlighter.highlight(content, filename, language)
|
||||
|
||||
granular_token_map = {
|
||||
# Keywords with semantic distinction
|
||||
"Token.Keyword": "keyword",
|
||||
"Token.Keyword.Constant": "keyword_constant",
|
||||
"Token.Keyword.Declaration": "keyword_declaration",
|
||||
"Token.Keyword.Namespace": "keyword_control",
|
||||
"Token.Keyword.Pseudo": "keyword_control",
|
||||
"Token.Keyword.Reserved": "keyword_control",
|
||||
"Token.Keyword.Type": "type_builtin",
|
||||
|
||||
# Names - functions with definition vs call distinction
|
||||
"Token.Name.Function": "function_call",
|
||||
"Token.Name.Function.Magic": "function_magic",
|
||||
"Token.Name.Class": "class_ref",
|
||||
"Token.Name.Builtin": "function_builtin",
|
||||
"Token.Name.Builtin.Pseudo": "constant_builtin",
|
||||
"Token.Name.Exception": "exception_builtin",
|
||||
"Token.Name.Decorator": "decorator",
|
||||
"Token.Name.Namespace": "namespace",
|
||||
"Token.Name.Attribute": "attribute",
|
||||
"Token.Name.Variable": "variable",
|
||||
"Token.Name.Variable.Magic": "function_magic",
|
||||
"Token.Name.Other": "name",
|
||||
"Token.Name": "name",
|
||||
"Token.Name.Tag": "keyword", # HTML/XML tags
|
||||
"Token.Name.Constant": "constant",
|
||||
"Token.Name.Label": "name",
|
||||
"Token.Name.Entity": "name",
|
||||
|
||||
# Literals - strings with detailed handling
|
||||
"Token.Literal.String": "string",
|
||||
"Token.Literal.String.Affix": "string", # f, r, b prefixes
|
||||
"Token.Literal.String.Backtick": "string",
|
||||
"Token.Literal.String.Char": "string",
|
||||
"Token.Literal.String.Delimiter": "string",
|
||||
"Token.Literal.String.Doc": "string_doc",
|
||||
"Token.Literal.String.Double": "string_quoted",
|
||||
"Token.Literal.String.Escape": "string_escape",
|
||||
"Token.Literal.String.Heredoc": "string",
|
||||
"Token.Literal.String.Interpol": "string_interpol",
|
||||
"Token.Literal.String.Other": "string",
|
||||
"Token.Literal.String.Regex": "string",
|
||||
"Token.Literal.String.Single": "string_quoted",
|
||||
"Token.Literal.String.Symbol": "string",
|
||||
|
||||
# Numbers
|
||||
"Token.Literal.Number": "number",
|
||||
"Token.Literal.Number.Bin": "number",
|
||||
"Token.Literal.Number.Float": "number_float",
|
||||
"Token.Literal.Number.Hex": "number_hex",
|
||||
"Token.Literal.Number.Integer": "number_integer",
|
||||
"Token.Literal.Number.Integer.Long": "number_integer",
|
||||
"Token.Literal.Number.Oct": "number",
|
||||
"Token.Literal": "string",
|
||||
"Token.Literal.Date": "string",
|
||||
|
||||
# Operators - all operators get distinct coloring
|
||||
"Token.Operator": "operator",
|
||||
"Token.Operator.Word": "operator_word",
|
||||
"Token.Operator.Comparison": "operator_comparison",
|
||||
"Token.Operator.Assignment": "operator_assignment",
|
||||
"Token.Operator.Arithmetic": "operator_arithmetic",
|
||||
|
||||
# Punctuation - braces, parens, colons, commas
|
||||
"Token.Punctuation": "punctuation",
|
||||
"Token.Punctuation.Marker": "punctuation",
|
||||
"Token.Punctuation.Brace": "punctuation_brace",
|
||||
"Token.Punctuation.Bracket": "punctuation_brace",
|
||||
"Token.Punctuation.Parenthesis": "punctuation_paren",
|
||||
"Token.Punctuation.Colon": "punctuation_colon",
|
||||
"Token.Punctuation.Comma": "punctuation_comma",
|
||||
|
||||
# Comments
|
||||
"Token.Comment": "comment",
|
||||
"Token.Comment.Hashbang": "comment",
|
||||
"Token.Comment.Multiline": "comment_doc",
|
||||
"Token.Comment.Preproc": "comment_preproc",
|
||||
"Token.Comment.Single": "comment",
|
||||
"Token.Comment.Special": "comment",
|
||||
|
||||
# Generic tokens
|
||||
"Token.Generic.Deleted": "generic_deleted",
|
||||
"Token.Generic.Emph": "text",
|
||||
"Token.Generic.Error": "generic_error",
|
||||
"Token.Generic.Heading": "generic_heading",
|
||||
"Token.Generic.Inserted": "generic_inserted",
|
||||
"Token.Generic.Output": "generic_output",
|
||||
"Token.Generic.Prompt": "generic_prompt",
|
||||
"Token.Generic.Strong": "text",
|
||||
"Token.Generic.Subheading": "generic_subheading",
|
||||
"Token.Generic.Traceback": "generic_error",
|
||||
"Token.Generic": "text",
|
||||
|
||||
# Text and whitespace
|
||||
"Token.Text": "text",
|
||||
"Token.Text.Whitespace": "whitespace",
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 sys
|
||||
from RNS.Utilities.rngit import client, server
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[0]
|
||||
if cmd == "rngit": ec = server.main()
|
||||
elif cmd == "git-remote-rns": ec = client.main()
|
||||
else: raise NotImplementedError(f"The {cmd} executable entrypoint is not yet implemented in rngit")
|
||||
|
||||
sys.exit(ec)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,816 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 re
|
||||
import RNS
|
||||
|
||||
# Validate ref names according to https://git-scm.com/docs/git-check-ref-format
|
||||
# This may be a bit overkill, since git validates names as well, but why not.
|
||||
def san_ref(ref):
|
||||
if ref.startswith("-"): return None
|
||||
if ref.startswith("/"): return None
|
||||
if ref.endswith("/"): return None
|
||||
if ref.endswith("."): return None
|
||||
|
||||
if " " in ref: return None
|
||||
if not "/" in ref: return None
|
||||
if ".." in ref: return None
|
||||
if "/." in ref: return None
|
||||
if "//" in ref: return None
|
||||
if "\\" in ref: return None
|
||||
|
||||
for comp in ref.split("/"):
|
||||
if comp.endswith(".lock"): return None
|
||||
|
||||
if not all(ord(c) >= 40 for c in ref): return None # Any control character
|
||||
if "\x7f" in ref: return None # ASCII DEL (177)
|
||||
if "~" in ref: return None
|
||||
if "^" in ref: return None
|
||||
if ":" in ref: return None
|
||||
if "?" in ref: return None
|
||||
if "*" in ref: return None
|
||||
if "[" in ref: return None
|
||||
if "@{" in ref: return None
|
||||
if "@" == ref: return None
|
||||
|
||||
return ref
|
||||
|
||||
def san_refs(refs):
|
||||
if not type(refs) == list: return None
|
||||
for ref in refs:
|
||||
if not san_ref(ref): return None
|
||||
|
||||
return refs
|
||||
|
||||
# Git SHA format validation
|
||||
def san_sha(sha):
|
||||
if len(sha) < 40: return None
|
||||
try: bytes.fromhex(sha)
|
||||
except: return None
|
||||
return sha
|
||||
|
||||
class MarkdownToMicron:
|
||||
BOLD = "`!"
|
||||
BOLD_END = "`!"
|
||||
ITALIC = "`*"
|
||||
ITALIC_END = "`*"
|
||||
UNDERLINE = "`_"
|
||||
UNDERLINE_END = "`_"
|
||||
|
||||
CODE_BG = "`BT282828"
|
||||
CODE_BG_INLINE = "`BT383838"
|
||||
CODE_FG = "`Fddd"
|
||||
CODE_RESET = "`f`b"
|
||||
|
||||
LITERAL_START = "`="
|
||||
LITERAL_END = "`="
|
||||
|
||||
BULLET = "•"
|
||||
|
||||
# Regex patterns for markdown elements
|
||||
HEADER_RE = re.compile(r'^(#{1,6})\s+(.+)$')
|
||||
CODE_FENCE_RE = re.compile(r'^(\s*)```(.*)$')
|
||||
HORIZONTAL_RULE_RE = re.compile(r'^(\s*)(---+|===+|\*\*\*+|___+)\s*$')
|
||||
UNORDERED_LIST_RE = re.compile(r'^(\s*)([-*+])\s+(.+)$')
|
||||
|
||||
# Table patterns
|
||||
TABLE_ROW_RE = re.compile(r'^\s*\|?(.+?)\|?\s*$')
|
||||
TABLE_SEP_RE = re.compile(r'^\s*\|?(?:\s*:?-+:?\s*\|)+\s*$')
|
||||
|
||||
# Quote pattern
|
||||
QUOTE_RE = re.compile(r'^>\s?(.*)$')
|
||||
|
||||
# Inline patterns (processed in order of specificity)
|
||||
LINK_RE = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
|
||||
INLINE_CODE_RE = re.compile(r'`([^`]+)`')
|
||||
BOLD_RE = re.compile(r'\*\*(.+?)\*\*|__(.+?)__')
|
||||
ITALIC_RE = re.compile(r'\*(.+?)\*|_(.+?)_')
|
||||
|
||||
TABLE_H = "─"
|
||||
TABLE_V = "│"
|
||||
TABLE_TL = "┌"
|
||||
TABLE_TR = "┐"
|
||||
TABLE_BL = "└"
|
||||
TABLE_BR = "┘"
|
||||
TABLE_ML = "├"
|
||||
TABLE_MR = "┤"
|
||||
TABLE_TM = "┬"
|
||||
TABLE_BM = "┴"
|
||||
TABLE_MM = "┼"
|
||||
|
||||
TABLE_MIN_COL_WIDTH = 3
|
||||
|
||||
def __init__(self, max_width=100, syntax_highlighter=None, url_scope=None):
|
||||
self.max_width = max_width
|
||||
self.local_url_scope = url_scope or ":/page/"
|
||||
self.__local_url_scope = self.local_url_scope
|
||||
self.syntax_highlighter = syntax_highlighter
|
||||
self.wcwidth = None
|
||||
|
||||
self.bold_links = True
|
||||
self.underline_links = True
|
||||
self.link_color = None
|
||||
|
||||
try:
|
||||
import wcwidth
|
||||
self.wcwidth = wcwidth
|
||||
|
||||
except: RNS.log(f"The wcwidth module is unavailable, display width calculations for some glyphs will be incorrect", RNS.LOG_WARNING)
|
||||
|
||||
def set_url_scope(self, url_scope): self.local_url_scope = url_scope
|
||||
def restore_url_scope(self, url_scope): self.local_url_scope = self.__local_url_scope
|
||||
|
||||
def display_width(self, text):
|
||||
if not self.wcwidth: return len(text)
|
||||
else:
|
||||
# wcswidth returns -1 for non-printable strings,
|
||||
# fallback to len in this case
|
||||
w = self.wcwidth.wcswidth(text)
|
||||
return w if w is not None and w >= 0 else len(text)
|
||||
|
||||
def format_block(self, text, url_scope=None):
|
||||
# text = text.replace("\\", "\\\\") # Now handled in format_line instead
|
||||
lines = text.split('\n')
|
||||
result_lines = []
|
||||
in_code_block = False
|
||||
code_block_lang = None
|
||||
code_buffer = []
|
||||
in_table = False
|
||||
table_buffer = []
|
||||
in_quote = False
|
||||
quote_buffer = []
|
||||
|
||||
def flush_quote_buffer():
|
||||
nonlocal result_lines, quote_buffer, in_quote
|
||||
if not quote_buffer:
|
||||
in_quote = False
|
||||
return
|
||||
|
||||
para = " ".join(quote_buffer)
|
||||
formatted = self._format_inline(para)
|
||||
|
||||
effective_width = self.max_width - 3
|
||||
if effective_width < 1: effective_width = 1
|
||||
wrapped_lines = self._wrap_text(formatted, effective_width)
|
||||
for wrapped_line in wrapped_lines: result_lines.append(f" │ {wrapped_line}")
|
||||
|
||||
quote_buffer = []
|
||||
in_quote = False
|
||||
|
||||
def flush_table_buffer():
|
||||
nonlocal result_lines, table_buffer, in_table
|
||||
if not table_buffer:
|
||||
in_table = False
|
||||
return
|
||||
|
||||
if len(table_buffer) >= 2 and self._is_table_separator(table_buffer[1]):
|
||||
formatted_lines = self.format_table(table_buffer)
|
||||
result_lines.extend(formatted_lines)
|
||||
|
||||
else:
|
||||
for line in table_buffer: result_lines.append(self.format_line(line))
|
||||
|
||||
table_buffer = []
|
||||
in_table = False
|
||||
|
||||
def flush_code_block():
|
||||
nonlocal result_lines, code_buffer, code_block_lang
|
||||
if not code_buffer:
|
||||
return
|
||||
|
||||
code_content = '\n'.join(code_buffer)
|
||||
|
||||
if self.syntax_highlighter and code_block_lang:
|
||||
if code_block_lang.lower() == "rawmu": result_lines.append(code_content)
|
||||
else:
|
||||
try:
|
||||
highlighted = self.syntax_highlighter.highlight(code_content, language=code_block_lang)
|
||||
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
|
||||
result_lines.append(highlighted)
|
||||
result_lines.append(self.CODE_RESET)
|
||||
|
||||
except Exception:
|
||||
# Fallback to plain literal block on any error
|
||||
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
|
||||
result_lines.append(self.LITERAL_START)
|
||||
result_lines.append(self._escape_literals(code_content))
|
||||
result_lines.append(self.LITERAL_END)
|
||||
result_lines.append(self.CODE_RESET)
|
||||
else:
|
||||
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
|
||||
result_lines.append(self.LITERAL_START)
|
||||
result_lines.append(self._escape_literals(code_content))
|
||||
result_lines.append(self.LITERAL_END)
|
||||
result_lines.append(self.CODE_RESET)
|
||||
|
||||
code_buffer = []
|
||||
|
||||
for line in lines:
|
||||
is_fence, lang_hint = self._detect_code_fence(line)
|
||||
|
||||
if is_fence:
|
||||
# Flush any pending structures before code fence
|
||||
flush_quote_buffer()
|
||||
flush_table_buffer()
|
||||
|
||||
if not in_code_block:
|
||||
# Opening fence, start buffering
|
||||
in_code_block = True
|
||||
code_block_lang = lang_hint.strip() if lang_hint else None
|
||||
code_buffer = []
|
||||
|
||||
else:
|
||||
# Closing fence, flush highlighted code
|
||||
flush_code_block()
|
||||
in_code_block = False
|
||||
code_block_lang = None
|
||||
|
||||
else:
|
||||
# Buffer code lines for later highlighting
|
||||
if in_code_block: code_buffer.append(line)
|
||||
else:
|
||||
quote_match = self.QUOTE_RE.match(line)
|
||||
if quote_match:
|
||||
if not in_quote:
|
||||
flush_table_buffer()
|
||||
in_quote = True
|
||||
quote_buffer = []
|
||||
|
||||
quote_buffer.append(quote_match.group(1))
|
||||
|
||||
else:
|
||||
if in_quote:
|
||||
flush_quote_buffer()
|
||||
if line.strip() != "":
|
||||
if self._is_table_row(line):
|
||||
in_table = True
|
||||
table_buffer = [line]
|
||||
|
||||
else:
|
||||
formatted = self.format_line(line)
|
||||
result_lines.append(formatted)
|
||||
|
||||
# Pass through blank line as separator
|
||||
else: result_lines.append("")
|
||||
|
||||
else:
|
||||
if self._is_table_row(line):
|
||||
if not in_table:
|
||||
in_table = True
|
||||
table_buffer = [line]
|
||||
|
||||
else: table_buffer.append(line)
|
||||
|
||||
else:
|
||||
# Line breaks table, flush buffer
|
||||
if in_table: flush_table_buffer()
|
||||
formatted = self.format_line(line)
|
||||
result_lines.append(formatted)
|
||||
|
||||
# Handle unclosed structures
|
||||
if in_quote: flush_quote_buffer()
|
||||
if in_table: flush_table_buffer()
|
||||
if in_code_block: flush_code_block()
|
||||
|
||||
return '\n'.join(result_lines)
|
||||
|
||||
def format_line(self, line, mode="normal"):
|
||||
if mode == "codeblock": return self._escape_literals(line)
|
||||
line = line.replace("\\", "\\\\")
|
||||
|
||||
if line.startswith("-") and not line.startswith("---") and not line.startswith("- "): line = f"\\{line}"
|
||||
if line.startswith("<"): line = f"\\{line}"
|
||||
# if line.startswith(">"): line = f"\\{line}" # Now handled by blockquotes
|
||||
|
||||
if self.HORIZONTAL_RULE_RE.match(line): return self._format_horizontal_rule()
|
||||
|
||||
header_match = self.HEADER_RE.match(line)
|
||||
if header_match: return self._format_header(header_match)
|
||||
|
||||
list_match = self.UNORDERED_LIST_RE.match(line)
|
||||
if list_match: return self._format_list_item(list_match)
|
||||
|
||||
line = self._format_inline(line)
|
||||
|
||||
return line
|
||||
|
||||
def _format_inline(self, text):
|
||||
code_blocks = []
|
||||
def extract_code(match):
|
||||
code_blocks.append(match.group(1))
|
||||
return f"\x00CODE{len(code_blocks)-1}\x00"
|
||||
|
||||
links = []
|
||||
def extract_link(match):
|
||||
links.append((match.group(1), match.group(2)))
|
||||
return f"\x00LINK{len(links)-1}\x00"
|
||||
|
||||
text = self.LINK_RE.sub(extract_link, text)
|
||||
text = self.INLINE_CODE_RE.sub(extract_code, text)
|
||||
text = self.BOLD_RE.sub(self._bold_sub, text)
|
||||
text = self.ITALIC_RE.sub(self._italic_sub, text)
|
||||
|
||||
def restore_link(match):
|
||||
idx = int(match.group(1))
|
||||
text, url = links[idx]
|
||||
|
||||
anchor_components = url.split("#")
|
||||
url = anchor_components[0]
|
||||
anchor = anchor_components[1] if len(anchor_components) > 1 else ""
|
||||
|
||||
if not ":/" in url:
|
||||
url = f"{self.local_url_scope}{url}"
|
||||
if anchor: url = f"{url}|anchor={anchor}"
|
||||
|
||||
undl = "`_" if self.underline_links else ""
|
||||
bold = "`!" if self.bold_links else ""
|
||||
text = text.replace('`', '')
|
||||
link = f"{undl}{bold}`[{text}`{url}]{bold}{undl}"
|
||||
|
||||
if self.link_color and len(self.link_color) == 3: link = f"`F{self.link_color}{link}`f"
|
||||
if self.link_color and len(self.link_color) == 6: link = f"`FT{self.link_color}{link}`f"
|
||||
|
||||
return link
|
||||
|
||||
text = re.sub(r'\x00LINK(\d+)\x00', restore_link, text)
|
||||
|
||||
def restore_code(match):
|
||||
idx = int(match.group(1))
|
||||
content = code_blocks[idx]
|
||||
content = content.replace('`', '\\`')
|
||||
return f"{self.CODE_BG_INLINE}{self.CODE_FG}{content}{self.CODE_RESET}"
|
||||
|
||||
text = re.sub(r'\x00CODE(\d+)\x00', restore_code, text)
|
||||
return text
|
||||
|
||||
def _highlight_inline_code(self, content):
|
||||
if not self.syntax_highlighter: return None
|
||||
return self.syntax_highlighter.highlight(content, language=None)
|
||||
|
||||
def _bold_sub(self, match):
|
||||
content = match.group(1) or match.group(2)
|
||||
return f"{self.BOLD}{content}{self.BOLD_END}"
|
||||
|
||||
def _italic_sub(self, match):
|
||||
content = match.group(1) or match.group(2)
|
||||
return f"{self.ITALIC}{content}{self.ITALIC_END}"
|
||||
|
||||
def _format_header(self, match):
|
||||
hashes = match.group(1)
|
||||
content = match.group(2)
|
||||
level = len(hashes)
|
||||
prefix = ">" * min(level, 6)
|
||||
return f"{prefix}{self._format_inline(content)}"
|
||||
|
||||
def _format_list_item(self, match):
|
||||
indent = match.group(1)
|
||||
content = match.group(3)
|
||||
content = self._format_inline(content)
|
||||
return f"{indent} {self.BULLET} {content}"
|
||||
|
||||
def _format_horizontal_rule(self):
|
||||
return "-"
|
||||
|
||||
def _detect_code_fence(self, line):
|
||||
match = self.CODE_FENCE_RE.match(line)
|
||||
if match:
|
||||
# match.group(2) contains everything after the backticks (language hint)
|
||||
return True, match.group(2)
|
||||
return False, ""
|
||||
|
||||
def _is_table_row(self, line):
|
||||
if '|' not in line: return False
|
||||
match = self.TABLE_ROW_RE.match(line)
|
||||
if match is None: return False
|
||||
content = match.group(1)
|
||||
return '|' in content or line.strip().startswith('|')
|
||||
|
||||
def _is_table_separator(self, line):
|
||||
if '|' not in line: return False
|
||||
match = self.TABLE_SEP_RE.match(line)
|
||||
return match is not None
|
||||
|
||||
def _escape_literals(self, text):
|
||||
return text.replace('`', '\\`')
|
||||
|
||||
def format_table(self, rows, align="c"):
|
||||
if len(rows) < 2: return rows
|
||||
|
||||
# Parse header and separator
|
||||
header_cells = self._parse_table_row(rows[0])
|
||||
alignments = self._parse_table_alignments(rows[1])
|
||||
|
||||
# Ensure alignment count matches header cells
|
||||
while len(alignments) < len(header_cells): alignments.append('left')
|
||||
alignments = alignments[:len(header_cells)]
|
||||
|
||||
# Parse data rows
|
||||
data_rows = []
|
||||
for i in range(2, len(rows)):
|
||||
cells = self._parse_table_row(rows[i])
|
||||
while len(cells) < len(header_cells): cells.append("")
|
||||
cells = cells[:len(header_cells)]
|
||||
data_rows.append(cells)
|
||||
|
||||
# Calculate column widths based on content
|
||||
num_cols = len(header_cells)
|
||||
col_widths = [0] * num_cols
|
||||
|
||||
all_rows = [header_cells] + data_rows
|
||||
for row in all_rows:
|
||||
for i, cell in enumerate(row):
|
||||
formatted = self._format_inline(cell)
|
||||
width = self._visible_width(formatted)
|
||||
col_widths[i] = max(col_widths[i], width)
|
||||
|
||||
# Apply minimum width and calculate total
|
||||
col_widths = [max(w, self.TABLE_MIN_COL_WIDTH) for w in col_widths]
|
||||
|
||||
# Check max_width constraint
|
||||
# Total = sum of columns + 3 chars per column (space + 2 borders) + 1 for final border
|
||||
total_width = sum(col_widths) + (num_cols * 3) + 1
|
||||
|
||||
if total_width > self.max_width:
|
||||
# Reduce widest columns proportionally
|
||||
excess = total_width - self.max_width
|
||||
indexed_widths = [(i, w) for i, w in enumerate(col_widths)]
|
||||
indexed_widths.sort(key=lambda x: -x[1])
|
||||
|
||||
for i, w in indexed_widths:
|
||||
if excess <= 0: break
|
||||
reduction = min(excess, w - self.TABLE_MIN_COL_WIDTH)
|
||||
col_widths[i] -= reduction
|
||||
excess -= reduction
|
||||
|
||||
# Build formatted table
|
||||
result = []
|
||||
|
||||
# Alignment start
|
||||
if align: result.append(f"`{align}")
|
||||
|
||||
# Top border
|
||||
border = self.TABLE_TL
|
||||
for i, w in enumerate(col_widths):
|
||||
border += self.TABLE_H * (w + 2)
|
||||
if i < len(col_widths) - 1: border += self.TABLE_TM
|
||||
else: border += self.TABLE_TR
|
||||
|
||||
result.append(self._escape_literals(border))
|
||||
|
||||
# Header row
|
||||
header_line = self.TABLE_V
|
||||
for i, cell in enumerate(header_cells):
|
||||
formatted = self._format_inline(cell)
|
||||
padded = self._pad_cell(formatted, col_widths[i], 'left')
|
||||
header_line += f" {padded} {self.TABLE_V}"
|
||||
result.append(self._escape_literals(header_line))
|
||||
|
||||
# Separator row
|
||||
sep_line = self.TABLE_ML
|
||||
for i, w in enumerate(col_widths):
|
||||
cell_width = w + 2
|
||||
sep_line += self.TABLE_H * cell_width
|
||||
|
||||
if i < len(col_widths) - 1: sep_line += self.TABLE_MM
|
||||
else: sep_line += self.TABLE_MR
|
||||
|
||||
result.append(self._escape_literals(sep_line))
|
||||
|
||||
# Data rows
|
||||
for row in data_rows:
|
||||
row_line = self.TABLE_V
|
||||
for i, cell in enumerate(row):
|
||||
formatted = self._format_inline(cell)
|
||||
padded = self._pad_cell(formatted, col_widths[i], alignments[i])
|
||||
row_line += f" {padded} {self.TABLE_V}"
|
||||
|
||||
result.append(row_line)
|
||||
|
||||
# Bottom border
|
||||
border = self.TABLE_BL
|
||||
for i, w in enumerate(col_widths):
|
||||
border += self.TABLE_H * (w + 2)
|
||||
if i < len(col_widths) - 1: border += self.TABLE_BM
|
||||
else: border += self.TABLE_BR
|
||||
|
||||
result.append(self._escape_literals(border))
|
||||
|
||||
# End alignment
|
||||
if align: result.append("`a")
|
||||
|
||||
return result
|
||||
|
||||
def format_table_raw(self, rows, align="c"):
|
||||
if len(rows) < 2: return rows
|
||||
|
||||
# Parse header and separator
|
||||
header_cells = self._parse_table_row(rows[0])
|
||||
alignments = self._parse_table_alignments(rows[1])
|
||||
|
||||
# Ensure alignment count matches header cells
|
||||
while len(alignments) < len(header_cells): alignments.append('left')
|
||||
alignments = alignments[:len(header_cells)]
|
||||
|
||||
# Parse data rows
|
||||
data_rows = []
|
||||
for i in range(2, len(rows)):
|
||||
cells = self._parse_table_row(rows[i])
|
||||
while len(cells) < len(header_cells): cells.append("")
|
||||
cells = cells[:len(header_cells)]
|
||||
data_rows.append(cells)
|
||||
|
||||
# Calculate column widths based on raw content
|
||||
num_cols = len(header_cells)
|
||||
col_widths = [0] * num_cols
|
||||
|
||||
all_rows = [header_cells] + data_rows
|
||||
for row in all_rows:
|
||||
for i, cell in enumerate(row):
|
||||
width = self._visible_width(cell)
|
||||
col_widths[i] = max(col_widths[i], width)
|
||||
|
||||
# Apply minimum width and calculate total
|
||||
col_widths = [max(w, self.TABLE_MIN_COL_WIDTH) for w in col_widths]
|
||||
|
||||
# Check max_width constraint
|
||||
total_width = sum(col_widths) + (num_cols * 3) + 1
|
||||
|
||||
if total_width > self.max_width:
|
||||
# Reduce widest columns proportionally
|
||||
excess = total_width - self.max_width
|
||||
indexed_widths = [(i, w) for i, w in enumerate(col_widths)]
|
||||
indexed_widths.sort(key=lambda x: -x[1])
|
||||
|
||||
for i, w in indexed_widths:
|
||||
if excess <= 0: break
|
||||
reduction = min(excess, w - self.TABLE_MIN_COL_WIDTH)
|
||||
col_widths[i] -= reduction
|
||||
excess -= reduction
|
||||
|
||||
# Build formatted table
|
||||
result = []
|
||||
|
||||
# Alignment start
|
||||
if align: result.append(f"`{align}")
|
||||
|
||||
# Top border
|
||||
border = self.TABLE_TL
|
||||
for i, w in enumerate(col_widths):
|
||||
border += self.TABLE_H * (w + 2)
|
||||
if i < len(col_widths) - 1: border += self.TABLE_TM
|
||||
else: border += self.TABLE_TR
|
||||
|
||||
result.append(self._escape_literals(border))
|
||||
|
||||
# Header row
|
||||
header_line = self.TABLE_V
|
||||
for i, cell in enumerate(header_cells):
|
||||
padded = self._pad_cell(cell, col_widths[i], 'left')
|
||||
header_line += f" {padded} {self.TABLE_V}"
|
||||
result.append(header_line)
|
||||
|
||||
# Separator row - clean horizontal lines without alignment markers
|
||||
sep_line = self.TABLE_ML
|
||||
for i, w in enumerate(col_widths):
|
||||
cell_width = w + 2
|
||||
sep_line += self.TABLE_H * cell_width
|
||||
|
||||
if i < len(col_widths) - 1: sep_line += self.TABLE_MM
|
||||
else: sep_line += self.TABLE_MR
|
||||
|
||||
result.append(self._escape_literals(sep_line))
|
||||
|
||||
# Data rows (with alignment)
|
||||
for row in data_rows:
|
||||
row_line = self.TABLE_V
|
||||
for i, cell in enumerate(row):
|
||||
padded = self._pad_cell(cell, col_widths[i], alignments[i])
|
||||
row_line += f" {padded} {self.TABLE_V}"
|
||||
|
||||
result.append(row_line)
|
||||
|
||||
# Bottom border
|
||||
border = self.TABLE_BL
|
||||
for i, w in enumerate(col_widths):
|
||||
border += self.TABLE_H * (w + 2)
|
||||
if i < len(col_widths) - 1: border += self.TABLE_BM
|
||||
else: border += self.TABLE_BR
|
||||
|
||||
result.append(self._escape_literals(border))
|
||||
|
||||
# End alignment
|
||||
if align: result.append("`a")
|
||||
|
||||
return result
|
||||
|
||||
def _parse_table_row(self, line):
|
||||
line = line.strip()
|
||||
if line.startswith('|'): line = line[1:]
|
||||
if line.endswith('|'): line = line[:-1]
|
||||
|
||||
cells = []
|
||||
current = ""
|
||||
escaped = False
|
||||
for char in line:
|
||||
if escaped:
|
||||
current += char
|
||||
escaped = False
|
||||
elif char == '\\':
|
||||
escaped = True
|
||||
elif char == '|':
|
||||
cells.append(current.strip())
|
||||
current = ""
|
||||
else:
|
||||
current += char
|
||||
|
||||
cells.append(current.strip())
|
||||
return cells
|
||||
|
||||
def _parse_table_alignments(self, line):
|
||||
cells = self._parse_table_row(line)
|
||||
alignments = []
|
||||
for cell in cells:
|
||||
cell = cell.strip()
|
||||
if cell.startswith(':') and cell.endswith(':'): alignments.append('center')
|
||||
elif cell.endswith(':'): alignments.append('right')
|
||||
else: alignments.append('left')
|
||||
|
||||
return alignments
|
||||
|
||||
def _visible_width(self, text):
|
||||
text = re.sub(r'`[FB][0-9a-fA-F]{3}', '', text)
|
||||
text = re.sub(r'`[FB]T[0-9a-fA-F]{6}', '', text)
|
||||
text = re.sub(r'`[!*_=]', '', text)
|
||||
text = re.sub(r'`f`b', '', text)
|
||||
text = re.sub(r'`f', '', text)
|
||||
text = re.sub(r'`b', '', text)
|
||||
return self.display_width(text)
|
||||
|
||||
def _pad_cell(self, text, width, align):
|
||||
text = self._truncate_cell(text, width)
|
||||
text_width = self._visible_width(text)
|
||||
padding = width - text_width
|
||||
|
||||
if align == 'right':
|
||||
return " " * padding + text
|
||||
elif align == 'center':
|
||||
left = padding // 2
|
||||
right = padding - left
|
||||
return " " * left + text + " " * right
|
||||
else:
|
||||
return text + " " * padding
|
||||
|
||||
def _truncate_cell(self, text, width):
|
||||
if self._visible_width(text) <= width: return text
|
||||
|
||||
truncation_point = len(text)
|
||||
while truncation_point > 0 and self._visible_width(text[0:truncation_point]) >= width:
|
||||
truncation_point -= 1
|
||||
|
||||
truncated = text[:truncation_point]
|
||||
|
||||
# Yes, this is convoluted, but if someone else has
|
||||
# a better idea on how to handle unclosed micron
|
||||
# tags in the truncated cells, I'm all ears.
|
||||
active_tags = set()
|
||||
fg_active = False
|
||||
bg_active = False
|
||||
|
||||
i = 0
|
||||
while i < len(truncated):
|
||||
if truncated[i] == '`':
|
||||
if i + 1 < len(truncated):
|
||||
tag_char = truncated[i + 1]
|
||||
|
||||
if tag_char in '!*_=':
|
||||
if tag_char in active_tags: active_tags.remove(tag_char)
|
||||
else: active_tags.add(tag_char)
|
||||
i += 2
|
||||
continue
|
||||
|
||||
elif tag_char == 'f':
|
||||
fg_active = False
|
||||
i += 2
|
||||
continue
|
||||
|
||||
elif tag_char == 'b':
|
||||
bg_active = False
|
||||
i += 2
|
||||
continue
|
||||
|
||||
elif tag_char == 'F':
|
||||
fg_active = True
|
||||
if i + 2 < len(truncated) and truncated[i + 2] == 'T': i += 8
|
||||
else: i += 5
|
||||
continue
|
||||
|
||||
elif tag_char == 'B':
|
||||
bg_active = True
|
||||
if i + 2 < len(truncated) and truncated[i + 2] == 'T': i += 8
|
||||
else: i += 5
|
||||
continue
|
||||
i += 1
|
||||
|
||||
closers = []
|
||||
if fg_active: closers.append('`f')
|
||||
if bg_active: closers.append('`b')
|
||||
for fmt in active_tags: closers.append(f'`{fmt}')
|
||||
|
||||
return truncated + ''.join(closers) + "…"
|
||||
|
||||
def _wrap_text(self, text, width):
|
||||
if not text: return [""]
|
||||
|
||||
words = text.split(' ')
|
||||
lines = []
|
||||
current_line = ""
|
||||
current_width = 0
|
||||
|
||||
for word in words:
|
||||
if not word: continue
|
||||
|
||||
word_width = self._visible_width(word)
|
||||
|
||||
# Check if word alone exceeds width to force break it
|
||||
if word_width > width:
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
current_line = ""
|
||||
current_width = 0
|
||||
|
||||
# Force break the long word character by character
|
||||
remaining = word
|
||||
while remaining:
|
||||
# Binary search for how many characters fit
|
||||
low, high = 1, len(remaining)
|
||||
fit_chars = 0
|
||||
|
||||
while low <= high:
|
||||
mid = (low + high) // 2
|
||||
test_substr = remaining[:mid]
|
||||
test_width = self._visible_width(test_substr)
|
||||
|
||||
if test_width <= width:
|
||||
fit_chars = mid
|
||||
low = mid + 1
|
||||
else:
|
||||
high = mid - 1
|
||||
|
||||
if fit_chars == 0: fit_chars = 1 # Need to force progress
|
||||
|
||||
lines.append(remaining[:fit_chars])
|
||||
remaining = remaining[fit_chars:]
|
||||
|
||||
continue
|
||||
|
||||
# Check if word fits on current line
|
||||
space_width = 1 if current_line else 0
|
||||
if current_width + space_width + word_width <= width:
|
||||
if current_line:
|
||||
current_line += " " + word
|
||||
current_width += space_width + word_width
|
||||
else:
|
||||
current_line = word
|
||||
current_width = word_width
|
||||
else:
|
||||
# Flush current line and start new one
|
||||
lines.append(current_line)
|
||||
current_line = word
|
||||
current_width = word_width
|
||||
|
||||
# Don't forget the last line
|
||||
if current_line: lines.append(current_line)
|
||||
|
||||
return lines if lines else [""]
|
||||
|
||||
|
||||
def convert_markdown_to_micron(text):
|
||||
converter = MarkdownToMicron()
|
||||
return converter.format_block(text)
|
||||
+1021
-554
File diff suppressed because it is too large
Load Diff
@@ -56,7 +56,7 @@ def main():
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0)
|
||||
parser.add_argument('-q', '--quiet', action='count', default=0)
|
||||
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="ir {version}".format(version=__version__))
|
||||
parser.add_argument("--version", action="version", version="rnir {version}".format(version=__version__))
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -75,8 +75,5 @@ def main():
|
||||
print("")
|
||||
exit()
|
||||
|
||||
__example_rns_config__ = '''# This is an example Identity Resolver file.
|
||||
'''
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+21
-14
@@ -49,9 +49,9 @@ import RNS
|
||||
RNS.logtimefmt = "%H:%M:%S"
|
||||
RNS.compact_log_fmt = True
|
||||
|
||||
program_version = "2.4.0"
|
||||
eth_addr = "0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73"
|
||||
btc_addr = "35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH"
|
||||
program_version = "2.5.0"
|
||||
eth_addr = "0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E"
|
||||
btc_addr = "bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6"
|
||||
xmr_addr = "87HcDx6jRSkMQ9nPRd5K9hGGpZLn2s7vWETjMaVM5KfV4TD36NcYa8J8WSxhTSvBzzFpqDwp2fg5GX2moZ7VAP9QMZCZGET"
|
||||
|
||||
rnode = None
|
||||
@@ -1185,8 +1185,8 @@ class RNode():
|
||||
print(" Always use a firmware downloaded as binaries or compiled from source")
|
||||
print(" from one of the following locations:")
|
||||
print(" ")
|
||||
print(" https://unsigned.io/rnode")
|
||||
print(" https://github.com/markqvist/rnode_firmware")
|
||||
print(" https://github.com/liberatedsystems/RNode_Firmware_CE")
|
||||
print(" ")
|
||||
print(" You can reflash and bootstrap this device to a verifiable state")
|
||||
print(" by using this utility. It is recommended to do so NOW!")
|
||||
@@ -1228,7 +1228,7 @@ class RNode():
|
||||
|
||||
selected_version = None
|
||||
selected_hash = None
|
||||
firmware_version_url = "https://unsigned.io/firmware/latest/?v="+program_version+"&variant="
|
||||
firmware_version_url = "https://github.com/markqvist/rnode_firmware/releases/latest/download/release.json"
|
||||
fallback_firmware_version_url = "https://github.com/markqvist/rnode_firmware/releases/latest/download/release.json"
|
||||
def ensure_firmware_file(fw_filename):
|
||||
global selected_version, selected_hash, upd_nocheck
|
||||
@@ -1269,9 +1269,15 @@ def ensure_firmware_file(fw_filename):
|
||||
try:
|
||||
# if custom firmware url, download latest release
|
||||
if selected_version == None and fw_url == None:
|
||||
version_url = firmware_version_url+fw_filename
|
||||
RNS.log("Retrieving latest version info from "+version_url)
|
||||
urlretrieve(firmware_version_url+fw_filename, UPD_DIR+"/"+fw_filename+".version.latest")
|
||||
urlretrieve(firmware_version_url, UPD_DIR+"/release_info.json")
|
||||
import json
|
||||
with open(UPD_DIR+"/release_info.json", "rb") as rif:
|
||||
rdat = json.loads(rif.read())
|
||||
variant = rdat[fw_filename]
|
||||
with open(UPD_DIR+"/"+fw_filename+".version.latest", "wb") as verf:
|
||||
inf_str = str(variant["version"])+" "+str(variant["hash"])
|
||||
verf.write(inf_str.encode("utf-8"))
|
||||
|
||||
else:
|
||||
if fw_url != None:
|
||||
if selected_version == None:
|
||||
@@ -1542,13 +1548,14 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
def print_donation_block():
|
||||
print(" Ethereum : "+eth_addr)
|
||||
print(" Bitcoin : "+btc_addr)
|
||||
print(" Monero : "+xmr_addr)
|
||||
print(" Ko-Fi : https://ko-fi.com/markqvist")
|
||||
print(" Ethereum : "+eth_addr)
|
||||
print(" Bitcoin : "+btc_addr)
|
||||
print(" Monero : "+xmr_addr)
|
||||
print(" Ko-Fi : https://ko-fi.com/markqvist")
|
||||
print(" LiberaPay : https://liberapay.com/reticulum")
|
||||
print("")
|
||||
print(" Info : https://unsigned.io/")
|
||||
print(" Code : https://github.com/markqvist")
|
||||
print(" Info : https://reticulum.network")
|
||||
print(" Code : https://github.com/markqvist")
|
||||
|
||||
if args.version:
|
||||
print("rnodeconf "+program_version)
|
||||
|
||||
+225
-256
@@ -39,7 +39,8 @@ import argparse
|
||||
from RNS._version import __version__
|
||||
|
||||
remote_link = None
|
||||
def connect_remote(destination_hash, auth_identity, timeout, no_output = False):
|
||||
output_rst_str = "\r \r"
|
||||
def connect_remote(destination_hash, auth_identity, timeout, no_output = False, purpose="management"):
|
||||
global remote_link, reticulum
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
if not no_output:
|
||||
@@ -51,107 +52,220 @@ def connect_remote(destination_hash, auth_identity, timeout, no_output = False):
|
||||
time.sleep(0.1)
|
||||
if time.time() - pr_time > timeout:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Path request timed out")
|
||||
exit(12)
|
||||
|
||||
remote_identity = RNS.Identity.recall(destination_hash)
|
||||
|
||||
def remote_link_closed(link):
|
||||
if link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
if link.teardown_reason == RNS.Link.INITIATOR_CLOSED: return
|
||||
elif link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("The link timed out, exiting now")
|
||||
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("The link was closed by the server, exiting now")
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Link closed unexpectedly, exiting now")
|
||||
exit(10)
|
||||
|
||||
def remote_link_established(link):
|
||||
global remote_link
|
||||
link.identify(auth_identity)
|
||||
if purpose == "management": link.identify(auth_identity)
|
||||
remote_link = link
|
||||
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Establishing link with remote transport instance...", end=" ")
|
||||
sys.stdout.flush()
|
||||
|
||||
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
|
||||
if purpose == "management": remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
|
||||
elif purpose == "blackhole": remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "info", "blackhole")
|
||||
link = RNS.Link(remote_destination)
|
||||
link.set_link_established_callback(remote_link_established)
|
||||
link.set_link_closed_callback(remote_link_closed)
|
||||
|
||||
def parse_hash(input_str):
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(input_str) != dest_len: raise ValueError("Hash length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
hash_bytes = bytes.fromhex(input_str)
|
||||
return hash_bytes
|
||||
except Exception as e: raise ValueError("Invalid hash entered. Check your input.")
|
||||
|
||||
def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity, timeout, drop_queues,
|
||||
drop_via, max_hops, remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT,
|
||||
no_output=False, json=False):
|
||||
blackholed=False, blackhole=False, unblackhole=False, blackhole_duration=None, blackhole_reason=None,
|
||||
remote_blackhole_list=False, remote_blackhole_list_filter=None, no_output=False, json=False):
|
||||
|
||||
global remote_link, reticulum
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
if remote:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(remote) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
if len(remote) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
identity_hash = bytes.fromhex(remote)
|
||||
remote_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.remote.management", identity_hash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
|
||||
identity = RNS.Identity.from_file(os.path.expanduser(management_identity))
|
||||
if identity == None:
|
||||
raise ValueError("Could not load management identity from "+str(management_identity))
|
||||
if identity == None: raise ValueError("Could not load management identity from "+str(management_identity))
|
||||
|
||||
try:
|
||||
connect_remote(remote_hash, identity, remote_timeout, no_output)
|
||||
except Exception as e:
|
||||
raise e
|
||||
try: connect_remote(remote_hash, identity, remote_timeout, no_output)
|
||||
except Exception as e: raise e
|
||||
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(20)
|
||||
|
||||
while remote_link == None:
|
||||
time.sleep(0.1)
|
||||
while remote_link == None: time.sleep(0.1)
|
||||
|
||||
if blackholed or remote_blackhole_list:
|
||||
blackholed_list = None
|
||||
if blackholed:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print(output_rst_str, end="")
|
||||
print("Listing blackholed identities on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
if table:
|
||||
try: blackholed_list = reticulum.get_blackholed_identities()
|
||||
except Exception as e:
|
||||
print(f"Could not get blackholed identities from RNS instance: {e}")
|
||||
exit(20)
|
||||
|
||||
elif remote_blackhole_list:
|
||||
try: identity_hash = parse_hash(destination_hexhash)
|
||||
except Exception as e:
|
||||
print(f"{e}")
|
||||
exit(20)
|
||||
|
||||
remote_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.info.blackhole", identity_hash)
|
||||
connect_remote(remote_hash, None, remote_timeout, no_output, purpose="blackhole")
|
||||
while remote_link == None: time.sleep(0.1)
|
||||
|
||||
if not no_output:
|
||||
print(output_rst_str, end="")
|
||||
print("Sending request...", end=" ")
|
||||
sys.stdout.flush()
|
||||
receipt = remote_link.request("/list")
|
||||
while not receipt.concluded(): time.sleep(0.1)
|
||||
response = receipt.get_response()
|
||||
if type(response) == dict:
|
||||
blackholed_list = response
|
||||
print(output_rst_str, end="")
|
||||
else:
|
||||
if not no_output:
|
||||
print(output_rst_str, end="")
|
||||
print("The remote request failed.")
|
||||
exit(10)
|
||||
|
||||
else:
|
||||
print(f"Nowhere to fetch blackhole list from")
|
||||
exit(255)
|
||||
|
||||
if not blackholed_list:
|
||||
print("No blackholed identity data available")
|
||||
exit(20)
|
||||
|
||||
else:
|
||||
rmlen = 64
|
||||
def trunc(input_str):
|
||||
if len(input_str) <= rmlen: return input_str
|
||||
else: return f"{input_str[:rmlen-1]}…"
|
||||
|
||||
try:
|
||||
now = time.time()
|
||||
for identity_hash in blackholed_list:
|
||||
until = blackholed_list[identity_hash]["until"]
|
||||
reason = blackholed_list[identity_hash]["reason"]
|
||||
source = blackholed_list[identity_hash]["source"]
|
||||
until_str = f"for {RNS.prettytime(max(0, until-now))}" if until else "indefinitely"
|
||||
reason_str = f" ({trunc(reason)})" if reason else ""
|
||||
by_str = f" by {RNS.prettyhexrep(source)}" if source != RNS.Transport.identity.hash else ""
|
||||
filter_str = f"{RNS.prettyhexrep(identity_hash)} {until_str} {reason_str} {by_str}"
|
||||
|
||||
if not remote_blackhole_list:
|
||||
if destination_hexhash and not destination_hexhash in filter_str: continue
|
||||
else:
|
||||
if remote_blackhole_list_filter and not remote_blackhole_list_filter in filter_str: continue
|
||||
|
||||
print(f"{RNS.prettyhexrep(identity_hash)} blackholed {until_str}{reason_str}{by_str}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error while displaying collected blackhole data: {e}")
|
||||
exit(20)
|
||||
|
||||
elif blackhole:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print(output_rst_str, end="")
|
||||
print("Blackholing identity on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
identity_hash = parse_hash(destination_hexhash)
|
||||
until = time.time()+blackhole_duration*60*60 if blackhole_duration else None
|
||||
result = reticulum.blackhole_identity(identity_hash, until=until, reason=blackhole_reason)
|
||||
if result == True: print(f"Blackholed identity {destination_hexhash}")
|
||||
elif result == None: print(f"Identity {destination_hexhash} already blackholed")
|
||||
else: print(f"Could not blackhole identity {destination_hexhash}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Could not blackhole identity: {e}")
|
||||
exit(20)
|
||||
|
||||
elif unblackhole:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print(output_rst_str, end="")
|
||||
print("Blackholing identity on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
identity_hash = parse_hash(destination_hexhash)
|
||||
result = reticulum.unblackhole_identity(identity_hash)
|
||||
if result == True: print(f"Lifted blackhole for identity {destination_hexhash}")
|
||||
elif result == None: print(f"Identity {destination_hexhash} not blackholed")
|
||||
else: print(f"Could not unblackhole identity {destination_hexhash}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Could not unblackhole identity: {e}")
|
||||
exit(20)
|
||||
|
||||
elif table:
|
||||
destination_hash = None
|
||||
if destination_hexhash != None:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
if len(destination_hexhash) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try: destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
if not remote_link:
|
||||
table = sorted(reticulum.get_path_table(max_hops=max_hops), key=lambda e: (e["interface"], e["hops"]) )
|
||||
if not remote_link: table = sorted(reticulum.get_path_table(max_hops=max_hops), key=lambda e: (e["interface"], e["hops"]) )
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Sending request...", end=" ")
|
||||
sys.stdout.flush()
|
||||
receipt = remote_link.request("/path", data = ["table", destination_hash, max_hops])
|
||||
while not receipt.concluded():
|
||||
time.sleep(0.1)
|
||||
while not receipt.concluded(): time.sleep(0.1)
|
||||
response = receipt.get_response()
|
||||
if response:
|
||||
table = response
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("The remote request failed. Likely authentication failure.")
|
||||
exit(10)
|
||||
|
||||
@@ -160,20 +274,18 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
import json
|
||||
for p in table:
|
||||
for k in p:
|
||||
if isinstance(p[k], bytes):
|
||||
p[k] = RNS.hexrep(p[k], delimit=False)
|
||||
if isinstance(p[k], bytes): p[k] = RNS.hexrep(p[k], delimit=False)
|
||||
|
||||
print(json.dumps(table))
|
||||
exit()
|
||||
|
||||
else:
|
||||
for path in table:
|
||||
if destination_hash == None or destination_hash == path["hash"]:
|
||||
displayed += 1
|
||||
exp_str = RNS.timestamp_str(path["expires"])
|
||||
if path["hops"] == 1:
|
||||
m_str = " "
|
||||
else:
|
||||
m_str = "s"
|
||||
if path["hops"] == 1: m_str = " "
|
||||
else: m_str = "s"
|
||||
print(RNS.prettyhexrep(path["hash"])+" is "+str(path["hops"])+" hop"+m_str+" away via "+RNS.prettyhexrep(path["via"])+" on "+path["interface"]+" expires "+RNS.timestamp_str(path["expires"]))
|
||||
|
||||
if destination_hash != None and displayed == 0:
|
||||
@@ -185,21 +297,17 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
if destination_hexhash != None:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
if len(destination_hexhash) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try: destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
if not remote_link:
|
||||
table = reticulum.get_rate_table()
|
||||
if not remote_link: table = reticulum.get_rate_table()
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Sending request...", end=" ")
|
||||
sys.stdout.flush()
|
||||
receipt = remote_link.request("/path", data = ["rates", destination_hash])
|
||||
@@ -208,10 +316,10 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
response = receipt.get_response()
|
||||
if response:
|
||||
table = response
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("The remote request failed. Likely authentication failure.")
|
||||
exit(10)
|
||||
|
||||
@@ -220,15 +328,12 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
import json
|
||||
for p in table:
|
||||
for k in p:
|
||||
if isinstance(p[k], bytes):
|
||||
p[k] = RNS.hexrep(p[k], delimit=False)
|
||||
if isinstance(p[k], bytes): p[k] = RNS.hexrep(p[k], delimit=False)
|
||||
|
||||
print(json.dumps(table))
|
||||
exit()
|
||||
else:
|
||||
if len(table) == 0:
|
||||
print("No information available")
|
||||
|
||||
if len(table) == 0: print("No information available")
|
||||
else:
|
||||
displayed = 0
|
||||
for entry in table:
|
||||
@@ -274,7 +379,7 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
elif drop_queues:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Dropping announce queues on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
@@ -284,24 +389,20 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
elif drop:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Dropping path on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
if len(destination_hexhash) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try: destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
if reticulum.drop_path(destination_hash):
|
||||
print("Dropped path to "+RNS.prettyhexrep(destination_hash))
|
||||
if reticulum.drop_path(destination_hash): print("Dropped path to "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print("Unable to drop path to "+RNS.prettyhexrep(destination_hash)+". Does it exist?")
|
||||
sys.exit(1)
|
||||
@@ -309,24 +410,20 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
elif drop_via:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Dropping all paths via specific transport instance on remote instances yet not implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
if len(destination_hexhash) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try: destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
if reticulum.drop_all_via(destination_hash):
|
||||
print("Dropped all paths via "+RNS.prettyhexrep(destination_hash))
|
||||
if reticulum.drop_all_via(destination_hash): print("Dropped all paths via "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print("Unable to drop paths via "+RNS.prettyhexrep(destination_hash)+". Does the transport instance exist?")
|
||||
sys.exit(1)
|
||||
@@ -334,18 +431,15 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
else:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Requesting paths on remote instances not implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
if len(destination_hexhash) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try: destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
@@ -374,166 +468,57 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
next_hop = RNS.prettyhexrep(next_hop_bytes)
|
||||
next_hop_interface = reticulum.get_next_hop_if_name(destination_hash)
|
||||
|
||||
if hops != 1:
|
||||
ms = "s"
|
||||
else:
|
||||
ms = ""
|
||||
if hops != 1: ms = "s"
|
||||
else: ms = ""
|
||||
|
||||
print("\rPath found, destination "+RNS.prettyhexrep(destination_hash)+" is "+str(hops)+" hop"+ms+" away via "+next_hop+" on "+next_hop_interface)
|
||||
else:
|
||||
print("\r \rPath not found")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description="Reticulum Path Discovery Utility")
|
||||
|
||||
parser.add_argument("--config",
|
||||
action="store",
|
||||
default=None,
|
||||
help="path to alternative Reticulum config directory",
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version="rnpath {version}".format(version=__version__)
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--table",
|
||||
action="store_true",
|
||||
help="show all known paths",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--max",
|
||||
action="store",
|
||||
metavar="hops",
|
||||
type=int,
|
||||
help="maximum hops to filter path table by",
|
||||
default=None
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--rates",
|
||||
action="store_true",
|
||||
help="show announce rate info",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--drop",
|
||||
action="store_true",
|
||||
help="remove the path to a destination",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-D",
|
||||
"--drop-announces",
|
||||
action="store_true",
|
||||
help="drop all queued announces",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-x", "--drop-via",
|
||||
action="store_true",
|
||||
help="drop all paths via specified transport instance",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
action="store",
|
||||
metavar="seconds",
|
||||
type=float,
|
||||
help="timeout before giving up",
|
||||
default=RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-R",
|
||||
action="store",
|
||||
metavar="hash",
|
||||
help="transport identity hash of remote instance to manage",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
action="store",
|
||||
metavar="path",
|
||||
help="path to identity used for remote management",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-W",
|
||||
action="store",
|
||||
metavar="seconds",
|
||||
type=float,
|
||||
help="timeout before giving up on remote queries",
|
||||
default=RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="output in JSON format",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"destination",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="hexadecimal hash of the destination",
|
||||
type=str
|
||||
)
|
||||
|
||||
parser = argparse.ArgumentParser(description="Reticulum Path Management Utility")
|
||||
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument("--version", action="version", version="rnpath {version}".format(version=__version__))
|
||||
parser.add_argument("-t", "--table", action="store_true", help="show all known paths", default=False)
|
||||
parser.add_argument("-m", "--max", action="store", metavar="hops", type=int, help="maximum hops to filter path table by", default=None)
|
||||
parser.add_argument("-r", "--rates", action="store_true", help="show announce rate info", default=False)
|
||||
parser.add_argument("-d", "--drop", action="store_true", help="remove the path to a destination", default=False)
|
||||
parser.add_argument("-D", "--drop-announces", action="store_true", help="drop all queued announces", default=False)
|
||||
parser.add_argument("-x", "--drop-via", action="store_true", help="drop all paths via specified transport instance", default=False)
|
||||
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
|
||||
parser.add_argument("-R", action="store", metavar="hash", help="transport identity hash of remote instance to manage", default=None, type=str)
|
||||
parser.add_argument("-i", action="store", metavar="path", help="path to identity used for remote management", default=None, type=str)
|
||||
parser.add_argument("-W", action="store", metavar="seconds", type=float, help="timeout before giving up on remote queries", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
|
||||
parser.add_argument("-b", "--blackholed", action="store_true", help="list blackholed identities", default=False)
|
||||
parser.add_argument("-B", "--blackhole", action="store_true", help="blackhole identity", default=False)
|
||||
parser.add_argument("-U", "--unblackhole", action="store_true", help="unblackhole identity", default=False)
|
||||
parser.add_argument( "--duration", action="store", type=float, help="duration of blackhole enforcement in hours", default=None)
|
||||
parser.add_argument( "--reason", action="store", type=str, help="reason for blackholing identity", default=None)
|
||||
parser.add_argument("-p", "--blackholed-list", action="store_true", help="view published blackhole list for remote transport instance", default=False)
|
||||
parser.add_argument("-j", "--json", action="store_true", help="output in JSON format", default=False)
|
||||
parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the destination", type=str)
|
||||
parser.add_argument("list_filter", nargs="?", default=None, help="filter for remote blackhole list view", type=str)
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.config:
|
||||
configarg = args.config
|
||||
else:
|
||||
configarg = None
|
||||
if args.config: configarg = args.config
|
||||
else: configarg = None
|
||||
|
||||
if not args.drop_announces and not args.table and not args.rates and not args.destination and not args.drop_via:
|
||||
if not args.drop_announces and not args.table and not args.rates and not args.destination and not args.drop_via and not args.blackholed:
|
||||
print("")
|
||||
parser.print_help()
|
||||
print("")
|
||||
else:
|
||||
program_setup(
|
||||
configdir = configarg,
|
||||
table = args.table,
|
||||
rates = args.rates,
|
||||
drop = args.drop,
|
||||
destination_hexhash = args.destination,
|
||||
verbosity = args.verbose,
|
||||
timeout = args.w,
|
||||
drop_queues = args.drop_announces,
|
||||
drop_via = args.drop_via,
|
||||
max_hops = args.max,
|
||||
remote=args.R,
|
||||
management_identity=args.i,
|
||||
remote_timeout=args.W,
|
||||
json=args.json,
|
||||
)
|
||||
program_setup(configdir = configarg, table = args.table, rates = args.rates, drop = args.drop, destination_hexhash = args.destination,
|
||||
verbosity = args.verbose, timeout = args.w, drop_queues = args.drop_announces, drop_via = args.drop_via, max_hops = args.max,
|
||||
remote=args.R, management_identity=args.i, remote_timeout=args.W, blackholed=args.blackholed, blackhole=args.blackhole,
|
||||
unblackhole=args.unblackhole, blackhole_duration=args.duration, blackhole_reason=args.reason, remote_blackhole_list=args.blackholed_list,
|
||||
remote_blackhole_list_filter=args.list_filter, json=args.json)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
@@ -543,38 +528,22 @@ def main():
|
||||
def pretty_date(time=False):
|
||||
from datetime import datetime
|
||||
now = datetime.now()
|
||||
if type(time) is int:
|
||||
diff = now - datetime.fromtimestamp(time)
|
||||
elif isinstance(time,datetime):
|
||||
diff = now - time
|
||||
elif not time:
|
||||
diff = now - now
|
||||
if type(time) is int: diff = now - datetime.fromtimestamp(time)
|
||||
elif isinstance(time,datetime): diff = now - time
|
||||
elif not time: diff = now - now
|
||||
second_diff = diff.seconds
|
||||
day_diff = diff.days
|
||||
if day_diff < 0:
|
||||
return ''
|
||||
if day_diff < 0: return ''
|
||||
if day_diff == 0:
|
||||
if second_diff < 10:
|
||||
return str(second_diff) + " seconds"
|
||||
if second_diff < 60:
|
||||
return str(second_diff) + " seconds"
|
||||
if second_diff < 120:
|
||||
return "1 minute"
|
||||
if second_diff < 3600:
|
||||
return str(int(second_diff / 60)) + " minutes"
|
||||
if second_diff < 7200:
|
||||
return "an hour"
|
||||
if second_diff < 86400:
|
||||
return str(int(second_diff / 3600)) + " hours"
|
||||
if day_diff == 1:
|
||||
return "1 day"
|
||||
if day_diff < 7:
|
||||
return str(day_diff) + " days"
|
||||
if day_diff < 31:
|
||||
return str(int(day_diff / 7)) + " weeks"
|
||||
if day_diff < 365:
|
||||
return str(int(day_diff / 30)) + " months"
|
||||
if second_diff < 10: return str(second_diff) + " seconds"
|
||||
if second_diff < 60: return str(second_diff) + " seconds"
|
||||
if second_diff < 70: return "1 minute"
|
||||
if second_diff < 7200: return str(int(second_diff / 60)) + " minutes"
|
||||
if second_diff < 86400: return str(int(second_diff / 3600)) + " hours"
|
||||
if day_diff == 1: return "1 day"
|
||||
if day_diff < 7: return str(day_diff) + " days"
|
||||
if day_diff < 31: return str(int(day_diff / 7)) + " weeks"
|
||||
if day_diff < 365: return str(int(day_diff / 30)) + " months"
|
||||
return str(int(day_diff / 365)) + " years"
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# 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 RNS
|
||||
import argparse
|
||||
import time
|
||||
|
||||
from RNS._version import __version__
|
||||
|
||||
def program_setup(configdir, verbosity = 0, quietness = 0, service = False):
|
||||
targetverbosity = verbosity-quietness
|
||||
|
||||
if service:
|
||||
targetlogdest = RNS.LOG_FILE
|
||||
targetverbosity = None
|
||||
else:
|
||||
targetlogdest = RNS.LOG_STDOUT
|
||||
|
||||
reticulum = RNS.Reticulum(configdir=configdir, verbosity=targetverbosity, logdest=targetlogdest)
|
||||
exit(0)
|
||||
|
||||
def main():
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description="Reticulum Meta Package Manager")
|
||||
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0)
|
||||
parser.add_argument('-q', '--quiet', action='count', default=0)
|
||||
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="rnpkg {version}".format(version=__version__))
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.exampleconfig:
|
||||
print(__example_rnpkg_config__)
|
||||
exit()
|
||||
|
||||
if args.config: configarg = args.config
|
||||
else: configarg = None
|
||||
|
||||
program_setup(configdir = configarg, verbosity=args.verbose, quietness=args.quiet)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
|
||||
__example_rnpkg_config__ = '''# This is an example package manager configuration file.
|
||||
'''
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -160,6 +160,55 @@ instance_name = default
|
||||
# remote_management_allowed = 9fb6d773498fb3feda407ed8ef2c3229, 2d882c5586e548d79b5af27bca1776dc
|
||||
|
||||
|
||||
# For easier management, discovery and configuration of
|
||||
# networks with many individual transport instances,
|
||||
# you can specify a network identity to be used across
|
||||
# a set of instances. If sending interface discovery
|
||||
# announces, these will all be signed by the specified
|
||||
# network identity, and other nodes discovering your
|
||||
# interfaces will be able to identify that they belong
|
||||
# to the same network, even though they exist on different
|
||||
# transport nodes.
|
||||
|
||||
# network_identity = ~/.reticulum/storage/identity/network
|
||||
|
||||
|
||||
# You can configure whether Reticulum should discover
|
||||
# available interfaces from other Transport Instances over
|
||||
# the network. If this option is enabled, Reticulum will
|
||||
# collect interface information discovered from the network.
|
||||
|
||||
# discover_interfaces = No
|
||||
|
||||
|
||||
# If you only want to discover interfaces from specific
|
||||
# networks, you can provide a list of network identities
|
||||
# from which to discover interfaces. If this option is not
|
||||
# provided, interfaces will be discovered from all transport
|
||||
# instances on all connected networks.
|
||||
|
||||
# interface_discovery_sources = 78616ff7c4b8d3886d67d494b440f333, cb127015e13aa6ea1e0a606cdc9123d0
|
||||
|
||||
|
||||
# It is possible to automatically bring up and connect new
|
||||
# interfaces discovered over the network. This option is
|
||||
# disabled by default, but allows you to specify a maximum
|
||||
# number of discovered interfaces to automatically connect.
|
||||
# Additionally, if this option is enabled, Reticulum will
|
||||
# also try to autoconnect available auto-discovered inter-
|
||||
# faces on startup, up to the maximum number specified.
|
||||
|
||||
# autoconnect_discovered_interfaces = 0
|
||||
|
||||
|
||||
# To prevent interface discovery spamming, a valid crypto-
|
||||
# graphic stamp is required per announced interface. You
|
||||
# can configure the minimum required value to accept as
|
||||
# valid for discovered interfaces.
|
||||
|
||||
# required_discovery_value = 14
|
||||
|
||||
|
||||
# You can configure Reticulum to panic and forcibly close
|
||||
# if an unrecoverable interface error occurs, such as the
|
||||
# hardware device for an interface disappearing. This is
|
||||
@@ -180,6 +229,31 @@ instance_name = default
|
||||
# respond_to_probes = No
|
||||
|
||||
|
||||
# You can publish your local list of blackholed identities
|
||||
# for other transport instances to use for automatic,
|
||||
# network-wide blackhole management.
|
||||
|
||||
# publish_blackhole = No
|
||||
|
||||
# List of remote transport identities from which to auto-
|
||||
# matically source lists of blackholed identities.
|
||||
#
|
||||
# If you're connecting to a large external network, you
|
||||
# can use one or more external blackhole list to block
|
||||
# spammy and excessive announces onto your network. This
|
||||
# funtionality is especially useful if you're hosting public
|
||||
# entrypoints or gateways. The list source below provides a
|
||||
# functional example, but better, more timely maintained
|
||||
# lists probably exist in the community.
|
||||
|
||||
# blackhole_sources = 521c87a83afb8f29e4455e77930b973b
|
||||
|
||||
# You can set the interval in minutes at which remote
|
||||
# blackhole sources are updated. Defaults to one hour.
|
||||
|
||||
# blackhole_update_interval = 60
|
||||
|
||||
|
||||
[logging]
|
||||
# Valid log levels are 0 through 7:
|
||||
# 0: Log only critical information
|
||||
@@ -193,6 +267,12 @@ instance_name = default
|
||||
|
||||
loglevel = 4
|
||||
|
||||
# You can disable timestamp inclusion in logs. Useful if
|
||||
# you want to use an external logging tool that provides
|
||||
# its own timestamps or custom formatting.
|
||||
|
||||
# logtimestamps = no
|
||||
|
||||
|
||||
# The interfaces section defines the physical and virtual
|
||||
# interfaces Reticulum will use to communicate on. This
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 ._version import __version__
|
||||
|
||||
import os
|
||||
module_abs_filename = os.path.abspath(__file__)
|
||||
module_dir = os.path.dirname(module_abs_filename)
|
||||
|
||||
def _get_version(): return __version__
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "0.2.0"
|
||||
@@ -0,0 +1,93 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 argparse
|
||||
import sys
|
||||
|
||||
from RNS.Utilities.rnsh._version import __version__ as __rnsh_version__
|
||||
from RNS._version import __version__
|
||||
|
||||
DEFAULT_SERVICE_NAME = "default"
|
||||
|
||||
def setup_argument_parser():
|
||||
parser = argparse.ArgumentParser(description="Reticulum Remote Shell Utility", epilog="When specifying a command to execute, separate rnsh\noptions from the command and its arguments with --\n\nFor example:\n rnsh -l -- /bin/bash --login\n rnsh <destination> -- ls -la /tmp", formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
|
||||
# Common options
|
||||
parser.add_argument("--config", "-c", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument("--identity", "-i", action="store", default=None, help="path to identity file to use", type=str)
|
||||
parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity")
|
||||
parser.add_argument("-q", "--quiet", action="count", default=0, help="decrease verbosity")
|
||||
parser.add_argument("-p", "--print-identity", action="store_true", default=False, help="print identity and destination info and exit")
|
||||
parser.add_argument("--version", action="version", version="rnsh {rv} (protocol {pv})".format(rv=__version__, pv=__rnsh_version__))
|
||||
|
||||
# Listener options
|
||||
parser.add_argument("-l", "--listen", action="store_true", default=False, help="listen (server) mode; any command specified after -- will be used as the default command when the initiator does not provide one or when remote command execution is disabled; if no command is specified, the default shell of the user running rnsh will be used")
|
||||
parser.add_argument("-s", "--service", action="store", default=None, help="service name for identity file if not the default", type=str)
|
||||
parser.add_argument("-b", "--announce",action="store", default=None,help="announce on startup and every PERIOD seconds; specify 0 to announce on startup only",metavar="PERIOD", type=int)
|
||||
parser.add_argument("-a", "--allowed", action="append", default=None, metavar="HASH", type=str, help="allow this identity to connect (may be specified multiple times); allowed identities can also be specified in ~/.rnsh/allowed_identities or ~/.config/rnsh/allowed_identities, one hash per line")
|
||||
parser.add_argument("-n", "--no-auth", action="store_true", default=False, help="disable authentication (allow any identity to connect)")
|
||||
parser.add_argument("-A", "--remote-command-as-args", action="store_true", default=False, help="concatenate remote command to the argument list of the default program or shell")
|
||||
parser.add_argument("-C", "--no-remote-command", action="store_true", default=False, help="disable executing command lines received from the remote initiator")
|
||||
|
||||
# Initiator options
|
||||
parser.add_argument("-N", "--no-id", action="store_true", default=False, help="disable identity announcement on connect")
|
||||
parser.add_argument("-m", "--mirror", action="store_true", default=False, help="return with the exit code of the remote process")
|
||||
parser.add_argument("-w", "--timeout", action="store", default=None, help="connect and request timeout in seconds", metavar="SECONDS", type=float)
|
||||
|
||||
parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the destination to connect to", type=str)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def parse_arguments(argv=None):
|
||||
if argv is None: argv = sys.argv[1:]
|
||||
|
||||
# Split at -- to separate rnsh options from the command to execute.
|
||||
# Everything before -- (or the entire argv if no --) goes to argparse.
|
||||
# Everything after -- becomes the command list.
|
||||
try:
|
||||
split_idx = argv.index("--")
|
||||
rnsh_argv = argv[:split_idx]
|
||||
command = argv[split_idx + 1:]
|
||||
except ValueError:
|
||||
rnsh_argv = argv
|
||||
command = []
|
||||
|
||||
parser = setup_argument_parser()
|
||||
args = parser.parse_args(rnsh_argv)
|
||||
args.command = command
|
||||
|
||||
if args.listen and not args.service: args.service = DEFAULT_SERVICE_NAME
|
||||
|
||||
return args, parser
|
||||
@@ -0,0 +1,60 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 contextlib
|
||||
from contextlib import AbstractContextManager
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
class permit(AbstractContextManager):
|
||||
"""Context manager to allow specified exceptions
|
||||
|
||||
The specified exceptions will be allowed to bubble up. Other
|
||||
exceptions are suppressed.
|
||||
|
||||
After a non-matching exception is suppressed, execution proceeds
|
||||
with the next statement following the with statement.
|
||||
|
||||
with allow(KeyboardInterrupt):
|
||||
time.sleep(300)
|
||||
# Execution still resumes here if no KeyboardInterrupt
|
||||
"""
|
||||
|
||||
def __init__(self, *exceptions): self._exceptions = exceptions
|
||||
|
||||
def __enter__(self): pass
|
||||
|
||||
def __exit__(self, exctype, excinst, exctb):
|
||||
return exctype is not None and not issubclass(exctype, self._exceptions)
|
||||
@@ -0,0 +1,59 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 asyncio
|
||||
import time
|
||||
|
||||
def bitwise_or_if(value: int, condition: bool, orval: int):
|
||||
if not condition: return value
|
||||
return value | orval
|
||||
|
||||
def check_and(value: int, andval: int) -> bool:
|
||||
return (value & andval) > 0
|
||||
|
||||
class SleepRate:
|
||||
def __init__(self, target_period: float):
|
||||
self.target_period = target_period
|
||||
self.last_wake = time.time()
|
||||
|
||||
def next_sleep_time(self) -> float:
|
||||
old_last_wake = self.last_wake
|
||||
self.last_wake = time.time()
|
||||
next_wake = max(old_last_wake + 0.01, self.last_wake)
|
||||
sleep_for = next_wake - self.last_wake
|
||||
return sleep_for if sleep_for > 0 else 0
|
||||
|
||||
async def sleep_async(self): await asyncio.sleep(self.next_sleep_time())
|
||||
|
||||
def sleep_block(self): time.sleep(self.next_sleep_time())
|
||||
@@ -0,0 +1,486 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import enum
|
||||
import functools
|
||||
import os
|
||||
import queue
|
||||
import shlex
|
||||
import signal
|
||||
import sys
|
||||
import termios
|
||||
import threading
|
||||
import time
|
||||
import tty
|
||||
from typing import Callable, TypeVar
|
||||
import RNS
|
||||
import RNS.Utilities.rnsh.exception as exception
|
||||
import RNS.Utilities.rnsh.process as process
|
||||
import RNS.Utilities.rnsh.retry as retry
|
||||
import RNS.Utilities.rnsh.session as session
|
||||
import re
|
||||
import contextlib
|
||||
|
||||
import pwd
|
||||
import bz2
|
||||
import RNS.Utilities.rnsh.protocol as protocol
|
||||
import RNS.Utilities.rnsh.helpers as helpers
|
||||
import RNS.Utilities.rnsh.rnsh as rnsh
|
||||
|
||||
_identity = None
|
||||
_reticulum = None
|
||||
_cmd: [str] | None = None
|
||||
DATA_AVAIL_MSG = "data available"
|
||||
_finished: asyncio.Event = None
|
||||
_retry_timer: retry.RetryThread | None = None
|
||||
_destination: RNS.Destination | None = None
|
||||
_loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
|
||||
async def _check_finished(timeout: float = 0):
|
||||
return _finished is not None and await process.event_wait(_finished, timeout=timeout)
|
||||
|
||||
def _sigint_handler(sig, loop):
|
||||
global _finished
|
||||
RNS.log(f"{signal.Signals(sig).name}", RNS.LOG_DEBUG)
|
||||
if _finished is not None: _finished.set()
|
||||
else: raise KeyboardInterrupt()
|
||||
|
||||
async def _spin_tty(until=None, msg=None, timeout=None):
|
||||
i = 0
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
if timeout != None: timeout = time.time()+timeout
|
||||
|
||||
print(msg+" ", end=" ")
|
||||
while (timeout == None or time.time()<timeout) and not until():
|
||||
await asyncio.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
print("\r"+" "*len(msg)+" \r", end="")
|
||||
|
||||
if timeout != None and time.time() > timeout: return False
|
||||
else: return True
|
||||
|
||||
|
||||
async def _spin_pipe(until: callable = None, msg=None, timeout: float | None = None) -> bool:
|
||||
if timeout is not None: timeout += time.time()
|
||||
|
||||
while (timeout is None or time.time() < timeout) and not until():
|
||||
if await _check_finished(0.1): raise asyncio.CancelledError()
|
||||
|
||||
if timeout is not None and time.time() > timeout: return False
|
||||
else: return True
|
||||
|
||||
async def _spin(until: callable = None, msg=None, timeout: float | None = None, quiet: bool = False) -> bool:
|
||||
if not quiet and os.isatty(1): return await _spin_tty(until, msg, timeout)
|
||||
else: return await _spin_pipe(until, msg, timeout)
|
||||
|
||||
_link: RNS.Link | None = None
|
||||
_remote_exec_grace = 2.0
|
||||
_pq = queue.Queue()
|
||||
|
||||
|
||||
class InitiatorState(enum.IntEnum):
|
||||
IS_INITIAL = 0
|
||||
IS_LINKED = 1
|
||||
IS_WAIT_VERS = 2
|
||||
IS_RUNNING = 3
|
||||
IS_TERMINATE = 4
|
||||
IS_TEARDOWN = 5
|
||||
|
||||
def _client_link_closed(link):
|
||||
if _finished: _finished.set()
|
||||
|
||||
def _client_message_handler(message: RNS.MessageBase): _pq.put(message)
|
||||
|
||||
def compute_target_rns_loglevel(verbosity: int, quietness: int, base_level: int = RNS.LOG_INFO) -> int:
|
||||
try:
|
||||
target = int(base_level) + int(verbosity) - int(quietness)
|
||||
if target < RNS.LOG_CRITICAL: target = RNS.LOG_CRITICAL
|
||||
if target > RNS.LOG_DEBUG: target = RNS.LOG_DEBUG
|
||||
return target
|
||||
|
||||
except Exception: return base_level
|
||||
|
||||
|
||||
class RemoteExecutionError(Exception):
|
||||
def __init__(self, msg): self.msg = msg
|
||||
|
||||
|
||||
async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0, quietness=0, noid=False, destination=None,
|
||||
timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
|
||||
global _identity, _reticulum, _link, _destination, _remote_exec_grace
|
||||
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
|
||||
if len(destination) != dest_len:
|
||||
raise RemoteExecutionError(
|
||||
"Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(
|
||||
hex=dest_len, byte=dest_len // 2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination)
|
||||
except Exception as e:
|
||||
raise RemoteExecutionError("Invalid destination entered. Check your input.")
|
||||
|
||||
if _reticulum is None:
|
||||
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_ERROR)
|
||||
RNS.logfile = os.path.join(configdir, "logfile")
|
||||
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel, logdest=RNS.LOG_FILE)
|
||||
|
||||
if _identity is None:
|
||||
_identity = rnsh.prepare_identity(identitypath)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
RNS.log(f"Requesting path...", RNS.LOG_INFO)
|
||||
if not await _spin(until=lambda: RNS.Transport.has_path(destination_hash), msg="Requesting path...",
|
||||
timeout=timeout, quiet=quietness > 0):
|
||||
raise RemoteExecutionError("Path not found")
|
||||
|
||||
if _destination is None:
|
||||
listener_identity = RNS.Identity.recall(destination_hash)
|
||||
_destination = RNS.Destination(
|
||||
listener_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
rnsh.APP_NAME
|
||||
)
|
||||
|
||||
if _link is None or _link.status == RNS.Link.PENDING:
|
||||
RNS.log("No link", RNS.LOG_DEBUG)
|
||||
_link = RNS.Link(_destination)
|
||||
_link.did_identify = False
|
||||
|
||||
_link.set_link_closed_callback(_client_link_closed)
|
||||
|
||||
RNS.log(f"Establishing link...", RNS.LOG_VERBOSE)
|
||||
if not await _spin(until=lambda: _link.status == RNS.Link.ACTIVE, msg="Establishing link...",
|
||||
timeout=timeout, quiet=quietness > 0):
|
||||
raise RemoteExecutionError("Could not establish link with " + RNS.prettyhexrep(destination_hash))
|
||||
|
||||
RNS.log("Have link", RNS.LOG_DEBUG)
|
||||
if not noid and not _link.did_identify:
|
||||
# Delay a tiny bit to allow listener to fully enter WAIT_IDENT state
|
||||
await asyncio.sleep(min(1, _link.rtt * 1.1 + 0.05))
|
||||
_link.identify(_identity)
|
||||
_link.did_identify = True
|
||||
|
||||
|
||||
async def _handle_error(errmsg: RNS.MessageBase):
|
||||
if isinstance(errmsg, protocol.ErrorMessage):
|
||||
with contextlib.suppress(Exception):
|
||||
if _link and _link.status == RNS.Link.ACTIVE:
|
||||
_link.teardown()
|
||||
await asyncio.sleep(0.1)
|
||||
raise RemoteExecutionError(f"Remote error: {errmsg.msg}")
|
||||
|
||||
|
||||
async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosity: int, quietness: int, noid: bool, destination: str,
|
||||
timeout: float, command: [str] | None = None):
|
||||
global _finished, _link
|
||||
if timeout is None:
|
||||
timeout = RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
with process.TTYRestorer(sys.stdin.fileno()) as ttyRestorer:
|
||||
loop = asyncio.get_running_loop()
|
||||
state = InitiatorState.IS_INITIAL
|
||||
data_buffer = bytearray(sys.stdin.buffer.read()) if not os.isatty(sys.stdin.fileno()) else bytearray()
|
||||
line_buffer = bytearray()
|
||||
|
||||
await _initiate_link(configdir=configdir,
|
||||
rnsconfigdir=rnsconfigdir,
|
||||
identitypath=identitypath,
|
||||
verbosity=verbosity,
|
||||
quietness=quietness,
|
||||
noid=noid,
|
||||
destination=destination,
|
||||
timeout=timeout)
|
||||
|
||||
if not _link or _link.status not in [RNS.Link.ACTIVE, RNS.Link.PENDING]:
|
||||
return 255
|
||||
|
||||
state = InitiatorState.IS_LINKED
|
||||
outlet = session.RNSOutlet(_link)
|
||||
channel = _link.get_channel()
|
||||
protocol.register_message_types(channel)
|
||||
channel.add_message_handler(_client_message_handler)
|
||||
|
||||
# Next step after linking and identifying: send version
|
||||
# if not await _spin(lambda: messenger.is_outlet_ready(outlet), timeout=5, quiet=quietness > 0):
|
||||
# print("Error bringing up link")
|
||||
# return 253
|
||||
|
||||
channel.send(protocol.VersionInfoMessage())
|
||||
try:
|
||||
vm = _pq.get(timeout=max(outlet.rtt * 20, 5))
|
||||
await _handle_error(vm)
|
||||
if not isinstance(vm, protocol.VersionInfoMessage):
|
||||
raise Exception("Invalid message received")
|
||||
RNS.log(f"Server version info: sw {vm.sw_version} prot {vm.protocol_version}", RNS.LOG_DEBUG)
|
||||
state = InitiatorState.IS_RUNNING
|
||||
except queue.Empty:
|
||||
print("Protocol error")
|
||||
return 254
|
||||
|
||||
winch = False
|
||||
def sigwinch_handler():
|
||||
nonlocal winch
|
||||
winch = True
|
||||
|
||||
esc = False
|
||||
pre_esc = True
|
||||
line_mode = False
|
||||
line_flush = False
|
||||
blind_write_count = 0
|
||||
flush_chars = ["\x01", "\x03", "\x04", "\x05", "\x0c", "\x11", "\x13", "\x15", "\x19", "\t", "\x1A", "\x1B"]
|
||||
def handle_escape(b):
|
||||
nonlocal line_mode
|
||||
if b == "?":
|
||||
os.write(1, "\n\r\n\rSupported rnsh escape sequences:".encode("utf-8"))
|
||||
os.write(1, "\n\r ~~ Send the escape character by typing it twice".encode("utf-8"))
|
||||
os.write(1, "\n\r ~. Terminate session and exit immediately".encode("utf-8"))
|
||||
os.write(1, "\n\r ~L Toggle line-interactive mode".encode("utf-8"))
|
||||
os.write(1, "\n\r ~? Display this quick reference\n\r".encode("utf-8"))
|
||||
os.write(1, "\n\r(Escape sequences are only recognized immediately after newline)\n\r".encode("utf-8"))
|
||||
return None
|
||||
elif b == ".":
|
||||
_link.teardown()
|
||||
return None
|
||||
elif b == "L":
|
||||
line_mode = not line_mode
|
||||
if line_mode:
|
||||
os.write(1, "\n\rLine-interactive mode enabled\n\r".encode("utf-8"))
|
||||
else:
|
||||
os.write(1, "\n\rLine-interactive mode disabled\n\r".encode("utf-8"))
|
||||
return None
|
||||
|
||||
return b
|
||||
|
||||
stdin_eof = False
|
||||
def stdin():
|
||||
nonlocal stdin_eof, pre_esc, esc, line_mode
|
||||
nonlocal line_flush, blind_write_count
|
||||
try:
|
||||
in_data = process.tty_read(sys.stdin.fileno())
|
||||
if in_data is not None:
|
||||
data = bytearray()
|
||||
for b in bytes(in_data):
|
||||
c = chr(b)
|
||||
if c == "\r":
|
||||
pre_esc = True
|
||||
line_flush = True
|
||||
data.append(b)
|
||||
elif line_mode and c in flush_chars:
|
||||
pre_esc = False
|
||||
line_flush = True
|
||||
data.append(b)
|
||||
elif line_mode and (c == "\b" or c == "\x7f"):
|
||||
pre_esc = False
|
||||
if len(line_buffer)>0:
|
||||
line_buffer.pop(-1)
|
||||
blind_write_count -= 1
|
||||
os.write(1, "\b \b".encode("utf-8"))
|
||||
elif pre_esc == True and c == "~":
|
||||
pre_esc = False
|
||||
esc = True
|
||||
elif esc == True:
|
||||
ret = handle_escape(c)
|
||||
if ret != None:
|
||||
if ret != "~":
|
||||
data.append(ord("~"))
|
||||
data.append(ord(ret))
|
||||
esc = False
|
||||
else:
|
||||
pre_esc = False
|
||||
data.append(b)
|
||||
|
||||
if not line_mode:
|
||||
data_buffer.extend(data)
|
||||
else:
|
||||
line_buffer.extend(data)
|
||||
if line_flush:
|
||||
data_buffer.extend(line_buffer)
|
||||
line_buffer.clear()
|
||||
os.write(1, ("\b \b"*blind_write_count).encode("utf-8"))
|
||||
line_flush = False
|
||||
blind_write_count = 0
|
||||
else:
|
||||
os.write(1, data)
|
||||
blind_write_count += len(data)
|
||||
|
||||
except EOFError:
|
||||
if os.isatty(0):
|
||||
data_buffer.extend(process.CTRL_D)
|
||||
stdin_eof = True
|
||||
process.tty_unset_reader_callbacks(sys.stdin.fileno())
|
||||
|
||||
process.tty_add_reader_callback(sys.stdin.fileno(), stdin)
|
||||
|
||||
tcattr = None
|
||||
rows, cols, hpix, vpix = (None, None, None, None)
|
||||
try:
|
||||
tcattr = termios.tcgetattr(0)
|
||||
rows, cols, hpix, vpix = process.tty_get_winsize(0)
|
||||
except:
|
||||
try:
|
||||
tcattr = termios.tcgetattr(1)
|
||||
rows, cols, hpix, vpix = process.tty_get_winsize(1)
|
||||
except:
|
||||
try:
|
||||
tcattr = termios.tcgetattr(2)
|
||||
rows, cols, hpix, vpix = process.tty_get_winsize(2)
|
||||
except:
|
||||
pass
|
||||
|
||||
await _spin(lambda: channel.is_ready_to_send(), "Waiting for channel...", 1, quietness > 0)
|
||||
channel.send(protocol.ExecuteCommandMesssage(cmdline=command,
|
||||
pipe_stdin=not os.isatty(0),
|
||||
pipe_stdout=not os.isatty(1),
|
||||
pipe_stderr=not os.isatty(2),
|
||||
tcflags=tcattr,
|
||||
term=os.environ.get("TERM", None),
|
||||
rows=rows,
|
||||
cols=cols,
|
||||
hpix=hpix,
|
||||
vpix=vpix))
|
||||
|
||||
loop.add_signal_handler(signal.SIGWINCH, sigwinch_handler)
|
||||
_finished = asyncio.Event()
|
||||
loop.add_signal_handler(signal.SIGINT, functools.partial(_sigint_handler, signal.SIGINT, loop))
|
||||
loop.add_signal_handler(signal.SIGTERM, functools.partial(_sigint_handler, signal.SIGTERM, loop))
|
||||
mdu = _link.MDU - 16
|
||||
sent_eof = False
|
||||
last_winch = time.time()
|
||||
sleeper = helpers.SleepRate(0.01)
|
||||
processed = False
|
||||
while not await _check_finished() and state in [InitiatorState.IS_RUNNING]:
|
||||
try:
|
||||
try:
|
||||
message = _pq.get(timeout=sleeper.next_sleep_time() if not processed else 0.0005)
|
||||
await _handle_error(message)
|
||||
processed = True
|
||||
if isinstance(message, protocol.StreamDataMessage):
|
||||
if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDOUT:
|
||||
if message.data and len(message.data) > 0:
|
||||
ttyRestorer.raw()
|
||||
RNS.log(f"stdout: {message.data}", RNS.LOG_DEBUG)
|
||||
os.write(1, message.data)
|
||||
sys.stdout.flush()
|
||||
if message.eof:
|
||||
os.close(1)
|
||||
if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDERR:
|
||||
if message.data and len(message.data) > 0:
|
||||
ttyRestorer.raw()
|
||||
RNS.log(f"stdout: {message.data}", RNS.LOG_DEBUG)
|
||||
os.write(2, message.data)
|
||||
sys.stderr.flush()
|
||||
if message.eof:
|
||||
os.close(2)
|
||||
elif isinstance(message, protocol.CommandExitedMessage):
|
||||
RNS.log(f"received return code {message.return_code}, exiting", RNS.LOG_DEBUG)
|
||||
return message.return_code
|
||||
elif isinstance(message, protocol.ErrorMessage):
|
||||
RNS.log(f"Remote error: {message.data}", RNS.LOG_ERROR)
|
||||
if message.fatal:
|
||||
_link.teardown()
|
||||
return 200
|
||||
|
||||
except queue.Empty:
|
||||
processed = False
|
||||
|
||||
if channel.is_ready_to_send():
|
||||
def compress_adaptive(buf: bytes):
|
||||
comp_tries = RNS.RawChannelWriter.COMPRESSION_TRIES
|
||||
comp_try = 1
|
||||
comp_success = False
|
||||
|
||||
chunk_len = len(buf)
|
||||
if chunk_len > RNS.RawChannelWriter.MAX_CHUNK_LEN:
|
||||
chunk_len = RNS.RawChannelWriter.MAX_CHUNK_LEN
|
||||
chunk_segment = None
|
||||
|
||||
chunk_segment = None
|
||||
max_data_len = channel.mdu - protocol.StreamDataMessage.OVERHEAD
|
||||
while chunk_len > 32 and comp_try < comp_tries:
|
||||
chunk_segment_length = int(chunk_len/comp_try)
|
||||
compressed_chunk = bz2.compress(buf[:chunk_segment_length])
|
||||
compressed_length = len(compressed_chunk)
|
||||
if compressed_length < max_data_len and compressed_length < chunk_segment_length:
|
||||
comp_success = True
|
||||
break
|
||||
else:
|
||||
comp_try += 1
|
||||
|
||||
if comp_success:
|
||||
diff = max_data_len - len(compressed_chunk)
|
||||
chunk = compressed_chunk
|
||||
processed_length = chunk_segment_length
|
||||
else:
|
||||
chunk = bytes(buf[:max_data_len])
|
||||
processed_length = len(chunk)
|
||||
|
||||
return comp_success, processed_length, chunk
|
||||
|
||||
comp_success, processed_length, chunk = compress_adaptive(data_buffer)
|
||||
stdin = chunk
|
||||
data_buffer = data_buffer[processed_length:]
|
||||
eof = not sent_eof and stdin_eof and len(stdin) == 0
|
||||
if len(stdin) > 0 or eof:
|
||||
channel.send(protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDIN, stdin, eof, comp_success))
|
||||
sent_eof = eof
|
||||
processed = True
|
||||
|
||||
# send window change, but rate limited
|
||||
if winch and time.time() - last_winch > _link.rtt * 25:
|
||||
last_winch = time.time()
|
||||
winch = False
|
||||
with contextlib.suppress(Exception):
|
||||
r, c, h, v = process.tty_get_winsize(0)
|
||||
channel.send(protocol.WindowSizeMessage(r, c, h, v))
|
||||
processed = True
|
||||
except RemoteExecutionError as e:
|
||||
print(e.msg)
|
||||
return 255
|
||||
except Exception as ex:
|
||||
print(f"Client exception: {ex}")
|
||||
if _link and _link.status != RNS.Link.CLOSED:
|
||||
_link.teardown()
|
||||
return 127
|
||||
|
||||
RNS.log("Main loop done", RNS.LOG_DEBUG)
|
||||
return 0
|
||||
@@ -0,0 +1,229 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import queue
|
||||
import shlex
|
||||
import signal
|
||||
import sys
|
||||
import termios
|
||||
import threading
|
||||
import time
|
||||
import tty
|
||||
from typing import Callable, TypeVar
|
||||
import RNS
|
||||
import RNS.Utilities.rnsh.exception as exception
|
||||
import RNS.Utilities.rnsh.process as process
|
||||
import RNS.Utilities.rnsh.retry as retry
|
||||
import RNS.Utilities.rnsh.session as session
|
||||
import re
|
||||
import contextlib
|
||||
|
||||
import pwd
|
||||
import RNS.Utilities.rnsh.protocol as protocol
|
||||
import RNS.Utilities.rnsh.helpers as helpers
|
||||
import RNS.Utilities.rnsh.rnsh as rnsh
|
||||
|
||||
|
||||
_identity = None
|
||||
_reticulum = None
|
||||
_allow_all = False
|
||||
_allowed_file = None
|
||||
_allowed_identity_hashes = []
|
||||
_allowed_file_identity_hashes = []
|
||||
_cmd: [str] | None = None
|
||||
DATA_AVAIL_MSG = "data available"
|
||||
_finished: asyncio.Event = None
|
||||
_retry_timer: retry.RetryThread | None = None
|
||||
_destination: RNS.Destination | None = None
|
||||
_loop: asyncio.AbstractEventLoop | None = None
|
||||
_no_remote_command = True
|
||||
_remote_cmd_as_args = False
|
||||
|
||||
|
||||
async def _check_finished(timeout: float = 0):
|
||||
return await process.event_wait(_finished, timeout=timeout)
|
||||
|
||||
|
||||
def _sigint_handler(sig, loop):
|
||||
global _finished
|
||||
RNS.log(f"Signal: {signal.Signals(sig).name}", RNS.LOG_DEBUG)
|
||||
if _finished is not None: _finished.set()
|
||||
else: raise KeyboardInterrupt()
|
||||
|
||||
def _reload_allowed_file():
|
||||
global _allowed_file, _allowed_file_identity_hashes
|
||||
if _allowed_file != None:
|
||||
try:
|
||||
with open(_allowed_file, "r") as file:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
|
||||
added = 0
|
||||
line = 0
|
||||
_allowed_file_identity_hashes = []
|
||||
for allow in file.read().replace("\r", "").split("\n"):
|
||||
line += 1
|
||||
if len(allow) == dest_len:
|
||||
try:
|
||||
destination_hash = bytes.fromhex(allow)
|
||||
_allowed_file_identity_hashes.append(destination_hash)
|
||||
added += 1
|
||||
except Exception:
|
||||
RNS.log(f"Discarded invalid Identity hash in {_allowed_file} at line {line}", RNS.LOG_DEBUG)
|
||||
|
||||
ms = "y" if added == 1 else "ies"
|
||||
RNS.log(f"Loaded {added} allowed identit{ms} from "+str(_allowed_file), RNS.LOG_DEBUG)
|
||||
|
||||
except Exception as e: RNS.log(f"Error while reloading allowed indetities file: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def compute_target_rns_loglevel(verbosity: int, quietness: int, base_level: int = RNS.LOG_INFO) -> int:
|
||||
try:
|
||||
target = int(base_level) + int(verbosity) - int(quietness)
|
||||
if target < RNS.LOG_CRITICAL: target = RNS.LOG_CRITICAL
|
||||
if target > RNS.LOG_DEBUG: target = RNS.LOG_DEBUG
|
||||
return target
|
||||
|
||||
except Exception: return base_level
|
||||
|
||||
async def listen(configdir, rnsconfigdir, command, identitypath=None, service_name=None, verbosity=0, quietness=0, allowed=None,
|
||||
allowed_file=None, disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False,
|
||||
loop: asyncio.AbstractEventLoop = None):
|
||||
global _identity, _allow_all, _allowed_identity_hashes, _allowed_file, _allowed_file_identity_hashes
|
||||
global _reticulum, _cmd, _destination, _no_remote_command, _remote_cmd_as_args, _finished
|
||||
|
||||
if not loop: loop = asyncio.get_running_loop()
|
||||
if service_name is None or len(service_name) == 0:
|
||||
service_name = "default"
|
||||
|
||||
RNS.log(f"Using service name {service_name}", RNS.LOG_INFO)
|
||||
|
||||
# More -v should increase verbosity (higher RNS.loglevel); -q should decrease it
|
||||
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_INFO)
|
||||
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel)
|
||||
_identity = rnsh.prepare_identity(identitypath, service_name)
|
||||
_destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.APP_NAME)
|
||||
|
||||
RNS.log(f"rnsh listening for commands on {RNS.prettyhexrep(_destination.hash)}", RNS.LOG_NOTICE)
|
||||
|
||||
_cmd = command
|
||||
if _cmd is None or len(_cmd) == 0:
|
||||
shell = None
|
||||
try: shell = pwd.getpwuid(os.getuid()).pw_shell
|
||||
except Exception as e: RNS.log(f"Error looking up shell: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"Using {shell} for default command.", RNS.LOG_INFO)
|
||||
|
||||
# Ensure a sane shell default. Fall back to /bin/sh if lookup fails.
|
||||
if not shell or len(shell) == 0: shell = "/bin/sh"
|
||||
_cmd = [shell]
|
||||
|
||||
else: RNS.log(f"Using command {shlex.join(_cmd)}", RNS.LOG_INFO)
|
||||
|
||||
_no_remote_command = no_remote_command
|
||||
session.ListenerSession.allow_remote_command = not no_remote_command
|
||||
_remote_cmd_as_args = remote_cmd_as_args
|
||||
if (_cmd is None or len(_cmd) == 0 or _cmd[0] is None or len(_cmd[0]) == 0) \
|
||||
and (_no_remote_command or _remote_cmd_as_args):
|
||||
raise Exception(f"Unable to look up shell for {os.getlogin}, cannot proceed with -A or -C and no <program>.")
|
||||
|
||||
session.ListenerSession.default_command = _cmd
|
||||
session.ListenerSession.remote_cmd_as_args = _remote_cmd_as_args
|
||||
|
||||
if disable_auth:
|
||||
_allow_all = True
|
||||
session.ListenerSession.allow_all = True
|
||||
else:
|
||||
if allowed_file is not None:
|
||||
_allowed_file = allowed_file
|
||||
_reload_allowed_file()
|
||||
|
||||
if allowed is not None:
|
||||
for a in allowed:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
|
||||
if len(a) != dest_len:
|
||||
raise ValueError(
|
||||
"Allowed destination length is invalid, must be {hex} hexadecimal " +
|
||||
"characters ({byte} bytes).".format(
|
||||
hex=dest_len, byte=dest_len // 2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(a)
|
||||
_allowed_identity_hashes.append(destination_hash)
|
||||
session.ListenerSession.allowed_identity_hashes.append(destination_hash)
|
||||
except Exception:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Unhandled error: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
exit(1)
|
||||
|
||||
if (len(_allowed_identity_hashes) < 1 and len(_allowed_file_identity_hashes) < 1) and not disable_auth:
|
||||
RNS.log("Warning: No allowed identities configured, rnsh will not accept any connections!", RNS.LOG_WARNING)
|
||||
|
||||
def link_established(lnk: RNS.Link):
|
||||
_reload_allowed_file()
|
||||
session.ListenerSession.allowed_file_identity_hashes = _allowed_file_identity_hashes
|
||||
session.ListenerSession(session.RNSOutlet.get_outlet(lnk), lnk.get_channel(), loop)
|
||||
_destination.set_link_established_callback(link_established)
|
||||
|
||||
_finished = asyncio.Event()
|
||||
signal.signal(signal.SIGINT, _sigint_handler)
|
||||
|
||||
if announce_period is not None: _destination.announce()
|
||||
|
||||
last_announce = time.time()
|
||||
sleeper = helpers.SleepRate(0.01)
|
||||
|
||||
try:
|
||||
while not await _check_finished():
|
||||
if announce_period and 0 < announce_period < time.time() - last_announce:
|
||||
last_announce = time.time()
|
||||
_destination.announce()
|
||||
if len(session.ListenerSession.sessions) > 0:
|
||||
# no sleep if there's work to do
|
||||
if not await session.ListenerSession.pump_all():
|
||||
await sleeper.sleep_async()
|
||||
else:
|
||||
await asyncio.sleep(0.25)
|
||||
finally:
|
||||
RNS.log("Shutting down", RNS.LOG_NOTICE)
|
||||
await session.ListenerSession.terminate_all("Shutting down")
|
||||
await asyncio.sleep(1)
|
||||
links_still_active = list(filter(lambda l: l.status != RNS.Link.CLOSED, _destination.links))
|
||||
for link in links_still_active:
|
||||
if link.status not in [RNS.Link.CLOSED]:
|
||||
link.teardown()
|
||||
await asyncio.sleep(0.01)
|
||||
@@ -0,0 +1,46 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 asyncio
|
||||
import functools
|
||||
from typing import Callable
|
||||
|
||||
def sig_handler_sys_to_loop(handler: Callable[[int, any], None]) -> Callable[[int, asyncio.AbstractEventLoop], None]:
|
||||
def wrapped(cb: Callable[[int, any], None], signal: int, loop: asyncio.AbstractEventLoop): cb(signal, None)
|
||||
return functools.partial(wrapped, handler)
|
||||
|
||||
def loop_set_signal(sig, handler: Callable[[int, asyncio.AbstractEventLoop], None], loop: asyncio.AbstractEventLoop = None):
|
||||
if loop is None: loop = asyncio.get_running_loop()
|
||||
loop.remove_signal_handler(sig)
|
||||
loop.add_signal_handler(sig, functools.partial(handler, sig, loop))
|
||||
@@ -0,0 +1,785 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 __future__ import annotations
|
||||
import asyncio
|
||||
import contextlib
|
||||
import copy
|
||||
import errno
|
||||
import fcntl
|
||||
import functools
|
||||
import os
|
||||
import pty
|
||||
import select
|
||||
import signal
|
||||
import struct
|
||||
import sys
|
||||
import termios
|
||||
import threading
|
||||
import tty
|
||||
import types
|
||||
import typing
|
||||
import RNS
|
||||
|
||||
import RNS.Utilities.rnsh.exception as exception
|
||||
|
||||
CTRL_C = "\x03".encode("utf-8")
|
||||
CTRL_D = "\x04".encode("utf-8")
|
||||
|
||||
def tty_add_reader_callback(fd: int, callback: callable, loop: asyncio.AbstractEventLoop = None):
|
||||
"""
|
||||
Add an async reader callback for a tty file descriptor.
|
||||
|
||||
Example usage:
|
||||
|
||||
def reader():
|
||||
data = tty_read(fd)
|
||||
# do something with data
|
||||
|
||||
tty_add_reader_callback(self._child_fd, reader, self._loop)
|
||||
|
||||
:param fd: file descriptor
|
||||
:param callback: callback function
|
||||
:param loop: asyncio event loop to which the reader should be added. If None, use the currently-running loop.
|
||||
"""
|
||||
if loop is None:
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.add_reader(fd, callback)
|
||||
|
||||
|
||||
def tty_read(fd: int) -> bytes:
|
||||
"""
|
||||
Read available bytes from a tty file descriptor. When used in a callback added to a file descriptor using
|
||||
tty_add_reader_callback(...), this function creates a solution for non-blocking reads from ttys.
|
||||
:param fd: tty file descriptor
|
||||
:return: bytes read
|
||||
"""
|
||||
if fd_is_closed(fd):
|
||||
raise EOFError
|
||||
|
||||
try:
|
||||
run = True
|
||||
result = bytearray()
|
||||
while not fd_is_closed(fd):
|
||||
ready, _, _ = select.select([fd], [], [], 0)
|
||||
if len(ready) == 0:
|
||||
break
|
||||
for f in ready:
|
||||
try:
|
||||
data = os.read(f, 4096)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EIO and e.errno != errno.EWOULDBLOCK:
|
||||
raise
|
||||
else:
|
||||
if not data: # EOF
|
||||
if data is not None and len(data) > 0:
|
||||
result.extend(data)
|
||||
return result
|
||||
elif len(result) > 0:
|
||||
return result
|
||||
else:
|
||||
raise EOFError
|
||||
if data is not None and len(data) > 0:
|
||||
result.extend(data)
|
||||
return result
|
||||
|
||||
except EOFError: raise
|
||||
except Exception as e: RNS.log(f"TTY read error: {e}", RNS.LOG_ERROR)
|
||||
|
||||
|
||||
def tty_read_poll(fd: int) -> bytes:
|
||||
"""
|
||||
Read available bytes from a tty file descriptor. When used in a callback added to a file descriptor using
|
||||
tty_add_reader_callback(...), this function creates a solution for non-blocking reads from ttys.
|
||||
:param fd: tty file descriptor
|
||||
:return: bytes read
|
||||
"""
|
||||
if fd_is_closed(fd):
|
||||
raise EOFError
|
||||
|
||||
result = bytearray()
|
||||
try:
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
while True:
|
||||
try:
|
||||
data = os.read(fd, 4096)
|
||||
if not data:
|
||||
# EOF
|
||||
if len(result) > 0:
|
||||
return result
|
||||
raise EOFError
|
||||
result.extend(data)
|
||||
# continue loop to drain
|
||||
except OSError as e:
|
||||
if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||
break
|
||||
if e.errno == errno.EIO:
|
||||
if len(result) > 0:
|
||||
return result
|
||||
raise EOFError
|
||||
raise
|
||||
except EOFError: raise
|
||||
except Exception as e: RNS.log(f"TTY read error: {e}", RNS.LOG_ERROR)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def fd_is_closed(fd: int) -> bool:
|
||||
"""
|
||||
Check if file descriptor is closed
|
||||
:param fd: file descriptor
|
||||
:return: True if file descriptor is closed
|
||||
"""
|
||||
try:
|
||||
fcntl.fcntl(fd, fcntl.F_GETFL) < 0
|
||||
except OSError as ose:
|
||||
return ose.errno == errno.EBADF
|
||||
|
||||
|
||||
def tty_unset_reader_callbacks(fd: int, loop: asyncio.AbstractEventLoop = None):
|
||||
"""
|
||||
Remove async reader callbacks for file descriptor.
|
||||
:param fd: file descriptor
|
||||
:param loop: asyncio event loop from which to remove callbacks
|
||||
"""
|
||||
with exception.permit(SystemExit):
|
||||
if loop is None:
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.remove_reader(fd)
|
||||
|
||||
|
||||
def tty_get_winsize(fd: int) -> [int, int, int, int]:
|
||||
"""
|
||||
Ge the window size of a tty.
|
||||
:param fd: file descriptor of tty
|
||||
:return: (rows, cols, h_pixels, v_pixels)
|
||||
"""
|
||||
packed = fcntl.ioctl(fd, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
|
||||
rows, cols, h_pixels, v_pixels = struct.unpack('HHHH', packed)
|
||||
return rows, cols, h_pixels, v_pixels
|
||||
|
||||
|
||||
def tty_set_winsize(fd: int, rows: int, cols: int, h_pixels: int, v_pixels: int):
|
||||
"""
|
||||
Set the window size on a tty.
|
||||
:param fd: file descriptor of tty
|
||||
:param rows: number of visible rows
|
||||
:param cols: number of visible columns
|
||||
:param h_pixels: number of visible horizontal pixels
|
||||
:param v_pixels: number of visible vertical pixels
|
||||
"""
|
||||
if fd < 0:
|
||||
return
|
||||
packed = struct.pack('HHHH', rows, cols, h_pixels, v_pixels)
|
||||
fcntl.ioctl(fd, termios.TIOCSWINSZ, packed)
|
||||
|
||||
|
||||
def process_exists(pid) -> bool:
|
||||
"""
|
||||
Check For the existence of a unix pid.
|
||||
:param pid: process id to check
|
||||
:return: True if process exists
|
||||
"""
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class TTYRestorer(contextlib.AbstractContextManager):
|
||||
# Indexes of flags within the attrs array
|
||||
ATTR_IDX_IFLAG = 0
|
||||
ATTR_IDX_OFLAG = 1
|
||||
ATTR_IDX_CFLAG = 2
|
||||
ATTR_IDX_LFLAG = 4
|
||||
ATTR_IDX_CC = 5
|
||||
|
||||
def __init__(self, fd: int, suppress_logs=False):
|
||||
"""
|
||||
Saves termios attributes for a tty for later restoration.
|
||||
|
||||
The attributes are an array of values with the following meanings.
|
||||
|
||||
tcflag_t c_iflag; /* input modes */
|
||||
tcflag_t c_oflag; /* output modes */
|
||||
tcflag_t c_cflag; /* control modes */
|
||||
tcflag_t c_lflag; /* local modes */
|
||||
cc_t c_cc[NCCS]; /* special characters */
|
||||
|
||||
:param fd: file descriptor of tty
|
||||
"""
|
||||
self._fd = fd
|
||||
self._tattr = None
|
||||
self._suppress_logs = suppress_logs
|
||||
self._tattr = self.current_attr()
|
||||
if not self._tattr and not self._suppress_logs: RNS.log(f"Could not get attrs for fd {fd}", RNS.LOG_DEBUG)
|
||||
|
||||
def raw(self):
|
||||
"""
|
||||
Set raw mode on tty
|
||||
"""
|
||||
if self._fd is None:
|
||||
return
|
||||
with contextlib.suppress(termios.error):
|
||||
tty.setraw(self._fd, termios.TCSANOW)
|
||||
|
||||
def original_attr(self) -> [any]:
|
||||
return copy.deepcopy(self._tattr)
|
||||
|
||||
def current_attr(self) -> [any]:
|
||||
"""
|
||||
Get the current termios attributes for the wrapped fd.
|
||||
:return: attribute array
|
||||
"""
|
||||
if self._fd is None:
|
||||
return None
|
||||
|
||||
with contextlib.suppress(termios.error):
|
||||
return copy.deepcopy(termios.tcgetattr(self._fd))
|
||||
return None
|
||||
|
||||
def set_attr(self, attr: [any], when: int = termios.TCSADRAIN):
|
||||
"""
|
||||
Set termios attributes
|
||||
:param attr: attribute list to set
|
||||
:param when: when attributes should be applied (termios.TCSANOW, termios.TCSADRAIN, termios.TCSAFLUSH)
|
||||
"""
|
||||
if not attr or self._fd is None:
|
||||
return
|
||||
|
||||
with contextlib.suppress(termios.error):
|
||||
termios.tcsetattr(self._fd, when, attr)
|
||||
|
||||
def isatty(self):
|
||||
return os.isatty(self._fd) if self._fd is not None else None
|
||||
|
||||
def restore(self):
|
||||
"""
|
||||
Restore termios settings to state captured in constructor.
|
||||
"""
|
||||
self.set_attr(self._tattr, termios.TCSADRAIN)
|
||||
|
||||
def __exit__(self, __exc_type: typing.Type[BaseException], __exc_value: BaseException,
|
||||
__traceback: types.TracebackType) -> bool:
|
||||
self.restore()
|
||||
return False #__exc_type is not None and issubclass(__exc_type, termios.error)
|
||||
|
||||
|
||||
def _task_from_event(evt: asyncio.Event, loop: asyncio.AbstractEventLoop = None):
|
||||
if not loop:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
#TODO: this is hacky
|
||||
async def wait():
|
||||
while not evt.is_set():
|
||||
await asyncio.sleep(0.1)
|
||||
return True
|
||||
|
||||
return loop.create_task(wait())
|
||||
|
||||
|
||||
class AggregateException(Exception):
|
||||
def __init__(self, inner_exceptions: [Exception]):
|
||||
super().__init__()
|
||||
self.inner_exceptions = inner_exceptions
|
||||
|
||||
def __str__(self):
|
||||
return "Multiple exceptions encountered: \n\n" + "\n\n".join(map(lambda e: str(e), self.inner_exceptions))
|
||||
|
||||
|
||||
async def event_wait_any(evts: [asyncio.Event], timeout: float = None) -> (any, any):
|
||||
tasks = list(map(lambda evt: (evt, _task_from_event(evt)), evts))
|
||||
try:
|
||||
finished, unfinished = await asyncio.wait(map(lambda t: t[1], tasks),
|
||||
timeout=timeout,
|
||||
return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
if len(unfinished) > 0:
|
||||
for task in unfinished:
|
||||
task.cancel()
|
||||
await asyncio.wait(unfinished)
|
||||
|
||||
exceptions = []
|
||||
|
||||
for f in finished:
|
||||
ex = f.exception()
|
||||
if ex and not isinstance(ex, asyncio.CancelledError) and not isinstance(ex, TimeoutError):
|
||||
exceptions.append(ex)
|
||||
|
||||
if len(exceptions) > 0:
|
||||
raise AggregateException(exceptions)
|
||||
|
||||
return next(map(lambda t: next(map(lambda tt: tt[0], tasks)), finished), None)
|
||||
finally:
|
||||
unfinished = []
|
||||
for task in map(lambda t: t[1], tasks):
|
||||
if task.done():
|
||||
if not task.cancelled():
|
||||
task.exception()
|
||||
else:
|
||||
task.cancel()
|
||||
unfinished.append(task)
|
||||
if len(unfinished) > 0:
|
||||
await asyncio.wait(unfinished)
|
||||
|
||||
|
||||
async def event_wait(evt: asyncio.Event, timeout: float) -> bool:
|
||||
"""
|
||||
Wait for event to be set, or timeout to expire.
|
||||
:param evt: asyncio.Event to wait on
|
||||
:param timeout: maximum number of seconds to wait.
|
||||
:return: True if event was set, False if timeout expired
|
||||
"""
|
||||
await event_wait_any([evt], timeout=timeout)
|
||||
return evt.is_set()
|
||||
|
||||
|
||||
def _launch_child(cmd_line: list[str], env: dict[str, str], stdin_is_pipe: bool, stdout_is_pipe: bool,
|
||||
stderr_is_pipe: bool) -> tuple[int, int, int, int]:
|
||||
# Set up PTY and/or pipes
|
||||
child_fd = parent_fd = None
|
||||
if not (stdin_is_pipe and stdout_is_pipe and stderr_is_pipe):
|
||||
parent_fd, child_fd = pty.openpty()
|
||||
child_stdin, parent_stdin = (os.pipe() if stdin_is_pipe else (child_fd, parent_fd))
|
||||
parent_stdout, child_stdout = (os.pipe() if stdout_is_pipe else (parent_fd, child_fd))
|
||||
parent_stderr, child_stderr = (os.pipe() if stderr_is_pipe else (parent_fd, child_fd))
|
||||
|
||||
# Fork
|
||||
pid = os.fork()
|
||||
|
||||
if pid == 0:
|
||||
try:
|
||||
# We are in the child process, so close all open sockets and pipes except for the PTY and/or pipes
|
||||
max_fd = os.sysconf("SC_OPEN_MAX")
|
||||
for fd in range(3, max_fd):
|
||||
if fd not in (child_stdin, child_stdout, child_stderr):
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Set up PTY and/or pipes
|
||||
os.dup2(child_stdin, 0)
|
||||
os.dup2(child_stdout, 1)
|
||||
os.dup2(child_stderr, 2)
|
||||
# Make PTY controlling if necessary so that CTRL_C/CTRL_D behave as expected
|
||||
if child_fd is not None:
|
||||
os.setsid()
|
||||
try:
|
||||
tty_fd = 0 if not stdin_is_pipe else (1 if not stdout_is_pipe else 2)
|
||||
# Set controlling TTY for this session
|
||||
fcntl.ioctl(tty_fd, termios.TIOCSCTTY, 0)
|
||||
except Exception:
|
||||
pass
|
||||
# Ensure the child is the foreground process group for the TTY
|
||||
try:
|
||||
os.setpgid(0, 0)
|
||||
pgid = os.getpgrp()
|
||||
import struct as _struct
|
||||
fcntl.ioctl(tty_fd, termios.TIOCSPGRP, _struct.pack('i', pgid))
|
||||
except Exception:
|
||||
pass
|
||||
# Ensure canonical input with signals and local echo enabled
|
||||
try:
|
||||
tty_fd = 0 if not stdin_is_pipe else (1 if not stdout_is_pipe else 2)
|
||||
attrs = termios.tcgetattr(tty_fd)
|
||||
lflag = attrs[3]
|
||||
lflag |= termios.ICANON | termios.ISIG | termios.ECHO
|
||||
attrs[3] = lflag
|
||||
termios.tcsetattr(tty_fd, termios.TCSANOW, attrs)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Execute the command
|
||||
os.execvpe(cmd_line[0], cmd_line, env)
|
||||
except Exception as err:
|
||||
exc_type, exc_obj, exc_tb = sys.exc_info()
|
||||
fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
|
||||
print(f"Unable to start {cmd_line[0]}: {err} ({fname}:{exc_tb.tb_lineno})")
|
||||
sys.stdout.flush()
|
||||
# don't let any other modules get in our way, do an immediate silent exit.
|
||||
os._exit(255)
|
||||
|
||||
else:
|
||||
# We are in the parent process, so close the child-side of the PTY and/or pipes
|
||||
if child_fd is not None:
|
||||
os.close(child_fd)
|
||||
if child_stdin != child_fd:
|
||||
os.close(child_stdin)
|
||||
if child_stdout != child_fd:
|
||||
os.close(child_stdout)
|
||||
if child_stderr != child_fd:
|
||||
os.close(child_stderr)
|
||||
# # Close the write end of the pipe if a pipe is used for standard input
|
||||
# if not stdin_is_pipe:
|
||||
# os.close(parent_stdin)
|
||||
# Return the child PID and the file descriptors for the PTY and/or pipes
|
||||
return pid, parent_stdin, parent_stdout, parent_stderr
|
||||
|
||||
|
||||
class CallbackSubprocess:
|
||||
# time between checks of child process
|
||||
PROCESS_POLL_TIME: float = 0.1
|
||||
# Close pipes soon after process exit to avoid scheduling on closed event loops
|
||||
PROCESS_PIPE_TIME: int = 1
|
||||
|
||||
def __init__(self, argv: [str], env: dict, loop: asyncio.AbstractEventLoop, stdout_callback: callable,
|
||||
stderr_callback: callable, terminated_callback: callable, stdin_is_pipe: bool, stdout_is_pipe: bool,
|
||||
stderr_is_pipe: bool):
|
||||
"""
|
||||
Fork a child process and generate callbacks with output from the process.
|
||||
:param argv: the command line, tokenized. The first element must be the absolute path to an executable file.
|
||||
:param env: environment variables to override
|
||||
:param loop: the asyncio event loop to use
|
||||
:param stdout_callback: callback for data, e.g. def callback(data:bytes) -> None
|
||||
:param terminated_callback: callback for termination/return code, e.g. def callback(return_code:int) -> None
|
||||
"""
|
||||
assert loop is not None, "loop should not be None"
|
||||
assert stdout_callback is not None, "stdout_callback should not be None"
|
||||
assert terminated_callback is not None, "terminated_callback should not be None"
|
||||
|
||||
self._command: [str] = argv
|
||||
self._env = env or {}
|
||||
self._loop = loop
|
||||
self._stdout_cb = stdout_callback
|
||||
self._stderr_cb = stderr_callback
|
||||
self._terminated_cb = terminated_callback
|
||||
self._pid: int = None
|
||||
self._child_stdin: int = None
|
||||
self._child_stdout: int = None
|
||||
self._child_stderr: int = None
|
||||
self._return_code: int = None
|
||||
self._stdout_eof: bool = False
|
||||
self._stderr_eof: bool = False
|
||||
self._stdin_is_pipe = stdin_is_pipe
|
||||
self._stdout_is_pipe = stdout_is_pipe
|
||||
self._stderr_is_pipe = stderr_is_pipe
|
||||
self._at_line_start: bool = True
|
||||
self._tty_line_buffer: bytearray = bytearray()
|
||||
|
||||
def _ensure_pipes_closed(self):
|
||||
stdin = self._child_stdin
|
||||
stdout = self._child_stdout
|
||||
stderr = self._child_stderr
|
||||
fds = set(filter(lambda x: x is not None, list({stdin, stdout, stderr})))
|
||||
RNS.log(f"Queuing close of pipes for ended process (fds: {fds})", RNS.LOG_DEBUG)
|
||||
|
||||
def ensure_pipes_closed_inner():
|
||||
RNS.log(f"Ensuring pipes are closed (fds: {fds})", RNS.LOG_DEBUG)
|
||||
for fd in fds:
|
||||
RNS.log(f"Closing fd {fd}", RNS.LOG_DEBUG)
|
||||
with contextlib.suppress(OSError): tty_unset_reader_callbacks(fd)
|
||||
with contextlib.suppress(OSError): os.close(fd)
|
||||
|
||||
self._child_stdin = None
|
||||
self._child_stdout = None
|
||||
self._child_stderr = None
|
||||
|
||||
# Avoid scheduling on a closed loop
|
||||
if self._loop.is_closed(): ensure_pipes_closed_inner()
|
||||
else: self._loop.call_later(CallbackSubprocess.PROCESS_PIPE_TIME, ensure_pipes_closed_inner)
|
||||
|
||||
def terminate(self, kill_delay: float = 1.0):
|
||||
"""
|
||||
Terminate child process if running
|
||||
:param kill_delay: if after kill_delay seconds the child process has not exited, escalate to SIGHUP and SIGKILL
|
||||
"""
|
||||
|
||||
RNS.log("terminate()", RNS.LOG_EXTREME)
|
||||
if not self.running: return
|
||||
|
||||
with exception.permit(SystemExit): os.kill(self._pid, signal.SIGTERM)
|
||||
|
||||
def kill():
|
||||
if process_exists(self._pid):
|
||||
RNS.log("kill()", RNS.LOG_EXTREME)
|
||||
with exception.permit(SystemExit):
|
||||
os.kill(self._pid, signal.SIGHUP)
|
||||
os.kill(self._pid, signal.SIGKILL)
|
||||
|
||||
self._loop.call_later(kill_delay, kill)
|
||||
|
||||
def wait():
|
||||
RNS.log("wait()", RNS.LOG_EXTREME)
|
||||
with contextlib.suppress(OSError): os.waitpid(self._pid, 0)
|
||||
self._ensure_pipes_closed()
|
||||
RNS.log("wait() finish", RNS.LOG_EXTREME)
|
||||
|
||||
threading.Thread(target=wait, daemon=True).start()
|
||||
|
||||
def close_stdin(self):
|
||||
with contextlib.suppress(Exception):
|
||||
os.close(self._child_stdin)
|
||||
# Encourage prompt shutdown if child lingers after stdin close
|
||||
def _ensure_terminate():
|
||||
if self.running:
|
||||
self.terminate(kill_delay=0.2)
|
||||
if not self._loop.is_closed():
|
||||
self._loop.call_later(0.05, _ensure_terminate)
|
||||
|
||||
@property
|
||||
def started(self) -> bool:
|
||||
"""
|
||||
:return: True if child process has been started
|
||||
"""
|
||||
return self._pid is not None
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
"""
|
||||
:return: True if child process is still running
|
||||
"""
|
||||
return self._pid is not None and process_exists(self._pid)
|
||||
|
||||
def write(self, data: bytes):
|
||||
"""
|
||||
Write bytes to the stdin of the child process.
|
||||
:param data: bytes to write
|
||||
"""
|
||||
|
||||
os.write(self._child_stdin, data)
|
||||
|
||||
# TODO: Check what this is actually supposed to solve.
|
||||
#
|
||||
# For pipe-in + TTY-out, echo should be visible immediately
|
||||
if self._stdin_is_pipe and not self._stdout_is_pipe and self._stdout_cb is not None and data not in (CTRL_C, CTRL_D):
|
||||
try: self._stdout_cb(data)
|
||||
except Exception: pass
|
||||
|
||||
def set_winsize(self, r: int, c: int, h: int, v: int):
|
||||
"""
|
||||
Set the window size on the tty of the child process.
|
||||
:param r: rows visible
|
||||
:param c: columns visible
|
||||
:param h: horizontal pixels visible
|
||||
:param v: vertical pixels visible
|
||||
:return:
|
||||
"""
|
||||
RNS.log(f"set_winsize({r},{c},{h},{v}", RNS.LOG_DEBUG)
|
||||
tty_set_winsize(self._child_stdout, r, c, h, v)
|
||||
|
||||
def copy_winsize(self, fromfd: int):
|
||||
"""
|
||||
Copy window size from one tty to another.
|
||||
:param fromfd: source tty file descriptor
|
||||
"""
|
||||
r, c, h, v = tty_get_winsize(fromfd)
|
||||
self.set_winsize(r, c, h, v)
|
||||
|
||||
def tcsetattr(self, when: int, attr: list[any]): # actual type is list[int | list[int | bytes]]
|
||||
"""
|
||||
Set tty attributes.
|
||||
:param when: when to apply change: termios.TCSANOW or termios.TCSADRAIN or termios.TCSAFLUSH
|
||||
:param attr: attributes to set
|
||||
"""
|
||||
termios.tcsetattr(self._child_stdin, when, attr)
|
||||
|
||||
def tcgetattr(self) -> list[any]: # actual type is list[int | list[int | bytes]]
|
||||
"""
|
||||
Get tty attributes.
|
||||
:return: tty attributes value
|
||||
"""
|
||||
return termios.tcgetattr(self._child_stdout)
|
||||
|
||||
def ttysetraw(self):
|
||||
tty.setraw(self._child_stdout, termios.TCSADRAIN)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start the child process.
|
||||
"""
|
||||
RNS.log("start()", RNS.LOG_EXTREME)
|
||||
|
||||
# # Using the parent environment seems to do some weird stuff, at least on macOS
|
||||
# parentenv = os.environ.copy()
|
||||
# env = {"HOME": parentenv["HOME"],
|
||||
# "PATH": parentenv["PATH"],
|
||||
# "TERM": self._term if self._term is not None else parentenv.get("TERM", "xterm"),
|
||||
# "LANG": parentenv.get("LANG"),
|
||||
# "SHELL": self._command[0]}
|
||||
|
||||
env = os.environ.copy()
|
||||
for key in self._env:
|
||||
env[key] = self._env[key]
|
||||
|
||||
program = self._command[0]
|
||||
assert isinstance(program, str)
|
||||
|
||||
# match = re.search("^/bin/(.*sh)$", program)
|
||||
# if match:
|
||||
# self._command[0] = "-" + match.group(1)
|
||||
# env["SHELL"] = program
|
||||
# self._log.debug(f"set login shell {self._command}")
|
||||
|
||||
self._pid, \
|
||||
self._child_stdin, \
|
||||
self._child_stdout, \
|
||||
self._child_stderr = _launch_child(self._command, env, self._stdin_is_pipe, self._stdout_is_pipe,
|
||||
self._stderr_is_pipe)
|
||||
RNS.log(f"Started pid {self.pid}, fds: {self._child_stdin}, {self._child_stdout}, {self._child_stderr}", RNS.LOG_DEBUG)
|
||||
|
||||
def poll():
|
||||
try:
|
||||
pid, self._return_code = os.waitpid(self._pid, os.WNOHANG)
|
||||
if self._return_code is not None:
|
||||
self._return_code = self._return_code & 0xff
|
||||
if self._return_code is not None and not process_exists(self._pid):
|
||||
RNS.log(f"polled return code {self._return_code}", RNS.LOG_DEBUG)
|
||||
self._terminated_cb(self._return_code)
|
||||
if self.running:
|
||||
self._loop.call_later(CallbackSubprocess.PROCESS_POLL_TIME, poll)
|
||||
else:
|
||||
self._ensure_pipes_closed()
|
||||
except Exception as e:
|
||||
if not hasattr(e, "errno") or e.errno != errno.ECHILD:
|
||||
RNS.log(f"Error in process poll: {e}", RNS.LOG_DEBUG)
|
||||
|
||||
self._loop.call_later(CallbackSubprocess.PROCESS_POLL_TIME, poll)
|
||||
|
||||
def stdout():
|
||||
try:
|
||||
with exception.permit(SystemExit):
|
||||
data = tty_read_poll(self._child_stdout)
|
||||
if data is not None and len(data) > 0:
|
||||
self._stdout_cb(data)
|
||||
# Opportunistically drain shortly after to coalesce immediate follow-up output
|
||||
if not self._loop.is_closed():
|
||||
self._loop.call_later(0.01, stdout)
|
||||
except EOFError:
|
||||
self._stdout_eof = True
|
||||
tty_unset_reader_callbacks(self._child_stdout)
|
||||
self._stdout_cb(bytearray())
|
||||
|
||||
def stderr():
|
||||
try:
|
||||
with exception.permit(SystemExit):
|
||||
data = tty_read_poll(self._child_stderr)
|
||||
if data is not None and len(data) > 0:
|
||||
self._stderr_cb(data)
|
||||
if not self._loop.is_closed():
|
||||
self._loop.call_later(0.01, stderr)
|
||||
except EOFError:
|
||||
self._stderr_eof = True
|
||||
tty_unset_reader_callbacks(self._child_stderr)
|
||||
self._stderr_cb(bytearray())
|
||||
|
||||
tty_add_reader_callback(self._child_stdout, stdout, self._loop)
|
||||
if self._child_stderr != self._child_stdout:
|
||||
tty_add_reader_callback(self._child_stderr, stderr, self._loop)
|
||||
|
||||
@property
|
||||
def stdout_eof(self):
|
||||
return self._stdout_eof or not self.running
|
||||
|
||||
@property
|
||||
def stderr_eof(self):
|
||||
return self._stderr_eof or not self.running
|
||||
|
||||
|
||||
@property
|
||||
def return_code(self) -> int:
|
||||
return self._return_code
|
||||
|
||||
@property
|
||||
def pid(self) -> int:
|
||||
return self._pid
|
||||
|
||||
|
||||
async def main():
|
||||
"""
|
||||
A test driver for the CallbackProcess class.
|
||||
python ./process.py /bin/zsh --login
|
||||
"""
|
||||
|
||||
if len(sys.argv) <= 1:
|
||||
print(f"Usage: {sys.argv} <absolute_path_to_child_executable> [child_arg ...]")
|
||||
exit(1)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
# asyncio.set_event_loop(loop)
|
||||
retcode = loop.create_future()
|
||||
|
||||
def stdout(data: bytes): os.write(sys.stdout.fileno(), data)
|
||||
|
||||
def terminated(rc: int): retcode.set_result(rc)
|
||||
|
||||
process = CallbackSubprocess(argv=sys.argv[1:],
|
||||
env={"TERM": os.environ.get("TERM", "xterm")},
|
||||
loop=loop,
|
||||
stdout_callback=stdout,
|
||||
terminated_callback=terminated)
|
||||
|
||||
def sigint_handler(sig, frame):
|
||||
if process is None or process.started and not process.running:
|
||||
raise KeyboardInterrupt
|
||||
elif process.running:
|
||||
process.write("\x03".encode("utf-8"))
|
||||
|
||||
def sigwinch_handler(sig, frame):
|
||||
process.copy_winsize(sys.stdin.fileno())
|
||||
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
signal.signal(signal.SIGWINCH, sigwinch_handler)
|
||||
|
||||
def stdin():
|
||||
try:
|
||||
data = tty_read(sys.stdin.fileno())
|
||||
if data is not None:
|
||||
process.write(data)
|
||||
|
||||
except EOFError:
|
||||
tty_unset_reader_callbacks(sys.stdin.fileno())
|
||||
process.write(CTRL_D)
|
||||
|
||||
tty_add_reader_callback(sys.stdin.fileno(), stdin)
|
||||
process.start()
|
||||
# call_soon called it too soon, not sure why.
|
||||
loop.call_later(0.001, functools.partial(process.copy_winsize, sys.stdin.fileno()))
|
||||
|
||||
val = await retcode
|
||||
RNS.log(f"Got return code {val}", RNS.LOG_DEBUG)
|
||||
return val
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tr = TTYRestorer(sys.stdin.fileno())
|
||||
try:
|
||||
tr.raw()
|
||||
asyncio.run(main())
|
||||
finally:
|
||||
tty_unset_reader_callbacks(sys.stdin.fileno())
|
||||
tr.restore()
|
||||
@@ -0,0 +1,149 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 __future__ import annotations
|
||||
|
||||
import RNS
|
||||
from RNS.vendor import umsgpack
|
||||
from RNS.Buffer import StreamDataMessage as RNSStreamDataMessage
|
||||
import RNS.Utilities.rnsh.retry
|
||||
import abc
|
||||
import contextlib
|
||||
import struct
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
MSG_MAGIC = 0xac
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
def _make_MSGTYPE(val: int):
|
||||
return ((MSG_MAGIC << 8) & 0xff00) | (val & 0x00ff)
|
||||
|
||||
|
||||
class NoopMessage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(0)
|
||||
def pack(self) -> bytes: return bytes()
|
||||
def unpack(self, raw): pass
|
||||
|
||||
|
||||
class WindowSizeMessage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(2)
|
||||
|
||||
def __init__(self, rows: int = None, cols: int = None, hpix: int = None, vpix: int = None):
|
||||
super().__init__()
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.hpix = hpix
|
||||
self.vpix = vpix
|
||||
|
||||
def pack(self) -> bytes: return umsgpack.packb((self.rows, self.cols, self.hpix, self.vpix))
|
||||
def unpack(self, raw): self.rows, self.cols, self.hpix, self.vpix = umsgpack.unpackb(raw)
|
||||
|
||||
|
||||
class ExecuteCommandMesssage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(3)
|
||||
|
||||
def __init__(self, cmdline: [str] = None, pipe_stdin: bool = False, pipe_stdout: bool = False,
|
||||
pipe_stderr: bool = False, tcflags: [any] = None, term: str | None = None, rows: int = None,
|
||||
cols: int = None, hpix: int = None, vpix: int = None):
|
||||
|
||||
super().__init__()
|
||||
self.cmdline = cmdline
|
||||
self.pipe_stdin = pipe_stdin
|
||||
self.pipe_stdout = pipe_stdout
|
||||
self.pipe_stderr = pipe_stderr
|
||||
self.tcflags = tcflags
|
||||
self.term = term
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.hpix = hpix
|
||||
self.vpix = vpix
|
||||
|
||||
def pack(self) -> bytes:
|
||||
return umsgpack.packb((self.cmdline, self.pipe_stdin, self.pipe_stdout, self.pipe_stderr,
|
||||
self.tcflags, self.term, self.rows, self.cols, self.hpix, self.vpix))
|
||||
|
||||
def unpack(self, raw):
|
||||
self.cmdline, self.pipe_stdin, self.pipe_stdout, self.pipe_stderr, self.tcflags, self.term, self.rows, \
|
||||
self.cols, self.hpix, self.vpix = umsgpack.unpackb(raw)
|
||||
|
||||
|
||||
# Create a version of RNS.Buffer.StreamDataMessage that we control
|
||||
class StreamDataMessage(RNSStreamDataMessage):
|
||||
MSGTYPE = _make_MSGTYPE(4)
|
||||
STREAM_ID_STDIN = 0
|
||||
STREAM_ID_STDOUT = 1
|
||||
STREAM_ID_STDERR = 2
|
||||
|
||||
|
||||
class VersionInfoMessage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(5)
|
||||
|
||||
def __init__(self, sw_version: str = None):
|
||||
super().__init__()
|
||||
self.sw_version = sw_version or RNS.Utilities.rnsh.__version__
|
||||
self.protocol_version = PROTOCOL_VERSION
|
||||
|
||||
def pack(self) -> bytes: return umsgpack.packb((self.sw_version, self.protocol_version))
|
||||
def unpack(self, raw): self.sw_version, self.protocol_version = umsgpack.unpackb(raw)
|
||||
|
||||
|
||||
class ErrorMessage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(6)
|
||||
|
||||
def __init__(self, msg: str = None, fatal: bool = False, data: dict = None):
|
||||
super().__init__()
|
||||
self.msg = msg
|
||||
self.fatal = fatal
|
||||
self.data = data
|
||||
|
||||
def pack(self) -> bytes: return umsgpack.packb((self.msg, self.fatal, self.data))
|
||||
def unpack(self, raw: bytes): self.msg, self.fatal, self.data = umsgpack.unpackb(raw)
|
||||
|
||||
|
||||
class CommandExitedMessage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(7)
|
||||
|
||||
def __init__(self, return_code: int = None):
|
||||
super().__init__()
|
||||
self.return_code = return_code
|
||||
|
||||
def pack(self) -> bytes: return umsgpack.packb(self.return_code)
|
||||
def unpack(self, raw: bytes): self.return_code = umsgpack.unpackb(raw)
|
||||
|
||||
|
||||
message_types = [NoopMessage, VersionInfoMessage, WindowSizeMessage, ExecuteCommandMesssage, StreamDataMessage,
|
||||
CommandExitedMessage, ErrorMessage]
|
||||
|
||||
def register_message_types(channel: RNS.Channel.Channel):
|
||||
for message_type in message_types: channel.register_message_type(message_type)
|
||||
@@ -0,0 +1,201 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 asyncio
|
||||
import threading
|
||||
import time
|
||||
import RNS.Utilities.rnsh.exception as exception
|
||||
from typing import Callable
|
||||
from contextlib import AbstractContextManager
|
||||
import types
|
||||
import typing
|
||||
|
||||
|
||||
class RetryStatus:
|
||||
def __init__(self, tag: any, try_limit: int, wait_delay: float, retry_callback: Callable[[any, int], any],
|
||||
timeout_callback: Callable[[any, int], None], tries: int = 1):
|
||||
|
||||
self.tag = tag
|
||||
self.try_limit = try_limit
|
||||
self.tries = tries
|
||||
self.wait_delay = wait_delay
|
||||
self.retry_callback = retry_callback
|
||||
self.timeout_callback = timeout_callback
|
||||
self.try_time = time.time()
|
||||
self.completed = False
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
ready = time.time() > self.try_time + self.wait_delay
|
||||
RNS.log(f"ready check {self.tag} try_time {self.try_time} wait_delay {self.wait_delay} " +
|
||||
f"next_try {self.try_time + self.wait_delay} now {time.time()} " +
|
||||
f"exceeded {time.time() - self.try_time - self.wait_delay} ready {ready}", RNS.LOG_DEBUG)
|
||||
return ready
|
||||
|
||||
@property
|
||||
def timed_out(self):
|
||||
return self.ready and self.tries >= self.try_limit
|
||||
|
||||
def timeout(self):
|
||||
self.completed = True
|
||||
self.timeout_callback(self.tag, self.tries)
|
||||
|
||||
def retry(self) -> any:
|
||||
self.tries = self.tries + 1
|
||||
self.try_time = time.time()
|
||||
return self.retry_callback(self.tag, self.tries)
|
||||
|
||||
|
||||
class RetryThread(AbstractContextManager):
|
||||
def __init__(self, loop_period: float = 0.25, name: str = "retry thread"):
|
||||
self._loop_period = loop_period
|
||||
self._statuses: list[RetryStatus] = []
|
||||
self._tag_counter = 0
|
||||
self._lock = threading.RLock()
|
||||
self._run = True
|
||||
self._finished: asyncio.Future = None
|
||||
self._thread = threading.Thread(name=name, target=self._thread_run, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def is_alive(self):
|
||||
return self._thread.is_alive()
|
||||
|
||||
def close(self, loop: asyncio.AbstractEventLoop = None) -> asyncio.Future:
|
||||
RNS.log("Stopping timer thread", RNS.LOG_DEBUG)
|
||||
if loop is None:
|
||||
self._run = False
|
||||
self._thread.join()
|
||||
return None
|
||||
else:
|
||||
self._finished = loop.create_future()
|
||||
return self._finished
|
||||
|
||||
def wait(self, timeout: float = None):
|
||||
if timeout:
|
||||
timeout = timeout + time.time()
|
||||
|
||||
while timeout is None or time.time() < timeout:
|
||||
with self._lock:
|
||||
task_count = len(self._statuses)
|
||||
if task_count == 0:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def _thread_run(self):
|
||||
while self._run and self._finished is None:
|
||||
time.sleep(self._loop_period)
|
||||
ready: list[RetryStatus] = []
|
||||
prune: list[RetryStatus] = []
|
||||
with self._lock: ready.extend(list(filter(lambda s: s.ready, self._statuses)))
|
||||
|
||||
for retry in ready:
|
||||
try:
|
||||
if not retry.completed:
|
||||
if retry.timed_out:
|
||||
RNS.log(f"Timed out {retry.tag} after {retry.try_limit} tries", RNS.LOG_DEBUG)
|
||||
retry.timeout()
|
||||
prune.append(retry)
|
||||
elif retry.ready:
|
||||
RNS.log(f"Retrying {retry.tag}, try {retry.tries + 1}/{retry.try_limit}", RNS.LOG_DEBUG)
|
||||
should_continue = retry.retry()
|
||||
if not should_continue: self.complete(retry.tag)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error processing retry id {retry.tag}: {e}", RNS.LOG_ERROR)
|
||||
prune.append(retry)
|
||||
|
||||
with self._lock:
|
||||
for retry in prune:
|
||||
RNS.log(f"pruned retry {retry.tag}, retry count {retry.tries}/{retry.try_limit}", RNS.LOG_DEBUG)
|
||||
with exception.permit(SystemExit): self._statuses.remove(retry)
|
||||
|
||||
if self._finished is not None: self._finished.set_result(None)
|
||||
|
||||
def _get_next_tag(self):
|
||||
self._tag_counter += 1
|
||||
return self._tag_counter
|
||||
|
||||
def has_tag(self, tag: any) -> bool:
|
||||
with self._lock: return next(filter(lambda s: s.tag == tag, self._statuses), None) is not None
|
||||
|
||||
def begin(self, try_limit: int, wait_delay: float, try_callback: Callable[[any, int], any],
|
||||
timeout_callback: Callable[[any, int], None]) -> any:
|
||||
|
||||
RNS.log(f"Running first try", RNS.LOG_DEBUG)
|
||||
tag = try_callback(None, 1)
|
||||
RNS.log(f"First try got id {tag}", RNS.LOG_DEBUG)
|
||||
|
||||
if not tag:
|
||||
RNS.log(f"Callback returned None/False/0, considering complete.", RNS.LOG_DEBUG)
|
||||
return None
|
||||
|
||||
with self._lock:
|
||||
if tag is None: tag = self._get_next_tag()
|
||||
self.complete(tag)
|
||||
|
||||
self._statuses.append(RetryStatus(tag=tag,
|
||||
tries=1,
|
||||
try_limit=try_limit,
|
||||
wait_delay=wait_delay,
|
||||
retry_callback=try_callback,
|
||||
timeout_callback=timeout_callback))
|
||||
|
||||
RNS.log(f"Added retry timer for {tag}", RNS.LOG_DEBUG)
|
||||
return tag
|
||||
|
||||
def complete(self, tag: any):
|
||||
assert tag is not None
|
||||
with self._lock:
|
||||
status = next(filter(lambda l: l.tag == tag, self._statuses), None)
|
||||
if status is not None:
|
||||
status.completed = True
|
||||
self._statuses.remove(status)
|
||||
RNS.log(f"completed {tag}", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
RNS.log(f"status not found to complete {tag}", RNS.LOG_DEBUG)
|
||||
|
||||
def complete_all(self):
|
||||
with self._lock:
|
||||
for status in self._statuses:
|
||||
status.completed = True
|
||||
RNS.log(f"completed {status.tag}", RNS.LOG_DEBUG)
|
||||
|
||||
self._statuses.clear()
|
||||
|
||||
def __exit__(self, __exc_type: typing.Type[BaseException], __exc_value: BaseException,
|
||||
__traceback: types.TracebackType) -> bool:
|
||||
self.close()
|
||||
return False
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
|
||||
import RNS
|
||||
import RNS.Utilities.rnsh.process as process
|
||||
import RNS.Utilities.rnsh.session as session
|
||||
import RNS.Utilities.rnsh.args
|
||||
import RNS.Utilities.rnsh.loop
|
||||
import RNS.Utilities.rnsh.listener as listener
|
||||
import RNS.Utilities.rnsh.initiator as initiator
|
||||
from RNS.Utilities.rnsh.args import parse_arguments
|
||||
|
||||
APP_NAME = "rnsh"
|
||||
loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
def _sanitize_service_name(service_name:str) -> str: return re.sub(r'\W+', '', service_name)
|
||||
|
||||
def prepare_identity(identity_path, service_name: str = None) -> tuple[RNS.Identity]:
|
||||
service_name = _sanitize_service_name(service_name or "")
|
||||
if identity_path is None:
|
||||
identity_path = RNS.Reticulum.identitypath + "/" + APP_NAME + \
|
||||
(f".{service_name}" if service_name and len(service_name) > 0 else "")
|
||||
|
||||
identity = None
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
|
||||
if identity is None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
|
||||
return identity
|
||||
|
||||
|
||||
def print_identity(configdir, identitypath, service_name, include_destination: bool):
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=RNS.LOG_INFO)
|
||||
if service_name and len(service_name) > 0:
|
||||
print(f"Using service name \"{service_name}\"")
|
||||
identity = prepare_identity(identitypath, service_name)
|
||||
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME)
|
||||
print("Identity : " + str(identity))
|
||||
if include_destination:
|
||||
print("Listening on : " + RNS.prettyhexrep(destination.hash))
|
||||
|
||||
exit(0)
|
||||
|
||||
verbose_set = False
|
||||
|
||||
def ensure_config_directory():
|
||||
if os.path.isdir(os.path.expanduser("~/.config/rnsh")): return os.path.expanduser("~/.config/rnsh")
|
||||
elif os.path.isdir(os.path.expanduser("~/.rnsh")): return os.path.expanduser("~/.rnsh")
|
||||
else:
|
||||
try:
|
||||
os.makedirs(os.path.expanduser("~/.rnsh"))
|
||||
return os.path.expanduser("~/.rnsh")
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not get or create rnsh configuration directory, aborting", RNS.LOG_CRITICAL)
|
||||
os._exit(1)
|
||||
|
||||
|
||||
async def _rnsh_cli_main():
|
||||
global verbose_set
|
||||
args, parser = parse_arguments()
|
||||
verbose_set = args.verbose > 0
|
||||
|
||||
configdir = ensure_config_directory()
|
||||
|
||||
if args.print_identity:
|
||||
print_identity(args.config, args.identity, args.service, args.listen)
|
||||
return 0
|
||||
|
||||
if args.listen:
|
||||
allowed_file = None
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if os.path.isfile(os.path.expanduser("~/.config/rnsh/allowed_identities")):
|
||||
allowed_file = os.path.expanduser("~/.config/rnsh/allowed_identities")
|
||||
elif os.path.isfile(os.path.expanduser("~/.rnsh/allowed_identities")):
|
||||
allowed_file = os.path.expanduser("~/.rnsh/allowed_identities")
|
||||
|
||||
await listener.listen(configdir=configdir,
|
||||
rnsconfigdir=args.config,
|
||||
command=args.command,
|
||||
identitypath=args.identity,
|
||||
service_name=args.service,
|
||||
verbosity=args.verbose,
|
||||
quietness=args.quiet,
|
||||
allowed=args.allowed or [],
|
||||
allowed_file=allowed_file,
|
||||
disable_auth=args.no_auth,
|
||||
announce_period=args.announce,
|
||||
no_remote_command=args.no_remote_command,
|
||||
remote_cmd_as_args=args.remote_command_as_args)
|
||||
return 0
|
||||
|
||||
if args.destination is not None:
|
||||
return_code = await initiator.initiate(configdir=configdir,
|
||||
rnsconfigdir=args.config,
|
||||
identitypath=args.identity,
|
||||
verbosity=args.verbose,
|
||||
quietness=args.quiet,
|
||||
noid=args.no_id,
|
||||
destination=args.destination,
|
||||
timeout=args.timeout,
|
||||
command=args.command
|
||||
)
|
||||
return return_code if args.mirror else 0
|
||||
else:
|
||||
print("")
|
||||
parser.print_help()
|
||||
print("")
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
global verbose_set
|
||||
return_code = 1
|
||||
exc = None
|
||||
try: return_code = asyncio.run(_rnsh_cli_main())
|
||||
except SystemExit: pass
|
||||
except KeyboardInterrupt: pass
|
||||
except Exception as e:
|
||||
print(f"{e}")
|
||||
exc = e
|
||||
|
||||
process.tty_unset_reader_callbacks(0)
|
||||
if verbose_set and exc: raise exc
|
||||
sys.exit(return_code if return_code is not None else 255)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,441 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 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 __future__ import annotations
|
||||
import contextlib
|
||||
import functools
|
||||
import asyncio
|
||||
import RNS.Utilities.rnsh.exception as exception
|
||||
import RNS.Utilities.rnsh.process as process
|
||||
import RNS.Utilities.rnsh.helpers as helpers
|
||||
import RNS.Utilities.rnsh.protocol as protocol
|
||||
import enum
|
||||
from typing import TypeVar, Generic, Callable, List
|
||||
from abc import abstractmethod, ABC
|
||||
from multiprocessing import Manager
|
||||
import os
|
||||
import bz2
|
||||
import RNS
|
||||
|
||||
_TLink = TypeVar("_TLink")
|
||||
_TIdentity = TypeVar("_TIdentity")
|
||||
|
||||
class SEType(enum.IntEnum):
|
||||
SE_LINK_CLOSED = 0
|
||||
|
||||
class SessionException(Exception):
|
||||
def __init__(self, setype: SEType, msg: str, *args):
|
||||
super().__init__(msg, args)
|
||||
self.type = setype
|
||||
|
||||
class LSState(enum.IntEnum):
|
||||
LSSTATE_WAIT_IDENT = 1
|
||||
LSSTATE_WAIT_VERS = 2
|
||||
LSSTATE_WAIT_CMD = 3
|
||||
LSSTATE_RUNNING = 4
|
||||
LSSTATE_ERROR = 5
|
||||
LSSTATE_TEARDOWN = 6
|
||||
|
||||
|
||||
class LSOutletBase(ABC):
|
||||
@abstractmethod
|
||||
def set_initiator_identified_callback(self, cb: Callable[[LSOutletBase, _TIdentity], None]): raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def set_link_closed_callback(self, cb: Callable[[LSOutletBase], None]): raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def unset_link_closed_callback(self): raise NotImplemented()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def rtt(self): raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def teardown(self): raise NotImplemented()
|
||||
|
||||
|
||||
class ListenerSession:
|
||||
sessions: List[ListenerSession] = []
|
||||
allowed_identity_hashes: [any] = []
|
||||
allowed_file_identity_hashes: [any] = []
|
||||
allow_all: bool = False
|
||||
allow_remote_command: bool = False
|
||||
default_command: [str] = []
|
||||
remote_cmd_as_args = False
|
||||
|
||||
def __init__(self, outlet: LSOutletBase, channel: RNS.Channel.Channel, loop: asyncio.AbstractEventLoop):
|
||||
RNS.log(f"Session started for {outlet}", RNS.LOG_INFO)
|
||||
self.outlet = outlet
|
||||
self.channel = channel
|
||||
self.outlet.set_initiator_identified_callback(self._initiator_identified)
|
||||
self.outlet.set_link_closed_callback(self._link_closed)
|
||||
self.loop = loop
|
||||
self.state: LSState = None
|
||||
self.remote_identity = None
|
||||
self.term: str | None = None
|
||||
self.stdin_is_pipe: bool = False
|
||||
self.stdout_is_pipe: bool = False
|
||||
self.stderr_is_pipe: bool = False
|
||||
self.tcflags: [any] = None
|
||||
self.cmdline: [str] = None
|
||||
self.rows: int = 0
|
||||
self.cols: int = 0
|
||||
self.hpix: int = 0
|
||||
self.vpix: int = 0
|
||||
self.stdout_buf = bytearray()
|
||||
self.stdout_eof_sent = False
|
||||
self.stderr_buf = bytearray()
|
||||
self.stderr_eof_sent = False
|
||||
self.return_code: int | None = None
|
||||
self.return_code_sent = False
|
||||
self.process: process.CallbackSubprocess | None = None
|
||||
|
||||
if self.allow_all: self._set_state(LSState.LSSTATE_WAIT_VERS)
|
||||
else: self._set_state(LSState.LSSTATE_WAIT_IDENT)
|
||||
|
||||
self.sessions.append(self)
|
||||
protocol.register_message_types(self.channel)
|
||||
self.channel.add_message_handler(self._handle_message)
|
||||
|
||||
def _terminated(self, return_code: int):
|
||||
self.return_code = return_code
|
||||
|
||||
def _set_state(self, state: LSState, timeout_factor: float = 10.0):
|
||||
timeout = max(self.outlet.rtt * timeout_factor, max(self.outlet.rtt * 2, 10)) if timeout_factor is not None else None
|
||||
RNS.log(f"Set state: {state.name}, timeout {timeout}", RNS.LOG_DEBUG)
|
||||
orig_state = self.state
|
||||
self.state = state
|
||||
if timeout_factor is not None:
|
||||
self._call(functools.partial(self._check_protocol_timeout, lambda: self.state == orig_state, state.name), timeout)
|
||||
|
||||
def _call(self, func: callable, delay: float = 0):
|
||||
def call_inner():
|
||||
if delay == 0: func()
|
||||
else: self.loop.call_later(delay, func)
|
||||
|
||||
self.loop.call_soon_threadsafe(call_inner)
|
||||
|
||||
def send(self, message: RNS.MessageBase):
|
||||
self.channel.send(message)
|
||||
|
||||
def _protocol_error(self, name: str):
|
||||
self.terminate(f"Protocol error ({name})")
|
||||
|
||||
def _protocol_timeout_error(self, name: str):
|
||||
self.terminate(f"Protocol timeout error: {name}")
|
||||
|
||||
def terminate(self, error: str = None):
|
||||
with contextlib.suppress(Exception):
|
||||
RNS.log("Terminating session" + (f": {error}" if error else ""), RNS.LOG_DEBUG)
|
||||
if error and self.state != LSState.LSSTATE_TEARDOWN:
|
||||
with contextlib.suppress(Exception):
|
||||
self.send(protocol.ErrorMessage(error, True))
|
||||
|
||||
self.state = LSState.LSSTATE_ERROR
|
||||
self._terminate_process()
|
||||
self._call(self._prune, max(self.outlet.rtt * 3, process.CallbackSubprocess.PROCESS_PIPE_TIME+5))
|
||||
|
||||
def _prune(self):
|
||||
self.state = LSState.LSSTATE_TEARDOWN
|
||||
RNS.log("Pruning session", RNS.LOG_DEBUG)
|
||||
with contextlib.suppress(ValueError):
|
||||
self.sessions.remove(self)
|
||||
with contextlib.suppress(Exception):
|
||||
self.outlet.teardown()
|
||||
|
||||
def _check_protocol_timeout(self, fail_condition: Callable[[], bool], name: str):
|
||||
timeout = True
|
||||
try: timeout = self.state != LSState.LSSTATE_TEARDOWN and fail_condition()
|
||||
except Exception as e: RNS.log(f"Error in protocol timeout: {e}", RNS.LOG_ERROR)
|
||||
if timeout: self._protocol_timeout_error(name)
|
||||
|
||||
def _link_closed(self, outlet: LSOutletBase):
|
||||
outlet.unset_link_closed_callback()
|
||||
|
||||
if outlet != self.outlet:
|
||||
RNS.log("Link closed received from incorrect outlet", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
RNS.log(f"link_closed {outlet}", RNS.LOG_DEBUG)
|
||||
self.terminate()
|
||||
|
||||
def _initiator_identified(self, outlet, identity):
|
||||
if outlet != self.outlet:
|
||||
RNS.log("Identity received from incorrect outlet", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
RNS.log(f"initiator_identified {identity} on link {outlet}", RNS.LOG_INFO)
|
||||
if self.state not in [LSState.LSSTATE_WAIT_IDENT, LSState.LSSTATE_WAIT_VERS]:
|
||||
self._protocol_error(LSState.LSSTATE_WAIT_IDENT.name)
|
||||
|
||||
if not self.allow_all and identity.hash not in self.allowed_identity_hashes and identity.hash not in self.allowed_file_identity_hashes:
|
||||
self.terminate("Identity is not allowed.")
|
||||
|
||||
self.remote_identity = identity
|
||||
self._set_state(LSState.LSSTATE_WAIT_VERS)
|
||||
|
||||
@classmethod
|
||||
async def pump_all(cls) -> True:
|
||||
processed_any = False
|
||||
for session in cls.sessions:
|
||||
processed = session.pump()
|
||||
processed_any = processed_any or processed
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
@classmethod
|
||||
async def terminate_all(cls, reason: str):
|
||||
for session in cls.sessions:
|
||||
session.terminate(reason)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
def pump(self) -> bool:
|
||||
def compress_adaptive(buf: bytes):
|
||||
comp_tries = RNS.RawChannelWriter.COMPRESSION_TRIES
|
||||
comp_try = 1
|
||||
comp_success = False
|
||||
|
||||
chunk_len = len(buf)
|
||||
if chunk_len > RNS.RawChannelWriter.MAX_CHUNK_LEN:
|
||||
chunk_len = RNS.RawChannelWriter.MAX_CHUNK_LEN
|
||||
chunk_segment = None
|
||||
|
||||
chunk_segment = None
|
||||
max_data_len = self.channel.mdu - protocol.StreamDataMessage.OVERHEAD
|
||||
while chunk_len > 32 and comp_try < comp_tries:
|
||||
chunk_segment_length = int(chunk_len/comp_try)
|
||||
compressed_chunk = bz2.compress(buf[:chunk_segment_length])
|
||||
compressed_length = len(compressed_chunk)
|
||||
if compressed_length < max_data_len and compressed_length < chunk_segment_length:
|
||||
comp_success = True
|
||||
break
|
||||
else:
|
||||
comp_try += 1
|
||||
|
||||
if comp_success:
|
||||
diff = max_data_len - len(compressed_chunk)
|
||||
chunk = compressed_chunk
|
||||
processed_length = chunk_segment_length
|
||||
else:
|
||||
chunk = bytes(buf[:max_data_len])
|
||||
processed_length = len(chunk)
|
||||
|
||||
return comp_success, processed_length, chunk
|
||||
|
||||
try:
|
||||
if self.state != LSState.LSSTATE_RUNNING:
|
||||
return False
|
||||
elif not self.channel.is_ready_to_send():
|
||||
return False
|
||||
elif len(self.stderr_buf) > 0:
|
||||
comp_success, processed_length, data = compress_adaptive(self.stderr_buf)
|
||||
self.stderr_buf = self.stderr_buf[processed_length:]
|
||||
send_eof = self.process.stderr_eof and len(data) == 0 and not self.stderr_eof_sent
|
||||
self.stderr_eof_sent = self.stderr_eof_sent or send_eof
|
||||
msg = protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDERR,
|
||||
data, send_eof, comp_success)
|
||||
self.send(msg)
|
||||
if send_eof:
|
||||
self.stderr_eof_sent = True
|
||||
return True
|
||||
elif len(self.stdout_buf) > 0:
|
||||
comp_success, processed_length, data = compress_adaptive(self.stdout_buf)
|
||||
self.stdout_buf = self.stdout_buf[processed_length:]
|
||||
send_eof = self.process.stdout_eof and len(data) == 0 and not self.stdout_eof_sent
|
||||
self.stdout_eof_sent = self.stdout_eof_sent or send_eof
|
||||
msg = protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDOUT,
|
||||
data, send_eof, comp_success)
|
||||
self.send(msg)
|
||||
if send_eof:
|
||||
self.stdout_eof_sent = True
|
||||
return True
|
||||
elif self.return_code is not None and not self.return_code_sent:
|
||||
msg = protocol.CommandExitedMessage(self.return_code)
|
||||
self.send(msg)
|
||||
self.return_code_sent = True
|
||||
self._call(functools.partial(self._check_protocol_timeout,
|
||||
lambda: self.state == LSState.LSSTATE_RUNNING, "CommandExitedMessage"),
|
||||
max(self.outlet.rtt * 5, 10))
|
||||
return False
|
||||
|
||||
except Exception as e: RNS.log(f"Error during pump: {e}", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
def _terminate_process(self):
|
||||
with contextlib.suppress(Exception):
|
||||
if self.process and self.process.running:
|
||||
self.process.terminate()
|
||||
|
||||
def _start_cmd(self, cmdline: [str], pipe_stdin: bool, pipe_stdout: bool, pipe_stderr: bool, tcflags: [any],
|
||||
term: str | None, rows: int, cols: int, hpix: int, vpix: int):
|
||||
|
||||
self.cmdline = self.default_command
|
||||
if not self.allow_remote_command and cmdline and len(cmdline) > 0:
|
||||
self.terminate("Remote command line not allowed by listener")
|
||||
return
|
||||
|
||||
if self.remote_cmd_as_args and cmdline and len(cmdline) > 0:
|
||||
self.cmdline.extend(cmdline)
|
||||
elif cmdline and len(cmdline) > 0:
|
||||
self.cmdline = cmdline
|
||||
|
||||
|
||||
self.stdin_is_pipe = pipe_stdin
|
||||
self.stdout_is_pipe = pipe_stdout
|
||||
self.stderr_is_pipe = pipe_stderr
|
||||
self.tcflags = tcflags
|
||||
self.term = term
|
||||
|
||||
def stdout(data: bytes):
|
||||
self.stdout_buf.extend(data)
|
||||
|
||||
def stderr(data: bytes):
|
||||
self.stderr_buf.extend(data)
|
||||
|
||||
try:
|
||||
self.process = process.CallbackSubprocess(argv=self.cmdline,
|
||||
env={"TERM": self.term or os.environ.get("TERM") or "xterm",
|
||||
"RNS_REMOTE_IDENTITY": (RNS.prettyhexrep(self.remote_identity.hash)
|
||||
if self.remote_identity and self.remote_identity.hash else "")},
|
||||
loop=self.loop,
|
||||
stdout_callback=stdout,
|
||||
stderr_callback=stderr,
|
||||
terminated_callback=self._terminated,
|
||||
stdin_is_pipe=self.stdin_is_pipe,
|
||||
stdout_is_pipe=self.stdout_is_pipe,
|
||||
stderr_is_pipe=self.stderr_is_pipe)
|
||||
self.process.start()
|
||||
self._set_window_size(rows, cols, hpix, vpix)
|
||||
except Exception as e:
|
||||
RNS.log(f"Unable to start process for link {self.outlet}: {e}", RNS.LOG_ERROR)
|
||||
self.terminate("Unable to start process")
|
||||
|
||||
def _set_window_size(self, rows: int, cols: int, hpix: int, vpix: int):
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.hpix = hpix
|
||||
self.vpix = vpix
|
||||
with contextlib.suppress(Exception):
|
||||
self.process.set_winsize(rows, cols, hpix, vpix)
|
||||
|
||||
def _received_stdin(self, data: bytes, eof: bool):
|
||||
if data and len(data) > 0:
|
||||
self.process.write(data)
|
||||
if eof:
|
||||
self.process.close_stdin()
|
||||
|
||||
def _handle_message(self, message: RNS.MessageBase):
|
||||
if self.state == LSState.LSSTATE_WAIT_IDENT:
|
||||
# Ignore any messages until the initiator has identified to avoid race conditions
|
||||
# between identity announcement and early protocol messages.
|
||||
RNS.log("Ignoring message while waiting for identification", RNS.LOG_DEBUG)
|
||||
return
|
||||
if self.state == LSState.LSSTATE_WAIT_VERS:
|
||||
if not isinstance(message, protocol.VersionInfoMessage):
|
||||
self._protocol_error(self.state.name)
|
||||
return
|
||||
RNS.log(f"Version {message.sw_version}, protocol {message.protocol_version} on link {self.outlet}", RNS.LOG_VERBOSE)
|
||||
if message.protocol_version != protocol.PROTOCOL_VERSION:
|
||||
self.terminate("Incompatible protocol")
|
||||
return
|
||||
self.send(protocol.VersionInfoMessage())
|
||||
self._set_state(LSState.LSSTATE_WAIT_CMD)
|
||||
return
|
||||
elif self.state == LSState.LSSTATE_WAIT_CMD:
|
||||
if not isinstance(message, protocol.ExecuteCommandMesssage):
|
||||
return self._protocol_error(self.state.name)
|
||||
RNS.log(f"Execute command message on link {self.outlet}: {message.cmdline}", RNS.LOG_VERBOSE)
|
||||
self._set_state(LSState.LSSTATE_RUNNING)
|
||||
self._start_cmd(message.cmdline, message.pipe_stdin, message.pipe_stdout, message.pipe_stderr,
|
||||
message.tcflags, message.term, message.rows, message.cols, message.hpix, message.vpix)
|
||||
return
|
||||
elif self.state == LSState.LSSTATE_RUNNING:
|
||||
if isinstance(message, protocol.WindowSizeMessage):
|
||||
self._set_window_size(message.rows, message.cols, message.hpix, message.vpix)
|
||||
elif isinstance(message, protocol.StreamDataMessage):
|
||||
if message.stream_id != protocol.StreamDataMessage.STREAM_ID_STDIN:
|
||||
RNS.log(f"Received stream data for invalid stream {message.stream_id} on link {self.outlet}", RNS.LOG_ERROR)
|
||||
return self._protocol_error(self.state.name)
|
||||
self._received_stdin(message.data, message.eof)
|
||||
return
|
||||
elif isinstance(message, protocol.NoopMessage):
|
||||
# echo noop only on listener--used for keepalive/connectivity check
|
||||
self.send(message)
|
||||
return
|
||||
elif self.state in [LSState.LSSTATE_ERROR, LSState.LSSTATE_TEARDOWN]:
|
||||
RNS.log(f"Received packet, but in state {self.state.name}", RNS.LOG_ERROR)
|
||||
return
|
||||
else:
|
||||
self._protocol_error("unexpected message")
|
||||
return
|
||||
|
||||
|
||||
class RNSOutlet(LSOutletBase):
|
||||
|
||||
def set_initiator_identified_callback(self, cb: Callable[[LSOutletBase, _TIdentity], None]):
|
||||
def inner_cb(link, identity: _TIdentity):
|
||||
cb(self, identity)
|
||||
|
||||
self.link.set_remote_identified_callback(inner_cb)
|
||||
|
||||
def set_link_closed_callback(self, cb: Callable[[LSOutletBase], None]):
|
||||
def inner_cb(link):
|
||||
cb(self)
|
||||
|
||||
self.link.set_link_closed_callback(inner_cb)
|
||||
|
||||
def unset_link_closed_callback(self):
|
||||
self.link.set_link_closed_callback(None)
|
||||
|
||||
def teardown(self):
|
||||
self.link.teardown()
|
||||
|
||||
@property
|
||||
def rtt(self) -> float:
|
||||
return self.link.rtt
|
||||
|
||||
def __str__(self):
|
||||
return f"Outlet RNS Link {self.link}"
|
||||
|
||||
def __init__(self, link: RNS.Link):
|
||||
self.link = link
|
||||
link.lsoutlet = self
|
||||
|
||||
@staticmethod
|
||||
def get_outlet(link: RNS.Link):
|
||||
if hasattr(link, "lsoutlet"):
|
||||
return link.lsoutlet
|
||||
|
||||
return RNSOutlet(link)
|
||||
+339
-182
@@ -35,6 +35,7 @@ import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import io
|
||||
|
||||
from RNS._version import __version__
|
||||
|
||||
@@ -59,8 +60,11 @@ def size_str(num, suffix='B'):
|
||||
|
||||
request_result = None
|
||||
request_concluded = False
|
||||
first_remote_req = True
|
||||
remote_destination = None
|
||||
remote_link = None
|
||||
def get_remote_status(destination_hash, include_lstats, identity, no_output=False, timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
|
||||
global request_result, request_concluded
|
||||
global request_result, request_concluded, first_remote_req, remote_destination, remote_link
|
||||
link_count = None
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
@@ -80,7 +84,8 @@ def get_remote_status(destination_hash, include_lstats, identity, no_output=Fals
|
||||
remote_identity = RNS.Identity.recall(destination_hash)
|
||||
|
||||
def remote_link_closed(link):
|
||||
if link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
if link.teardown_reason == RNS.Link.INITIATOR_CLOSED: return
|
||||
elif link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("The link timed out, exiting now")
|
||||
@@ -106,49 +111,53 @@ def get_remote_status(destination_hash, include_lstats, identity, no_output=Fals
|
||||
response = request_receipt.response
|
||||
if isinstance(response, list):
|
||||
status = response[0]
|
||||
if len(response) > 1:
|
||||
link_count = response[1]
|
||||
else:
|
||||
link_count = None
|
||||
if len(response) > 1: link_count = response[1]
|
||||
else: link_count = None
|
||||
|
||||
request_result = (status, link_count)
|
||||
|
||||
request_concluded = True
|
||||
|
||||
def remote_link_established(link):
|
||||
if not no_output:
|
||||
global first_remote_req
|
||||
if not no_output and first_remote_req:
|
||||
print("\r \r", end="")
|
||||
print("Sending request...", end=" ")
|
||||
sys.stdout.flush()
|
||||
link.identify(identity)
|
||||
link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed)
|
||||
first_remote_req = False
|
||||
|
||||
if not no_output:
|
||||
if not remote_link and not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Establishing link with remote transport instance...", end=" ")
|
||||
sys.stdout.flush()
|
||||
|
||||
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
|
||||
link = RNS.Link(remote_destination)
|
||||
link.set_link_established_callback(remote_link_established)
|
||||
link.set_link_closed_callback(remote_link_closed)
|
||||
if not remote_destination:
|
||||
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
|
||||
|
||||
if remote_link and remote_link.status == RNS.Link.ACTIVE:
|
||||
request_concluded = False
|
||||
remote_link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed)
|
||||
|
||||
while not request_concluded:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
remote_link = RNS.Link(remote_destination)
|
||||
remote_link.set_link_established_callback(remote_link_established)
|
||||
remote_link.set_link_closed_callback(remote_link_closed)
|
||||
|
||||
while not request_concluded: time.sleep(0.1)
|
||||
|
||||
if request_result != None:
|
||||
print("\r \r", end="")
|
||||
|
||||
return request_result
|
||||
|
||||
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False,
|
||||
lstats=False, sorting=None, sort_reverse=False, remote=None, management_identity=None,
|
||||
remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, must_exit=True, rns_instance=None, traffic_totals=False):
|
||||
|
||||
if remote:
|
||||
require_shared = False
|
||||
else:
|
||||
require_shared = True
|
||||
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, pstats=False, lstats=False, sorting=None,
|
||||
sort_reverse=False, remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, must_exit=True,
|
||||
rns_instance=None, traffic_totals=False, discovered_interfaces=False, config_entries=False, burst_filter=False):
|
||||
|
||||
if remote: require_shared = False
|
||||
else: require_shared = True
|
||||
|
||||
try:
|
||||
if rns_instance:
|
||||
@@ -159,56 +168,177 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
|
||||
except Exception as e:
|
||||
print("No shared RNS instance available to get status from")
|
||||
if must_exit:
|
||||
exit(1)
|
||||
else:
|
||||
return
|
||||
if must_exit: exit(1)
|
||||
else: return
|
||||
|
||||
link_count = None
|
||||
stats = None
|
||||
|
||||
details = False
|
||||
if config_entries:
|
||||
discovered_interfaces = True
|
||||
details = True
|
||||
|
||||
if discovered_interfaces:
|
||||
if_discovery = RNS.Discovery.InterfaceDiscovery(discover_interfaces=False)
|
||||
ifs = if_discovery.list_discovered_interfaces()
|
||||
print("")
|
||||
|
||||
if json:
|
||||
import json
|
||||
for i in ifs:
|
||||
for e in i:
|
||||
if isinstance(i[e], bytes): i[e] = RNS.hexrep(i[e], delimit=False)
|
||||
|
||||
print(json.dumps(ifs))
|
||||
|
||||
else:
|
||||
filtered_ifs = []
|
||||
for i in ifs:
|
||||
name = i["name"]
|
||||
if not name_filter or name_filter.lower() in name.lower(): filtered_ifs.append(i)
|
||||
|
||||
if details:
|
||||
for idx, i in enumerate(filtered_ifs):
|
||||
try:
|
||||
name = i["name"]
|
||||
if_type = i["type"]
|
||||
status = i["status"]
|
||||
|
||||
if status == "available": status_display = "Available"
|
||||
elif status == "unknown": status_display = "Unknown"
|
||||
elif status == "stale": status_display = "Stale"
|
||||
else: status_display = status
|
||||
|
||||
now = time.time()
|
||||
dago = now-i["discovered"]
|
||||
hago = now-i["last_heard"]
|
||||
discovered_display = f"{RNS.prettytime(dago, compact=True)} ago"
|
||||
last_heard_display = f"{RNS.prettytime(hago, compact=True)} ago"
|
||||
transport_str = "Enabled" if i["transport"] else "Disabled"
|
||||
|
||||
if i["latitude"] is not None and i["longitude"] is not None:
|
||||
lat = round(i["latitude"], 4)
|
||||
lon = round(i["longitude"], 4)
|
||||
if i["height"] != None: height = ", "+str(i["height"])+"m h"
|
||||
else: height = ""
|
||||
location = f"{lat}, {lon}{height}"
|
||||
else: location = "Unknown"
|
||||
|
||||
transport_id = None
|
||||
network = None
|
||||
if "transport_id" in i: transport_id = i["transport_id"]
|
||||
if "transport_id" in i and "network_id" in i and i["transport_id"] != i["network_id"]:
|
||||
network = i["network_id"]
|
||||
|
||||
if idx > 0: print("\n"+"="*32+"\n")
|
||||
if network: print(f"Network ID : {network}")
|
||||
if transport_id: print(f"Transport ID : {transport_id}")
|
||||
|
||||
print(f"Name : {name}")
|
||||
print(f"Type : {if_type}")
|
||||
print(f"Status : {status_display}")
|
||||
print(f"Transport : {transport_str}")
|
||||
print(f"Distance : {i['hops']} hop{'' if i['hops'] == 1 else 's'}")
|
||||
print(f"Discovered : {discovered_display}")
|
||||
print(f"Last Heard : {last_heard_display}")
|
||||
print(f"Location : {location}")
|
||||
|
||||
if "frequency" in i: print(f"Frequency : {i['frequency']:,} Hz")
|
||||
if "bandwidth" in i: print(f"Bandwidth : {i['bandwidth']:,} Hz")
|
||||
if "sf" in i: print(f"Sprd. Factor : {i['sf']}")
|
||||
if "cr" in i: print(f"Coding Rate : {i['cr']}")
|
||||
if "modulation" in i: print(f"Modulation : {i['modulation']}")
|
||||
if "reachable_on" in i: print(f"Address : {i['reachable_on']}")
|
||||
if "port" in i: print(f"Port : {i['port']}")
|
||||
|
||||
print(f"Stamp Value : {i['value']}")
|
||||
|
||||
print(f"\nConfiguration Entry:")
|
||||
config_lines = i["config_entry"].split('\n')
|
||||
for line in config_lines: print(f" {line}")
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
else:
|
||||
print(f"{'Name':<25} {'Type':<12} {'Status':<12} {'Last Heard':<12} {'Value':<8} {'Location':<15}")
|
||||
print("-" * 89)
|
||||
|
||||
for i in filtered_ifs:
|
||||
try:
|
||||
name = i["name"][:24] + "…" if len(i["name"]) > 24 else i["name"]
|
||||
|
||||
if_type = i["type"].replace("Interface", "")
|
||||
|
||||
status = i["status"]
|
||||
if status == "available": status_display = "✓ Available"
|
||||
elif status == "unknown": status_display = "? Unknown"
|
||||
elif status == "stale": status_display = "× Stale"
|
||||
else: status_display = status
|
||||
|
||||
now = time.time()
|
||||
last_heard = i["last_heard"]
|
||||
diff = now - last_heard
|
||||
|
||||
if diff < 60: last_heard_display = "Just now"
|
||||
elif diff < 3600:
|
||||
mins = int(diff / 60)
|
||||
last_heard_display = f"{mins}m ago"
|
||||
elif diff < 86400:
|
||||
hours = int(diff / 3600)
|
||||
last_heard_display = f"{hours}h ago"
|
||||
else:
|
||||
days = int(diff / 86400)
|
||||
last_heard_display = f"{days}d ago"
|
||||
|
||||
value = str(i["value"])
|
||||
|
||||
if i["latitude"] is not None and i["longitude"] is not None:
|
||||
lat = round(i["latitude"], 4)
|
||||
lon = round(i["longitude"], 4)
|
||||
location = f"{lat}, {lon}"
|
||||
else: location = "N/A"
|
||||
|
||||
print(f"{name:<25} {if_type:<12} {status_display:<12} {last_heard_display:<12} {value:<8} {location:<15}")
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if must_exit: exit(0)
|
||||
else: return
|
||||
|
||||
if remote:
|
||||
try:
|
||||
if management_identity is None:
|
||||
raise ValueError("Remote management requires an identity file. Use -i to specify the path to a management identity.")
|
||||
if management_identity is None: raise ValueError("Remote management requires an identity file. Use -i to specify the path to a management identity.")
|
||||
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(remote) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
if len(remote) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
identity_hash = bytes.fromhex(remote)
|
||||
destination_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.remote.management", identity_hash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
|
||||
identity = RNS.Identity.from_file(os.path.expanduser(management_identity))
|
||||
if identity == None:
|
||||
raise ValueError("Could not load management identity from "+str(management_identity))
|
||||
if identity == None: raise ValueError("Could not load management identity from "+str(management_identity))
|
||||
|
||||
try:
|
||||
remote_status = get_remote_status(destination_hash, lstats, identity, no_output=json, timeout=remote_timeout)
|
||||
if remote_status != None:
|
||||
stats, link_count = remote_status
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
if remote_status != None: stats, link_count = remote_status
|
||||
except Exception as e: raise e
|
||||
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
if must_exit:
|
||||
exit(20)
|
||||
else:
|
||||
return
|
||||
if must_exit: exit(20)
|
||||
else: return
|
||||
|
||||
else:
|
||||
if lstats:
|
||||
try:
|
||||
link_count = reticulum.get_link_count()
|
||||
except Exception as e:
|
||||
pass
|
||||
try: link_count = reticulum.get_link_count()
|
||||
except Exception as e: pass
|
||||
|
||||
try:
|
||||
stats = reticulum.get_interface_stats()
|
||||
except Exception as e:
|
||||
pass
|
||||
try: stats = reticulum.get_interface_stats()
|
||||
except Exception as e: pass
|
||||
|
||||
if stats != None:
|
||||
if json:
|
||||
@@ -225,10 +355,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
i[k] = RNS.hexrep(i[k], delimit=False)
|
||||
|
||||
print(json.dumps(stats))
|
||||
if must_exit:
|
||||
exit()
|
||||
else:
|
||||
return
|
||||
if must_exit: exit()
|
||||
else: return
|
||||
|
||||
interfaces = stats["interfaces"]
|
||||
if sorting != None and isinstance(sorting, str):
|
||||
@@ -251,10 +379,14 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
interfaces.sort(key=lambda i: i["incoming_announce_frequency"], reverse=not sort_reverse)
|
||||
if sorting == "atx":
|
||||
interfaces.sort(key=lambda i: i["outgoing_announce_frequency"], reverse=not sort_reverse)
|
||||
if sorting == "prx":
|
||||
interfaces.sort(key=lambda i: i["incoming_pr_frequency"], reverse=not sort_reverse)
|
||||
if sorting == "ptx":
|
||||
interfaces.sort(key=lambda i: i["outgoing_pr_frequency"], reverse=not sort_reverse)
|
||||
if sorting == "held":
|
||||
interfaces.sort(key=lambda i: i["held_announces"], reverse=not sort_reverse)
|
||||
|
||||
|
||||
|
||||
for ifstat in interfaces:
|
||||
name = ifstat["name"]
|
||||
|
||||
@@ -269,7 +401,18 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
):
|
||||
|
||||
if not (name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False)):
|
||||
if name_filter == None or name_filter.lower() in name.lower():
|
||||
if name_filter == None and burst_filter == None: show_if = True
|
||||
elif not burst_filter:
|
||||
if not name_filter or name_filter.lower() in name.lower(): show_if = True
|
||||
else: show_if = False
|
||||
elif burst_filter:
|
||||
burst_act = True if ("burst_active" in ifstat and "pr_burst_active" in ifstat) and (ifstat["burst_active"] or ifstat["pr_burst_active"]) else False
|
||||
nfilt = name_filter.lower() in name.lower() if name_filter else False
|
||||
if burst_act or nfilt: show_if = True
|
||||
else: show_if = False
|
||||
else: show_if = True
|
||||
|
||||
if show_if:
|
||||
print("")
|
||||
|
||||
if ifstat["status"]: ss = "Up"
|
||||
@@ -312,6 +455,9 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
|
||||
print(" {n}".format(n=ifstat["name"]))
|
||||
|
||||
if "autoconnect_source" in ifstat and ifstat["autoconnect_source"] != None:
|
||||
print(" Source : Auto-connect via <{ns}>".format(ns=ifstat["autoconnect_source"]))
|
||||
|
||||
if "ifac_netname" in ifstat and ifstat["ifac_netname"] != None:
|
||||
print(" Network : {nn}".format(nn=ifstat["ifac_netname"]))
|
||||
|
||||
@@ -364,7 +510,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
|
||||
if "airtime_short" in ifstat and "airtime_long" in ifstat:
|
||||
print(" Airtime : {ats}% (15s), {atl}% (1h)".format(ats=str(ifstat["airtime_short"]),atl=str(ifstat["airtime_long"])))
|
||||
|
||||
|
||||
if "channel_load_short" in ifstat and "channel_load_long" in ifstat:
|
||||
print(" Ch. Load : {ats}% (15s), {atl}% (1h)".format(ats=str(ifstat["channel_load_short"]),atl=str(ifstat["channel_load_long"])))
|
||||
|
||||
@@ -389,7 +535,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
if "ifac_signature" in ifstat and ifstat["ifac_signature"] != None:
|
||||
sigstr = "<…"+RNS.hexrep(ifstat["ifac_signature"][-5:], delimit=False)+">"
|
||||
print(" Access : {nb}-bit IFAC by {sig}".format(nb=ifstat["ifac_size"]*8, sig=sigstr))
|
||||
|
||||
|
||||
if "i2p_b32" in ifstat and ifstat["i2p_b32"] != None:
|
||||
print(" I2P B32 : {ep}".format(ep=str(ifstat["i2p_b32"])))
|
||||
|
||||
@@ -399,32 +545,94 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
print(" Queued : {np} announce".format(np=aqn))
|
||||
else:
|
||||
print(" Queued : {np} announces".format(np=aqn))
|
||||
|
||||
|
||||
if astats and "held_announces" in ifstat and ifstat["held_announces"] != None and ifstat["held_announces"] > 0:
|
||||
aqn = ifstat["held_announces"]
|
||||
if aqn == 1:
|
||||
print(" Held : {np} announce".format(np=aqn))
|
||||
else:
|
||||
print(" Held : {np} announces".format(np=aqn))
|
||||
|
||||
if astats and "incoming_announce_frequency" in ifstat and ifstat["incoming_announce_frequency"] != None:
|
||||
print(" Announces : {iaf}↑".format(iaf=RNS.prettyfrequency(ifstat["outgoing_announce_frequency"])))
|
||||
print(" {iaf}↓".format(iaf=RNS.prettyfrequency(ifstat["incoming_announce_frequency"])))
|
||||
|
||||
art = None; arp = None; arg = None
|
||||
if astats and "announce_rate_target" in ifstat: art = ifstat["announce_rate_target"]
|
||||
if astats and "announce_rate_penalty" in ifstat: arp = ifstat["announce_rate_penalty"]
|
||||
if astats and "announce_rate_grace" in ifstat: arg = ifstat["announce_rate_grace"]
|
||||
if art and arp != None and arg: art_str = f"(t:{RNS.prettytime(art)}/p:{RNS.prettytime(arp)}/g:{arg})"
|
||||
elif art and arp != None: art_str = f"(t:{RNS.prettytime(art)}/p:{RNS.prettytime(arp)})"
|
||||
elif art: art_str = f"(t:{RNS.prettytime(art)})"
|
||||
else: art_str = ""
|
||||
|
||||
burst_str = ""
|
||||
if "burst_active" in ifstat and ifstat["burst_active"]:
|
||||
for_str = RNS.prettytime(time.time()-ifstat["burst_activated"])
|
||||
burst_str = f" burst for {for_str}"
|
||||
|
||||
pburst_str = ""
|
||||
if "pr_burst_active" in ifstat and ifstat["pr_burst_active"]:
|
||||
for_str = RNS.prettytime(time.time()-ifstat["pr_burst_activated"])
|
||||
pburst_str = f"burst for {for_str}"
|
||||
|
||||
rxb_str = "↓"+RNS.prettysize(ifstat["rxb"])
|
||||
txb_str = "↑"+RNS.prettysize(ifstat["txb"])
|
||||
strdiff = len(rxb_str)-len(txb_str)
|
||||
if strdiff > 0:
|
||||
txb_str += " "*strdiff
|
||||
elif strdiff < 0:
|
||||
rxb_str += " "*-strdiff
|
||||
|
||||
asr = False
|
||||
if astats and "incoming_announce_frequency" in ifstat and ifstat["incoming_announce_frequency"] != None:
|
||||
oan = ifstat["outgoing_announce_frequency"]
|
||||
ian = ifstat["incoming_announce_frequency"]
|
||||
if name.startswith("Shared Instance[") and clients and clients > 0: oan = oan-(oan/clients) # Sub rnstatus own part
|
||||
oaf = RNS.prettyfrequency(oan, d=1, lpf=True)
|
||||
iaf = RNS.prettyfrequency(ian, d=1, lpf=True)
|
||||
|
||||
cspec = "c"
|
||||
if clients == None and "peers" in ifstat and ifstat["peers"]: clients = ifstat["peers"]; cspec = "p"
|
||||
if clients != None and clients > 0: pc_str = f"{RNS.prettyfrequency(ifstat['outgoing_announce_frequency']/clients, d=1, lpf=True)}/{cspec}"
|
||||
else: pc_str = ""
|
||||
asr = True
|
||||
|
||||
psr = False
|
||||
if pstats and "incoming_pr_frequency" in ifstat and ifstat["incoming_pr_frequency"] != None:
|
||||
opn = ifstat["outgoing_pr_frequency"]
|
||||
ipn = ifstat["incoming_pr_frequency"]
|
||||
if name.startswith("Shared Instance[") and clients and clients > 0: opn = opn-(opn/clients) # Sub rnstatus own part
|
||||
if astats:
|
||||
opf = "↑"+RNS.prettyfrequency(opn, d=1, lpf=True)
|
||||
ipf = "↓"+RNS.prettyfrequency(ipn, d=1, lpf=True)
|
||||
else:
|
||||
opf = RNS.prettyfrequency(opn,d=1, lpf=True)+"↑"
|
||||
ipf = RNS.prettyfrequency(ipn,d=1, lpf=True)+"↓"
|
||||
cspec = "c"
|
||||
if clients == None and "peers" in ifstat and ifstat["peers"]: clients = ifstat["peers"]; cspec = "p"
|
||||
if clients != None and clients > 0: rpc_str = f"{RNS.prettyfrequency(ifstat['outgoing_pr_frequency']/clients, d=1, lpf=True)}/{cspec}"
|
||||
else: rpc_str = ""
|
||||
psr = True
|
||||
|
||||
if not asr: iaf = ""; oaf = ""
|
||||
if not psr: ipf = ""; opf = ""
|
||||
amlen = max(len(iaf), len(oaf))
|
||||
iaf += (amlen-len(iaf))*" "+"↓"
|
||||
oaf += (amlen-len(oaf))*" "+"↑"
|
||||
mlen = max(max(len(iaf), len(oaf), len(rxb_str), len(txb_str), len(ipf), len(opf)), 10)
|
||||
iaf += (mlen-len(iaf))*" "
|
||||
oaf += (mlen-len(oaf))*" "
|
||||
ipf += (mlen-len(ipf))*" "
|
||||
opf += (mlen-len(opf))*" "
|
||||
rxb_str += (mlen-len(rxb_str))*" "
|
||||
txb_str += (mlen-len(txb_str))*" "
|
||||
|
||||
if psr:
|
||||
print(f" Path Rqs. : {opf} {rpc_str}")
|
||||
print(f" {ipf} {pburst_str}")
|
||||
|
||||
if asr:
|
||||
print(f" Announces : {oaf} {pc_str}")
|
||||
print(f" {iaf} {art_str}{burst_str}")
|
||||
|
||||
rxstat = rxb_str
|
||||
txstat = txb_str
|
||||
if "rxs" in ifstat and "txs" in ifstat:
|
||||
rxstat += " "+RNS.prettyspeed(ifstat["rxs"])
|
||||
txstat += " "+RNS.prettyspeed(ifstat["txs"])
|
||||
|
||||
|
||||
print(f" Traffic : {txstat}\n {rxstat}")
|
||||
|
||||
lstr = ""
|
||||
@@ -450,6 +658,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
|
||||
if "transport_id" in stats and stats["transport_id"] != None:
|
||||
print("\n Transport Instance "+RNS.prettyhexrep(stats["transport_id"])+" running")
|
||||
if "network_id" in stats and stats["network_id"] != None:
|
||||
print(" Network Identity "+RNS.prettyhexrep(stats["network_id"]))
|
||||
if "probe_responder" in stats and stats["probe_responder"] != None:
|
||||
print(" Probe responder at "+RNS.prettyhexrep(stats["probe_responder"])+ " active")
|
||||
if "transport_uptime" in stats and stats["transport_uptime"] != None:
|
||||
@@ -459,7 +669,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
print(f"\n{lstr}")
|
||||
|
||||
print("")
|
||||
|
||||
|
||||
else:
|
||||
if not remote:
|
||||
print("Could not get RNS status")
|
||||
@@ -476,125 +686,72 @@ def main(must_exit=True, rns_instance=None):
|
||||
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument("--version", action="version", version="rnstatus {version}".format(version=__version__))
|
||||
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="show all interfaces",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-A",
|
||||
"--announce-stats",
|
||||
action="store_true",
|
||||
help="show announce stats",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--link-stats",
|
||||
action="store_true",
|
||||
help="show link stats",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--totals",
|
||||
action="store_true",
|
||||
help="display traffic totals",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--sort",
|
||||
action="store",
|
||||
help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, announces, arx, atx, held]",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--reverse",
|
||||
action="store_true",
|
||||
help="reverse sorting",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="output in JSON format",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-R",
|
||||
action="store",
|
||||
metavar="hash",
|
||||
help="transport identity hash of remote instance to get status from",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
action="store",
|
||||
metavar="path",
|
||||
help="path to identity used for remote management",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
action="store",
|
||||
metavar="seconds",
|
||||
type=float,
|
||||
help="timeout before giving up on remote queries",
|
||||
default=RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
parser.add_argument("-a", "--all", action="store_true", help="show all interfaces", default=False)
|
||||
parser.add_argument("-A", "--announce-stats", action="store_true", help="show announce stats", default=False)
|
||||
parser.add_argument("-P", "--pr-stats", action="store_true", help="show path request stats", default=False)
|
||||
parser.add_argument("-l", "--link-stats", action="store_true", help="show link stats", default=False)
|
||||
parser.add_argument("-B", "--burst", action="store_true", help="only show interfaces with active bursts", default=False)
|
||||
parser.add_argument("-t", "--totals", action="store_true", help="display traffic totals", default=False)
|
||||
parser.add_argument("-s", "--sort", action="store", help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, announces, arx, atx, prx, ptx, held]", default=None, type=str)
|
||||
parser.add_argument("-r", "--reverse", action="store_true", help="reverse sorting", default=False)
|
||||
parser.add_argument("-j", "--json", action="store_true", help="output in JSON format", default=False)
|
||||
parser.add_argument("-R", action="store", metavar="hash", help="transport identity hash of remote instance to get status from", default=None, type=str)
|
||||
parser.add_argument("-i", action="store", metavar="path", help="path to identity used for remote management", default=None, type=str)
|
||||
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="timeout before giving up on remote queries", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
|
||||
parser.add_argument("-d", "--discovered", action="store_true", help="list discovered interfaces", default=False)
|
||||
parser.add_argument("-D", action="store_true", help="show details and config entries for discovered interfaces", default=False)
|
||||
parser.add_argument("-m", "--monitor", action="store_true", help="continuously monitor status", default=False)
|
||||
parser.add_argument("-I", "--monitor-interval", action="store", metavar="seconds", type=float, help="refresh interval for monitor mode (default: 1)", default=1.0)
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0)
|
||||
|
||||
parser.add_argument("filter", nargs="?", default=None, help="only display interfaces with names including filter", type=str)
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.config:
|
||||
configarg = args.config
|
||||
else:
|
||||
configarg = None
|
||||
if args.config: configarg = args.config
|
||||
else: configarg = None
|
||||
|
||||
program_setup(
|
||||
configdir = configarg,
|
||||
dispall = args.all,
|
||||
verbosity=args.verbose,
|
||||
name_filter=args.filter,
|
||||
json=args.json,
|
||||
astats=args.announce_stats,
|
||||
lstats=args.link_stats,
|
||||
sorting=args.sort,
|
||||
sort_reverse=args.reverse,
|
||||
remote=args.R,
|
||||
management_identity=args.i,
|
||||
remote_timeout=args.w,
|
||||
must_exit=must_exit,
|
||||
rns_instance=rns_instance,
|
||||
traffic_totals=args.totals,
|
||||
)
|
||||
if args.monitor:
|
||||
if args.R: require_shared = False
|
||||
else: require_shared = True
|
||||
|
||||
try: reticulum = RNS.Reticulum(configdir=configarg, loglevel=3+args.verbose, require_shared_instance=require_shared)
|
||||
except Exception as e:
|
||||
print("No shared RNS instance available to get status from")
|
||||
exit(1)
|
||||
|
||||
while True:
|
||||
st = time.time()
|
||||
buffer = io.StringIO()
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = buffer
|
||||
|
||||
try:
|
||||
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json,
|
||||
astats=args.announce_stats, pstats=args.pr_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse,
|
||||
remote=args.R, management_identity=args.i, remote_timeout=args.w, must_exit=False, rns_instance=reticulum,
|
||||
traffic_totals=args.totals, discovered_interfaces=args.discovered, config_entries=args.D, burst_filter=args.burst)
|
||||
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
|
||||
output = buffer.getvalue()
|
||||
print("\033[H\033[2J", end="")
|
||||
print(output, end="", flush=True)
|
||||
|
||||
td = time.time()-st
|
||||
sleeptime = max(args.monitor_interval-td, 0.2)
|
||||
time.sleep(sleeptime)
|
||||
|
||||
else:
|
||||
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json,
|
||||
astats=args.announce_stats, pstats=args.pr_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse,
|
||||
remote=args.R, management_identity=args.i, remote_timeout=args.w, must_exit=must_exit, rns_instance=rns_instance,
|
||||
traffic_totals=args.totals, discovered_interfaces=args.discovered, config_entries=args.D, burst_filter=args.burst)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
if must_exit:
|
||||
exit()
|
||||
else:
|
||||
return
|
||||
if must_exit: exit()
|
||||
else: return
|
||||
|
||||
def speed_str(num, suffix='bps'):
|
||||
units = ['','k','M','G','T','P','E','Z']
|
||||
|
||||
+131
-118
@@ -44,6 +44,7 @@ from .Link import Link, RequestReceipt
|
||||
from .Channel import MessageBase
|
||||
from .Buffer import Buffer, RawChannelReader, RawChannelWriter
|
||||
from .Transport import Transport
|
||||
from .Discovery import InterfaceAnnouncer
|
||||
from .Destination import Destination
|
||||
from .Packet import Packet
|
||||
from .Packet import PacketReceipt
|
||||
@@ -81,6 +82,7 @@ loglevel = LOG_NOTICE
|
||||
logfile = None
|
||||
logdest = LOG_STDOUT
|
||||
logcall = None
|
||||
logtimestamps = True
|
||||
logtimefmt = "%Y-%m-%d %H:%M:%S"
|
||||
logtimefmt_p = "%H:%M:%S.%f"
|
||||
compact_log_fmt = False
|
||||
@@ -93,22 +95,14 @@ _always_override_destination = False
|
||||
logging_lock = threading.Lock()
|
||||
|
||||
def loglevelname(level):
|
||||
if (level == LOG_CRITICAL):
|
||||
return "[Critical]"
|
||||
if (level == LOG_ERROR):
|
||||
return "[Error] "
|
||||
if (level == LOG_WARNING):
|
||||
return "[Warning] "
|
||||
if (level == LOG_NOTICE):
|
||||
return "[Notice] "
|
||||
if (level == LOG_INFO):
|
||||
return "[Info] "
|
||||
if (level == LOG_VERBOSE):
|
||||
return "[Verbose] "
|
||||
if (level == LOG_DEBUG):
|
||||
return "[Debug] "
|
||||
if (level == LOG_EXTREME):
|
||||
return "[Extra] "
|
||||
if (level == LOG_CRITICAL): return "[Critical]"
|
||||
if (level == LOG_ERROR): return "[Error] "
|
||||
if (level == LOG_WARNING): return "[Warning] "
|
||||
if (level == LOG_NOTICE): return "[Notice] "
|
||||
if (level == LOG_INFO): return "[Info] "
|
||||
if (level == LOG_VERBOSE): return "[Verbose] "
|
||||
if (level == LOG_DEBUG): return "[Debug] "
|
||||
if (level == LOG_EXTREME): return "[Extra] "
|
||||
|
||||
return "Unknown"
|
||||
|
||||
@@ -126,34 +120,30 @@ def timestamp_str(time_s):
|
||||
def precise_timestamp_str(time_s):
|
||||
return datetime.datetime.now().strftime(logtimefmt_p)[:-3]
|
||||
|
||||
def sl(level=3): return loglevel >= level
|
||||
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:
|
||||
if pt:
|
||||
logstring = "["+precise_timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
|
||||
if pt: logstring = "["+precise_timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
|
||||
else:
|
||||
if not compact_log_fmt:
|
||||
logstring = "["+timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
|
||||
else:
|
||||
logstring = "["+timestamp_str(time.time())+"] "+msg
|
||||
if not compact_log_fmt: logstring = ("["+timestamp_str(time.time())+"] " if logtimestamps else "")+loglevelname(level)+" "+msg
|
||||
else: logstring = ("["+timestamp_str(time.time())+"] " if logtimestamps else "")+msg
|
||||
|
||||
with logging_lock:
|
||||
if (logdest == LOG_STDOUT or _always_override_destination or _override_destination):
|
||||
if not threading.main_thread().is_alive(): return
|
||||
else: print(logstring)
|
||||
else:
|
||||
try: print(logstring)
|
||||
except: pass
|
||||
|
||||
elif (logdest == LOG_FILE and logfile != None):
|
||||
try:
|
||||
file = open(logfile, "a")
|
||||
file.write(logstring+"\n")
|
||||
file.close()
|
||||
|
||||
with open(logfile, "a") as file: file.write(logstring+"\n")
|
||||
if os.path.getsize(logfile) > LOG_MAXSIZE:
|
||||
prevfile = logfile+".1"
|
||||
if os.path.isfile(prevfile):
|
||||
os.unlink(prevfile)
|
||||
if os.path.isfile(prevfile): os.unlink(prevfile)
|
||||
os.rename(logfile, prevfile)
|
||||
|
||||
except Exception as e:
|
||||
@@ -163,8 +153,7 @@ def log(msg, level=3, _override_destination = False, pt=False):
|
||||
log(msg, level)
|
||||
|
||||
elif logdest == LOG_CALLBACK:
|
||||
try:
|
||||
logcall(logstring)
|
||||
try: logcall(logstring)
|
||||
except Exception as e:
|
||||
_always_override_destination = True
|
||||
log("Exception occurred while calling external log handler: "+str(e), LOG_CRITICAL)
|
||||
@@ -183,14 +172,11 @@ def trace_exception(e):
|
||||
log(exception_info, LOG_ERROR)
|
||||
|
||||
def hexrep(data, delimit=True):
|
||||
try:
|
||||
iter(data)
|
||||
except TypeError:
|
||||
data = [data]
|
||||
try: iter(data)
|
||||
except TypeError: data = [data]
|
||||
|
||||
delimiter = ":"
|
||||
if not delimit:
|
||||
delimiter = ""
|
||||
if not delimit: delimiter = ""
|
||||
hexrep = delimiter.join("{:02x}".format(c) for c in data)
|
||||
return hexrep
|
||||
|
||||
@@ -213,22 +199,24 @@ def prettysize(num, suffix='B'):
|
||||
|
||||
for unit in units:
|
||||
if abs(num) < 1000.0:
|
||||
if unit == "":
|
||||
return "%.0f %s%s" % (num, unit, suffix)
|
||||
else:
|
||||
return "%.2f %s%s" % (num, unit, suffix)
|
||||
if unit == "": return "%.0f %s%s" % (num, unit, suffix)
|
||||
else: return "%.2f %s%s" % (num, unit, suffix)
|
||||
num /= 1000.0
|
||||
|
||||
return "%.2f%s%s" % (num, last_unit, suffix)
|
||||
|
||||
def prettyfrequency(hz, suffix="Hz"):
|
||||
num = hz*1e6
|
||||
units = ["µ", "m", "", "K","M","G","T","P","E","Z"]
|
||||
def prettyfrequency(hz, suffix="Hz", d=2, lpf=False):
|
||||
if hz == 0: return "0 Hz"
|
||||
if not lpf: num = hz*1e6
|
||||
else: num = hz
|
||||
if not lpf: units = ["µ", "m", "", "K","M","G","T","P","E","Z"]
|
||||
else: units = ["", "K","M","G","T","P","E","Z"]
|
||||
last_unit = "Y"
|
||||
|
||||
for unit in units:
|
||||
if abs(num) < 1000.0:
|
||||
return "%.2f %s%s" % (num, unit, suffix)
|
||||
if d == 2: return "%.2f %s%s" % (num, unit, suffix)
|
||||
else: return "%s %s%s" % (str(round(num,d)), unit, suffix)
|
||||
num /= 1000.0
|
||||
|
||||
return "%.2f%s%s" % (num, last_unit, suffix)
|
||||
@@ -243,8 +231,7 @@ def prettydistance(m, suffix="m"):
|
||||
if unit == "m": divisor = 10
|
||||
if unit == "c": divisor = 100
|
||||
|
||||
if abs(num) < divisor:
|
||||
return "%.2f %s%s" % (num, unit, suffix)
|
||||
if abs(num) < divisor: return "%.2f %s%s" % (num, unit, suffix)
|
||||
num /= divisor
|
||||
|
||||
return "%.2f %s%s" % (num, last_unit, suffix)
|
||||
@@ -261,10 +248,8 @@ def prettytime(time, verbose=False, compact=False):
|
||||
time %= 3600
|
||||
minutes = int(time // 60)
|
||||
time %= 60
|
||||
if compact:
|
||||
seconds = int(time)
|
||||
else:
|
||||
seconds = round(time, 2)
|
||||
if compact: seconds = int(time)
|
||||
else: seconds = round(time, 2)
|
||||
|
||||
ss = "" if seconds == 1 else "s"
|
||||
sm = "" if minutes == 1 else "s"
|
||||
@@ -293,22 +278,16 @@ def prettytime(time, verbose=False, compact=False):
|
||||
tstr = ""
|
||||
for c in components:
|
||||
i += 1
|
||||
if i == 1:
|
||||
pass
|
||||
elif i < len(components):
|
||||
tstr += ", "
|
||||
elif i == len(components):
|
||||
tstr += " and "
|
||||
if i == 1: pass
|
||||
elif i < len(components): tstr += ", "
|
||||
elif i == len(components): tstr += " and "
|
||||
|
||||
tstr += c
|
||||
|
||||
if tstr == "":
|
||||
return "0s"
|
||||
if tstr == "": return "0s"
|
||||
else:
|
||||
if not neg:
|
||||
return tstr
|
||||
else:
|
||||
return f"-{tstr}"
|
||||
if not neg: return tstr
|
||||
else: return f"-{tstr}"
|
||||
|
||||
def prettyshorttime(time, verbose=False, compact=False):
|
||||
neg = False
|
||||
@@ -320,10 +299,8 @@ def prettyshorttime(time, verbose=False, compact=False):
|
||||
seconds = int(time // 1e6); time %= 1e6
|
||||
milliseconds = int(time // 1e3); time %= 1e3
|
||||
|
||||
if compact:
|
||||
microseconds = int(time)
|
||||
else:
|
||||
microseconds = round(time, 2)
|
||||
if compact: microseconds = int(time)
|
||||
else: microseconds = round(time, 2)
|
||||
|
||||
ss = "" if seconds == 1 else "s"
|
||||
sms = "" if milliseconds == 1 else "s"
|
||||
@@ -347,22 +324,16 @@ def prettyshorttime(time, verbose=False, compact=False):
|
||||
tstr = ""
|
||||
for c in components:
|
||||
i += 1
|
||||
if i == 1:
|
||||
pass
|
||||
elif i < len(components):
|
||||
tstr += ", "
|
||||
elif i == len(components):
|
||||
tstr += " and "
|
||||
if i == 1: pass
|
||||
elif i < len(components): tstr += ", "
|
||||
elif i == len(components): tstr += " and "
|
||||
|
||||
tstr += c
|
||||
|
||||
if tstr == "":
|
||||
return "0us"
|
||||
if tstr == "": return "0us"
|
||||
else:
|
||||
if not neg:
|
||||
return tstr
|
||||
else:
|
||||
return f"-{tstr}"
|
||||
if not neg: return tstr
|
||||
else: return f"-{tstr}"
|
||||
|
||||
def phyparams():
|
||||
print("Required Physical Layer MTU : "+str(Reticulum.MTU)+" bytes")
|
||||
@@ -373,8 +344,7 @@ def phyparams():
|
||||
print("Link Public Key Size : "+str(Link.ECPUBSIZE*8)+" bits")
|
||||
print("Link Private Key Size : "+str(Link.KEYSIZE*8)+" bits")
|
||||
|
||||
def panic():
|
||||
os._exit(255)
|
||||
def panic(): os._exit(255)
|
||||
|
||||
exit_called = False
|
||||
def exit(code=0):
|
||||
@@ -384,6 +354,10 @@ def exit(code=0):
|
||||
Reticulum.exit_handler()
|
||||
os._exit(code)
|
||||
|
||||
def _detach_stdout():
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
sys.stderr = open(os.devnull, "w")
|
||||
|
||||
class Profiler:
|
||||
_ran = False
|
||||
profilers = {}
|
||||
@@ -391,8 +365,7 @@ class Profiler:
|
||||
|
||||
@staticmethod
|
||||
def get_profiler(tag=None, super_tag=None):
|
||||
if tag in Profiler.profilers:
|
||||
return Profiler.profilers[tag]
|
||||
if tag in Profiler.profilers: return Profiler.profilers[tag]
|
||||
else:
|
||||
profiler = Profiler(tag, super_tag)
|
||||
Profiler.profilers[tag] = profiler
|
||||
@@ -404,13 +377,14 @@ class Profiler:
|
||||
self.pause_started = None
|
||||
self.tag = tag
|
||||
self.super_tag = super_tag
|
||||
|
||||
if self.super_tag in Profiler.profilers:
|
||||
self.super_profiler = Profiler.profilers[self.super_tag]
|
||||
self.pause_super = self.super_profiler.pause
|
||||
self.resume_super = self.super_profiler.resume
|
||||
|
||||
else:
|
||||
def noop(self=None):
|
||||
pass
|
||||
def noop(self=None): pass
|
||||
self.super_profiler = None
|
||||
self.pause_super = noop
|
||||
self.resume_super = noop
|
||||
@@ -420,8 +394,7 @@ class Profiler:
|
||||
tag = self.tag
|
||||
super_tag = self.super_tag
|
||||
thread_ident = threading.get_ident()
|
||||
if not tag in Profiler.tags:
|
||||
Profiler.tags[tag] = {"threads": {}, "super": super_tag}
|
||||
if not tag in Profiler.tags: Profiler.tags[tag] = {"threads": {}, "super": super_tag}
|
||||
if not thread_ident in Profiler.tags[tag]["threads"]:
|
||||
Profiler.tags[tag]["threads"][thread_ident] = {"current_start": None, "captures": []}
|
||||
|
||||
@@ -457,8 +430,7 @@ class Profiler:
|
||||
self.resume_super()
|
||||
|
||||
@staticmethod
|
||||
def ran():
|
||||
return Profiler._ran
|
||||
def ran(): return Profiler._ran
|
||||
|
||||
@staticmethod
|
||||
def results():
|
||||
@@ -475,41 +447,35 @@ class Profiler:
|
||||
sample_count = len(thread_captures)
|
||||
|
||||
if sample_count > 1:
|
||||
thread_results = {
|
||||
"count": sample_count,
|
||||
"mean": mean(thread_captures),
|
||||
"median": median(thread_captures),
|
||||
"stdev": stdev(thread_captures)
|
||||
}
|
||||
thread_results = { "count": sample_count,
|
||||
"mean": mean(thread_captures),
|
||||
"median": median(thread_captures),
|
||||
"stdev": stdev(thread_captures) }
|
||||
|
||||
elif sample_count == 1:
|
||||
thread_results = {
|
||||
"count": sample_count,
|
||||
"mean": mean(thread_captures),
|
||||
"median": median(thread_captures),
|
||||
"stdev": None
|
||||
}
|
||||
thread_results = { "count": sample_count,
|
||||
"mean": mean(thread_captures),
|
||||
"median": median(thread_captures),
|
||||
"stdev": None }
|
||||
|
||||
tag_captures.extend(thread_captures)
|
||||
|
||||
sample_count = len(tag_captures)
|
||||
if sample_count > 1:
|
||||
tag_results = {
|
||||
"name": tag,
|
||||
"super": tag_entry["super"],
|
||||
"count": len(tag_captures),
|
||||
"mean": mean(tag_captures),
|
||||
"median": median(tag_captures),
|
||||
"stdev": stdev(tag_captures)
|
||||
}
|
||||
tag_results = { "name": tag,
|
||||
"super": tag_entry["super"],
|
||||
"count": len(tag_captures),
|
||||
"mean": mean(tag_captures),
|
||||
"median": median(tag_captures),
|
||||
"stdev": stdev(tag_captures) }
|
||||
|
||||
elif sample_count == 1:
|
||||
tag_results = {
|
||||
"name": tag,
|
||||
"super": tag_entry["super"],
|
||||
"count": len(tag_captures),
|
||||
"mean": mean(tag_captures),
|
||||
"median": median(tag_captures),
|
||||
"stdev": None
|
||||
}
|
||||
tag_results = { "name": tag,
|
||||
"super": tag_entry["super"],
|
||||
"count": len(tag_captures),
|
||||
"mean": mean(tag_captures),
|
||||
"median": median(tag_captures),
|
||||
"stdev": None }
|
||||
|
||||
results[tag] = tag_results
|
||||
|
||||
@@ -541,4 +507,51 @@ class Profiler:
|
||||
if tag["super"] == None:
|
||||
print_results_recursive(tag, results)
|
||||
|
||||
profile = Profiler.get_profiler
|
||||
profile = Profiler.get_profiler
|
||||
|
||||
# The base-256 table is likely to change. Currently, it is just
|
||||
# experimental, so don't count on it too much just yet.
|
||||
b256 = [
|
||||
# 0 1 2 3 4 5 6 7 8 9 A B C D F F
|
||||
"a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p", # 0x0 Latin & numerals
|
||||
"q","r","s","t","u","v","x","y","z","æ","ø","0","1","2","3","4", # 0x1 Latin & numerals
|
||||
"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P", # 0x2 Latin & numerals
|
||||
"Q","R","S","T","U","W","X","Y","Z","Æ","Ø","5","6","7","8","9", # 0x3 Latin & numerals
|
||||
"α","β","γ","δ","ε","ζ","η","θ","ι","κ","λ","μ","ν","ξ","π","ρ", # 0x4 Greek
|
||||
"σ","τ","φ","χ","ψ","ω","Γ","Δ","Θ","Λ","Ξ","Π","Σ","Φ","Ψ","Ω", # 0x5 Greek
|
||||
"Б","Д","Ж","З","И","Л","П","Ц","Ч","Ш","Щ","Ъ","Ы","Э","Ю","Я", # 0x6 Cyrillic
|
||||
"б","д","ж","з","и","л","п","ц","ч","ш","щ","ъ","ы","э","ю","я", # 0x7 Cyrillic
|
||||
"Ա","Բ","Գ","Դ","Ե","Զ","Է","Ը","Թ","Ժ","Ի","Խ","Ծ","Կ","Հ","Ձ", # 0x8 Armenian Capitals
|
||||
"Ղ","Ճ","Մ","Յ","Ն","Շ","Ո","Չ","Պ","Ջ","Վ","Ր","Ց","Ւ","Ք","Ֆ", # 0x9 Armenian Captials
|
||||
"ᚠ","ᚢ","ᚦ","ᚱ","ᚹ","ᚺ","ᚾ","ᛈ","ᛇ","ᛉ","ᛊ","ᛏ","ᛒ","ᛖ","ᛗ","ᛟ", # 0xA Elder Futhark
|
||||
"イ","ウ","オ","カ","キ","ケ","サ","シ","ス","セ","タ","チ","テ","ト","ナ","ヌ", # 0xB Katakana
|
||||
"ネ","ヒ","フ","ヘ","ホ","マ","ミ","ム","メ","モ","ヤ","ラ","リ","ル","レ","ワ", # 0xC Katakana
|
||||
"𐑐","𐑑","𐑒","𐑔","𐑕","𐑗","𐑙","𐑳","𐑶","𐑸","𐑹","𐑺","𐑻","𐑽","𐑾","𐑿", # 0xD Shavian
|
||||
"᱑","᱕","᱘","᱙","ᱚ","ᱝ","ᱟ","ᱣ","ᱦ","ᱨ","ᱬ","ᱭ","ᱰ","ᱳ","ᱶ","ᱷ", # 0xE Ol Chiki
|
||||
"𐌳","𐌸","𐌾","𐐀","𐐁","𐐂","𐐆","𐐇","𐐈","𐐉","𐐊","𐐋","𐐌","𐐍","𐐎","𐐏", # 0xF Gothic & Deseret
|
||||
]
|
||||
|
||||
def b256rep(data): return "".join(bytes_to_b256(data))
|
||||
def prettyb256rep(data): return f"<{b256rep(data)}>"
|
||||
|
||||
def b256_to_byte(point):
|
||||
if not type(point) == str or not len(point) == 1: raise TypeError("Invalid input data for base256 byte decode")
|
||||
try: return b256.index(point)
|
||||
except Exception as e: raise ValueError(f"Could not decode base256 byte: {e}")
|
||||
|
||||
def b256_to_bytes(b256rep):
|
||||
if not type(b256rep) == str: raise TypeError("Invalid input data for base256 decode")
|
||||
try: return bytes([b256.index(c) for c in b256rep])
|
||||
except Exception as e: raise ValueError(f"Could not decode base256: {e}")
|
||||
|
||||
def byte_to_b256(input_byte):
|
||||
if type(input_byte) == bytes and not len(input_byte) == 1: TypeError("Invalid input data for base256 byte encode")
|
||||
if type(input_byte) == bytes and len(input_byte) == 1: input_byte = ord(input_byte)
|
||||
if not type(input_byte) == int: raise TypeError("Invalid input data for base256 byte encode")
|
||||
try: return b256[int(input_byte)]
|
||||
except Exception as e: raise TypeError(f"Could not encode byte to base256: {e}")
|
||||
|
||||
def bytes_to_b256(data):
|
||||
if not type(data) == bytes: raise TypeError("Invalid input data for base256 encode")
|
||||
try: return [byte_to_b256(c) for c in data]
|
||||
except Exception as e: raise TypeError(f"Could not encode to base256: {e}")
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "1.0.4"
|
||||
__version__ = "1.3.2"
|
||||
|
||||
Vendored
+537
@@ -0,0 +1,537 @@
|
||||
# validate.py
|
||||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=
|
||||
#
|
||||
# A Validator object.
|
||||
#
|
||||
# Copyright (C) 2005-2014:
|
||||
# (name) : (email)
|
||||
# Michael Foord: fuzzyman AT voidspace DOT org DOT uk
|
||||
# Mark Andrews: mark AT la-la DOT com
|
||||
# Nicola Larosa: nico AT tekNico DOT net
|
||||
# Rob Dennis: rdennis AT gmail DOT com
|
||||
# Eli Courtwright: eli AT courtwright DOT org
|
||||
|
||||
# This software is licensed under the terms of the BSD license.
|
||||
# http://opensource.org/licenses/BSD-3-Clause
|
||||
|
||||
# ConfigObj 5 - main repository for documentation and issue tracking:
|
||||
# https://github.com/DiffSK/configobj
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pprint import pprint
|
||||
|
||||
__version__ = '1.0.1'
|
||||
|
||||
__all__ = (
|
||||
'dottedQuadToNum',
|
||||
'numToDottedQuad',
|
||||
'ValidateError',
|
||||
'VdtUnknownCheckError',
|
||||
'VdtParamError',
|
||||
'VdtTypeError',
|
||||
'VdtValueError',
|
||||
'VdtValueTooSmallError',
|
||||
'VdtValueTooBigError',
|
||||
'VdtValueTooShortError',
|
||||
'VdtValueTooLongError',
|
||||
'VdtMissingValue',
|
||||
'Validator',
|
||||
'is_integer',
|
||||
'is_float',
|
||||
'is_boolean',
|
||||
'is_list',
|
||||
'is_tuple',
|
||||
'is_ip_addr',
|
||||
'is_string',
|
||||
'is_int_list',
|
||||
'is_bool_list',
|
||||
'is_float_list',
|
||||
'is_string_list',
|
||||
'is_ip_addr_list',
|
||||
'is_mixed_list',
|
||||
'is_option',
|
||||
)
|
||||
|
||||
_list_arg = re.compile(r'''
|
||||
(?:
|
||||
([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*list\(
|
||||
(
|
||||
(?:
|
||||
\s*
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s\)][^,\)]*?) # unquoted
|
||||
)
|
||||
\s*,\s*
|
||||
)*
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s\)][^,\)]*?) # unquoted
|
||||
)? # last one
|
||||
)
|
||||
\)
|
||||
)
|
||||
''', re.VERBOSE | re.DOTALL) # two groups
|
||||
|
||||
_list_members = re.compile(r'''
|
||||
(
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s=][^,=]*?) # unquoted
|
||||
)
|
||||
(?:
|
||||
(?:\s*,\s*)|(?:\s*$) # comma
|
||||
)
|
||||
''', re.VERBOSE | re.DOTALL) # one group
|
||||
|
||||
_paramstring = r'''
|
||||
(?:
|
||||
(
|
||||
(?:
|
||||
[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*list\(
|
||||
(?:
|
||||
\s*
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s\)][^,\)]*?) # unquoted
|
||||
)
|
||||
\s*,\s*
|
||||
)*
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s\)][^,\)]*?) # unquoted
|
||||
)? # last one
|
||||
\)
|
||||
)|
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s=][^,=]*?)| # unquoted
|
||||
(?: # keyword argument
|
||||
[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*
|
||||
(?:
|
||||
(?:".*?")| # double quotes
|
||||
(?:'.*?')| # single quotes
|
||||
(?:[^'",\s=][^,=]*?) # unquoted
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(?:
|
||||
(?:\s*,\s*)|(?:\s*$) # comma
|
||||
)
|
||||
)
|
||||
'''
|
||||
|
||||
_matchstring = '^%s*' % _paramstring
|
||||
|
||||
def dottedQuadToNum(ip):
|
||||
# import here to avoid it when ip_addr values are not used
|
||||
import socket, struct
|
||||
|
||||
try:
|
||||
return struct.unpack('!L',
|
||||
socket.inet_aton(ip.strip()))[0]
|
||||
except socket.error:
|
||||
raise ValueError('Not a good dotted-quad IP: %s' % ip)
|
||||
return
|
||||
|
||||
def numToDottedQuad(num):
|
||||
# import here to avoid it when ip_addr values are not used
|
||||
import socket, struct
|
||||
|
||||
# no need to intercept here, 4294967295L is fine
|
||||
if num > int(4294967295) or num < 0:
|
||||
raise ValueError('Not a good numeric IP: %s' % num)
|
||||
try:
|
||||
return socket.inet_ntoa(
|
||||
struct.pack('!L', int(num)))
|
||||
except (socket.error, struct.error, OverflowError):
|
||||
raise ValueError('Not a good numeric IP: %s' % num)
|
||||
|
||||
class ValidateError(Exception):
|
||||
"""
|
||||
This error indicates that the check failed.
|
||||
It can be the base class for more specific errors.
|
||||
"""
|
||||
|
||||
class VdtMissingValue(ValidateError):
|
||||
"""No value was supplied to a check that needed one."""
|
||||
|
||||
class VdtUnknownCheckError(ValidateError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the check "{}" is unknown.'.format(value))
|
||||
|
||||
|
||||
class VdtParamError(SyntaxError):
|
||||
NOT_GIVEN = object()
|
||||
|
||||
def __init__(self, name_or_msg, value=NOT_GIVEN):
|
||||
if value is self.NOT_GIVEN:
|
||||
SyntaxError.__init__(self, name_or_msg)
|
||||
else:
|
||||
SyntaxError.__init__(self, 'passed an incorrect value "{}" for parameter "{}".'.format(value, name_or_msg))
|
||||
|
||||
|
||||
class VdtTypeError(ValidateError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the value "{}" is of the wrong type.'.format(value))
|
||||
|
||||
|
||||
class VdtValueError(ValidateError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the value "{}" is unacceptable.'.format(value))
|
||||
|
||||
|
||||
class VdtValueTooSmallError(VdtValueError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the value "{}" is too small.'.format(value))
|
||||
|
||||
|
||||
class VdtValueTooBigError(VdtValueError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the value "{}" is too big.'.format(value))
|
||||
|
||||
|
||||
class VdtValueTooShortError(VdtValueError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(
|
||||
self,
|
||||
'the value "{}" is too short.'.format(value))
|
||||
|
||||
class VdtValueTooLongError(VdtValueError):
|
||||
def __init__(self, value):
|
||||
ValidateError.__init__(self, 'the value "{}" is too long.'.format(value))
|
||||
|
||||
class Validator(object):
|
||||
# this regex does the initial parsing of the checks
|
||||
_func_re = re.compile(r'([^\(\)]+?)\((.*)\)', re.DOTALL)
|
||||
|
||||
# this regex takes apart keyword arguments
|
||||
_key_arg = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$', re.DOTALL)
|
||||
|
||||
|
||||
# this regex finds keyword=list(....) type values
|
||||
_list_arg = _list_arg
|
||||
|
||||
# this regex takes individual values out of lists - in one pass
|
||||
_list_members = _list_members
|
||||
|
||||
# These regexes check a set of arguments for validity
|
||||
# and then pull the members out
|
||||
_paramfinder = re.compile(_paramstring, re.VERBOSE | re.DOTALL)
|
||||
_matchfinder = re.compile(_matchstring, re.VERBOSE | re.DOTALL)
|
||||
|
||||
def __init__(self, functions=None):
|
||||
self.functions = {
|
||||
'': self._pass,
|
||||
'integer': is_integer,
|
||||
'float': is_float,
|
||||
'boolean': is_boolean,
|
||||
'ip_addr': is_ip_addr,
|
||||
'string': is_string,
|
||||
'list': is_list,
|
||||
'tuple': is_tuple,
|
||||
'int_list': is_int_list,
|
||||
'float_list': is_float_list,
|
||||
'bool_list': is_bool_list,
|
||||
'ip_addr_list': is_ip_addr_list,
|
||||
'string_list': is_string_list,
|
||||
'mixed_list': is_mixed_list,
|
||||
'pass': self._pass,
|
||||
'option': is_option,
|
||||
'force_list': force_list,
|
||||
}
|
||||
if functions is not None:
|
||||
self.functions.update(functions)
|
||||
# tekNico: for use by ConfigObj
|
||||
self.baseErrorClass = ValidateError
|
||||
self._cache = {}
|
||||
|
||||
def check(self, check, value, missing=False):
|
||||
fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check)
|
||||
|
||||
if missing:
|
||||
if default is None:
|
||||
# no information needed here - to be handled by caller
|
||||
raise VdtMissingValue()
|
||||
value = self._handle_none(default)
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return self._check_value(value, fun_name, fun_args, fun_kwargs)
|
||||
|
||||
def _handle_none(self, value):
|
||||
if value == 'None':
|
||||
return None
|
||||
elif value in ("'None'", '"None"'):
|
||||
# Special case a quoted None
|
||||
value = self._unquote(value)
|
||||
return value
|
||||
|
||||
def _parse_with_caching(self, check):
|
||||
if check in self._cache:
|
||||
fun_name, fun_args, fun_kwargs, default = self._cache[check]
|
||||
# We call list and dict below to work with *copies* of the data
|
||||
# rather than the original (which are mutable of course)
|
||||
fun_args = list(fun_args)
|
||||
fun_kwargs = dict(fun_kwargs)
|
||||
else:
|
||||
fun_name, fun_args, fun_kwargs, default = self._parse_check(check)
|
||||
fun_kwargs = {str(key): value for (key, value) in list(fun_kwargs.items())}
|
||||
self._cache[check] = fun_name, list(fun_args), dict(fun_kwargs), default
|
||||
return fun_name, fun_args, fun_kwargs, default
|
||||
|
||||
def _check_value(self, value, fun_name, fun_args, fun_kwargs):
|
||||
try:
|
||||
fun = self.functions[fun_name]
|
||||
except KeyError:
|
||||
raise VdtUnknownCheckError(fun_name)
|
||||
else:
|
||||
return fun(value, *fun_args, **fun_kwargs)
|
||||
|
||||
def _parse_check(self, check):
|
||||
fun_match = self._func_re.match(check)
|
||||
if fun_match:
|
||||
fun_name = fun_match.group(1)
|
||||
arg_string = fun_match.group(2)
|
||||
arg_match = self._matchfinder.match(arg_string)
|
||||
if arg_match is None:
|
||||
# Bad syntax
|
||||
raise VdtParamError('Bad syntax in check "%s".' % check)
|
||||
fun_args = []
|
||||
fun_kwargs = {}
|
||||
# pull out args of group 2
|
||||
for arg in self._paramfinder.findall(arg_string):
|
||||
# args may need whitespace removing (before removing quotes)
|
||||
arg = arg.strip()
|
||||
listmatch = self._list_arg.match(arg)
|
||||
if listmatch:
|
||||
key, val = self._list_handle(listmatch)
|
||||
fun_kwargs[key] = val
|
||||
continue
|
||||
keymatch = self._key_arg.match(arg)
|
||||
if keymatch:
|
||||
val = keymatch.group(2)
|
||||
if not val in ("'None'", '"None"'):
|
||||
# Special case a quoted None
|
||||
val = self._unquote(val)
|
||||
fun_kwargs[keymatch.group(1)] = val
|
||||
continue
|
||||
|
||||
fun_args.append(self._unquote(arg))
|
||||
else:
|
||||
# allows for function names without (args)
|
||||
return check, (), {}, None
|
||||
|
||||
# Default must be deleted if the value is specified too,
|
||||
# otherwise the check function will get a spurious "default" keyword arg
|
||||
default = fun_kwargs.pop('default', None)
|
||||
return fun_name, fun_args, fun_kwargs, default
|
||||
|
||||
def _unquote(self, val):
|
||||
if (len(val) >= 2) and (val[0] in ("'", '"')) and (val[0] == val[-1]):
|
||||
val = val[1:-1]
|
||||
return val
|
||||
|
||||
def _list_handle(self, listmatch):
|
||||
out = []
|
||||
name = listmatch.group(1)
|
||||
args = listmatch.group(2)
|
||||
for arg in self._list_members.findall(args):
|
||||
out.append(self._unquote(arg))
|
||||
return name, out
|
||||
|
||||
def _pass(self, value):
|
||||
return value
|
||||
|
||||
def get_default_value(self, check):
|
||||
fun_name, fun_args, fun_kwargs, default = self._parse_with_caching(check)
|
||||
if default is None:
|
||||
raise KeyError('Check "%s" has no default value.' % check)
|
||||
value = self._handle_none(default)
|
||||
if value is None:
|
||||
return value
|
||||
return self._check_value(value, fun_name, fun_args, fun_kwargs)
|
||||
|
||||
def _is_num_param(names, values, to_float=False):
|
||||
fun = to_float and float or int
|
||||
out_params = []
|
||||
for (name, val) in zip(names, values):
|
||||
if val is None:
|
||||
out_params.append(val)
|
||||
elif isinstance(val, (int, float, str)):
|
||||
try:
|
||||
out_params.append(fun(val))
|
||||
except ValueError:
|
||||
raise VdtParamError(name, val)
|
||||
else:
|
||||
raise VdtParamError(name, val)
|
||||
return out_params
|
||||
|
||||
# built in checks
|
||||
# you can override these by setting the appropriate name
|
||||
# in Validator.functions
|
||||
# note: if the params are specified wrongly in your input string,
|
||||
# you will also raise errors.
|
||||
def is_integer(value, min=None, max=None):
|
||||
(min_val, max_val) = _is_num_param( # pylint: disable=unbalanced-tuple-unpacking
|
||||
('min', 'max'), (min, max))
|
||||
if not isinstance(value, (int, str)):
|
||||
raise VdtTypeError(value)
|
||||
if isinstance(value, str):
|
||||
# if it's a string - does it represent an integer ?
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
raise VdtTypeError(value)
|
||||
if (min_val is not None) and (value < min_val):
|
||||
raise VdtValueTooSmallError(value)
|
||||
if (max_val is not None) and (value > max_val):
|
||||
raise VdtValueTooBigError(value)
|
||||
return value
|
||||
|
||||
|
||||
def is_float(value, min=None, max=None):
|
||||
(min_val, max_val) = _is_num_param(
|
||||
('min', 'max'), (min, max), to_float=True)
|
||||
if not isinstance(value, (int, float, str)):
|
||||
raise VdtTypeError(value)
|
||||
if not isinstance(value, float):
|
||||
# if it's a string - does it represent a float ?
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
raise VdtTypeError(value)
|
||||
if (min_val is not None) and (value < min_val):
|
||||
raise VdtValueTooSmallError(value)
|
||||
if (max_val is not None) and (value > max_val):
|
||||
raise VdtValueTooBigError(value)
|
||||
return value
|
||||
|
||||
bool_dict = {
|
||||
True: True, 'on': True, '1': True, 'true': True, 'yes': True,
|
||||
False: False, 'off': False, '0': False, 'false': False, 'no': False,
|
||||
}
|
||||
|
||||
def is_boolean(value):
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return bool_dict[value.lower()]
|
||||
except KeyError:
|
||||
raise VdtTypeError(value)
|
||||
# we do an equality test rather than an identity test
|
||||
# this ensures Python 2.2 compatibility
|
||||
# and allows 0 and 1 to represent True and False
|
||||
if value == False:
|
||||
return False
|
||||
elif value == True:
|
||||
return True
|
||||
else:
|
||||
raise VdtTypeError(value)
|
||||
|
||||
|
||||
def is_ip_addr(value):
|
||||
if not isinstance(value, str):
|
||||
raise VdtTypeError(value)
|
||||
value = value.strip()
|
||||
try:
|
||||
dottedQuadToNum(value)
|
||||
except ValueError:
|
||||
raise VdtValueError(value)
|
||||
return value
|
||||
|
||||
|
||||
def is_list(value, min=None, max=None):
|
||||
(min_len, max_len) = _is_num_param( # pylint: disable=unbalanced-tuple-unpacking
|
||||
('min', 'max'), (min, max))
|
||||
if isinstance(value, str):
|
||||
raise VdtTypeError(value)
|
||||
try:
|
||||
num_members = len(value)
|
||||
except TypeError:
|
||||
raise VdtTypeError(value)
|
||||
if min_len is not None and num_members < min_len:
|
||||
raise VdtValueTooShortError(value)
|
||||
if max_len is not None and num_members > max_len:
|
||||
raise VdtValueTooLongError(value)
|
||||
return list(value)
|
||||
|
||||
|
||||
def is_tuple(value, min=None, max=None):
|
||||
return tuple(is_list(value, min, max))
|
||||
|
||||
def is_string(value, min=None, max=None):
|
||||
if not isinstance(value, str):
|
||||
raise VdtTypeError(value)
|
||||
(min_len, max_len) = _is_num_param(
|
||||
('min', 'max'), (min, max))
|
||||
try:
|
||||
num_members = len(value)
|
||||
except TypeError:
|
||||
raise VdtTypeError(value)
|
||||
if min_len is not None and num_members < min_len:
|
||||
raise VdtValueTooShortError(value)
|
||||
if max_len is not None and num_members > max_len:
|
||||
raise VdtValueTooLongError(value)
|
||||
return value
|
||||
|
||||
|
||||
def is_int_list(value, min=None, max=None):
|
||||
return [is_integer(mem) for mem in is_list(value, min, max)]
|
||||
|
||||
def is_bool_list(value, min=None, max=None):
|
||||
return [is_boolean(mem) for mem in is_list(value, min, max)]
|
||||
|
||||
def is_float_list(value, min=None, max=None):
|
||||
return [is_float(mem) for mem in is_list(value, min, max)]
|
||||
|
||||
def is_string_list(value, min=None, max=None):
|
||||
if isinstance(value, str):
|
||||
raise VdtTypeError(value)
|
||||
return [is_string(mem) for mem in is_list(value, min, max)]
|
||||
|
||||
def is_ip_addr_list(value, min=None, max=None):
|
||||
return [is_ip_addr(mem) for mem in is_list(value, min, max)]
|
||||
|
||||
def force_list(value, min=None, max=None):
|
||||
if not isinstance(value, (list, tuple)):
|
||||
value = [value]
|
||||
return is_list(value, min, max)
|
||||
|
||||
fun_dict = {
|
||||
int: is_integer,
|
||||
'int': is_integer,
|
||||
'integer': is_integer,
|
||||
float: is_float,
|
||||
'float': is_float,
|
||||
'ip_addr': is_ip_addr,
|
||||
str: is_string,
|
||||
'str': is_string,
|
||||
'string': is_string,
|
||||
bool: is_boolean,
|
||||
'bool': is_boolean,
|
||||
'boolean': is_boolean,
|
||||
}
|
||||
|
||||
def is_mixed_list(value, *args):
|
||||
try: length = len(value)
|
||||
except TypeError: raise VdtTypeError(value)
|
||||
if length < len(args): raise VdtValueTooShortError(value)
|
||||
elif length > len(args): raise VdtValueTooLongError(value)
|
||||
try: return [fun_dict[arg](val) for arg, val in zip(args, value)]
|
||||
except KeyError as cause: raise VdtParamError('mixed_list', cause)
|
||||
|
||||
|
||||
def is_option(value, *options):
|
||||
if not isinstance(value, str): raise VdtTypeError(value)
|
||||
if not value in options: raise VdtValueError(value)
|
||||
return value
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
Recently, and mostly from people who I've never seen before, the opinions about how this project should be run has started flooding in again. In a recent forum thread of such opinions, specifically about:
|
||||
|
||||
- The decision to no longer mirror release notes on GitHub.
|
||||
- Some people feeling there were too many "barriers to entry" to joining RNS development.
|
||||
- The project not really being "open source" because random strangers couldn't just "contribute".
|
||||
|
||||
Joakim posted some very relevant observations about how Reticulum operates, along with the following quote:
|
||||
|
||||
> The modern industrial system has a built-in tendency to grow; it cannot really work unless it is growing. The word “stability” has been struck from its dictionary and replaced by “stagnation”. Its continuous growth pursues no particular aims or objectives: it is growth for the sake of growing. No one even enquires after its final shape. There is none; there is no “saturation point”.
|
||||
|
||||
That E. F. Schumacher quote perfectly illustrates the ontological schism that makes it so tiresome to deal with stuff like this.
|
||||
|
||||
There is, in this day and age, between different people, widely different base conceptual integrations of what "open source" means. For many people, "open source" has become synonymous **not** with skilled people working together in a coordinated and careful way on complex engineering challenges, but a sort of growth- and attention-focused "free-for-all" *behavioral* codex that must be followed above all else; a *social* modus operandi of fake inclusivity where everyone "should have their voice heard", and adherence to that specific process is weighed much higher than the final results.
|
||||
|
||||
I do not subscribe to, and consequently do not operate the Reticulum project under *any* versions of that idea.
|
||||
|
||||
**Here's the statistical, boring reality:**
|
||||
|
||||
- Around 90% of pull requests and "recommendations" I received when people could just submit stuff via GitHub would
|
||||
have *severely* broken things, introduced bugs or security issues, created roadblocks for future work, or otherwise
|
||||
damaged the software. Usually just for the sake of satisfying a random newcomers "idea" or personal preference.
|
||||
|
||||
- Similarly, around 90% "bug reports" were actually people asking for help, because of having failed to read even the
|
||||
most basic parts of the documentation.
|
||||
|
||||
- The people with the least amount of understanding, skill and effort invested tend to be loudest and most vocal. When
|
||||
all you have is "opinions", those are iterated upon ad infinitum, apparently.
|
||||
|
||||
Can you imagine how much time that wasted? Can you imagine what we could have accomplished with that time instead?
|
||||
|
||||
The only thing that this creates is *noise* and confusion. Clogging up the mental and physical workspaces, of people who are actually investing time and effort on the project with stuff like that is objectively just taking time that could have been used on development, and replacing it with *nothing*.
|
||||
|
||||
I was receiving *actual* bug reports, pull requests, proper technical investigations and patches via methods outside GitHub and "public" internet-based channels *way* before GitHub interaction and similar was closed down. That was were almost *all* of the real contributions were coming from, anyway. Apparently, and not unsurprisingly, the people who has invested the time and effort to understand Reticulum also prefer to collaborate in this way. Since leaving the GitHub madhouse behind, the signal-to-noise ratio has **significantly** increased.
|
||||
|
||||
Managing a public "issue" tracker with global read/write access is a futile and useless endeavor. Consider this:
|
||||
|
||||
- User A reports a "bug" that is really just a failure of understanding.
|
||||
- User B sees this and seconds is, proposing a "fix" that in continued failure of understanding would actually break functionality X.
|
||||
- User C joins the bandwagon and asks why this hasn't already been implemented like that? It's obvious!
|
||||
|
||||
The sensible response here from the developer is closing the issue with "No. Go RTFM". Today, though, this usually results in hurt feelings, animosity towards the developer and in some cases (as experienced and documented in the case of RNS), months of perfidious personal vendetta against the developer for being so brazen as to suggest the user was wrong and wasting people's time.
|
||||
|
||||
When this pattern repeats, over and over, the only sensible, measured and constructive course of action is to shrug your shoulders and say:
|
||||
|
||||
*"This system is fundamentally broken. It ain't working. I can give up here, or I can go build something better that has a chance of working."*
|
||||
|
||||
So, now it's your turn. Go look at the diffs for the last six months. What does it look like I have been doing?
|
||||
|
||||
But I will be damned straight with you all, and say that part of that solution is **absolutely** to erect barriers to entry. You can fucking bet your arse on that. I don't want opinionated man-babies running around in my living-room at 3am. I don't want to clean up the floor after a wannabe "dev-ops stars" with LLMs and a peripheral case of influencitis has puked all over the office.
|
||||
|
||||
- If you want to join the fun of changing core networking code that thousands of people rely on for communication
|
||||
daily, you better know what the fuck you're doing.
|
||||
|
||||
- I'm not here to provide validation and hugs to random strangers. I'm here to make sure the reference implementation
|
||||
of Reticulum works.
|
||||
|
||||
- If you cannot figure out how to submit a patch or valid bug investigation over RNS, you cannot expect I will take
|
||||
you seriously. At all.
|
||||
|
||||
If someone can't handle that, they should find their entertainment elsewhere.
|
||||
|
||||
I've said it before: I've provided the information and code required to make Reticulum *work*, and build networked systems, protocols and applications on top of it. That information is deep, complex, and requires you to read hundreds of pages, and put in weeks of efforts to get the *full* picture. A lot less is required to get started, but it *will* still be a steep learning experience.
|
||||
|
||||
This is a full networking stack, based on some pretty complex principles, for crying out loud. It's **not** a `hello_world` designed to make you feel good about yourself. It turns almost everything you know about networked systems on its head. That's **challenging** for *anyone*. Climb the mountain, and it will be satisfying in the end. Refuse to climb... Well, what do you think will happen?
|
||||
|
||||
As for barriers to entry of *using* RNS and related programs, utilities and clients, it's not my task to teach every single user how to do X, Y and Z. The information *is* out there. If it wasn't organized optimally for your way of learning, you can choose to "raise your concerns" about it, discuss "the fact of it" on a forum or chatroom, or: *You can choose to remedy that, and help others along*.
|
||||
|
||||
I sure know what *I* would have done.
|
||||
@@ -0,0 +1,415 @@
|
||||
# Zen of Reticulum
|
||||
|
||||
## I: The Illusion Of The Center
|
||||
|
||||
For the better part of a generation, we have been taught to visualize the digital world through the lens of hierarchy. The mental maps we carry are dominated by a single, misleading image: **The Cloud**.
|
||||
|
||||
We imagine the network as a vast, ethereal space "up there" or "out there". A centralized repository of services and data to which we, the lowly clients, must connect. We build our software with this assumption hardcoded into our logic: *There is a server. The server has the authority. The server knows the way. I must find the server to function*.
|
||||
|
||||
This is the Client-Server mental model, and it is the primary obstacle to understanding Reticulum.
|
||||
|
||||
### Fallacy Of The Cloud
|
||||
|
||||
The first step in the Zen of Reticulum is to realize that *there is no cloud*. There is only other people's computers. When you build for the cloud, you are building *for* a landlord. You are accepting that your application's existence is conditional on the permission, uptime, and continued goodwill of a central authority.
|
||||
|
||||
In Reticulum, you must shift your thinking from "connecting to" to "being among". Reticulum is not a service you subscribe to - *it is a fabric you inhabit*. There is no "up there". There is only *here* and *there*, and the space between them is peer-to-peer.
|
||||
|
||||
### Decentralization Or Uncentralizability?
|
||||
|
||||
It is common to hear the word "decentralized" thrown around in modern tech circles. But often, this is merely a marketing term for "slightly distributed centralization". A blockchain with a few dominant miners, or a federated protocol with a few giant servers. *In practice*, it's still centralized. It simply has a few centers instead of one.
|
||||
|
||||
Reticulum goes further. It wants **Uncentralizability**.
|
||||
|
||||
This is not a wishful political stance, but a foundational mathematical characteristic of the protocol, onto which everything else has been built. Reticulum assumes that every peer on the network is potentially hostile, and every link is potentially compromised. It is designed with no "privileged" nodes. While some nodes may act as Transport Instances - forwarding traffic for others - they do so *blindly*, and they only know about their immediate surroundings, and nothing more. They route based on cryptographic proofs, not on administrative privilege. They cannot see who is talking to whom, nor can they selectively manipulate traffic without breaking their own ability to route entirely.
|
||||
|
||||
The system is designed to make hierarchy structurally impossible. You cannot hijack an address, because there is no central registry to hijack. You cannot block a user, because there is no central switch to flip. You can offer paths through the network, but you can't force anyone to use them.
|
||||
|
||||
### Death To The Address
|
||||
|
||||
To break free of the center, you must also let go of the concept of the "Address".
|
||||
|
||||
In the IP world, an address is a location. It is a coordinate in a *deeply hierarchical* and static grid. If you move your computer to a different house, your address changes. If your router reboots, your address might change. Your *identity* is bound to your *location*, and therefore, it is fragile, and easily controlled.
|
||||
|
||||
Reticulum abolishes this link between *Identity* and *Location*.
|
||||
|
||||
In Reticulum, an address is not a place; it is a **Hash of an Identity**. It is a cryptographic representation of *who* you are, not *where* you are. Because of this, your address is portable. You can take a laptop from a WiFi cafe in Berlin, to a LoRa mesh in the mountains, to a packet radio link on a boat, and your "address" - your *Destination Hash* - never changes.
|
||||
|
||||
The network does not route to a place; it routes to a *person* (or a machine). When you send a packet, you are not targeting a coordinate in a grid; you are encrypting a message for a specific entity. The network dynamically discovers where that entity currently resides, and it does so in a way where no one really knows where that entity is actually located physically.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I am at `192.168.1.5`. Come find me"*.
|
||||
- **The Zen Way:** *"I am `<327c1b2f87c9353e01769b01090b18f2>`. Wherever I am, my peers can reach me"*.
|
||||
|
||||
Once you stop thinking about servers and start thinking about portable identities, where everyone can always reach everyone else directly, the illusion of the center fades away. You realize there *is* no center holding the network together. No coordinators or bureaucrats required. The network is simply the sum of its peers, communicating directly, sovereignly, and without a master.
|
||||
|
||||
|
||||
## II: Physics Of Trust
|
||||
*Paranoia Is A Great Design Principle*
|
||||
|
||||
If we accept that there is no center - that the network is a chaotic, peer-to-peer mesh - we are forced to confront a terrifying reality: **There is no one guarding the door**.
|
||||
|
||||
In the traditional networking mindset, we rely on the concept of the "trusted core". We assume our local coffee shop WiFi is safe, or that the backbone providers are neutral custodians. We build our security like a castle: strong walls on the outside, soft and trusting on the inside. We use encryption only when we step out into the "wild" internet.
|
||||
|
||||
### Hostile Environments
|
||||
|
||||
The Zen of Reticulum requires you to invert this. You must assume that *every* environment is hostile. This isn't cynicism, just uncaring physics.
|
||||
|
||||
When you transmit information over radio waves, you are shouting into a crowded room. Anyone can listen. When you traverse the internet, your packets pass through routers controlled by strangers, corporations, and state actors. Assuming privacy in this environment without cryptographic protection is not optimism but gross negligence.
|
||||
|
||||
Reticulum is built on the premise that every link is tapped, and every peer is a potential adversary. If your system cannot survive an adversary owning the physical layer, it cannot survive at all.
|
||||
|
||||
But this is the paradox: By assuming the network is hostile, you make it safe. When you accept the dangers for what they are, they become manageable. When you stop trusting the infrastructure and start trusting the math, you eliminate the single point of failure: Human integrity.
|
||||
|
||||
### Encryption Is Not A Feature
|
||||
|
||||
In the world of TCP/IP, encryption is an afterthought. It is a layer we slap on top of the protocol (HTTPS, TLS) to patch the security holes of the original design. It is a "feature" you sometimes *enable* for "sensitive data". This is fundamentally flawed, since all data is sensitive.
|
||||
|
||||
In Reticulum, encryption is **gravity**.
|
||||
|
||||
It is not optional. It is not a plugin. It is the *fundamental force that allows the network to exist*. If you were to strip the encryption from Reticulum, the routing would break. The Transport system uses cryptographic signatures and entropy to verify paths and pass information. If packets were plaintext, intermediate nodes could not prove that a route was valid, nor could endpoints prevent spoofing or tampering.
|
||||
|
||||
In Reticulum, the entropy of the encrypted packet *is* the routing logic.
|
||||
|
||||
To ask for a version of Reticulum without encryption is like asking for a version of the ocean without liquid. You are not asking for a feature change; you're asking for a different physical universe. We design for a universe where information has mass, structure, and integrity.
|
||||
|
||||
### Zero-Trust Architectures
|
||||
|
||||
We must unlearn our reliance on **Institutional Trust**.
|
||||
|
||||
For decades, we have been trained to trust authorities. We trust a website because a chain of Certificate Authorities (companies we don't know) vouches for it. We trust an app because it is in an app store (run by a corporation we don't control). We trust a message because it comes from a phone number assigned by a telecom. Yet, everything in our digital information sphere today is more untrustworthy and risky than a medieval second-hand underwear market.
|
||||
|
||||
Reticulum replaces institutional trust with **Cryptographic Proof**.
|
||||
|
||||
In Reticulum, you do not trust a node because it has a nice hostname or because it is listed in a directory. You trust it because it holds the private key corresponding to the Destination Hash you are communicating with. This trust is binary, mathematical, and **absolute**. Either the signature matches, or it does not. There is no "maybe".
|
||||
|
||||
This shift moves the power from the institution to the individual. You become the ultimate arbiter of your own trust relationships. You decide which keys to accept, which paths to follow, and which identities to recognize.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I trust this site because the browser says the lock icon is green"*.
|
||||
- **The Zen Way:** *"I trust this destination because I have verified its hash fingerprint out-of-band, and the math confirms the signature"*.
|
||||
|
||||
When you internalize the Physics of Trust, you stop looking for protection from firewalls, VPNs, and Terms of Service agreements. You realize that true security comes from the design of the protocol itself. You can stop trusting the cloud, and you start trusting the code - because you can verify it yourself.
|
||||
|
||||
|
||||
## III: Merits Of Scarcity
|
||||
*Every Bit Counts*
|
||||
|
||||
We have grown addicted to abundance. In the modern digital ecosystem, bandwidth is treated as an endless, flat ocean. We stream high-definition video without a thought, we ship entire libraries of code just to render a single button, and we measure performance in gigabits per second. This abundance has hollowed out our craft. When constraints vanish, efficiency dies, and with it, a certain kind of Clarity and Quality.
|
||||
|
||||
Reticulum asks you to step out of the ocean and onto the tightrope.
|
||||
|
||||
### The Bandwidth Fallacy
|
||||
|
||||
The Zen of Reticulum requires the realization that **5 bits per second is a valid speed**.
|
||||
|
||||
To a modern developer, this sounds like paralysis. But there is a profound freedom in limits: When you have a gigabit connection, you can be incredibly sloppy. You can be wasteful. You can push your problems onto the infrastructure. *"It’s slow? Get a faster router"*.
|
||||
|
||||
But on a high-latency, low-bandwidth link (be it a noisy HF radio channel or a tenuous LoRa hop) you cannot push problems anywhere. You must solve them. The network does not negotiate with waste.
|
||||
|
||||
This forces a shift from consumption to interaction. You are no longer, then, consuming a service provided by a fat pipe; you are engaging in a careful negotiation with the physical medium. The medium becomes a partner in the conversation, not just a dumb conduit. You suddenly need to *understand the world to be in it*.
|
||||
|
||||
### Cost Of A Byte
|
||||
|
||||
In a scarce economy, a byte is not just data, but energy, time, and space.
|
||||
|
||||
Every byte you transmit consumes battery life on a solar-powered node. It occupies valuable airtime that could have been used by another peer. It represents a measurable slice of the electromagnetic spectrum.
|
||||
|
||||
When you internalize this, you begin to write code differently. You stop asking, "How much data can I send?" and start asking, "What is the *minimum* amount of information required to convey this intent? How can I best utilize my informational entropy?"
|
||||
|
||||
This is where the elegance of Reticulum shines. The protocol is designed to strip away the non-essential. A link establishment takes three very small packets. A destination hash fits in 16 bytes. The overhead is vanishingly small, leaving almost the entire channel for the message itself.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I need to send a status update. I'll send a JSON object with metadata, timestamps, and user profile info (15KB)."*
|
||||
- **The Zen Way:** *"I need to send a status update. I'll send a single byte representing the state code. The context is already known."*
|
||||
|
||||
This is of course optimization, but more importantly, *it is a form of respect*. Efficiency in a shared medium is an act of stewardship. By taking only what you need from the network, you leave room for others. The network listens to those who speak with purpose.
|
||||
|
||||
### Flow & Time
|
||||
|
||||
Scarcity also teaches us about time. We have become addicted to the *synchronous* now - the instant ping, the real-time stream. But Reticulum embraces *asynchronous* time.
|
||||
|
||||
When links are intermittent and latency is measured in minutes or hours, "real-time" is an illusion. Reticulum doesn't encourage **Store and Forward** as a mere fallback, but as a primary mode of existence. You write a message, it propagates when it can, and it arrives when it arrives.
|
||||
|
||||
This changes the psychological texture of communication. It removes the anxiety of the immediate response. It allows for contemplation. You are not demanding the recipient's attention *right now*; you are placing a gift in their path, to be found when they are ready.
|
||||
|
||||
By designing for delay, you design for resilience. You are no longer building a house of cards that collapses when a single packet drops. You are building a stone arch that distributes the load *over time*.
|
||||
|
||||
### Liberation From Limits
|
||||
|
||||
There is a strange optimism in scarcity. When you are forced to work within strict constraints, you are forced to prioritize. *You* must decide what truly matters. *That* is the real core of agency.
|
||||
|
||||
In the infinite fantasy world of The Cloud, everything is urgent, so nothing is. In the economy of Reticulum, the cost of transmission forces you to weigh the value of your message. Do you really need to send that heart beat? Is that photo essential?
|
||||
|
||||
When you strip away the noise, what remains is *signal*.
|
||||
|
||||
This discipline creates a different kind of developer. It creates a craftsman who understands that the best code is the code you don't have to write. It creates a user who understands that the most powerful message is the one that is *understood*, not the one that is loudest. In the world of Reticulum, you are not a mere consumer of bandwidth; you are an architect of intent.
|
||||
|
||||
|
||||
## IV: Sovereignty Through Infrastructure
|
||||
**Be Your Own Network**
|
||||
|
||||
We live in an era of digital tenancy. We lease our connectivity from ISPs. We rent our storage from cloud providers. We even borrow our identity from social media platforms. We are tenants in a house we did not build, governed by rules we did not write, subject to eviction at the whim of a landlord who has never met us.
|
||||
|
||||
The Zen of Reticulum is the realization that you *can* own the house.
|
||||
|
||||
### A Carrier-Grade Fallacy
|
||||
|
||||
For decades, we have been gaslit into believing that networking is really not just hard, but impossible. It is presented as a dark art reserved for telcos and billionaires, requiring millions of dollars of fiber optics, climate-controlled data centers, and armies of engineers. We are told that building reliable infrastructure is "too complex" for the individual or small organization.
|
||||
|
||||
This is a big, fat lie.
|
||||
|
||||
Physics is simple. A radio wave needs a transmitter and a receiver. A packet needs a path. The "complexity" of the modern internet is largely bureaucratic - a mountain of billing systems, regulatory capture, and legacy cruft designed to keep the gatekeepers in power.
|
||||
|
||||
Reticulum strips away the bureaucracy. It runs on hardware that costs the price of a dinner. It runs on spectrum that is free to use. It demonstrates that a robust, planetary-scale network does not require a Fortune 500 company. It requires only the will to deploy, and the distributed, uncoordinated efforts of many individuals.
|
||||
|
||||
### Personal Infrastructure
|
||||
|
||||
This is where the rubber meets the road. You can read about Reticulum, you can understand the theory, but the insights only arrive when you plug in a radio and run a Transport Node. Suddenly, you are no longer a consumer. You're an operator.
|
||||
|
||||
This shift is subtle but profound. When you run your own infrastructure, the network ceases to be a service that is provided *to* you. It becomes a space that you *inhabit*. You become responsible for the flow of information. You gain an intimate understanding of the medium - the way the weather affects the radio waves, the way the topology changes, the way the packets dance through the ether.
|
||||
|
||||
There is a quiet competence that comes from this. You stop asking "Is the internet down?" and start asking "Is *my* links up?" You stop waiting for a technician and start checking the logs. This is a form of strength. To understand the system that carries your words is to be free from the mystery that keeps you dependent.
|
||||
|
||||
### The Ability To Disconnect
|
||||
|
||||
Why go to the trouble? Why buy the radio, write the config, and leave the Pi running in the corner?
|
||||
|
||||
Because the old, centralized network is fragile. And because most of us doesn't even really want to be there anymore.
|
||||
|
||||
The internet we rely on today is a chain of single points of failure. Cut the undersea cable, and a continent goes dark. Shut down the power grid, and the cloud evaporates. Deprioritize the "wrong" traffic, and the flow of information is strangled.
|
||||
|
||||
Sovereignty is the ability to survive the cut, whether or not that cut was an accident or on purpose.
|
||||
|
||||
When you build your own infrastructure, you build a lifeline. Reticulum is designed to function over media that the traditional internet cannot touch - bare wires, battery-powered radios, ad-hoc WiFi meshes. When the grid fails, or the censors arrive, or the bill goes unpaid, your Reticulum network continues to hum.
|
||||
|
||||
This is not about "dropping out" of society. It is about building a substrate on which an actual *Society* can function.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** "My connection is slow. I should call my ISP and complain."
|
||||
- **The Zen Way:** "The path is noisy. I will adjust the antenna or find a better route."
|
||||
|
||||
By taking ownership of the infrastructure, you take ownership of your voice. You stop shouting into someone else's megaphone and start building your own. The network is no longer something that happens to you; it is something you make happen.
|
||||
|
||||
|
||||
# V: Identity and Nomadism
|
||||
**A Fluid Self**
|
||||
|
||||
In the old world, you are defined by your coordinates. If you are at `34.109.71.5`, you're *here*. If you unplug the cable and walk down the street, you vanish. Your digital self evaporates because it was tethered to the wall. You are a ghost in the endless machinations of gears, levers and transistors, bound to the hardware, and those that own it.
|
||||
|
||||
This creates a subtle, constant anxiety. We are terrified of disconnecting because, in the architecture of the old web, disconnecting is a kind of death.
|
||||
|
||||
The Zen of Reticulum offers a different way to be.
|
||||
|
||||
### Portable Existence
|
||||
|
||||
In Reticulum, your identity is not a location, or a username granted by a service. It is a cryptographic key - a complex, unique mathematical signature that exists independently of the physical world. You can carry it only in your mind, if you want to.
|
||||
|
||||
Think of it less like a street address and more like a name. *A true name*.
|
||||
|
||||
If you travel from Berlin to Tokyo, you do not change your name. You are still you. The people who know you can still recognize you. Reticulum applies this principle to the network layer. Your Destination Hash is **invariant**. It travels with you, stored securely on your device, *immutable as a stone*.
|
||||
|
||||
This changes the relationship between you and the machine. You are not "logged into" the network via a specific gateway. You *are* the endpoint. The network does not connect to a place; *it converges on you*.
|
||||
|
||||
### Roaming Nodes
|
||||
|
||||
This freedom introduces a new concept of time and space: **Nomadism**.
|
||||
|
||||
Because your identity is portable, your connectivity can be fluid. You can be sitting at a desk connected to a fiber backbone one moment, and walking through a field connected only to a long-range LoRa mesh the next. To the rest of the network, nothing has changed. Your friends do not need to update your contact info. The messages they send do not bounce back. The network senses the shift in the medium and reroutes the flow of data automatically.
|
||||
|
||||
You are no longer a stationary node in a fixed grid. You are a wanderer in a fluid medium.
|
||||
|
||||
The interfaces - whether it is WiFi, Ethernet, Packet Radio, or a physical wire - is merely the clothing your node wears. You change it to suit the environment. Underneath, you remain the same. This is the liberation of the protocol. It treats the physical medium as a transient circumstance, not a definition of self.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I lost connection. I have to reconnect to the VPN to tell them where I am now."*
|
||||
- **The Zen Way:** *"I moved. The network subtly bends to accomodate this new reality."*
|
||||
|
||||
### Announcing Presence
|
||||
|
||||
How does the network find a wanderer? It listens.
|
||||
|
||||
In the IP world, we query directories. We ask a server, "Where is Mark?" The server checks its database and gives us a coordinate. This means that someone, somewhere, is keeping track of you. It assumes and *requires* surveillance.
|
||||
|
||||
Reticulum replaces surveillance with **Announces**.
|
||||
|
||||
Instead of asking a central authority where you are, you simply state your presence. You broadcast a cryptographic proof: "I am here, and I am who I say I am". This ripples out through the mesh. Your neighbors hear it, update their path tables, and pass it on.
|
||||
|
||||
This is a quiet, organic process. It is the digital equivalent of lighting lanterns in the dark. You do not need to chase the light; you let the light find you. It respects your autonomy. You choose when to announce, how often to speak, and to whom. You also choose when to disappear - for but a moment or perpetually.
|
||||
|
||||
### Anchor In The Flow
|
||||
|
||||
There is a deep peace in this nomadism. It teaches you that stability does not come from standing still. Stability comes from *internal coherence*.
|
||||
|
||||
By holding your own private key, you hold your own center of gravity. The world around you; the infrastructure, the topography and the availability of links can all shift chaotically. Storms can knock out towers. Cables can be cut. The internet can go down.
|
||||
|
||||
But as long as you possess your key, you possess your identity. The entire infrastructure can be destroyed and rebuilt, and you are still you. Nothing lasts, yet nothing is lost.
|
||||
|
||||
You become a sovereign entity moving through the noise, connected not by the rigidity of cables, but by the fluidity of recognition. The network becomes a place you inhabit, rather than a utility you subscribe to: You are at home in the ether.
|
||||
|
||||
|
||||
## VI: Ethics Of The Tool
|
||||
**Technology With Conscience**
|
||||
|
||||
You have unlearned the center. You have accepted the physics of trust. You have embraced the economy of scarcity and the freedom of unbound nomadism. You are standing in a new space. Now, look at the tool in your hand.
|
||||
|
||||
In the old world, we were taught that technology is neutral. We are told that "guns don't kill people, people do", or that a component is just a component, indifferent to what its combinatorial potential is. This is a convenient lie. It serves only to allow the builders to wash their hands of responsibility.
|
||||
|
||||
But we know better now. We know that **architecture is politics**, and *politics is control*. The way you build a system determines how it will be used. If you build a system optimized for mass surveillance, you *will* get a panopticon. If you build a system optimized for centralized control, you *will* get a dictatorship. If you build a system optimized for extraction, you *will* get a parasite.
|
||||
|
||||
The Zen of Reticulum asserts that a tool is never neutral.
|
||||
|
||||
On the very contrary: A tool is intent, **crystallized**.
|
||||
|
||||
### The Harm Principle
|
||||
|
||||
Why does the Reticulum License forbid the software from being used in systems designed to harm humans? Is it not just a restriction on freedom?
|
||||
|
||||
It is a restriction on *license*, yes, but it is an expansion of *freedom*.
|
||||
|
||||
Building powerful tools without a moral compass is in no way virtuous or commendable, it is plain and simple irresponsibility.
|
||||
|
||||
A tool that can easily be used to oppress is a real danger to the user. If you build a network that can be turned against you by a tyrant, you are not free. You are merely waiting for the leash to tighten. By encoding the "Harm Principle" into the legal DNA of the reference implementation, we are building a safeguard. We are stating, clearly and immutably, that *this tool* is for **life**, not for death.
|
||||
|
||||
This aligns the software with the interests of humanity. It cements that the network cannot be conscripted into a kill-system, a weaponized drone controller, or a torture device without breaking the license and the law. It is a line drawn in the sand - not by a government or external authority, but by the creators of the tool itself.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"It's just software. How people use it is not my problem."*
|
||||
- **The Zen Way:** *"This software is a habitat. I will not allow it to be used to build a cage."*
|
||||
|
||||
It is *your* choice whether to align with this - we are not forcing this stance on anyone. If you choose to align with life over death, with creativity over destruction, we grant you an immensely powerful tool, to own and build with as you please. If you do not, we deny it.
|
||||
|
||||
If you do not like this, we most assuredly do not need you here, and you are on your own.
|
||||
|
||||
### Public Domain Protocol
|
||||
|
||||
This leads to a vital distinction: The difference between the *idea* and the *implementation*.
|
||||
|
||||
The protocol - the mathematical rules of how Reticulum works - is dedicated to the Public Domain. It belongs to humanity. **No one can own it**. Anyone can implement it, improve it, or adapt it. This is the core idea of free communication, which itself must be forever free.
|
||||
|
||||
But the functional, deployed *reference implementation* - the Python code, the maintenance, the years of labor - has a conscience. This distinction is the engine of sustainability. It allows the protocol to be universal, while ensuring that the specific labor of the builders is not hijacked to undermine the foundational intent of the project itself. From this document, it should be very clear what this intent is.
|
||||
|
||||
If you want to build a system with Reticulum that manipulates and damages users for profits or targets missiles, you can use the public domain protocol, and start from scratch. But you cannot take our work. You must do your own. This serves as a pillar of accountability. If you want to build a weapon, *you* go and forge the steel yourself, while the world observes. And when the blood is drawn - it is on **your** hands.
|
||||
|
||||
### Preserving Human Agency
|
||||
|
||||
We live in an era of predatory extraction. The open-source commons is being scraped, ingested, and regurgitated by machine learning algorithms, whose corporate owners seek to replace the very humans who built those commons. Our code, our words, and our creativity is being used to train systems that are specifically designed to make us obsolete, without offering anything else in return than serfdom and leashes.
|
||||
|
||||
Reticulum stands against this.
|
||||
|
||||
The license protects the software from being used to feed the beast. It draws a hard line: This tool is for *people*. It is for human-to-human connection. It is not a dataset to be strip-mined for the purpose of building a synthetic overlord, puppeteered by a miniscule conglomerate of controllers.
|
||||
|
||||
This is a radical act of preservation. By protecting the code from AI appropriation, we are protecting space for human agency. We are ensuring that there remains a digital realm where the actors are flesh, blood and soul, where decisions are made by minds, not overlords hiding behind models.
|
||||
|
||||
When you use Reticulum, you are using a tool that respects you. It does not see you as a product to be tracked. It does not see your data as fuel for an algorithm. It sees you as a sovereign, equal peer.
|
||||
|
||||
This changes the foundational premise of using the technology. It restores dignity to the interaction. You are not the user of a service; you are a participant in a mutual covenant. The tool aligns with your autonomy, rather than eroding it.
|
||||
|
||||
In this way, ethics is not a restriction, but a foundation. It is the foundation that helps ensure the network will still belong to you tomorrow.
|
||||
|
||||
|
||||
## VII: Design Patterns For Post-IP Systems
|
||||
**Practical Philosophy for Developers**
|
||||
|
||||
The philosophy is useless if it cannot be hammered into code. The metaphors we have explored - nomadism, scarcity, trust - are not just poetry, but real-world engineering constraints. When you sit down to write software for Reticulum, these concepts must shape the very structure of your application.
|
||||
|
||||
We are now moving from the *why* to the *how*. This is where the abstract becomes concrete, and where you will see the true depth of the patterns we have been weaving.
|
||||
|
||||
### Store & Forward
|
||||
|
||||
The web has trained us to be impatient. We write synchronous code. We fire a request and we wait, blocking the UI, holding our breath. If the response doesn't come in 250 milliseconds, we show a spinner. If it doesn't come in five seconds, we show an error. We treat network connectivity as a binary state: either we are "online" or we are "broken".
|
||||
|
||||
This is brittle. It is a rejection of reality.
|
||||
|
||||
In Reticulum, connectivity is a spectrum, and presence is asynchronous. If at all applicable to your intent, you must design your applications to embrace **Store & Forward**.
|
||||
|
||||
Instead of demanding an immediate answer, your application should act as a patient participant. You create a message for someone or something in the mesh. The network holds it. It carries it from node to node, perhaps over hours or days, waiting for the recipient to appear. When they finally surface, the message is delivered. This requires a shift from "request/response" to "event/handler". How exactly you do this is a challenge for you to solve intelligently within your problem domain, but Reticulum-based systems already exist that does this extremely well, and you can use them for inspiration.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** `Connect() -> Send() -> Wait() -> Crash if timeout.`
|
||||
- **The Zen Way:** `Send() -> Continue living. -> Receive() when it arrives.`
|
||||
|
||||
This changes the user experience profoundly. It removes the anxiety of the loading bar. It creates a sense of continuity. The user is not "waiting for the network"; they are interacting with a persistent log of communication that lives in the network itself.
|
||||
|
||||
### Naming Is Power
|
||||
|
||||
In the IP world, we are slaves to the Domain Name System. We rely on a hierarchy of registrars to map human-readable names to machine-readable addresses. This hierarchy is a choke point. If the registrar revokes your domain, or if the DNS server goes down, you vanish.
|
||||
|
||||
Reticulum dissolves this hierarchy with **Hash-based Identity**.
|
||||
|
||||
In this design pattern, a name is not a string you look up; it is a cryptographic destination you verify. When you design for Reticulum, you stop asking the user for a URL and start asking for a Destination or Identity Hash.
|
||||
|
||||
This feels strange at first. A hash like `<83b7328926fed0d2e6a10a7671f9e237>` looks alien compared to `myfriend.com`. But that alienness is the armor. It **cannot** be spoofed. It **cannot** be censored by a registrar. It is **absolute**.
|
||||
|
||||
Designing for this means shifting your UI metaphors. You are no longer browsing a web of pages; you are managing a ledger of keys. You are building an "Address Book" that is actually a keyring. The names are given by the user, and the power stays with them. That hashes look complex is directly analogous to the strengths of the bonds formed by their use. It forces the user to engage in a moment of verification, an out-of-band handshake, which restores the human element of trust that SSL certificates stripped away.
|
||||
|
||||
### The Interface Is The Medium
|
||||
|
||||
One of the most liberating patterns in Reticulum is **Transport Agnosticism**.
|
||||
|
||||
In traditional networking, your code is often littered with transport logic. "Am I on WiFi? Check bandwidth. Am I on Cellular? Check data plan. Am I on Ethernet?". You are constantly micromanaging the pipe.
|
||||
|
||||
In Reticulum, you write to the API, and the API writes to the medium. You send a packet to a Destination. You do not care if that packet travels over a TCP tunnel, a LoRa radio wave, or a serial wire interface. That is the stack's concern.
|
||||
|
||||
This allows you to write **Universal Applications**.
|
||||
Imagine a messaging app. You write it once. It works on a laptop connected to fiber. It works on a phone in the city using WiFi. And, without a single line of code changed, it works on a device in the wilderness, talking only to other devices via radio.
|
||||
|
||||
The pattern is simple: **Never code to the hardware. Code to the intent.**
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** `socket.connect(ip, port)`
|
||||
- **The Zen Way:** `RNS.Packet(destination, data).send()`
|
||||
|
||||
By abstracting the medium, you make your software immortal to changes in infrastructure. The user might switch from a 4G hotspot to a HF modem tomorrow. Your software doesn't need to know. It simply continues the conversation.
|
||||
|
||||
### Emergent Patterns
|
||||
|
||||
When you combine these patterns - *Store & Forward*, *Hash-based Identity*, and *Transport Agnosticism* - you create software that feels fundamentally different.
|
||||
|
||||
It feels *grounded*. It doesn't flicker when the signal drops. It doesn't panic when the server is down. It has weight. It has persistence. It has *relevance*.
|
||||
|
||||
You are no longer building a "client" that begs a "server" for attention. You are building an autonomous agent that exists within the mesh. It speaks when it needs to, listens when it can, and carries its identity with it wherever it goes.
|
||||
|
||||
This is the culmination of the Zen. The code is not just a set of instructions: It is a behavioral envelope. It is a way of *being* in the network.
|
||||
|
||||
|
||||
## VIII: Fabric Of The Independent
|
||||
|
||||
We have stripped away the illusions. We have seen that the center is empty, that trust *must* be hard, that resources are finite, and that we must own our infrastructure. We have seen that tools have ethics and that our identity can move fluidly.
|
||||
|
||||
This is a reclaiming of the commons. For too long, we have allowed the most vital substrate of human society - *our ability to speak to one another* - to be colonized by entities that do not share our interests. We have allowed the architecture of our communication to be designed by accountants rather than architects.
|
||||
|
||||
We are taking it back. Not by petitioning the masters, but by building the new world within, over, under and around the shell of the old.
|
||||
|
||||
### The Work Is Finished
|
||||
|
||||
The heavy lifting is done.
|
||||
|
||||
The protocol is in the public domain, a gift to humanity that can never be taken away. The software is written, tested, and running on devices scattered across the globe. The manual lies open before you. The source code for the reference implementation is now distributed on hundreds of thousands of devices across the planet. No one can delete or destroy it. The hardware is accessible and abundant.
|
||||
|
||||
It was a hard road to get here, but we got here. Now, there is no roadmap committee waiting for approval. There is no venture capital dictating the user experience. There is no CEO to sign off on the next feature release.
|
||||
|
||||
There is only you.
|
||||
|
||||
The barrier to entry is no longer complexity: It is the mere habit of dependency. You were conditioned to wait. Wait for the app update. Wait for the ISP to fix the line. Wait for the platform to allow the post. Wait for the government to change the policies. Wait for the likes. Wait for the revolution to be televised.
|
||||
|
||||
The revolution never was televised.
|
||||
|
||||
It is packetized.
|
||||
|
||||
### Open Sky
|
||||
|
||||
The future of this technology is a construction project.
|
||||
|
||||
It looks like a single node on a windowsill, listening to the static. It looks like a message sent to a neighbor, bypassing the noise of the commercial web. It looks like a community mesh that grows, link by link, hop by hop, carried by hands that care more about connection than profit.
|
||||
|
||||
You have the blueprints. You have the tools. You have the philosophy. The noise of the old world has fallen away, leaving you with the quiet clarity of the open spectrum.
|
||||
|
||||
*Mark, early 2026*
|
||||
@@ -35,3 +35,10 @@ help:
|
||||
cp -r build/epub/ReticulumNetworkStack.epub ./Reticulum\ Manual.epub; \
|
||||
echo "EPUB Manual Generated"; \
|
||||
fi
|
||||
|
||||
@if [ $@ = "markdown" ]; then \
|
||||
rm -rf markdown; \
|
||||
cp -r build/markdown ./; \
|
||||
./clean_md.py ./markdown \
|
||||
echo "Markdown Manual Generated"; \
|
||||
fi
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
LINE_START_PATTERNS = [
|
||||
r'<a\s+', # HTML anchor tags: <a id="..."></a>
|
||||
r'\\\\newpage', # LaTeX newpage commands
|
||||
]
|
||||
|
||||
LINE_ANY_PATTERNS = [
|
||||
# r'<br/>',
|
||||
# r'<div[^>]*>',
|
||||
# r'</div>',
|
||||
]
|
||||
|
||||
def compile_patterns():
|
||||
start_patterns = [re.compile(p) for p in LINE_START_PATTERNS]
|
||||
any_patterns = [re.compile(p) for p in LINE_ANY_PATTERNS]
|
||||
return start_patterns, any_patterns
|
||||
|
||||
|
||||
def should_remove_line(line, start_patterns, any_patterns):
|
||||
stripped = line.strip()
|
||||
|
||||
for pattern in start_patterns:
|
||||
if pattern.match(stripped):
|
||||
return True
|
||||
|
||||
for pattern in any_patterns:
|
||||
if pattern.search(stripped):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def clean_markdown_content(content, start_patterns, any_patterns, api_ref=False):
|
||||
content = content.replace("**\n : ", "**\n ")
|
||||
content = content.replace("\n* **", "\n\n* **")
|
||||
content = content.replace("\n\n\n", "\n\n")
|
||||
|
||||
lines = content.split('\n')
|
||||
result = []
|
||||
skip_next_empty = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if should_remove_line(line, start_patterns, any_patterns):
|
||||
skip_next_empty = True
|
||||
continue
|
||||
|
||||
if skip_next_empty:
|
||||
if line.strip() == '': continue
|
||||
else: skip_next_empty = False
|
||||
|
||||
if api_ref:
|
||||
if line.startswith("### ") or line.startswith("#### "):
|
||||
line = line.replace("*", "")
|
||||
line = line.replace("\\_", "_")
|
||||
if line.startswith("### "): line = line.replace("### ", "### `")
|
||||
if line.startswith("#### "): line = line.replace("#### ", "#### `")
|
||||
line = f"{line}`"
|
||||
|
||||
line = line.replace("<br/>", "")
|
||||
|
||||
result.append(line)
|
||||
|
||||
# Remove trailing empty lines from end of file
|
||||
while result and result[-1].strip() == '':
|
||||
result.pop()
|
||||
|
||||
return '\n'.join(result)
|
||||
|
||||
|
||||
def process_file(filepath, start_patterns, any_patterns):
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
original_content = f.read()
|
||||
|
||||
api_ref = str(filepath) == "markdown/reference.md"
|
||||
cleaned_content = clean_markdown_content(original_content, start_patterns, any_patterns, api_ref=api_ref)
|
||||
|
||||
if cleaned_content != original_content:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(cleaned_content)
|
||||
return True
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing {filepath}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def find_markdown_files(directory):
|
||||
md_files = []
|
||||
for root, _, files in os.walk(directory):
|
||||
for filename in files:
|
||||
if filename.endswith('.md'): md_files.append(Path(root) / filename)
|
||||
|
||||
return md_files
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python clean_markdown.py <directory_path>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
directory = sys.argv[1]
|
||||
|
||||
if not os.path.isdir(directory):
|
||||
print(f"Error: '{directory}' is not a valid directory", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
start_patterns, any_patterns = compile_patterns()
|
||||
md_files = find_markdown_files(directory)
|
||||
|
||||
if not md_files:
|
||||
print(f"No markdown files found in '{directory}'")
|
||||
return
|
||||
|
||||
modified_count = 0
|
||||
for filepath in md_files:
|
||||
if process_file(filepath, start_patterns, any_patterns):
|
||||
print(f"Cleaned: {filepath}")
|
||||
modified_count += 1
|
||||
|
||||
print(f"\nProcessed {len(md_files)} file(s), modified {modified_count}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,4 +1,4 @@
|
||||
# Sphinx build info version 1
|
||||
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: b259ec0dae0fa8339c3fe8ae4c431f72
|
||||
config: af83acf0d95a344cb0386ad87adf5695
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -0,0 +1,148 @@
|
||||
.. _distributed-development:
|
||||
|
||||
***********************
|
||||
Distributed Development
|
||||
***********************
|
||||
|
||||
This chapter of the manual provides the conceptual basis for understanding *why* ``rngit`` exists, what it aims to achieve, and the kinds of spaces it seeks to reestablish. For the practical details of operating the system, refer to the :ref:`Git Over Reticulum<git-main>` chapter.
|
||||
|
||||
|
||||
The Original Architecture
|
||||
=========================
|
||||
|
||||
When Torvalds created Git in 2005, he designed a tool that reflected a specific philosophy of collaboration. Every copy of a repository would be a complete, sovereign instance. There was no central server, no single point of failure, no gatekeeper. Developers would be able to work independently, exchange patches directly, and maintain their own branches indefinitely. This concept was - and is - both beautiful and revolutionary. It's execution is peer-to-peer not as a marketing term, but in the most foundational sense: As fundamental, structural reality.
|
||||
|
||||
Such a design emerged from necessity. The Linux kernel development process operated across geographical boundaries, time zones, and organizational affiliations. Contributors did not "log in" to a shared server to do their work; they maintained their own trees, and the flow of code between these trees was negotiated through patches, reviews, and merge decisions. The architecture of Git mirrored the social architecture of the community: Autonomous, competent, and fundamentally distributed in its technical operation.
|
||||
|
||||
*The result of that work is, in the most direct sense, what makes it possible for you to read this today.*
|
||||
|
||||
There's something very important to take note of here: With Git, developers could collaborate effectively and perfectly well without any central server being present, without platform-mediated visibility into each other's work, and without a centralized authority validating their contributions. They needed *only* a protocol for exchanging differences and a mechanism for verification of authorship. Everything else - social organization, quality control, release management - was handled by careful *human judgment* operating on top of the technical substrate.
|
||||
|
||||
What Git provided was not a development environment, but a **language for versioning**. It specified how to represent history, how to compute differences, how to merge divergent branches. It did not specify who could participate, how they should communicate, or what workflows they should follow. These were left to the competence and discretion of the creators using the system.
|
||||
|
||||
The Platform Interregnum
|
||||
========================
|
||||
|
||||
What followed represents a very familiar pattern: Tools designed to distribute power were re-centralized by platforms that offered convenience in exchange for control. GitHub, GitLab, and similar services reintroduced the centralization that Git had eliminated architecturally. The activity feed replaced durable artifacts with ephemeral notifications. The social graph and open interaction became as important as the code itself, if not more.
|
||||
|
||||
This re-centralization was not technical, as such. It was **ontological**. When every developer pushes to the same server, when every merge is in theory controllable by a platform, when every issue is tracked in a database controlled by a corporation, the nature of collaboration changes. The platform, and its social dynamics, becomes the ground of reality. The platform mediates not just the technical exchange of information and the programmatics, but the social recognition and codices of contribution, the future archival prospects of the work, and the very identity of the project itself.
|
||||
|
||||
The consequences extend beyond individual inconvenience. Centralized platforms create single points of failure for entire ecosystem. When a platform changes its terms of service, suspends accounts, removes repositories or ceases operation, entire project histories and community relationships can be disrupted or destroyed. The extractive economics of platform capitalism mean that value created by open-source communities is captured by corporations, while communities remain dependent on infrastructure they do not control. And the surveillance inherent in platform operation means that every action - every commit, every comment, every page view - is logged, analyzed, and potentially monetized or weaponized.
|
||||
|
||||
More insidiously, platforms have completely reshaped the culture of development itself. They have created what we could call the **Teahouse Developer**: A participant who treats engineering projects as social venues for opinion-sharing rather than sites of disciplined and careful production. These personages have no actual stakes in the projects they act as leeches upon, and only a very base consciousness of the damage they are incurring in order to feed their attention and external validation dependencies.
|
||||
|
||||
When platforms optimize for engagement, when growth is the only metric, when every user with an opinion must have their voice heard, when a random social process is elevated to higher importance than results, the signal-to-noise ratio collapses catastrophically. Competent engineers find themselves drowning in feedback from the incompetent, managing the emotional needs and dysregulations of drive-by commentators rather than solving technical problems.
|
||||
|
||||
The platform model is predicated on **unsaturable expansion**. Like almost any industrial system, it cannot function without growth. It pursues no particular aims; it is growth for the sake of growing. There is no saturation point, no concept of "enough". Every barrier to entry must be put down to the very lowest common denominator, every voice must be amplified, every interaction must be converted into content that feeds the machine. This is fundamentally incompatible with the nature of social beings itself. It is also incompatible with serious engineering, which requires focus, discernment, and the right of people who know better to say "no".
|
||||
|
||||
Restoration
|
||||
===========
|
||||
|
||||
The ``rngit`` system represents a return to Git's original architectural principles, fortified with cryptographic networking capabilities that were not available in 2005. The ``rngit`` system *is* Git - but running over Reticulum. Welcome back to a world where your work is your own, but where everyone can still reach you - if you want them to.
|
||||
|
||||
Just as Git eliminated the need for a central version control server, ``rngit`` eliminates the need for a central hosting platform, "servers" or any kinds of middle-men between the people actually doing the work. By operating over Reticulum, it eliminates the visibility of development activity to platform operators, network observers, state actors and other malicious third-parties.
|
||||
|
||||
In this model, the repository node is a **sovereign entity**. It is reachable from anywhere in the Reticulum network but owned, operated, and controlled by the developer or community that runs it. It is an actual home for creative output, not an extraction mechanism to which dues are paid. The node operator decides who may contribute, what standards must be met, and which voices are worth listening to. This is not exclusion; it is **discernment**. It is the necessary exercise of judgment that separates engineering from theatrics.
|
||||
|
||||
I did not create this in a fit of nostalgia. I created it because it is a necessary response to the failures of the centralized model. Git's technical architecture was - and *is* - correct. It was the social and economic superstructure built atop it that introduced fragility, exploitation, and environments toxic to actual creativity. By returning to first principles - distributed version control on distributed infrastructure - we recover not just a technical capability, but a mode of collaboration that respects the autonomy of individual developers and the sovereignty of actual communities.
|
||||
|
||||
|
||||
Protocols Over Platforms
|
||||
========================
|
||||
|
||||
The distinction between platforms and protocols is fundamental to understanding the architecture of sovereignty in networked systems. A platform is a service you access; a protocol is a grammar you speak; actions you live. A platform requires permission to enter, a protocol requires only *comprehension* to employ. A platform can change its rules, suspend your account, or cease operation entirely, a protocol persists as long as there are participants who *understand* and *use* it. A protocol is an *idea*, a platform is a machine that turns its users into products.
|
||||
|
||||
Platforms operate on a client-server model that inherently creates power asymmetry. Even when platforms are built atop open-source software, the operational instance remains a black box of corporate control. You *may* be able to download *some* of your data, but you cannot download the connections to the people that are the true value-base of the platform, or take them with you if you want to leave.
|
||||
|
||||
Protocols, by contrast, are agreements. They specify how systems should communicate, but not who may communicate or on what terms. Email is a protocol; Gmail is a platform. HTTP is a protocol; Facebook is a platform. Git is a protocol; GitHub is a platform. The protocol persists regardless of any particular implementation's success or failure.
|
||||
|
||||
The power of protocols lies in their **permissionlessness**. Anyone can implement a protocol without approval. Anyone can extend it, fork it, or use it for purposes unforeseen by its creators. This creates resilience: protocols cannot be easily censored, monopolized, or shut down because they exist as shared understanding rather than centralized infrastructure.
|
||||
|
||||
Reticulum is a protocol in this strict sense. It specifies how packets should be formatted, how paths should be discovered, how encryption should be applied. The ``rngit`` system extends this protocol approach to development workflows. It is not an external platform that hosts your repositories; it is a protocol for exchanging repository data, release artifacts, and work documents over Reticulum's encrypted transport. But with a few commands and an old computer, it creates your own infrastructure for hosting repositories, or sharing them with who you choose. *That* is how tools should function, in case we had forgotten.
|
||||
|
||||
Unlike platforms, which extract value by creating dependency, there is no entity that can grant or deny you the privilege of running ``rngit``. Your Reticulum identity is not endowed by any platform; it is generated locally and certified by its own cryptographic properties. Your repositories are hosted on nodes you control or nodes operated by communities you trust. Your relationships with other developers are peer-to-peer connections established through cryptographic addressing, not social graph connections managed by recommendation algorithms.
|
||||
|
||||
On a platform, exit means abandonment: you lose your history, your relationships, your visibility. With protocols, exit is just migration. When you change your infrastructure, your identity and your work travel with you. There are no middlemen between you and your collaborators. If push comes to shove, you can write your entire life's work and connections to an SD card, swim across the lake, and set up camp on the other side.
|
||||
|
||||
|
||||
Sovereignty Through Infrastructure
|
||||
==================================
|
||||
|
||||
The concept of sovereignty - supreme authority within a territory - has traditionally been applied to nation-states. But in an age where creative work is conducted through digital infrastructure, sovereignty is essential for individuals and communities. **Creative sovereignty** means having supreme authority over the artifacts you produce, the processes by which you produce them, and the terms under which they are distributed. It means not merely legal ownership of copyright, but operational control of the infrastructure that mediates creation, collaboration, and dissemination.
|
||||
|
||||
Centralized development platforms strip away most layers of sovereignty. When you host code on a corporate platform, you retain *some* legal ownership of copyright, but you surrender complete operational control. The platform decides what content is acceptable, who can access it, and how it is presented. They can delete your repository, suspend your account, or change the visibility of your work without consent. In reality, legal ownership becomes meaningless as operational control is ceded.
|
||||
|
||||
Running your own ``rngit`` node restores this sovereignty. You control the hardware, the network configuration, the backup strategies, and the access permissions. You decide what constitutes acceptable use, who may contribute, and how contributions are evaluated. Taking this responsibility on yourself is an assertion that your creative work is not a product to be harvested by platform economics, but an autonomous activity to be conducted on your own terms.
|
||||
|
||||
This sovereignty and responsibility extends to the entry barriers you establish. The ``rngit`` system allows you to configure access controls that filter participants based on cryptographic identity and demonstrated competence. If, for example, someone cannot navigate a command line, or use Reticulum to submit a patch, they most likely lack the required competence to modify your code. In a world that apparently labels this as "exclusion", I would simply refer to it as a minimally acceptable level of quality control.
|
||||
|
||||
Such a stance protects projects from the noise that so often overwhelms and completely dilutes platform-based development, where every user with an opinion believes themselves entitled to attention and access to the decision process.
|
||||
|
||||
|
||||
Artifact-Centered Workflows
|
||||
===========================
|
||||
|
||||
Contemporary platform-based development has shifted focus from durable artifacts to ephemeral *activity*. It does not matter what constitutes this activity, as long as it's there. The primary interface is not the repository itself, not the produced artifacts, but the activity feed: *Notifications* of commits, comments, pull requests, and social interactions. Work is measured by velocity, throughput, and the constant stream of updates. This activity-centric model creates constant urgency, discourages discernment, encourages reactive rather than reflective work patterns, and produces vast quantities of ephemeral and useless communication that obscures actual project state and productivity.
|
||||
|
||||
The ``rngit`` system enables a return to **artifact-centered workflows**, where the focus is on durable, attributable, versioned outputs rather than the stream of notifications surrounding them. The fundamental unit of work is the commit - signed, immutable records of change. The fundamental unit of production is the signed artifact - a self-verifying package of functionality. The fundamental unit of discussion is the work document - a structured, threaded conversation attached to repositories.
|
||||
|
||||
Artifacts can persist independently of any platform's continued operation. A commit signed with your Reticulum identity is attributable to you regardless of where it is stored. A release signed with your private key is verifiable as authentic regardless of which network it traverses, and can be verified offline on any system running Reticulum. The work exists as **cryptographic fact**, distributed over the planet, not as database entries in a corporate cloud.
|
||||
|
||||
Such a shift has real psychological consequences. When work is measured in artifacts rather than activity, the pace changes. There is no need for constant visibility, no pressure to perform busyness. Developers can work deeply, reflectively, and submit complete solutions rather than incremental updates designed to maintain presence in an activity feed. The work becomes **substantial**, in the physical sense of the word, rather than performative.
|
||||
|
||||
Composable Primitives
|
||||
=====================
|
||||
|
||||
The ``rngit`` system is not a monolithic application prescribing a specific workflow; it is a collection of **composable primitives** that can be arranged to support diverse creative processes. Understanding these primitives as separate, orthogonal capabilities enables users to construct workflows suited to their specific needs and to recombine these primitives in ways unforeseen by the system's designers.
|
||||
|
||||
The core primitives include:
|
||||
|
||||
* **Repository Hosting**: Bare Git repositories served over Reticulum links, accessible via standard Git commands through the ``rns://`` URL scheme.
|
||||
* **Identity-Based Access Control**: Fine-grained permissions managed through cryptographically verifiable identity hashes, configurable at the group, repository, or document level.
|
||||
* **Release Distribution**: Cryptographically signed release artifacts with embedded provenance information, verifiable offline and distributable through any Reticulum or physical path.
|
||||
* **Work Document Tracking**: Structured, threaded work management attached to repositories, with precise permission controls, and the ability to contain updates or discussions.
|
||||
* **Forking and Mirroring**: Automated replication of repositories from any accessible Git URL, with metadata tracking upstream relationships for synchronization.
|
||||
* **Nomad Network Integration**: Page node functionality for browsing repository contents, commit history, and release information through the Nomad Network protocol.
|
||||
|
||||
These primitives can be composed into workflows ranging from single-developer projects to complex multi-organizational collaborations. A solo developer might use only repository hosting and release distribution. A research group might add work document tracking for structured peer review. A software distribution network might combine mirroring with cryptographic release verification to create resilient update channels.
|
||||
|
||||
The entire system is incredibly light-weight, and can host hundreds of repositories on a Raspberry Pi.
|
||||
|
||||
Composability is essential because **creative work is diverse**. Software development, academic research, technical writing, hardware design, music production and data analysis all have different requirements for collaboration, review, and distribution. A platform prescribes a single workflow and forces all users to conform. A protocol provides primitives and allows users to construct workflows appropriate to their domain.
|
||||
|
||||
With ``rngit``, you can re-build the system into anything you can imagine. Everything can be modified, extended and hooked into. Adding functionality or automation is never further away than a shell script, a cron-job, or a Python modification of the source.
|
||||
|
||||
Distribution Without Intermediaries
|
||||
===================================
|
||||
|
||||
Creating software is only part of the work. Then comes actually getting it to the people needing to use it. Centralized platforms handle distribution through their own infrastructure: Content delivery networks, central package registries, and download servers accessed through platform-controlled interfaces. This convenience masks a fundamental dependency: Your ability to distribute depends on the platform's continued operation, their policies regarding your content, and their technical infrastructure's reach.
|
||||
|
||||
The ``rngit`` release system enables distribution strategies **decoupled from any single infrastructure provider**. Releases are cryptographically signed using Ed25519 signatures and packaged in signed release manifests (``.rsm`` files). These manifests contain embedded signatures for each artifact. The manifest provides full verifiability of all release information, and contains embedded release artifact lists, per-file ``.rsg`` signatures, origin information, and the creator's Reticulum Identity. It can also be used to fetch verified updates of the software package over the network, and can always be verified completely offline.
|
||||
|
||||
Because releases are self-verifying, they can traverse any network or physical path that Reticulum can establish. A release can travel over LoRa radio, be carried on USB drives through areas without internet connectivity, disseminated over a mirror network, or be distributed through store-and-forward mechanisms on intermittent infrastructure. Recipients can verify authenticity regardless of how they obtained the files. This is particularly valuable in low-connectivity environments where Reticulum may be the only available communication channel.
|
||||
|
||||
The ``rngit release`` command provides tools for creating, publishing, fetching, and verifying releases. When fetching a release using an ``.rsm`` manifest, the system validates the manifest signature against the required Reticulum Identity, extracts the origin node and repository path, connects to the origin over Reticulum, retrieves the latest release manifest, and verifies each downloaded artifact against the signatures embedded in the manifest. If any verification fails, the fetch aborts, preventing installation of corrupted or tampered files.
|
||||
|
||||
This cryptographic verification replaces the trust model of platform distribution. Instead of trusting that a platform has not been compromised, users verify that artifacts match the signatures created by the developer's identity. It doesn't matter *how* they obtained the artifacts, they can **always** be verified. This security model shifts from **institutional trust** (just believe in the goodness of the platform) to **cryptographic proof** (verify the signatures).
|
||||
|
||||
Long Archive
|
||||
============
|
||||
|
||||
Software development is often conceived as an activity of the present only: Solving today's problems, meeting current deadlines, responding to immediate feedback. But the artifacts produced - code, documentation, releases - have lifespans extending *far* beyond their creation. They may be used for decades, studied by future developers, depended upon by systems not yet imagined, or preserved as historical records of technological development.
|
||||
|
||||
The ``rngit`` system is designed with this **extended timeframe** in mind, supporting the creation of archives that are durable, portable, and intelligible across generational timescales. Git repositories are always internally complete; they contain full history and can be migrated to new infrastructure without loss of information. Everything that ``rngit`` adds on top of this is stored in normal files in standard formats right next to the Git repository folders, not an esoteric database-cluster two thousand kilometers away. Because releases are cryptographically signed, they remain verifiable as authentic regardless of when or where they are retrieved. Because the system operates over Reticulum, it can function over communication mediums that may outlast the internet as we know it.
|
||||
|
||||
This long-term perspective influences technical decisions. The use of well-established cryptographic primitives ensures that signatures will remain verifiable for centuries. The use of standard formats ensures that repositories will remain readable by future tools. The protocol-based architecture ensures that the system can evolve without losing compatibility with existing data.
|
||||
|
||||
For critical infrastructure, this archival durability is not optional; it is essential. Communication systems, cryptographic libraries, and safety-critical code must remain available and verifiable for the lifespans of the systems that depend on them. The ``rngit`` system provides the tools to create such archives: distributed across multiple nodes, cryptographically verified, and independent of any corporate or governmental infrastructure, which as history has shown repeatedly, does *not* persist.
|
||||
|
||||
Start Of The Road
|
||||
=================
|
||||
|
||||
Distributed development and production over Reticulum is a *different mode of existence* for creative work. It restores the autonomy originally created by Git. It provides local sovereignty over production infrastructure, composability of workflow, and durability of artifact. It lets you filter participation through competence and cryptography rather than incentives of platform operators, raising the quality and enjoyment of work, and protecting the focus of real engineering and creative expression.
|
||||
|
||||
This is not a system for everyone, and that is the point. It requires investment - in understanding Reticulum, in configuring infrastructure, in establishing workflows. It requires accepting responsibility for your own tools rather than delegating them to platform operators. It requires the discipline to maintain your own node, manage your own backups, and nurture your own community.
|
||||
|
||||
But for those who make this investment, the returns are substantial. You gain **immunity from platform failure**; your work persists regardless of corporate decisions or service outages. You gain **shelter from surveillance**; your development activity is visible only to those that *you* choose to involve. You gain **control over process**; you decide how work is conducted, reviewed, and released, unmediated by terms of service, algorithmic feeds and thousands of uninformed and irrelevant opinions.
|
||||
|
||||
Most importantly, though, you regain the **dignity of craft**. Development becomes an activity conducted among peers, equals among equals, mediated by skill and cryptographic proof rather than corporate permission, producing artifacts that stand as independent testimony to competence, functionality or beauty rather than as content feeding engagement metrics. The *work* becomes the point. The artifacts become durable. And the network becomes *one* of the tools you wield in this endeavor.
|
||||
@@ -4,18 +4,14 @@
|
||||
Code Examples
|
||||
*************
|
||||
|
||||
A number of examples are included in the source distribution of Reticulum.
|
||||
You can use these examples to learn how to write your own programs.
|
||||
A number of examples are included in the source distribution of Reticulum. You can use these examples to learn how to write your own programs.
|
||||
|
||||
.. _example-minimal:
|
||||
|
||||
Minimal
|
||||
=======
|
||||
|
||||
The *Minimal* example demonstrates the bare-minimum setup required to connect to
|
||||
a Reticulum network from your program. In about five lines of code, you will
|
||||
have the Reticulum Network Stack initialised, and ready to pass traffic in your
|
||||
program.
|
||||
The *Minimal* example demonstrates the bare-minimum setup required to connect to a Reticulum network from your program. In about five lines of code, you will have the Reticulum Network Stack initialised, and ready to pass traffic in your program.
|
||||
|
||||
.. literalinclude:: ../../Examples/Minimal.py
|
||||
|
||||
@@ -26,9 +22,7 @@ This example can also be found at `<https://github.com/markqvist/Reticulum/blob/
|
||||
Announce
|
||||
========
|
||||
|
||||
The *Announce* example builds upon the previous example by exploring how to
|
||||
announce a destination on the network, and how to let your program receive
|
||||
notifications about announces from relevant destinations.
|
||||
The *Announce* example builds upon the previous example by exploring how to announce a destination on the network, and how to let your program receive notifications about announces from relevant destinations.
|
||||
|
||||
.. literalinclude:: ../../Examples/Announce.py
|
||||
|
||||
@@ -38,8 +32,7 @@ This example can also be found at `<https://github.com/markqvist/Reticulum/blob/
|
||||
|
||||
Broadcast
|
||||
=========
|
||||
The *Broadcast* example explores how to transmit plaintext broadcast messages
|
||||
over the network.
|
||||
The *Broadcast* example explores how to transmit plaintext broadcast messages over the network.
|
||||
|
||||
.. literalinclude:: ../../Examples/Broadcast.py
|
||||
|
||||
@@ -50,8 +43,7 @@ This example can also be found at `<https://github.com/markqvist/Reticulum/blob/
|
||||
Echo
|
||||
====
|
||||
|
||||
The *Echo* example demonstrates communication between two destinations using
|
||||
the Packet interface.
|
||||
The *Echo* example demonstrates communication between two destinations using the Packet interface.
|
||||
|
||||
.. literalinclude:: ../../Examples/Echo.py
|
||||
|
||||
@@ -62,8 +54,7 @@ This example can also be found at `<https://github.com/markqvist/Reticulum/blob/
|
||||
Link
|
||||
====
|
||||
|
||||
The *Link* example explores establishing an encrypted link to a remote
|
||||
destination, and passing traffic back and forth over the link.
|
||||
The *Link* example explores establishing an encrypted link to a remote destination, and passing traffic back and forth over the link.
|
||||
|
||||
.. literalinclude:: ../../Examples/Link.py
|
||||
|
||||
@@ -74,8 +65,7 @@ This example can also be found at `<https://github.com/markqvist/Reticulum/blob/
|
||||
Identification
|
||||
==============
|
||||
|
||||
The *Identify* example explores identifying an intiator of a link, once
|
||||
the link has been established.
|
||||
The *Identify* example explores identifying an intiator of a link, once the link has been established.
|
||||
|
||||
.. literalinclude:: ../../Examples/Identify.py
|
||||
|
||||
@@ -97,8 +87,7 @@ This example can also be found at `<https://github.com/markqvist/Reticulum/blob/
|
||||
Channel
|
||||
=======
|
||||
|
||||
The *Channel* example explores using a ``Channel`` to send structured
|
||||
data between peers of a ``Link``.
|
||||
The *Channel* example explores using a ``Channel`` to send structured data between peers of a ``Link``.
|
||||
|
||||
.. literalinclude:: ../../Examples/Channel.py
|
||||
|
||||
@@ -107,8 +96,7 @@ This example can also be found at `<https://github.com/markqvist/Reticulum/blob/
|
||||
Buffer
|
||||
======
|
||||
|
||||
The *Buffer* example explores using buffered readers and writers to send
|
||||
binary data between peers of a ``Link``.
|
||||
The *Buffer* example explores using buffered readers and writers to send binary data between peers of a ``Link``.
|
||||
|
||||
.. literalinclude:: ../../Examples/Buffer.py
|
||||
|
||||
@@ -119,9 +107,7 @@ This example can also be found at `<https://github.com/markqvist/Reticulum/blob/
|
||||
Filetransfer
|
||||
============
|
||||
|
||||
The *Filetransfer* example implements a basic file-server program that
|
||||
allow clients to connect and download files. The program uses the Resource
|
||||
interface to efficiently pass files of any size over a Reticulum :ref:`Link<api-link>`.
|
||||
The *Filetransfer* example implements a basic file-server program that allow clients to connect and download files. The program uses the Resource interface to efficiently pass files of any size over a Reticulum :ref:`Link<api-link>`.
|
||||
|
||||
.. literalinclude:: ../../Examples/Filetransfer.py
|
||||
|
||||
@@ -132,10 +118,7 @@ This example can also be found at `<https://github.com/markqvist/Reticulum/blob/
|
||||
Custom Interfaces
|
||||
=================
|
||||
|
||||
The *ExampleInterface* demonstrates creating custom interfaces for Reticulum.
|
||||
Any number of custom interfaces can be loaded and utilised by Reticulum, and
|
||||
will be fully on-par with natively included interfaces, including all supported
|
||||
:ref:`interface modes<interfaces-modes>` and :ref:`common configuration options<interfaces-options>`.
|
||||
The *ExampleInterface* demonstrates creating custom interfaces for Reticulum. Any number of custom interfaces can be loaded and utilised by Reticulum, and will be fully on-par with natively included interfaces, including all supported :ref:`interface modes<interfaces-modes>` and :ref:`common configuration options<interfaces-options>`.
|
||||
|
||||
.. literalinclude:: ../../Examples/ExampleInterface.py
|
||||
|
||||
|
||||
@@ -2,51 +2,38 @@
|
||||
Getting Started Fast
|
||||
********************
|
||||
|
||||
The best way to get started with the Reticulum Network Stack depends on what
|
||||
you want to do. This guide will outline sensible starting paths for different
|
||||
scenarios.
|
||||
The best way to get started with the Reticulum Network Stack depends on what you want to do. This guide will outline sensible starting paths for different scenarios.
|
||||
|
||||
|
||||
Standalone Reticulum Installation
|
||||
=============================================
|
||||
If you simply want to install Reticulum and related utilities on a system,
|
||||
the easiest way is via the ``pip`` package manager:
|
||||
=================================
|
||||
If you simply want to install Reticulum and related utilities on a system, the easiest way is via the ``pip`` package manager:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
pip install rns
|
||||
|
||||
If you do not already have pip installed, you can install it using the package manager
|
||||
of your system with a command like ``sudo apt install python3-pip``,
|
||||
``sudo pamac install python-pip`` or similar.
|
||||
If you do not already have pip installed, you can install it using the package manager of your system with a command like ``sudo apt install python3-pip``, ``sudo pamac install python-pip`` or similar.
|
||||
|
||||
You can also dowload the Reticulum release wheels from GitHub, or other release channels,
|
||||
and install them offline using ``pip``:
|
||||
You can also dowload the Reticulum release wheels from GitHub, or other release channels, and install them offline using ``pip``:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
pip install ./rns-1.0.2-py3-none-any.whl
|
||||
pip install ./rns-1.1.2-py3-none-any.whl
|
||||
|
||||
On platforms that limit user package installation via ``pip``, you may need to manually
|
||||
allow this using the ``--break-system-packages`` command line flag when installing. This
|
||||
will not actually break any packages, unless you have installed Reticulum directly via
|
||||
your operating system's package manager.
|
||||
On platforms that limit user package installation via ``pip``, you may need to manually allow this using the ``--break-system-packages`` command line flag when installing. This will not actually break any packages, unless you have installed Reticulum directly via your operating system's package manager.
|
||||
|
||||
.. code:: shell
|
||||
|
||||
pip install rns --break-system-packages
|
||||
|
||||
For more detailed installation instructions, please see the
|
||||
:ref:`Platform-Specific Install Notes<install-guides>` section.
|
||||
For more detailed installation instructions, please see the :ref:`Platform-Specific Install Notes<install-guides>` section.
|
||||
|
||||
After installation is complete, it might be helpful to refer to the
|
||||
:ref:`Using Reticulum on Your System<using-main>` chapter.
|
||||
After installation is complete, it might be helpful to refer to the :ref:`Using Reticulum on Your System<using-main>` chapter.
|
||||
|
||||
Resolving Dependency & Installation Issues
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On some platforms, there may not be binary packages available for all dependencies, and
|
||||
``pip`` installation may fail with an error message. In these cases, the issue can usually
|
||||
be resolved by installing the development essentials packages for your platform:
|
||||
On some platforms, there may not be binary packages available for all dependencies, and ``pip`` installation may fail with an error message. In these cases, the issue can usually be resolved by installing the development essentials packages for your platform:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
@@ -59,259 +46,139 @@ be resolved by installing the development essentials packages for your platform:
|
||||
# Fedora
|
||||
sudo dnf groupinstall "Development Tools" "Development Libraries"
|
||||
|
||||
With the base development packages installed, ``pip`` should be able to compile any missing
|
||||
dependencies from source, and complete installation even on platforms that don't have pre-
|
||||
compiled packages available.
|
||||
With the base development packages installed, ``pip`` should be able to compile any missing dependencies from source, and complete installation even on platforms that don't have pre-compiled packages available.
|
||||
|
||||
Try Using a Reticulum-based Program
|
||||
=============================================
|
||||
===================================
|
||||
|
||||
If you simply want to try using a program built with Reticulum, a few different
|
||||
programs exist that allow basic communication and a range of other useful functions,
|
||||
even over extremely low-bandwidth Reticulum networks.
|
||||
If you simply want to try using a program built with Reticulum, a :ref:`range of different programs <software-main>` exist that allow basic communication and a various other useful functions, even over extremely low-bandwidth Reticulum networks.
|
||||
|
||||
These programs will let you get a feel for how Reticulum works. They have been designed
|
||||
to run well over networks based on LoRa or packet radio, but can also be used over fast
|
||||
links, such as local WiFi, wired Ethernet, the Internet, or any combination.
|
||||
|
||||
As such, it is easy to get started experimenting, without having to set up any radio
|
||||
transceivers or infrastructure just to try it out. Launching the programs on separate
|
||||
devices connected to the same WiFi network is enough to get started, and physical
|
||||
radio interfaces can then be added later.
|
||||
|
||||
Remote Shell
|
||||
^^^^^^^^^^^^
|
||||
|
||||
The `rnsh <https://github.com/acehoss/rnsh>`_ program lets you establish fully interactive
|
||||
remote shell sessions over Reticulum. It also allows you to pipe any program to or from a
|
||||
remote system, and is similar to how ``ssh`` works. The ``rnsh`` is very efficient, and
|
||||
can facilitate fully interactive shell sessions, even over extremely low-bandwidth links,
|
||||
such as LoRa or packet radio.
|
||||
|
||||
Nomad Network
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
The terminal-based program `Nomad Network <https://github.com/markqvist/nomadnet>`_
|
||||
provides a complete encrypted communications suite built with Reticulum. It features
|
||||
encrypted messaging (both direct and delayed-delivery for offline users), file sharing,
|
||||
and has a built-in text-browser and page server with support for dynamically rendered pages,
|
||||
user authentication and more.
|
||||
|
||||
.. image:: screenshots/nomadnet_3.png
|
||||
:target: _images/nomadnet_3.png
|
||||
|
||||
`Nomad Network <https://github.com/markqvist/nomadnet>`_ is a user-facing client
|
||||
for the messaging and information-sharing protocol
|
||||
`LXMF <https://github.com/markqvist/lxmf>`_, another project built with Reticulum.
|
||||
|
||||
You can install Nomad Network via pip:
|
||||
|
||||
.. code::
|
||||
|
||||
# Install ...
|
||||
pip install nomadnet
|
||||
|
||||
# ... and run
|
||||
nomadnet
|
||||
|
||||
.. note::
|
||||
If this is the very first time you use ``pip`` to install a program
|
||||
on your system, you might need to reboot your system for your program to become
|
||||
available. If you get a "command not found" error or similar when running the
|
||||
program, reboot your system and try again. In some cases, you may even need to
|
||||
manually add the ``pip`` install path to your ``PATH`` environment variable.
|
||||
|
||||
Sideband
|
||||
^^^^^^^^
|
||||
|
||||
If you would rather use a program with a graphical user interface, you can take
|
||||
a look at `Sideband <https://unsigned.io/sideband>`_, which is available for Android,
|
||||
Linux, macOS and Windows.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/sideband_devices.webp
|
||||
:align: center
|
||||
:target: _images/sideband_devices.webp
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/sideband_devices.png
|
||||
:align: center
|
||||
:target: _images/sideband_devices.png
|
||||
|
||||
Sideband allows you to communicate with other people or LXMF-compatible
|
||||
systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR
|
||||
Paper Messages, or anything else Reticulum supports. It also interoperates with
|
||||
the Nomad Network program.
|
||||
|
||||
MeshChat
|
||||
^^^^^^^^
|
||||
|
||||
The `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ application
|
||||
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
|
||||
page browser and other interesting functionality.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/meshchat_1.webp
|
||||
:align: center
|
||||
:target: _images/meshchat_1.webp
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/meshchat_1.png
|
||||
:align: center
|
||||
:target: _images/meshchat_1.png
|
||||
|
||||
Reticulum MeshChat is of course also compatible with Sideband and Nomad Network, or
|
||||
any other LXMF client.
|
||||
|
||||
Using the Included Utilities
|
||||
=============================================
|
||||
Reticulum comes with a range of included utilities that make it easier to
|
||||
manage your network, check connectivity and make Reticulum available to other
|
||||
programs on your system.
|
||||
============================
|
||||
Reticulum comes with a range of included utilities that make it easier to manage your network, check connectivity and make Reticulum available to other programs on your system.
|
||||
|
||||
You can use ``rnsd`` to run Reticulum as a background or foreground service,
|
||||
and the ``rnstatus``, ``rnpath`` and ``rnprobe`` utilities to view and query
|
||||
network status and connectivity.
|
||||
You can use ``rnsd`` to run Reticulum as a background or foreground service, and the ``rnstatus``, ``rnpath`` and ``rnprobe`` utilities to view and query network status and connectivity.
|
||||
|
||||
To learn more about these utility programs, have a look at the
|
||||
:ref:`Using Reticulum on Your System<using-main>` chapter of this manual.
|
||||
To learn more about these utility programs, have a look at the :ref:`Using Reticulum on Your System<using-main>` chapter of this manual.
|
||||
|
||||
|
||||
Creating a Network With Reticulum
|
||||
=============================================
|
||||
To create a network, you will need to specify one or more *interfaces* for
|
||||
Reticulum to use. This is done in the Reticulum configuration file, which by
|
||||
default is located at ``~/.reticulum/config``. You can get an example
|
||||
configuration file with all options via ``rnsd --exampleconfig``.
|
||||
=================================
|
||||
To create a network, you will need to specify one or more *interfaces* for Reticulum to use. This is done in the Reticulum configuration file, which by default is located at ``~/.reticulum/config``. You can get an example configuration file with all options via ``rnsd --exampleconfig``.
|
||||
|
||||
When Reticulum is started for the first time, it will create a default
|
||||
configuration file, with one active interface. This default interface uses
|
||||
your existing Ethernet and WiFi networks (if any), and only allows you to
|
||||
communicate with other Reticulum peers within your local broadcast domains.
|
||||
When Reticulum is started for the first time, it will create a default configuration file, with one active interface. This default interface uses your existing Ethernet and WiFi networks (if any), and only allows you to communicate with other Reticulum peers within your local broadcast domains.
|
||||
|
||||
To communicate further, you will have to add one or more interfaces. The default
|
||||
configuration includes a number of examples, ranging from using TCP over the
|
||||
internet, to LoRa and Packet Radio interfaces.
|
||||
To communicate further, you will have to add one or more interfaces. The default configuration includes a number of examples, ranging from using TCP over the internet, to LoRa and Packet Radio interfaces.
|
||||
|
||||
With Reticulum, you only need to configure what interfaces you want to communicate
|
||||
over. There is no need to configure address spaces, subnets, routing tables,
|
||||
or other things you might be used to from other network types.
|
||||
With Reticulum, you only need to configure what interfaces you want to communicate over. There is no need to configure address spaces, subnets, routing tables, or other things you might be used to from other network types.
|
||||
|
||||
Once Reticulum knows which interfaces it should use, it will automatically
|
||||
discover topography and configure transport of data to any destinations it
|
||||
knows about.
|
||||
Once Reticulum knows which interfaces it should use, it will automatically discover topography and configure transport of data to any destinations it knows about.
|
||||
|
||||
In situations where you already have an established WiFi or Ethernet network, and
|
||||
many devices that want to utilise the same external Reticulum network paths (for example over
|
||||
LoRa), it will often be sufficient to let one system act as a Reticulum gateway, by
|
||||
adding any external interfaces to the configuration of this system, and then enabling transport on it. Any
|
||||
other device on your local WiFi will then be able to connect to this wider Reticulum
|
||||
network just using the default (:ref:`AutoInterface<interfaces-auto>`) configuration.
|
||||
In situations where you already have an established WiFi or Ethernet network, and many devices that want to utilise the same external Reticulum network paths (for example over LoRa), it will often be sufficient to let one system act as a Reticulum gateway, by adding any external interfaces to the configuration of this system, and then enabling transport on it. Any other device on your local WiFi will then be able to connect to this wider Reticulum network just using the default (:ref:`AutoInterface<interfaces-auto>`) configuration.
|
||||
|
||||
Possibly, the examples in the config file are enough to get you started. If
|
||||
you want more information, you can read the :ref:`Building Networks<networks-main>`
|
||||
and :ref:`Interfaces<interfaces-main>` chapters of this manual.
|
||||
|
||||
Connecting Reticulum Instances Over the Internet
|
||||
================================================
|
||||
Reticulum currently offers three interfaces suitable for connecting instances over the Internet: :ref:`Backbone<interfaces-backbone>`, :ref:`TCP<interfaces-tcps>`
|
||||
and :ref:`I2P<interfaces-i2p>`. Each interface offers a different set of features, and Reticulum
|
||||
users should carefully choose the interface which best suites their needs.
|
||||
|
||||
The ``TCPServerInterface`` allows users to host an instance accessible over TCP/IP. This
|
||||
method is generally faster, lower latency, and more energy efficient than using ``I2PInterface``,
|
||||
however it also leaks more data about the server host.
|
||||
|
||||
The ``BackboneInterface`` is a very fast and efficient interface type available on POSIX operating
|
||||
systems, designed to handle many hundreds of connections simultaneously with low memory, processing
|
||||
and I/O overhead. It is fully compatible with the TCP-based interface types.
|
||||
|
||||
TCP connections reveal the IP address of both your instance and the server to anyone who can
|
||||
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
|
||||
inspecting your packets may be able to record packet metadata like time of transmission and packet size.
|
||||
Even though Reticulum encrypts traffic, TCP does not, so an adversary may be able to use
|
||||
packet inspection to learn that a system is running Reticulum, and what other IP addresses connect to it.
|
||||
Hosting a publicly reachable instance over TCP also requires a publicly reachable IP address,
|
||||
which most Internet connections don't offer anymore.
|
||||
|
||||
The ``I2PInterface`` routes messages through the `Invisible Internet Protocol
|
||||
(I2P) <https://geti2p.net/en/>`_. To use this interface, users must also run an I2P daemon in
|
||||
parallel to ``rnsd``. For always-on I2P nodes it is recommended to use `i2pd <https://i2pd.website/>`_.
|
||||
|
||||
By default, I2P will encrypt and mix all traffic sent over the Internet, and
|
||||
hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node
|
||||
will also relay other I2P user's encrypted packets, which will use extra
|
||||
bandwidth and compute power, but also makes timing attacks and other forms of
|
||||
deep-packet-inspection much more difficult.
|
||||
|
||||
I2P also allows users to host globally available Reticulum instances from non-public IP's and behind firewalls and NAT.
|
||||
|
||||
In general it is recommended to use an I2P node if you want to host a publicly accessible
|
||||
instance, while preserving anonymity. If you care more about performance, and a slightly
|
||||
easier setup, use TCP.
|
||||
Possibly, the examples in the config file are enough to get you started. If you want more information, you can read the :ref:`Building Networks<networks-main>` and :ref:`Interfaces<interfaces-main>` chapters of this manual, but most importantly, start with reading the next section, :ref:`Bootstrapping Connectivity<bootstrapping-connectivity>`, as this provides the most essential understanding of how to ensure reliable connectivity with a minimum of maintenance.
|
||||
|
||||
|
||||
Connect to the Public Testnet
|
||||
===========================================
|
||||
.. _bootstrapping-connectivity:
|
||||
|
||||
An experimental public testnet has been made accessible by volunteers in the community. You
|
||||
can find interface definitions for adding to your ``.reticulum/config`` file on the
|
||||
`Reticulum Website <https://reticulum.network/connect.html>`_ or the
|
||||
`Community Wiki <https://github.com/markqvist/Reticulum/wiki/Community-Node-List>`_
|
||||
Bootstrapping Connectivity
|
||||
==========================
|
||||
|
||||
You can connect your devices or instances to one or more of these to gain access to any
|
||||
Reticulum networks they are physically connected to. Simply add one or more interface
|
||||
snippets to your config file in the ``[interface]`` section, like in the example below:
|
||||
Reticulum is not a service you subscribe to, nor is it a single global network you "join". It is a *networking stack*; a toolkit for building communications systems that align with your specific values, requirements, and operational environment. The way you choose to connect to other Reticulum peers is entirely your own choice.
|
||||
|
||||
.. code:: ini
|
||||
One of the most powerful aspects of Reticulum is that it provides a multitude of tools to establish, maintain, and optimize connectivity. You can use these tools in isolation or combine them in complex configurations to achieve a vast array of goals.
|
||||
|
||||
# TCP/IP interface to the BetweenTheBorders Hub (community-provided)
|
||||
[[RNS Testnet BetweenTheBorders]]
|
||||
type = TCPClientInterface
|
||||
enabled = yes
|
||||
target_host = reticulum.betweentheborders.com
|
||||
target_port = 4242
|
||||
Whether your aim is to create a completely private, air-gapped network for your family; to build a resilient community mesh that survives infrastructure collapse; to connect far and wide to as many nodes as possible; or simply to maintain a reliable, encrypted link to a specific organization you care about, Reticulum provides the mechanisms to make it happen.
|
||||
|
||||
There is no "right" or "wrong" way to build a Reticulum network, and you don't need to be a network engineer just to get started. If the information flows in the way you intend, and your privacy and security requirements are met, your configuration is a success. Reticulum is designed to make the most challenging and difficult scenarios attainable, even when other networking technologies fail.
|
||||
|
||||
|
||||
Finding Your Way
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
When you first start using Reticulum, you need a way to obtain connectivity with the peers you want to communicate with - the process of *bootstrapping connectivity*.
|
||||
|
||||
.. important::
|
||||
|
||||
A common mistake in modern networking is the reliance on a few centralized, hard-coded entrypoints. If every user simply connects to the same list of public IP addresses found on a website, the network becomes brittle, centralized, and ultimately fails to deliver on the promise of decentralization and resilience. You have a responsibility here.
|
||||
|
||||
Reticulum encourages the approach of *organic growth*. Instead of relying on permanent static connections to distant servers, you can use temporary bootstrap connections to continously *discover* more relevant or local infrastructure. Once discovered, your system can automatically form stronger, more direct links to these peers, and discard the temporary bootstrap links. This results in a web of connections that are geographically relevant, resilient and efficient.
|
||||
|
||||
It *is* possible to simply add a few public entrypoints to the ``[interfaces]`` section of your Reticulum configuration and be connected, but a better option is to enable :ref:`interface discovery<using-interface_discovery>` and either manually select relevant, local interfaces, or enable discovered interface auto-connection.
|
||||
|
||||
A relevant option in this context is the :ref:`bootstrap only<interfaces-options>` interface option. This is an automated tool for better distributing connectivity. By enabling interface discovery and auto-connection, and marking an interface as ``bootstrap_only``, you tell Reticulum to use that interface primarliy to find connectivity options, and then disconnect it once sufficient entrypoints have been discovered. This helps create a network topology that favors locality and resilience over the simple centralization caused by using only a few static entrypoints.
|
||||
|
||||
Good places to find interface definitions for bootstrapping connectivity are websites like
|
||||
`directory.rns.recipes <https://directory.rns.recipes/>`_ and `rmap.world <https://rmap.world/>`_.
|
||||
|
||||
|
||||
Build Personal Infrastructure
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You do not need a datacenter to be a meaningful part of the Reticulum ecosystem. In fact, the most important nodes in the network are often the smallest ones.
|
||||
|
||||
We strongly encourage everyone, even home users, to think in terms of building **personal infrastructure**. Don't connect every phone, tablet, and computer in your house directly to a public internet gateway. Instead, repurpose an old computer, a Raspberry Pi, or a supported router to act as your own, personal **Transport Node**:
|
||||
|
||||
* Your local Transport Node sits in your home, connected to your WiFi and perhaps a radio interface (like an RNode).
|
||||
* You configure this node with a ``bootstrap_only`` interface (perhaps a TCP tunnel to a wider network) and enable interface discovery.
|
||||
* While you sleep, work, or cook, your node listens to the network. It discovers other local community members, validates their Network Identities, and automatically establishes direct links.
|
||||
* Your personal devices now connect to your *local* node, which is integrated into a living, breathing local mesh. Your traffic flows through local paths provided by other real people in the community rather than bouncing off a distant server.
|
||||
|
||||
**Don't wait for others to build the networks you want to see**. Every network is important, perhaps even most so those that support individual families and persons. Once enough of this personal, local infrastructure exist, connecting them directly to each other, without traversing the public Internet, becomes inevitable.
|
||||
|
||||
|
||||
Mixing Strategies
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
There is no requirement to commit to a single strategy. The most robust setups often mix static, dynamic, and discovered interfaces.
|
||||
|
||||
* **Static Interfaces:** You maintain a permanent interface to a trusted friend or organization using a static configuration.
|
||||
* **Bootstrap Links:** You connect a ``bootstrap_only`` interface to a public gateway on the Internet to scan for new connectable peers or to regain connectivity if your other interfaces fail.
|
||||
* **Local Wide-Area Connectivity:** You run a ``RNodeInterface`` on a shared frequency, giving you completely self-sovereign and private wide-area access to both your own network and other Reticulum peers globally, without any "service providers" being able to control or monitor how you interact with people.
|
||||
|
||||
By combining these methods, you create a system that is secure against single points of failure, adaptable to changing network conditions, and better integrated into your physical and social reality.
|
||||
|
||||
|
||||
Network Health & Responsibility
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
As you participate in the wider networks you discover and build, you will inevitably encounter peers that are misconfigured, malicious, or simply broken. To protect your resources and those of your local peers, you can utilize the :ref:`Blackhole Management<using-blackhole_management>` system.
|
||||
|
||||
Whether you manually block a spamming identity or subscribe to a blackhole list maintained by a trusted Network Identity, these tools help ensure that *your* transport capacity is used for what *you* consider legitimate communication. This keeps your local segment efficient and contributes to the health of the wider network.
|
||||
|
||||
|
||||
Contributing to the Global Ret
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have the means to host a stable node with a public IP address, consider becoming a :ref:`Public Entrypoint<hosting-entrypoints>`. By :ref:`publishing your interface as discoverable<interfaces-discoverable>`, you provide a potential connection point for others, helping the network grow and reach new areas.
|
||||
|
||||
For guidelines on how to properly configure a public entrypoint, refer to the :ref:`Hosting Public Entrypoints<hosting-entrypoints>` section.
|
||||
|
||||
Connect to the Distributed Backbone
|
||||
===================================
|
||||
|
||||
A global, distributed backbone of Reticulum Transport Nodes is being run by volunteers from around the world. This network constitutes a heterogenous collection of both public and private nodes that form an uncoordinated, voluntary inter-networking backbone that currently provides global transport and internetworking capabilities for Reticulum.
|
||||
|
||||
As a good starting point, you can find interface definitions for connecting your own networks to this backbone on websites such as `directory.rns.recipes <https://directory.rns.recipes/>`_ and `rmap.world <https://rmap.world/>`_.
|
||||
|
||||
.. tip::
|
||||
Ideally, set up a Reticulum Transport Node that your own devices can reach locally, and then
|
||||
connect that transport node to a couple of public entrypoints. This will provide efficient
|
||||
connections and redundancy in case any of them go down.
|
||||
Don't rely on just a single connection to the distributed backbone for everyday use. It is much better to have several redundant connections configured, and enable the interface discovery options, so your nodes can continously discover peering opportunities as the network evolves. Refer to the :ref:`Bootstrapping Connectivity<bootstrapping-connectivity>` section to understand the options.
|
||||
|
||||
Many other Reticulum instances are connecting to this testnet, and you can also join it
|
||||
via other entry points if you know them. There is absolutely no control over the network
|
||||
topography, usage or what types of instances connect. It will also occasionally be used
|
||||
to test various failure scenarios, and there are no availability or service guarantees.
|
||||
Expect weird things to happen on this network, as people experiment and try out things.
|
||||
|
||||
.. warning::
|
||||
It probably goes without saying, but *don't use the testnet entry-points as
|
||||
hardcoded or default interfaces in any applications you ship to users*. When
|
||||
shipping applications, the best practice is to provide your own default
|
||||
connectivity solutions, if needed and applicable, or in most cases, simply
|
||||
leave it up to the user which networks to connect to, and how.
|
||||
|
||||
.. _hosting-entrypoints:
|
||||
|
||||
Hosting Public Entrypoints
|
||||
===========================================
|
||||
==========================
|
||||
|
||||
If you want to host a public (or private) entry-point to a Reticulum network over the
|
||||
Internet, this section offers some helpful pointers. You will need a machine, physical or
|
||||
virtual with a public IP address, that can be reached by other devices on the Internet.
|
||||
If you want to help build a strong global interconnection backbone, you can host a public (or private) entry-point to a Reticulum network over the Internet. This section offers some helpful pointers. Once you have set up your public entrypoint, it is a great idea to :ref:`make it discoverable over Reticulum<interfaces-discoverable>`.
|
||||
|
||||
The most efficient and performant way to host a connectable entry-point supporting many
|
||||
users is to use the ``BackboneInterface``. This interface type is fully compatible with
|
||||
the ``TCPClientInterface`` and ``TCPServerInterface`` types, but much faster and uses
|
||||
less system resources, allowing your device to handle thousands of connections even on
|
||||
small systems.
|
||||
You will need a machine, physical or virtual with a public IP address, that can be reached by other devices on the Internet.
|
||||
|
||||
It is also important to set your connectable interface to ``gateway`` mode, since this
|
||||
will greatly improve network convergence time and path resolution for anyone connecting
|
||||
to your entry-point.
|
||||
The most efficient and performant way to host a connectable entry-point supporting many users is to use the ``BackboneInterface``. This interface type is fully compatible with the ``TCPClientInterface`` and ``TCPServerInterface`` types, but much faster and uses less system resources, allowing your device to handle thousands of connections even on small systems.
|
||||
|
||||
It is also important to set your connectable interface to ``gateway`` mode, since this will greatly improve network convergence time and path resolution for anyone connecting to your entry-point.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
@@ -326,8 +193,14 @@ to your entry-point.
|
||||
listen_on = 0.0.0.0
|
||||
port = 4242
|
||||
|
||||
If instead you want to make a private entry-point from the Internet, you can use the
|
||||
:ref:`IFAC name and passphrase options<interfaces-options>` to secure your interface with a network name and passphrase.
|
||||
# On publicly available interfaces, it is
|
||||
# essential to configure sensible announce
|
||||
# rate targets.
|
||||
announce_rate_target = 3600
|
||||
announce_rate_penalty = 3600
|
||||
announce_rate_grace = 6
|
||||
|
||||
If instead you want to make a private entry-point from the Internet, you can use the :ref:`IFAC name and passphrase options<interfaces-options>` to secure your interface with a network name and passphrase.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
@@ -343,139 +216,79 @@ If instead you want to make a private entry-point from the Internet, you can use
|
||||
network_name = private_ret
|
||||
passphrase = 2owjajquafIanPecAc
|
||||
|
||||
If you are hosting an entry-point on an operating system that does not support
|
||||
``BackboneInterface``, you can use ``TCPServerInterface`` instead, although it will
|
||||
not be as performant.
|
||||
If you are hosting an entry-point on an operating system that does not support ``BackboneInterface``, you can use ``TCPServerInterface`` instead, although it will not be as performant.
|
||||
|
||||
|
||||
Connecting Reticulum Instances Over the Internet
|
||||
================================================
|
||||
Reticulum currently offers three interfaces suitable for connecting instances over the Internet: :ref:`Backbone<interfaces-backbone>`, :ref:`TCP<interfaces-tcps>` and :ref:`I2P<interfaces-i2p>`. Each interface offers a different set of features, and Reticulum users should carefully choose the interface which best suites their needs.
|
||||
|
||||
The ``TCPServerInterface`` allows users to host an instance accessible over TCP/IP. This method is generally faster, lower latency, and more energy efficient than using ``I2PInterface``, however it also leaks more data about the server host.
|
||||
|
||||
The ``BackboneInterface`` is a very fast and efficient interface type available on POSIX operating systems, designed to handle thousands of connections simultaneously with low memory, processing and I/O overhead. It is fully compatible with the TCP-based interface types.
|
||||
|
||||
TCP connections reveal the IP address of both your instance and the server to anyone who can inspect the connection. Someone could use this information to determine your location or identity. Adversaries inspecting your packets may be able to record packet metadata like time of transmission and packet size. Even though Reticulum encrypts traffic, TCP does not, so an adversary may be able to use packet inspection to learn that a system is running Reticulum, and what other IP addresses connect to it. Hosting a publicly reachable instance over TCP also requires a publicly reachable IP address, which most Internet connections don't offer anymore.
|
||||
|
||||
The ``I2PInterface`` routes messages through the `Invisible Internet Protocol (I2P) <https://geti2p.net/en/>`_. To use this interface, users must also run an I2P daemon in parallel to ``rnsd``. For always-on I2P nodes it is recommended to use `i2pd <https://i2pd.website/>`_.
|
||||
|
||||
By default, I2P will encrypt and mix all traffic sent over the Internet, and hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node will also relay other I2P user's encrypted packets, which will use extra bandwidth and compute power, but also makes timing attacks and other forms of deep-packet-inspection much more difficult.
|
||||
|
||||
I2P also allows users to host globally available Reticulum instances from non-public IP's and behind firewalls and NAT.
|
||||
|
||||
In general it is recommended to use an I2P node if you want to host a publicly accessible instance, while preserving anonymity. If you care more about performance, and a slightly easier setup, use TCP.
|
||||
|
||||
Adding Radio Interfaces
|
||||
==============================================
|
||||
Once you have Reticulum installed and working, you can add radio interfaces with
|
||||
any compatible hardware you have available. Reticulum supports a wide range of radio
|
||||
hardware, and if you already have any available, it is very likely that it will
|
||||
work with Reticulum. For information on how to configure this, see the
|
||||
:ref:`Interfaces<interfaces-main>` section of this manual.
|
||||
=======================
|
||||
Once you have Reticulum installed and working, you can add radio interfaces with any compatible hardware you have available. Reticulum supports a wide range of radio hardware, and if you already have any available, it is very likely that it will work with Reticulum. For information on how to configure this, see the :ref:`Interfaces<interfaces-main>` section of this manual.
|
||||
|
||||
If you do not already have transceiver hardware available, you can easily and
|
||||
cheaply build an :ref:`RNode<rnode-main>`, which is a general-purpose long-range
|
||||
digital radio transceiver, that integrates easily with Reticulum.
|
||||
If you do not already have transceiver hardware available, you can easily and cheaply build an :ref:`RNode<rnode-main>`, which is a general-purpose long-range digital radio transceiver, that integrates easily with Reticulum.
|
||||
|
||||
To build one yourself requires installing a custom firmware on a supported LoRa
|
||||
development board with an auto-install script. Please see the :ref:`Communications Hardware<hardware-main>`
|
||||
chapter for a guide. If you prefer purchasing a ready-made unit, you can refer to the
|
||||
:ref:`list of suppliers<rnode-suppliers>`. For more information on RNode, you can also
|
||||
refer to these additional external resources:
|
||||
To build one yourself requires installing a custom firmware on a supported LoRa development board with an auto-install script or web-based flasher. Please see the :ref:`Communications Hardware<hardware-main>` chapter for a guide. If you prefer purchasing a ready-made unit, you can refer to the :ref:`list of suppliers<rnode-suppliers>`.
|
||||
|
||||
* `How To Make Your Own RNodes <https://unsigned.io/how-to-make-your-own-rnodes/>`_
|
||||
* `Installing RNode Firmware on Compatible LoRa Devices <https://unsigned.io/installing-rnode-firmware-on-supported-devices/>`_
|
||||
* `Private, Secure and Uncensorable Messaging Over a LoRa Mesh <https://unsigned.io/private-messaging-over-lora/>`_
|
||||
* `RNode Firmware <https://github.com/markqvist/RNode_Firmware/>`_
|
||||
Other radio-based hardware interfaces are being developed and made available by the broader Reticulum community. You can find more information on such topics over Reticulum-based information sharing systems.
|
||||
|
||||
If you have communications hardware that is not already supported by any of the
|
||||
:ref:`existing interface types<interfaces-main>`, but you think would be suitable for use with Reticulum,
|
||||
you are welcome to head over to the `GitHub discussion pages <https://github.com/markqvist/Reticulum/discussions>`_
|
||||
and propose adding an interface for the hardware.
|
||||
If you have communications hardware that is not already supported by any of the :ref:`existing interface types<interfaces-main>`, it is easy to write (and potentially publish) a :ref:`custom interface module<interfaces-custom>` that makes it compatible with Reticulum.
|
||||
|
||||
|
||||
Creating and Using Custom Interfaces
|
||||
===========================================
|
||||
====================================
|
||||
|
||||
While Reticulum includes a flexible and broad range of built-in interfaces, these
|
||||
will not cover every conceivable type of communications hardware that Reticulum
|
||||
can potentially use to communicate.
|
||||
While Reticulum includes a flexible and broad range of built-in interfaces, these will not cover every conceivable type of communications hardware that Reticulum can potentially use to communicate.
|
||||
|
||||
It is therefore possible to easily write your own interface modules, that can be
|
||||
loaded at run-time and used on-par with any of the built-in interface types.
|
||||
It is therefore possible to easily write your own interface modules, that can be loaded at run-time and used on-par with any of the built-in interface types.
|
||||
|
||||
For more information on this subject, and code examples to build on, please see
|
||||
the :ref:`Configuring Interfaces<interfaces-main>` chapter.
|
||||
For more information on this subject, and code examples to build on, please see the :ref:`Configuring Interfaces<interfaces-main>` chapter.
|
||||
|
||||
|
||||
Develop a Program with Reticulum
|
||||
===========================================
|
||||
If you want to develop programs that use Reticulum, the easiest way to get
|
||||
started is to install the latest release of Reticulum via pip:
|
||||
================================
|
||||
If you want to develop programs that use Reticulum, the easiest way to get started is to install the latest release of Reticulum via pip:
|
||||
|
||||
.. code::
|
||||
|
||||
pip install rns
|
||||
|
||||
The above command will install Reticulum and dependencies, and you will be
|
||||
ready to import and use RNS in your own programs. The next step will most
|
||||
likely be to look at some :ref:`Example Programs<examples-main>`.
|
||||
The above command will install Reticulum and dependencies, and you will be ready to import and use RNS in your own programs. The next step will most likely be to look at some :ref:`Example Programs<examples-main>`.
|
||||
|
||||
The entire Reticulum API is documented in the :ref:`API Reference<api-main>`
|
||||
chapter of this manual.
|
||||
The entire Reticulum API is documented in the :ref:`API Reference<api-main>` chapter of this manual. Before diving in, it's probably a good idea to read this manual in full, but at least start with the :ref:`Understanding Reticulum<understanding-main>` chapter.
|
||||
|
||||
|
||||
Participate in Reticulum Development
|
||||
==============================================
|
||||
If you want to participate in the development of Reticulum and associated
|
||||
utilities, you'll want to get the latest source from GitHub. In that case,
|
||||
don't use pip, but try this recipe:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
# Install dependencies
|
||||
pip install cryptography pyserial
|
||||
|
||||
# Clone repository
|
||||
git clone https://github.com/markqvist/Reticulum.git
|
||||
|
||||
# Move into Reticulum folder and symlink library to examples folder
|
||||
cd Reticulum
|
||||
ln -s ../RNS ./Examples/
|
||||
|
||||
# Run an example
|
||||
python Examples/Echo.py -s
|
||||
|
||||
# Unless you've manually created a config file, Reticulum will do so now,
|
||||
# and immediately exit. Make any necessary changes to the file:
|
||||
nano ~/.reticulum/config
|
||||
|
||||
# ... and launch the example again.
|
||||
python Examples/Echo.py -s
|
||||
|
||||
# You can now repeat the process on another computer,
|
||||
# and run the same example with -h to get command line options.
|
||||
python Examples/Echo.py -h
|
||||
|
||||
# Run the example in client mode to "ping" the server.
|
||||
# Replace the hash below with the actual destination hash of your server.
|
||||
python Examples/Echo.py 174a64852a75682259ad8b921b8bf416
|
||||
|
||||
# Have a look at another example
|
||||
python Examples/Filetransfer.py -h
|
||||
|
||||
When you have experimented with the basic examples, it's time to go read the
|
||||
:ref:`Understanding Reticulum<understanding-main>` chapter. Before submitting
|
||||
your first pull request, it is probably a good idea to introduce yourself on
|
||||
the `disucssion forum on GitHub <https://github.com/markqvist/Reticulum/discussions>`_,
|
||||
or ask one of the developers or maintainers for a good place to start.
|
||||
|
||||
.. _install-guides:
|
||||
|
||||
Platform-Specific Install Notes
|
||||
==============================================
|
||||
===============================
|
||||
|
||||
Some platforms require a slightly different installation procedure, or have
|
||||
various quirks that are worth being aware of. These are listed here.
|
||||
Some platforms require a slightly different installation procedure, or have various quirks that are worth being aware of. These are listed here.
|
||||
|
||||
Android
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Reticulum can be used on Android in different ways. The easiest way to get
|
||||
started is using an app like `Sideband <https://unsigned.io/sideband>`_.
|
||||
^^^^^^^
|
||||
Reticulum can be used on Android in different ways. The easiest way to get started is using an app like `Sideband <https://unsigned.io/sideband>`_.
|
||||
|
||||
For more control and features, you can use Reticulum and related programs via
|
||||
the `Termux app <https://termux.com/>`_, at the time of writing available on
|
||||
`F-droid <https://f-droid.org>`_.
|
||||
For more control and features, you can use Reticulum and related programs via the `Termux app <https://termux.com/>`_, at the time of writing available on `F-droid <https://f-droid.org>`_.
|
||||
|
||||
Termux is a terminal emulator and Linux environment for Android based devices,
|
||||
which includes the ability to use many different programs and libraries,
|
||||
including Reticulum.
|
||||
Termux is a terminal emulator and Linux environment for Android based devices, which includes the ability to use many different programs and libraries, including Reticulum.
|
||||
|
||||
To use Reticulum within the Termux environment, you will need to install
|
||||
``python`` and the ``python-cryptography`` library using ``pkg``, the package-manager
|
||||
build into Termux. After that, you can use ``pip`` to install Reticulum.
|
||||
To use Reticulum within the Termux environment, you will need to install ``python`` and the ``python-cryptography`` library using ``pkg``, the package-manager build into Termux. After that, you can use ``pip`` to install Reticulum.
|
||||
|
||||
From within Termux, execute the following:
|
||||
|
||||
@@ -494,9 +307,7 @@ From within Termux, execute the following:
|
||||
# Install Reticulum
|
||||
pip install rns
|
||||
|
||||
If for some reason the ``python-cryptography`` package is not available for
|
||||
your platform via the Termux package manager, you can attempt to build it
|
||||
locally on your device using the following command:
|
||||
If for some reason the ``python-cryptography`` package is not available for your platform via the Termux package manager, you can attempt to build it locally on your device using the following command:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
@@ -523,16 +334,12 @@ locally on your device using the following command:
|
||||
# Reticulum and any related software
|
||||
pip install rns
|
||||
|
||||
It is also possible to include Reticulum in apps compiled and distributed as
|
||||
Android APKs. A detailed tutorial and example source code will be included
|
||||
here at a later point. Until then you can use the `Sideband source code <https://github.com/markqvist/sideband>`_ as an example and starting point.
|
||||
It is also possible to include Reticulum in apps compiled and distributed as Android APKs. A detailed tutorial and example source code will be included here at a later point. Until then you can use the `Sideband source code <https://github.com/markqvist/sideband>`_ as an example and starting point.
|
||||
|
||||
|
||||
ARM64
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On some architectures, including ARM64, not all dependencies have precompiled
|
||||
binaries. On such systems, you may need to install ``python3-dev`` (or similar) before
|
||||
installing Reticulum or programs that depend on Reticulum.
|
||||
^^^^^
|
||||
On some architectures, including ARM64, not all dependencies have precompiled binaries. On such systems, you may need to install ``python3-dev`` (or similar) before installing Reticulum or programs that depend on Reticulum.
|
||||
|
||||
.. code:: shell
|
||||
|
||||
@@ -548,12 +355,8 @@ on your system locally.
|
||||
|
||||
|
||||
Debian Bookworm
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On versions of Debian released after April 2023, it is no longer possible by default
|
||||
to use ``pip`` to install packages onto your system. Unfortunately, you will need to
|
||||
use the replacement ``pipx`` command instead, which places installed packages in an
|
||||
isolated environment. This should not negatively affect Reticulum, but will not work
|
||||
for including and using Reticulum in your own scripts and programs.
|
||||
^^^^^^^^^^^^^^^
|
||||
On versions of Debian released after April 2023, it is no longer possible by default to use ``pip`` to install packages onto your system. Unfortunately, you will need to use the replacement ``pipx`` command instead, which places installed packages in an isolated environment. This should not negatively affect Reticulum, but will not work for including and using Reticulum in your own scripts and programs.
|
||||
|
||||
.. code:: shell
|
||||
|
||||
@@ -566,42 +369,30 @@ for including and using Reticulum in your own scripts and programs.
|
||||
# Install Reticulum
|
||||
pipx install rns
|
||||
|
||||
Alternatively, you can restore normal behaviour to ``pip`` by creating or editing
|
||||
the configuration file located at ``~/.config/pip/pip.conf``, and adding the
|
||||
following section:
|
||||
Alternatively, you can restore normal behaviour to ``pip`` by creating or editing the configuration file located at ``~/.config/pip/pip.conf``, and adding the following section:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[global]
|
||||
break-system-packages = true
|
||||
|
||||
For a one-shot installation of Reticulum, without globally enabling the ``break-system-packages``
|
||||
option, you can use the following command:
|
||||
For a one-shot installation of Reticulum, without globally enabling the ``break-system-packages`` option, you can use the following command:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
pip install rns --break-system-packages
|
||||
|
||||
.. note::
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing ``pip`` packages user- and system-wide. While this *could* in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice of words. Setting it will of course not break any system packages, but will simply allow installing ``pip`` packages user- and system-wide. While this *could* in rare cases lead to version conflicts, it does not generally pose any problems, especially not in the case of installing Reticulum.
|
||||
|
||||
|
||||
MacOS
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
To install Reticulum on macOS, you will need to have Python and the ``pip`` package
|
||||
manager installed.
|
||||
^^^^^
|
||||
To install Reticulum on macOS, you will need to have Python and the ``pip`` package manager installed.
|
||||
|
||||
Systems running macOS can vary quite widely in whether or not Python is pre-installed,
|
||||
and if it is, which version is installed, and whether the ``pip`` package manager is
|
||||
also installed and set up. If in doubt, you can `download and install <https://www.python.org/downloads/>`_
|
||||
Python manually.
|
||||
Systems running macOS can vary quite widely in whether or not Python is pre-installed, and if it is, which version is installed, and whether the ``pip`` package manager is also installed and set up. If in doubt, you can `download and install <https://www.python.org/downloads/>`_ Python manually.
|
||||
|
||||
When Python and ``pip`` is available on your system, simply open a terminal window
|
||||
and use one of the following commands:
|
||||
When Python and ``pip`` is available on your system, simply open a terminal window and use one of the following commands:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
@@ -613,16 +404,9 @@ and use one of the following commands:
|
||||
pip3 install rns --break-system-packages
|
||||
|
||||
.. note::
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing ``pip`` packages user- and system-wide. While this *could* in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice of words. Setting it will of course not break any system packages, but will simply allow installing ``pip`` packages user- and system-wide. While this *could* in rare cases lead to version conflicts, it does not generally pose any problems, especially not in the case of installing Reticulum.
|
||||
|
||||
Additionally, some version combinations of macOS and Python require you to
|
||||
manually add your installed ``pip`` packages directory to your `PATH` environment
|
||||
variable, before you can use installed commands in your terminal. Usually, adding
|
||||
the following line to your shell init script (for example ``~/.zshrc``) will be enough:
|
||||
Additionally, some version combinations of macOS and Python require you to manually add your installed ``pip`` packages directory to your `PATH` environment variable, before you can use installed commands in your terminal. Usually, adding the following line to your shell init script (for example ``~/.zshrc``) will be enough:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
@@ -632,20 +416,13 @@ Adjust Python version and shell init script location according to your system.
|
||||
|
||||
|
||||
OpenWRT
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On OpenWRT systems with sufficient storage and memory, you can install
|
||||
Reticulum and related utilities using the `opkg` package manager and `pip`.
|
||||
^^^^^^^
|
||||
On OpenWRT systems with sufficient storage and memory, you can install Reticulum and related utilities using the `opkg` package manager and `pip`.
|
||||
|
||||
.. note::
|
||||
At the time of releasing this manual, work is underway to create pre-built Reticulum packages for OpenWRT, with full configuration, service and ``uci`` integration. Please see the `feed-reticulum <https://github.com/gretel/feed-reticulum>`_ and `reticulum-openwrt <https://github.com/gretel/reticulum-openwrt>`_ repositories for more information.
|
||||
|
||||
At the time of releasing this manual, work is underway to create pre-built
|
||||
Reticulum packages for OpenWRT, with full configuration, service
|
||||
and ``uci`` integration. Please see the `feed-reticulum <https://github.com/gretel/feed-reticulum>`_
|
||||
and `reticulum-openwrt <https://github.com/gretel/reticulum-openwrt>`_
|
||||
repositories for more information.
|
||||
|
||||
To install Reticulum on OpenWRT, first log into a command line session, and
|
||||
then use the following instructions:
|
||||
To install Reticulum on OpenWRT, first log into a command line session, and then use the following instructions:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
@@ -659,30 +436,15 @@ then use the following instructions:
|
||||
rnsd -vvv
|
||||
|
||||
.. note::
|
||||
|
||||
The above instructions have been verified and tested on OpenWRT 21.02 only.
|
||||
It is likely that other versions may require slightly altered installation
|
||||
commands or package names. You will also need enough free space in your
|
||||
overlay FS, and enough free RAM to actually run Reticulum and any related
|
||||
programs and utilities.
|
||||
The above instructions have been verified and tested on OpenWRT 21.02 only. It is likely that other versions may require slightly altered installation commands or package names. You will also need enough free space in your overlay FS, and enough free RAM to actually run Reticulum and any related programs and utilities.
|
||||
|
||||
Depending on your device configuration, you may need to adjust firewall rules
|
||||
for Reticulum connectivity to and from your device to work. Until proper
|
||||
packaging is ready, you will also need to manually create a service or startup
|
||||
script to automatically laucnh Reticulum at boot time.
|
||||
Depending on your device configuration, you may need to adjust firewall rules for Reticulum connectivity to and from your device to work. Until proper packaging is ready, you will also need to manually create a service or startup script to automatically laucnh Reticulum at boot time.
|
||||
|
||||
Please also note that the `AutoInterface` requires link-local IPv6 addresses
|
||||
to be enabled for any Ethernet and WiFi devices you intend to use. If ``ip a``
|
||||
shows an address starting with ``fe80::`` for the device in question,
|
||||
``AutoInterface`` should work for that device.
|
||||
Please also note that the `AutoInterface` requires link-local IPv6 addresses to be enabled for any Ethernet and WiFi devices you intend to use. If ``ip a`` shows an address starting with ``fe80::`` for the device in question, ``AutoInterface`` should work for that device.
|
||||
|
||||
Raspberry Pi
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
It is currently recommended to use a 64-bit version of the Raspberry Pi OS
|
||||
if you want to run Reticulum on Raspberry Pi computers, since 32-bit versions
|
||||
don't always have packages available for some dependencies. If Python and the
|
||||
`pip` package manager is not already installed, do that first, and then
|
||||
install Reticulum using `pip`.
|
||||
^^^^^^^^^^^^
|
||||
It is currently recommended to use a 64-bit version of the Raspberry Pi OS if you want to run Reticulum on Raspberry Pi computers, since 32-bit versions don't always have packages available for some dependencies. If Python and the `pip` package manager is not already installed, do that first, and then install Reticulum using `pip`.
|
||||
|
||||
.. code:: shell
|
||||
|
||||
@@ -693,22 +455,14 @@ install Reticulum using `pip`.
|
||||
pip install rns --break-system-packages
|
||||
|
||||
.. note::
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing ``pip`` packages user- and system-wide. While this *could* in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice of words. Setting it will of course not break any system packages, but will simply allow installing ``pip`` packages user- and system-wide. While this *could* in rare cases lead to version conflicts, it does not generally pose any problems, especially not in the case of installing Reticulum.
|
||||
|
||||
While it is possible to install and run Reticulum on 32-bit Rasperry Pi OSes,
|
||||
it will require manually configuring and installing required build dependencies,
|
||||
and is not detailed in this manual.
|
||||
While it is possible to install and run Reticulum on 32-bit Rasperry Pi OSes, it will require manually configuring and installing required build dependencies, and is not detailed in this manual.
|
||||
|
||||
|
||||
RISC-V
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On some architectures, including RISC-V, not all dependencies have precompiled
|
||||
binaries. On such systems, you may need to install ``python3-dev`` (or similar) before
|
||||
installing Reticulum or programs that depend on Reticulum.
|
||||
^^^^^^
|
||||
On some architectures, including RISC-V, not all dependencies have precompiled binaries. On such systems, you may need to install ``python3-dev`` (or similar) before installing Reticulum or programs that depend on Reticulum.
|
||||
|
||||
.. code:: shell
|
||||
|
||||
@@ -719,17 +473,12 @@ installing Reticulum or programs that depend on Reticulum.
|
||||
# Install Reticulum
|
||||
python3 -m pip install rns
|
||||
|
||||
With these packages installed, ``pip`` will be able to build any missing dependencies
|
||||
on your system locally.
|
||||
With these packages installed, ``pip`` will be able to build any missing dependencies on your system locally.
|
||||
|
||||
|
||||
Ubuntu Lunar
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On versions of Ubuntu released after April 2023, it is no longer possible by default
|
||||
to use ``pip`` to install packages onto your system. Unfortunately, you will need to
|
||||
use the replacement ``pipx`` command instead, which places installed packages in an
|
||||
isolated environment. This should not negatively affect Reticulum, but will not work
|
||||
for including and using Reticulum in your own scripts and programs.
|
||||
^^^^^^^^^^^^
|
||||
On versions of Ubuntu released after April 2023, it is no longer possible by default to use ``pip`` to install packages onto your system. Unfortunately, you will need to use the replacement ``pipx`` command instead, which places installed packages in an isolated environment. This should not negatively affect Reticulum, but will not work for including and using Reticulum in your own scripts and programs.
|
||||
|
||||
.. code:: shell
|
||||
|
||||
@@ -742,42 +491,29 @@ for including and using Reticulum in your own scripts and programs.
|
||||
# Install Reticulum
|
||||
pipx install rns
|
||||
|
||||
Alternatively, you can restore normal behaviour to ``pip`` by creating or editing
|
||||
the configuration file located at ``~/.config/pip/pip.conf``, and adding the
|
||||
following section:
|
||||
Alternatively, you can restore normal behaviour to ``pip`` by creating or editing the configuration file located at ``~/.config/pip/pip.conf``, and adding the following section:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[global]
|
||||
break-system-packages = true
|
||||
|
||||
For a one-shot installation of Reticulum, without globally enabling the ``break-system-packages``
|
||||
option, you can use the following command:
|
||||
For a one-shot installation of Reticulum, without globally enabling the ``break-system-packages`` option, you can use the following command:
|
||||
|
||||
.. code:: text
|
||||
|
||||
pip install rns --break-system-packages
|
||||
|
||||
.. note::
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing ``pip`` packages user- and system-wide. While this *could* in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice of words. Setting it will of course not break any system packages, but will simply allow installing ``pip`` packages user- and system-wide. While this *could* in rare cases lead to version conflicts, it does not generally pose any problems, especially not in the case of installing Reticulum.
|
||||
|
||||
Windows
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On Windows operating systems, the easiest way to install Reticulum is by using the
|
||||
``pip`` package manager from the command line (either the command prompt or Windows
|
||||
Powershell).
|
||||
^^^^^^^
|
||||
On Windows operating systems, the easiest way to install Reticulum is by using the ``pip`` package manager from the command line (either the command prompt or Windows Powershell).
|
||||
|
||||
If you don't already have Python installed, `download and install Python <https://www.python.org/downloads/>`_.
|
||||
At the time of publication of this manual, the recommended version is `Python 3.12.7 <https://www.python.org/downloads/release/python-3127>`_.
|
||||
If you don't already have Python installed, `download and install Python <https://www.python.org/downloads/>`_. At the time of publication of this manual, the recommended version is `Python 3.12.7 <https://www.python.org/downloads/release/python-3127>`_.
|
||||
|
||||
**Important!** When asked by the installer, make sure to add the Python program to
|
||||
your PATH environment variables. If you don't do this, you will not be able to
|
||||
use the ``pip`` installer, or run the included Reticulum utility programs (such as
|
||||
``rnsd`` and ``rnstatus``) from the command line.
|
||||
**Important!** When asked by the installer, make sure to add the Python program to your PATH environment variables. If you don't do this, you will not be able to use the ``pip`` installer, or run the included Reticulum utility programs (such as ``rnsd`` and ``rnstatus``) from the command line.
|
||||
|
||||
After installing Python, open the command prompt or Windows Powershell, and type:
|
||||
|
||||
@@ -785,11 +521,10 @@ After installing Python, open the command prompt or Windows Powershell, and type
|
||||
|
||||
pip install rns
|
||||
|
||||
You can now use Reticulum and all included utility programs directly from your
|
||||
preferred command line interface.
|
||||
You can now use Reticulum and all included utility programs directly from your preferred command line interface.
|
||||
|
||||
Pure-Python Reticulum
|
||||
==============================================
|
||||
=====================
|
||||
|
||||
.. warning::
|
||||
If you use the ``rnspure`` package to run Reticulum on systems that
|
||||
@@ -797,17 +532,6 @@ Pure-Python Reticulum
|
||||
important that you read and understand the :ref:`Cryptographic Primitives <understanding-primitives>`
|
||||
section of this manual.
|
||||
|
||||
In some rare cases, and on more obscure system types, it is not possible to
|
||||
install one or more dependencies. In such situations,
|
||||
you can use the ``rnspure`` package instead of the ``rns`` package, or use ``pip``
|
||||
with the ``--no-dependencies`` command-line option. The ``rnspure``
|
||||
package requires no external dependencies for installation. Please note that the
|
||||
actual contents of the ``rns`` and ``rnspure`` packages are *completely identical*.
|
||||
The only difference is that the ``rnspure`` package lists no dependencies required
|
||||
for installation.
|
||||
In some rare cases, and on more obscure system types, it is not possible to install one or more dependencies. In such situations, you can use the ``rnspure`` package instead of the ``rns`` package, or use ``pip`` with the ``--no-dependencies`` command-line option. The ``rnspure`` package requires no external dependencies for installation. Please note that the actual contents of the ``rns`` and ``rnspure`` packages are *completely identical*. The only difference is that the ``rnspure`` package lists no dependencies required for installation.
|
||||
|
||||
No matter how Reticulum is installed and started, it will load external dependencies
|
||||
only if they are *needed* and *available*. If for example you want to use Reticulum
|
||||
on a system that cannot support ``pyserial``, it is perfectly possible to do so using
|
||||
the `rnspure` package, but Reticulum will not be able to use serial-based interfaces.
|
||||
All other available modules will still be loaded when needed.
|
||||
No matter how Reticulum is installed and started, it will load external dependencies only if they are *needed* and *available*. If for example you want to use Reticulum on a system that cannot support ``pyserial``, it is perfectly possible to do so using the `rnspure` package, but Reticulum will not be able to use serial-based interfaces. All other available modules will still be loaded when needed.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,13 +20,18 @@ to participate in the development of Reticulum itself.
|
||||
|
||||
whatis
|
||||
gettingstartedfast
|
||||
zen
|
||||
software
|
||||
using
|
||||
understanding
|
||||
hardware
|
||||
interfaces
|
||||
networks
|
||||
distributed
|
||||
git
|
||||
support
|
||||
examples
|
||||
license
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
@@ -356,6 +356,7 @@ software-based soundmodems. To do this, use the ``kiss_framing`` option:
|
||||
kiss_framing = True
|
||||
target_host = 127.0.0.1
|
||||
target_port = 8001
|
||||
fixed_mtu = 500
|
||||
|
||||
**Caution!** Only use the KISS framing option when connecting to external devices
|
||||
and programs like soundmodems and similar over TCP. When using the
|
||||
@@ -364,6 +365,9 @@ never enable ``kiss_framing``, since this will disable internal reliability and
|
||||
recovery mechanisms that greatly improves performance over unreliable and
|
||||
intermittent TCP links.
|
||||
|
||||
For KISS devices that need only supports a particular MTU, you can use the
|
||||
``fixed_mtu`` option.
|
||||
|
||||
.. note::
|
||||
The TCP interfaces support tunneling over I2P, but to do so reliably,
|
||||
you must use the i2p_tunneled option:
|
||||
@@ -907,6 +911,213 @@ beaconing functionality described above.
|
||||
# small internal packet buffer.
|
||||
flow_control = false
|
||||
|
||||
.. _interfaces-discoverable:
|
||||
|
||||
Discoverable Interfaces
|
||||
=======================
|
||||
|
||||
Reticulum includes a powerful system for publishing your local interfaces to the wider network, allowing other peers to :ref:`discover, validate, and automatically connect to them<using-interface_discovery>`. This feature is particularly useful for creating decentralized networks where peers can dynamically find entrypoints, such as public Internet gateways or local radio access points, without relying on static configuration files or centralized directories.
|
||||
|
||||
When an interface is made **discoverable**, your Reticulum instance will periodically broadcast an announce packet containing the connection details and parameters required for other peers to establish a connection. These announces are propagated over the network using the standard Reticulum announce mechanism using the ``rnstransport.discovery.interface`` destination type.
|
||||
|
||||
.. note::
|
||||
To use the interface discovery functionality, the ``LXMF`` module must be installed in your Python environment. You can install it using pip:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
pip install lxmf
|
||||
|
||||
|
||||
Enabling Discovery
|
||||
------------------
|
||||
|
||||
Interface discovery is enabled on a per-interface basis. To make a specific interface discoverable, you must add the ``discoverable`` option to that interface's configuration block and set it to ``yes``.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[[My Public Gateway]]
|
||||
type = BackboneInterface
|
||||
...
|
||||
discoverable = yes
|
||||
|
||||
Once enabled, Reticulum will automatically handle the generation, signing, stamping, and broadcasting of the discovery announces. It is not *required* to enable Transport to publish interface discovery information, but for most use cases where you want others to connect to you, you will likely want ``enable_transport`` set to ``yes`` in the ``[reticulum]`` section of your configuration.
|
||||
|
||||
|
||||
Discovery Parameters
|
||||
--------------------
|
||||
|
||||
When ``discoverable`` is enabled, a variety of additional options become available to control how the interface is presented to the network. These parameters allow you to fine-tune the metadata, security requirements, and visibility of your interface.
|
||||
|
||||
**Basic Metadata**
|
||||
|
||||
``discovery_name``
|
||||
A human-readable name for the interface. This name will be displayed to users on remote systems when they list discovered interfaces. If not specified, the interface name (the section header) will be used.
|
||||
|
||||
``announce_interval``
|
||||
The interval in minutes between successive discovery announces for this interface. Default is 360 minutes (6 hours). For stable, long-running infrastructure, higher intervals (12 to 22 hours) are usually sufficient and reduce network load. Minimum allowed value is 5 minutes (but expect to have your announces throttled if using intervals below one hour).
|
||||
|
||||
**Connectivity Specification**
|
||||
|
||||
``reachable_on``
|
||||
Specifies the address that remote peers should use to connect to this interface.
|
||||
|
||||
* For TCP and Backbone interfaces, this is typically the public IP address or hostname. Do not include the port, this is fetched automatically from the interface.
|
||||
* For I2P interfaces, this is usually the I2P ``b32`` address. This value is fetched automatically from the ``I2PInterface`` once it is up and connected to the I2P network, so you should not set this manually, unless you absolutely know what you're doing.
|
||||
|
||||
**Dynamic Resolution:** This option also accepts a path to an external executable script or binary. If a path is provided, Reticulum will execute the script and use its ``stdout`` as the reachability address. This is useful for devices behind dynamic DNS, NATs, or complex cloud environments where the external IP is not known locally. The script must simply print the address to stdout and exit.
|
||||
|
||||
.. note::
|
||||
When using an executable script for ``reachable_on``, Reticulum expects the script to output only the IP address or hostname to ``stdout``, followed by a newline character. Any additional output or errors may cause the resolution to fail. Ensure the script has executable permissions and is robust against temporary network failures.
|
||||
|
||||
A minimal example of a script that resolves the externally available, public IP of an internet-connected system could look like this:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
#!/bin/bash
|
||||
curl -s ip.me
|
||||
exit $?
|
||||
|
||||
On a real system, you should make the script robust enough to deal with intermittent Internet or service failures, such that the script *always* returns a sensible value, or if not possible at least exits with a non-zero exit return code, so Reticulum knows the output is invalid.
|
||||
|
||||
**Security & Cost**
|
||||
|
||||
``discovery_stamp_value``
|
||||
Defines the proof-of-work difficulty for the cryptographic stamp included in the announce. This value acts as a cost barrier to prevent network flooding. The default value is ``14``. Increasing this value makes it computationally more expensive to generate an announce, which can be useful to prevent spam on very large networks, but it also increases CPU load on your system when generating announces. Stamps are cached, and only generated if interface information changes, or at instance restart. If you have the computational resources, it is generally advisable to use as high a stamp value as possible.
|
||||
|
||||
**Privacy & Encryption**
|
||||
|
||||
``discovery_encrypt``
|
||||
If set to ``yes``, the discovery announce payload will be encrypted. To decrypt the announce, remote peers must possess the *network identity* configured for your instance (see ``network_identity`` in the ``[reticulum]`` section). This allows you to publish private interfaces that are only discoverable to specific trusted networks.
|
||||
|
||||
.. important::
|
||||
If you enable ``discovery_encrypt`` but do not configure a valid ``network_identity`` in the ``[reticulum]`` section of your configuration, Reticulum will abort the interface discovery announce. Encryption requires a valid network identity key to function.
|
||||
|
||||
``publish_ifac``
|
||||
If set to ``yes``, the Interface Access Code (IFAC) name and passphrase for this interface will be included in the discovery announce. This allows peers to automatically configure the correct authentication parameters when connecting to the interface.
|
||||
|
||||
**Physical Location**
|
||||
|
||||
``latitude``, ``longitude``, ``height``
|
||||
Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters.
|
||||
|
||||
**Radio Parameters**
|
||||
|
||||
For physical radio interfaces like ``RNodeInterface`` or ``KISSInterface``, the following optional parameters allow you to broadcast the operating frequency and characteristics, allowing clients to verify compatibility before connecting:
|
||||
|
||||
``discovery_frequency``
|
||||
The operating frequency in Hz. Auto-configured on RNode interfaces. Necessary on KISS-based radio interfaces and ``TCPClientInterfaces`` connecting to radio modems.
|
||||
|
||||
``discovery_bandwidth``
|
||||
The signal bandwidth in Hz. Auto-configured on RNode interfaces. Useful on KISS-based radio interfaces and ``TCPClientInterfaces`` connecting to radio modems.
|
||||
|
||||
``discovery_modulation``
|
||||
The modulation type or scheme. Auto-configured on RNode interfaces, but highly advisable to include on other radio-based interfaces.
|
||||
|
||||
|
||||
Interface Modes
|
||||
---------------
|
||||
|
||||
When you enable discovery on an interface, Reticulum enforces certain interface modes to ensure the interface is actually useful for remote peers.
|
||||
|
||||
If an interface is configured as ``discoverable``, but its mode is not explicitly set to ``gateway`` (for server-style interfaces like ``BackboneInterface`` or ``TCPServerInterface``) or ``access_point`` (for radio interfaces like ``RNodeInterface``), Reticulum will automatically configure the appropriate mode and log a notice.
|
||||
|
||||
For example, if you enable discovery on a ``RNodeInterface`` without specifying the mode, Reticulum will automatically set it to ``access_point`` mode.
|
||||
|
||||
Security Considerations
|
||||
-----------------------
|
||||
|
||||
When making interfaces discoverable, you are effectively broadcasting an invitation to connect to your system. It is important to understand the security implications of the configuration options you choose.
|
||||
|
||||
**Publishing Credentials**
|
||||
|
||||
If you enable ``publish_ifac = yes``, your interface's authentication passphrase will be included in the announce. If you are operating a public network and want anyone to connect, this is acceptable. However, if you wish to restrict access to a specific group of users, you **must** enable ``discovery_encrypt = yes``. This ensures that only peers possessing the correct ``network_identity`` can decode the passphrase.
|
||||
|
||||
**Topology Exposure**
|
||||
|
||||
A discoverable interface announces its presence, location (if configured), and capabilities to the network. Even if the connection details are encrypted, the *fact* that a connectable node exists within a certain network becomes public information. In high-security or scenarios requiring operational secrecy, consider the implications of advertising your infrastructure's existence.
|
||||
|
||||
Example Configuration
|
||||
---------------------
|
||||
|
||||
Below is an example configuration for a public backbone gateway. This configuration publishes a high-value, publicly discoverable interface, that anyone can connect to.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[[My Public Gateway]]
|
||||
type = BackboneInterface
|
||||
mode = gateway
|
||||
listen_on = 0.0.0.0
|
||||
port = 4242
|
||||
|
||||
# Enable Discovery
|
||||
discoverable = yes
|
||||
|
||||
# Interface Details
|
||||
discovery_name = Region A Public Entrypoint
|
||||
announce_interval = 720
|
||||
|
||||
# Use external script to resolve dynamic IP
|
||||
reachable_on = /usr/local/bin/get_external_ip.sh
|
||||
|
||||
# Generate high stamp value
|
||||
discovery_stamp_value = 24
|
||||
|
||||
# Optional location data
|
||||
latitude = 51.99714
|
||||
longitude = -0.74195
|
||||
height = 15
|
||||
|
||||
The next example create an encrypted discovery-enabled interface, requiring a specific network identity to decode, and includes IFAC credentials for seamless authentication.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[[My Private Gateway]]
|
||||
type = BackboneInterface
|
||||
mode = gateway
|
||||
listen_on = 0.0.0.0
|
||||
port = 5858
|
||||
network_name = internal_1
|
||||
passphrase = Mevpekyafshak5Wr
|
||||
|
||||
# Enable Discovery
|
||||
discoverable = yes
|
||||
|
||||
# Interface Details
|
||||
discovery_name = Region A Private Backbone
|
||||
announce_interval = 720
|
||||
|
||||
# Use external script to resolve dynamic IP
|
||||
reachable_on = /usr/local/bin/get_external_ip.sh
|
||||
|
||||
# Target stamp value
|
||||
discovery_stamp_value = 22
|
||||
|
||||
# Encrypt announces for our network only
|
||||
discovery_encrypt = yes
|
||||
|
||||
# Include credentials so trusted
|
||||
# peers can connect automatically
|
||||
publish_ifac = yes
|
||||
|
||||
# Optional location data
|
||||
latitude = 34.06915
|
||||
longitude = -118.44318
|
||||
height = 15
|
||||
|
||||
In the ``[reticulum]`` section of your configuration, you would define the network identity used for encryption as follows:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
# The identity used to sign/encrypt discovery announces
|
||||
network_identity = ~/.reticulum/storage/identities/my_network_identity
|
||||
...
|
||||
|
||||
With these configuration options applied, your Reticulum instance will actively participate in the network's discovery ecosystem. Other peers running Reticulum with discovery enabled will be able to see your interface, validate its cryptographic stamp, and (depending on their configuration) automatically connect to it.
|
||||
|
||||
For information on how to use these discovered interfaces and configure your system to auto-connect to them, refer to the :ref:`Discovering Interfaces<using-interface_discovery>` chapter.
|
||||
|
||||
.. _interfaces-options:
|
||||
|
||||
Common Interface Options
|
||||
@@ -987,6 +1198,15 @@ These can be used to control various aspects of interface behaviour.
|
||||
option, to set the interface speed in *bits per second*.
|
||||
|
||||
|
||||
* | The ``bootstrap_only`` option designates an interface as a temporary
|
||||
bridge for initial connectivity. If this option is enabled, the
|
||||
interface will be monitored and automatically detached once the
|
||||
number of auto-connected interfaces reaches the limit configured by
|
||||
``autoconnect_discovered_interfaces``. This is particularly useful
|
||||
for using a slow or expensive connection (such as a single LoRa
|
||||
link or a remote TCP tunnel) solely to discover better local
|
||||
infrastructure, which then supersedes the bootstrap interface.
|
||||
|
||||
.. _interfaces-modes:
|
||||
|
||||
Interface Modes
|
||||
@@ -1073,11 +1293,14 @@ Announce Rate Control
|
||||
=====================
|
||||
|
||||
The built-in announce control mechanisms and the default ``announce_cap``
|
||||
option described above are sufficient most of the time, but in some cases, especially on fast
|
||||
interfaces, it may be useful to control the target announce rate. Using the
|
||||
``announce_rate_target``, ``announce_rate_grace`` and ``announce_rate_penalty``
|
||||
options, this can be done on a per-interface basis, and moderates the *rate at
|
||||
which received announces are re-broadcasted to other interfaces*.
|
||||
option described above are sufficient most of the time, but in some cases,
|
||||
especially on fast interfaces, or when connecting to large public networks,
|
||||
it may be useful to control the target announce rate.
|
||||
|
||||
Using the ``announce_rate_target``, ``announce_rate_grace`` and ``announce_rate_penalty``
|
||||
options, this can be done on a per-interface basis, or by setting instance-wide defaults.
|
||||
When configured, this moderates the *rate at which received announces are
|
||||
re-broadcasted to other interfaces*.
|
||||
|
||||
* | The ``announce_rate_target`` option sets the minimum amount of time,
|
||||
in seconds, that should pass between received announces, for any one
|
||||
@@ -1095,20 +1318,37 @@ which received announces are re-broadcasted to other interfaces*.
|
||||
destination in question will only have its announces propagated every
|
||||
3 hours, until it lowers its actual announce rate to within the target.
|
||||
|
||||
You can also configure default announce rate parameters for all interfaces that
|
||||
do not have these parameters set explicitly by setting the ``default_ar_target``
|
||||
``default_ar_penalty`` and ``default_ar_grace`` options in the ``[reticulum]``
|
||||
section of the configuration file. If any of these options are set, they will
|
||||
automatically be applied to any interface if transport is enabled, and the
|
||||
interface does not have the parameters set explicitly.
|
||||
|
||||
For auto-connected interfaces, sensible default announce rate control parameters
|
||||
will **always** be set, even if the defaults are not configured explicitly, but
|
||||
if you set the defaults, auto-connected interfaces will adhere to these as well.
|
||||
|
||||
These mechanisms, in conjunction with the ``annouce_cap`` mechanisms mentioned
|
||||
above means that it is essential to select a balanced announce strategy for
|
||||
your destinations. The more balanced you can make this decision, the easier
|
||||
it will be for your destinations to make it into slower networks that many hops
|
||||
away. Or you can prioritise only reaching high-capacity networks with more frequent
|
||||
announces.
|
||||
it will be for your destinations to make it into slower networks, or networks that
|
||||
are many hops away.
|
||||
|
||||
Current statistics and information about announce rates can be viewed using the
|
||||
``rnpath -r`` command.
|
||||
Statistics and information about announce rates can be viewed using the
|
||||
``rnpath -r`` and ``rnstatus -A`` commands.
|
||||
|
||||
It is important to note that there is no one right or wrong way to set up announce
|
||||
rates. Slower networks will naturally tend towards using less frequent announces to
|
||||
It is important to note, that while there is no one right or wrong way to set up announce
|
||||
rates, it should generally not be necessary to announce any kind of destination.
|
||||
more often than once every few hours. Most applications can announce simply when
|
||||
the application starts, and then only once every 6 hours or so.
|
||||
|
||||
If you're designing an application where you think you need to annonuce more
|
||||
often than once an hour, you're most likely doing something wrong.
|
||||
|
||||
Slower networks will naturally tend towards using less frequent announces to
|
||||
conserve bandwidth, while very fast networks can support applications that
|
||||
need very frequent announces. Reticulum implements these mechanisms to ensure
|
||||
need more frequent announces. Reticulum implements these mechanisms to ensure
|
||||
that a large span of network types can seamlessly *co-exist* and interconnect.
|
||||
|
||||
.. _interfaces-ingress-control:
|
||||
@@ -1132,11 +1372,12 @@ a large amount of bogus destinations, and then disconnect, these destination wil
|
||||
never make it into path tables and waste network bandwidth on retransmitted
|
||||
announces.
|
||||
|
||||
**It's important to note** that the ingress control works at the level of *individual
|
||||
sub-interfaces*. As an example, this means that one client on a :ref:`TCP Server Interface<interfaces-tcps>`
|
||||
cannot disrupt processing of incoming announces for other connected clients on the same
|
||||
:ref:`TCP Server Interface<interfaces-tcps>`. All other clients on the same interface will still have new announces
|
||||
processed without interruption.
|
||||
.. note::
|
||||
It's important to remember that the ingress control works at the level of *individual
|
||||
sub-interfaces*. As an example, this means that one client on a :ref:`TCP Server Interface<interfaces-tcps>`
|
||||
cannot disrupt processing of incoming announces for other connected clients on the same
|
||||
:ref:`TCP Server Interface<interfaces-tcps>`. All other clients on the same interface
|
||||
will still have new announces processed without interruption.
|
||||
|
||||
By default, Reticulum will handle this automatically, and ingress announce
|
||||
control will be enabled on interface where it is sensible to do so. It should
|
||||
@@ -1144,8 +1385,7 @@ generally not be neccessary to modify the ingress control configuration,
|
||||
but all the parameters are exposed for configuration if needed.
|
||||
|
||||
* | The ``ingress_control`` option tells Reticulum whether or not
|
||||
to enable announce ingress control on the interface. Defaults to
|
||||
``True``.
|
||||
to enable ingress control on the interface. Defaults to ``True``.
|
||||
|
||||
* | The ``ic_new_time`` option configures how long (in seconds) an
|
||||
interface is considered newly spawned. Defaults to ``2*60*60`` seconds. This
|
||||
@@ -1182,3 +1422,59 @@ but all the parameters are exposed for configuration if needed.
|
||||
must pass between releasing each held announce from the queue. Defaults
|
||||
to ``30`` seconds.
|
||||
|
||||
All of the above settings can be configured both as instance-wide defaults
|
||||
under the ``[reticulum]`` section of the configuration file, or on a per-
|
||||
interface basis under the relevant interface configuration section.
|
||||
|
||||
|
||||
Path Request Burst Control
|
||||
==========================
|
||||
|
||||
In addition the announce controls for newly created destination, Reticulum will also
|
||||
monitor incoming path request activity, and enforce burst controls if per-client rates
|
||||
exceed configured limits. Once path request burst control is activated on an
|
||||
interface, path requests will no longer be propagated further on the network.
|
||||
As with announce burst control, this happens on a per sub-interface basis. One
|
||||
client connecting to a public gateway will not be able to disrupt path request
|
||||
processing for other clients.
|
||||
|
||||
.. warning::
|
||||
Applications that send large amounts of unnecessary path requests will very
|
||||
quickly get rate limited by transport nodes, and the entire system they are
|
||||
running on will not be able to resolve any paths on the network, until the
|
||||
burst subsides and hold period expires. **Do not** write applications like
|
||||
this. Only request paths for destinations you need to communicate with.
|
||||
|
||||
By default, Reticulum will handle this automatically, and ingress path request
|
||||
control will be enabled on interface where it is sensible to do so. It should
|
||||
generally not be neccessary to modify the ingress control configuration,
|
||||
but all the parameters are exposed for configuration if needed.
|
||||
|
||||
* | The ``ingress_control`` option tells Reticulum whether or not
|
||||
to enable ingress control on the interface. Defaults to ``True``.
|
||||
|
||||
* | The ``ic_new_time`` option configures how long (in seconds) an
|
||||
interface is considered newly spawned. Defaults to ``2*60*60`` seconds. This
|
||||
option is useful on publicly accessible interfaces that spawn new
|
||||
sub-interfaces when a new client connects.
|
||||
|
||||
* | The ``ic_pr_burst_freq_new`` option sets the maximum path request
|
||||
ingress frequency for newly spawned interfaces. Defaults to ``3``
|
||||
path requests per second.
|
||||
|
||||
* | The ``ic_pr_burst_freq`` option sets the maximum path request
|
||||
ingress frequency for other interfaces. Defaults to ``8`` path requests
|
||||
per second.
|
||||
|
||||
*If an interface exceeds its burst frequency, incoming path requests
|
||||
from that system will not traverse the network further.*
|
||||
|
||||
* | The ``egress_control`` option enables hard-limiting path request egress
|
||||
control per-interface. Defaults to ``False``
|
||||
|
||||
* | The ``ec_pr_freq`` option sets the hard limit for outbound path requests
|
||||
per second on a given interface.
|
||||
|
||||
All of the above settings can be configured both as instance-wide defaults
|
||||
under the ``[reticulum]`` section of the configuration file, or on a per-
|
||||
interface basis under the relevant interface configuration section.
|
||||
@@ -0,0 +1,36 @@
|
||||
.. _license:
|
||||
|
||||
Reticulum License
|
||||
=================
|
||||
|
||||
.. code:: text
|
||||
|
||||
Reticulum License
|
||||
|
||||
Copyright (c) 2016-2026 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.
|
||||
@@ -4,17 +4,47 @@
|
||||
Building Networks
|
||||
*****************
|
||||
|
||||
This chapter will provide you with the knowledge needed to build networks with
|
||||
Reticulum, which can often be easier than using traditional stacks, since you
|
||||
don't have to worry about coordinating addresses, subnets and routing for an
|
||||
This chapter will provide you with the high-level knowledge needed to build networks with
|
||||
Reticulum. It will not, however tell you all you need to know to succesfully
|
||||
design and configure every kind of network you can imagine. For this, you will
|
||||
most likely need to read this manual in its entirity, invest significant time
|
||||
into experimenting with the stack, and learning functionality intuitively.
|
||||
|
||||
Still, after reading this chapter, you should be well equipped to *start* that
|
||||
journey. While Reticulum is **fundamentally different** compared to other
|
||||
networking technologies, it can often be easier than using traditional stacks.
|
||||
If you've built networks before, you will probably have to forget, or at least
|
||||
temporarily ignore, a lot of things at this point. It will all makes sense in
|
||||
the end though. Hopefully.
|
||||
|
||||
If you're used to protocols like IP, let's at least start with some relief:
|
||||
You don't have to worry about coordinating addresses, subnets and routing for an
|
||||
entire network that you might not know how will evolve in the future. With
|
||||
Reticulum, you can simply add more segments to your network when it becomes
|
||||
necessary, and Reticulum will handle the convergence of the entire network
|
||||
automatically.
|
||||
automatically. There's plenty more neat aspects like that to Reticulum, but
|
||||
we're getting ahead of ourselves. Let's cover the basics first.
|
||||
|
||||
Concepts & Overview
|
||||
--------------------
|
||||
|
||||
Before you start building your own networks, it's important to understand the
|
||||
fundamental principles that distinguish Reticulum networks from traditional
|
||||
networking approaches. These principles shape how you design your network,
|
||||
what trade-offs you encounter, and what capabilities you can rely on.
|
||||
|
||||
Reticulum is not a single network you "join", it is a toolkit for *creating* networks.
|
||||
You decide what mediums to use, how nodes connect, what trust boundaries exist,
|
||||
and what the network's purpose is. Reticulum provides the cryptographic foundation,
|
||||
the transport mechanisms, and the convergence algorithms that make your design
|
||||
workable. You provide the intent and the structure.
|
||||
|
||||
This approach offers tremendous flexibility, but it requires thinking in terms of
|
||||
different abstractions than those used in conventional networking.
|
||||
|
||||
Introductory Considerations
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
There are important points that need to be kept in mind when building networks
|
||||
with Reticulum:
|
||||
|
||||
@@ -31,6 +61,11 @@ with Reticulum:
|
||||
interconnect with much larger and higher bandwidth networks without issue.
|
||||
Reticulum automatically manages the flow of information to and from various
|
||||
network segments, and when bandwidth is limited, local traffic is prioritised.
|
||||
You will, however, need to configure your interfaces correctly. If you tell
|
||||
Reticulum to pass all announce traffic from a gigabit link to a LoRa interfaces,
|
||||
it will try as best as possible to comply with this, while still respecting
|
||||
bandwidth limits, but you *will* waste a lot of precious bandwidth and airtime,
|
||||
and your LoRa network will not work very well.
|
||||
|
||||
* | Reticulum provides sender/initiator anonymity by default. There is no way
|
||||
to filter traffic or discriminate it based on the source of the traffic.
|
||||
@@ -89,81 +124,227 @@ Any number of interfaces can be configured, and Reticulum will automatically
|
||||
decide which are suitable to use in any given situation, depending on where
|
||||
traffic needs to flow.
|
||||
|
||||
Example Scenarios
|
||||
-----------------
|
||||
Destinations, Not Addresses
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This section illustrates a few example scenarios, and how they would, in general
|
||||
terms, be planned, implemented and configured.
|
||||
In traditional networking, addresses are allocated from a managed space. If you want to
|
||||
communicate with another node, you need to know its address, and that address
|
||||
must be unique within the network segment. This requires coordination, either
|
||||
through manual assignment, DHCP servers, or other allocation mechanisms.
|
||||
|
||||
Interconnected LoRa Sites
|
||||
=========================
|
||||
Reticulum replaces addresses with **destinations**. A destination is identified by a 16-byte
|
||||
hash (128 bits) derived from a SHA-256 hash of the destination's identifying
|
||||
characteristics. This hash serves as the address on the network. On the network, it
|
||||
is represented in binary, but when displayed to human users, it will usually look something like
|
||||
this ``<13425ec15b621c1d928589718000d814>``.
|
||||
|
||||
An organisation wants to provide communication and information services to it's
|
||||
members, which are located mainly in three separate areas. Three suitable hill-top
|
||||
locations are found, where the organisation can install equipment: Site A, B and C.
|
||||
The critical difference is that *any node can generate as many destinations as it
|
||||
needs, without coordination*. A destination's uniqueness is guaranteed by the
|
||||
collision resistance of SHA-256 and the inclusion of the node's public key in the
|
||||
hash calculation. Two nodes can both use the destination name
|
||||
``messenger.user.inbox``, but they will have different destination hashes because
|
||||
their public keys differ. Both can coexist on the same network without conflict.
|
||||
|
||||
Since the amount of data that needs to be exchanged between users is mainly text-
|
||||
based, the bandwidth requirements are low, and LoRa radios are chosen to connect
|
||||
users to the network.
|
||||
This has profound implications for network design:
|
||||
|
||||
Due to the hill-top locations found, there is radio line-of-sight between site A
|
||||
and B, and also between site B and C. Because of this, the organisation does not
|
||||
need to use the Internet to interconnect the sites, but purchases four Point-to-Point
|
||||
WiFi based radios for interconnecting the sites.
|
||||
* **No address allocation planning:** You never need to reserve address ranges,
|
||||
plan subnets, or coordinate with other network operators. Nodes simply generate
|
||||
destinations and announce them.
|
||||
|
||||
At each site, a Raspberry Pi is installed to function as a gateway. A LoRa radio
|
||||
is connected to the Pi with a USB cable, and the WiFi radio is connected to the
|
||||
Ethernet port of the Pi. At site B, two WiFi radios are needed to be able to reach
|
||||
both site A and site C, so an extra Ethernet adapter is connected to the Pi in
|
||||
this location.
|
||||
* **Global portability:** A destination is not tied to a physical location or
|
||||
network segment. A node can move its destinations across interfaces, mediums,
|
||||
or even between entirely separate Reticulum networks simply by sending an
|
||||
announce on the new medium.
|
||||
|
||||
Once the hardware has been installed, Reticulum is installed on all the Pis, and at
|
||||
site A and C, one interface is added for the LoRa radio, as well as one for the WiFi
|
||||
radio. At site B, an interface for the LoRa radio, and one interface for each WiFi
|
||||
radio is added to the Reticulum configuration file. The transport node option is
|
||||
enabled in the configuration of all three gateways.
|
||||
* **Implicit authentication:** Because destinations are bound to public keys,
|
||||
communication to a destination is inherently cryptographically authenticated.
|
||||
Only the holder of the corresponding private key can decrypt and respond to
|
||||
traffic addressed to that destination. This also makes application-level
|
||||
authentication *much* simpler, since it can directly use the foundational
|
||||
identity verification built into the core networking layer.
|
||||
|
||||
The network is now operational, and ready to serve users across all three areas.
|
||||
The organisation prepares a LoRa radio that is supplied to the end users, along
|
||||
with a Reticulum configuration file, that contains the right parameters for
|
||||
communicating with the LoRa radios installed at the gateway sites.
|
||||
* **Identity abstraction:** A single Reticulum Identity can create multiple
|
||||
destinations. This allows a single entity (a person, a device, a service) to
|
||||
present multiple endpoints without needing multiple cryptographic keypairs.
|
||||
|
||||
Once users connect to the network, anyone will be able to communicate with anyone
|
||||
else across all three sites.
|
||||
|
||||
Bridging Over the Internet
|
||||
==========================
|
||||
Transport Nodes and Instances
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
As the organisation grows, several new communities form in places too far away
|
||||
from the core network to be reachable over WiFi links. New gateways similar to those
|
||||
previously installed are set up for the new communities at the new sites D and E, but
|
||||
they are islanded from the core network, and only serve the local users.
|
||||
Reticulum distinguishes between two types of nodes: **Instances**
|
||||
and **Transport Nodes**. Every node running Reticulum is an Instance, but not
|
||||
every Instance is a Transport Node.
|
||||
|
||||
After investigating the options, it is found that it is possible to install an
|
||||
Internet connection at site A, and an interface on the Internet connection is
|
||||
configured for Reticulum on the Raspberry Pi at site A.
|
||||
A **Reticulum Instance** is any system running the Reticulum stack. It can create
|
||||
destinations, send and receive packets, establish links, and communicate with
|
||||
other nodes. It can also host destinations that are connectable for *anyone* else
|
||||
in the network. This means you can easily host globally available services from
|
||||
any location, including your home or office. Network-wide, global connectivity
|
||||
for all destinations is guaranteed, as long as there is *some* physical way to
|
||||
actually transport the packets. Instances are the default state and are appropriate for most end-user devices,
|
||||
such as phones, laptops, sensors, or any device that primarily consumes network services.
|
||||
|
||||
A member of the organisation at site D, named Dori, is willing to help by sharing
|
||||
the Internet connection she already has in her home, and is able to leave a Raspberry
|
||||
Pi running. A new Reticulum interface is configured on her Pi, connecting to the newly
|
||||
enabled Internet interface on the gateway at site A. Dori is now connected to both
|
||||
the nodes at her own local site (through the hill-top LoRa gateway), and all the
|
||||
combined users of sites A, B and C. She then enables transport on her node, and
|
||||
traffic from site D can now reach everyone at site A, B and C, and vice versa.
|
||||
A **Transport Node** is an Instance that has been explicitly configured to
|
||||
participate in network-wide transport. Transport nodes forward packets across
|
||||
hops, propagate announces, maintain path tables, and serve path requests on
|
||||
behalf of other nodes. When a destination sends an announce, Transport Nodes
|
||||
receive it, remember the path, and rebroadcast it to other interfaces. When a node
|
||||
needs to reach a destination it doesn't have a path for, Transport Nodes help
|
||||
resolve the path through the network.
|
||||
|
||||
Growth and Convergence
|
||||
======================
|
||||
Even devices hosting services or serving content should probably just be configured
|
||||
as instances, and themselves connect to wider networks via a Transport Node.
|
||||
In some situations, this may not be practical though, and as an example, it is
|
||||
entirely viable to host a personal Transport Node on a Raspberry Pi, while it
|
||||
is at the same time running an LXMF propagation node, and hosting your personal
|
||||
site or files over Reticulum.
|
||||
|
||||
As the organisation grows, more gateways are added to keep up with the growing user
|
||||
base. Some local gateways even add VHF radios and packet modems to reach outlying users
|
||||
and communities that are out of reach for the LoRa radios and WiFi backhauls.
|
||||
The distinction is important. **Not** every node should be a Transport Node:
|
||||
|
||||
As more sites, gateways and users are connected, the amount of coordination required
|
||||
is kept to a minimum. If one community wants to add connectivity to the next one
|
||||
over, it can simply be done without having to involve everyone or coordinate address
|
||||
space or routing tables.
|
||||
* **Resource consumption:** Transport nodes maintain path tables, process
|
||||
announces, and forward traffic. This requires memory and CPU resources that
|
||||
may be limited on low-powered devices.
|
||||
|
||||
With the added geographical coverage, the operators at site A one day find that
|
||||
the original internet bridged interfaces are no longer utilised. The network has
|
||||
converged to be completely self-connected, and the sites that were once poorly
|
||||
connected outliers are now an integral part of the network.
|
||||
* **Stability requirements:** Transport nodes contribute to network convergence.
|
||||
If Transport Nodes frequently go offline, path tables become stale and
|
||||
convergence suffers. Stable, always-on nodes make better Transport Nodes.
|
||||
|
||||
* **Bandwidth considerations:** Transport nodes process and rebroadcast network
|
||||
maintenance traffic. On very low-bandwidth mediums, having too many Transport
|
||||
Nodes will consume capacity that should be used for actual data.
|
||||
|
||||
In practice, a network typically has a relatively small number of Transport Nodes
|
||||
strategically placed to provide coverage and connectivity. End-user devices run
|
||||
as Instances, connecting through nearby Transport Nodes to reach the wider network.
|
||||
This pattern mirrors traditional networking where routers forward traffic while
|
||||
end hosts simply consume connectivity, but with the crucial difference that any
|
||||
node *can* become a router if needed, and the decision is yours to make based on
|
||||
your network's requirements.
|
||||
|
||||
Transport nodes also function as distributed cryptographic keystores. When a
|
||||
destination announces itself, Transport Nodes cache the public key and destination
|
||||
information. Other nodes can request unknown public keys from the network, and
|
||||
Transport Nodes respond with the cached information. This eliminates the need for
|
||||
a central directory service while ensuring that public keys remain available
|
||||
throughout the network.
|
||||
|
||||
Trustless Networking
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Traditional network security models assume high levels of trust at
|
||||
specific layers. You might trust your ISP to deliver packets without inspection,
|
||||
or trust your VPN provider to handle your traffic, or trust the network
|
||||
administrator to configure firewalls appropriately. These trust relationships
|
||||
create vulnerabilities and dependencies.
|
||||
|
||||
Reticulum is designed to function in **open, trustless environments**. This
|
||||
means the protocol makes no assumptions about the trustworthiness of the network
|
||||
infrastructure, the other participants, or the transport mediums. Every aspect
|
||||
of communication is secured cryptographically:
|
||||
|
||||
* **Traffic encryption:** All traffic to single destinations is encrypted using
|
||||
ephemeral keys.
|
||||
|
||||
* **Source anonymity:** Reticulum packets do not include source addresses.
|
||||
An observer intercepting a packet cannot determine who sent it, only who it is
|
||||
addressed to (unless IFAC is enabled, in which case nothing can be determined).
|
||||
This provides initiator anonymity by default.
|
||||
|
||||
* **Path verification:** The announce mechanism includes cryptographic signatures that
|
||||
prove the authenticity of destination announcements.
|
||||
|
||||
* **Unforgeable delivery confirmations:** When a destination proves receipt of a
|
||||
packet, the proof is signed with the destination's identity key. This prevents
|
||||
false acknowledgments and ensures reliable delivery verification.
|
||||
|
||||
* **Interface authentication:** When using Interface Access Codes (IFAC), packets
|
||||
on authenticated interfaces carry signatures derived from a shared secret. Only
|
||||
nodes with the correct network name and passphrase can generate valid packets, allowing creation
|
||||
of virtual private networks on shared mediums.
|
||||
|
||||
The trustless design has important consequences for network design:
|
||||
|
||||
* **Open-access networks are viable:** You can build networks that anyone can
|
||||
join without pre-approval. Because traffic is encrypted and authenticated end-
|
||||
to-end, participants cannot interfere with each other's private communication,
|
||||
even if they share the same transport infrastructure.
|
||||
|
||||
* **No traffic inspection or prioritization:** Because traffic contents and
|
||||
sources are opaque to intermediate nodes, there is no mechanism for filtering,
|
||||
prioritizing, or throttling traffic based on its type or origin. All traffic
|
||||
is treated equally. From a neutrality perspective, this is a feature.
|
||||
|
||||
* **Adversarial resilience:** The network can operate even if some nodes are
|
||||
malicious or controlled by adversaries. While a malicious Transport Node could
|
||||
refuse to forward certain traffic or drop packets, it cannot decrypt, modify,
|
||||
or impersonate legitimate traffic. Redundant paths and multiple Transport Nodes
|
||||
mitigate the impact of malicious nodes.
|
||||
|
||||
Of course, you can also create closed networks. Interface Access
|
||||
Codes allow you to restrict participation on specific interfaces. Network
|
||||
Identities enable you to verify that discovered interfaces belong to trusted
|
||||
operators. Blackhole management lets you block malicious identities. Reticulum
|
||||
provides both the tools for open networks and the controls for closed ones. The
|
||||
choice is yours based on your requirements.
|
||||
|
||||
|
||||
Heterogeneous Connectivity
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In conventional networking, mixing different transport mediums typically requires
|
||||
gateways, translation layers, and careful configuration. A WiFi network doesn't
|
||||
natively interoperate with a packet radio network without additional infrastructure,
|
||||
and you can't just download a car over a serial port, or send an encrypted message
|
||||
in a QR code.
|
||||
|
||||
Reticulum treats **heterogeneity as a core premise**. The protocol is designed
|
||||
to seamlessly mix mediums with vastly different characteristics:
|
||||
|
||||
* **Bandwidth:** LoRa links operating at a few hundred bits per second can
|
||||
interconnect with gigabit Ethernet backbones. Reticulum automatically manages
|
||||
the flow of information, prioritizing local traffic on slow segments while
|
||||
allowing global convergence.
|
||||
|
||||
* **Latency:** Satellite links with multi-second latency can coexist with local
|
||||
links measured in milliseconds. The transport system handles timing, asynchronous
|
||||
delivery and retransmissions transparently.
|
||||
|
||||
* **Topology:** Point-to-point microwave links, broadcast radio networks,
|
||||
switched Ethernet fabrics, and virtual tunnels over the Internet can all be
|
||||
part of the same Reticulum network.
|
||||
|
||||
* **Reliability:** Intermittent connections that come and go (such as mobile
|
||||
devices or opportunistic radio contacts) can participate alongside always-on
|
||||
infrastructure. Reticulum gracefully handles link loss and reconnection.
|
||||
|
||||
This heterogeneity is achieved through several design elements:
|
||||
|
||||
* **Expandable, medium-agnostic interface system:** Reticulum communicates with the physical
|
||||
world through interface modules. Adding support for a new medium is a matter
|
||||
of implementing an interface class. The protocol itself remains unchanged.
|
||||
|
||||
* **Interface modes:** Different modes (``full``, ``gateway``, ``access_point``,
|
||||
``roaming``, ``boundary``) allow you to configure how interfaces interact with
|
||||
the wider network based on their characteristics and role.
|
||||
|
||||
* **Announce propagation rules:** Announces are forwarded between interfaces
|
||||
according to rules that account for bandwidth limitations and interface modes.
|
||||
Slow segments are not overwhelmed by traffic from fast segments.
|
||||
|
||||
* **Local traffic prioritization:** When bandwidth is constrained, Reticulum
|
||||
prioritizes announces for nearby destinations. This ensures that local
|
||||
connectivity remains functional even when global convergence is incomplete.
|
||||
|
||||
For network designers, this means you are free to use whatever mediums are
|
||||
available, affordable, or appropriate for your situation. You might use LoRa for
|
||||
wide-area low-bandwidth coverage, WiFi for local high-capacity links, I2P for
|
||||
anonymous Internet connectivity, and Ethernet for infrastructure backhauls, all
|
||||
within the same network. Reticulum handles the translation and coordination
|
||||
automatically.
|
||||
|
||||
The key design consideration is not whether different mediums can work together
|
||||
(they can), but **how** they should work together based on your goals. A node
|
||||
with multiple interfaces spanning heterogeneous mediums needs to be configured
|
||||
with appropriate interface modes so that traffic flows efficiently. A gateway
|
||||
connecting a slow LoRa segment to a fast Internet backbone should be configured
|
||||
differently than a mobile device roaming between radio cells.
|
||||
@@ -0,0 +1,305 @@
|
||||
.. _software-main:
|
||||
|
||||
************************
|
||||
Programs Using Reticulum
|
||||
************************
|
||||
|
||||
This chapter provides a non-exhaustive list of notable programs, systems and application-layer
|
||||
protocols that have been built using Reticulum.
|
||||
|
||||
These programs will let you get a feel for how Reticulum works. Most of them have been designed
|
||||
to run well even over slow networks based on LoRa or packet radio, but all can also be used over fast
|
||||
links, such as local WiFi, wired Ethernet, the Internet, or any combination.
|
||||
|
||||
As such, it is easy to get started experimenting, without having to set up any radio
|
||||
transceivers or infrastructure just to try it out. Launching the programs on separate
|
||||
devices connected to the same WiFi network is enough to get started, and physical
|
||||
radio interfaces can then be added later.
|
||||
|
||||
Programs & Utilities
|
||||
====================
|
||||
|
||||
Many different applications using Reticulum already exist, serving a wide variety of purposes
|
||||
from day-to-day communication and information sharing to systems administration and tackling
|
||||
advanced networking and communications challenges.
|
||||
|
||||
Development of Reticulum-based applications and systems is ongoing, so consider this list
|
||||
a non-exhaustive starting point of *some* of the options available. With a bit of searching,
|
||||
primarily over Reticulum itself, you will find many more interesting things.
|
||||
|
||||
Remote Shell
|
||||
^^^^^^^^^^^^
|
||||
|
||||
The `rnsh <https://github.com/acehoss/rnsh>`_ program lets you establish fully interactive
|
||||
remote shell sessions over Reticulum. It also allows you to pipe any program to or from a
|
||||
remote system, and is similar to how ``ssh`` works. The ``rnsh`` program is very efficient, and
|
||||
can facilitate fully interactive shell sessions, even over extremely low-bandwidth links,
|
||||
such as LoRa or packet radio.
|
||||
|
||||
In addition to the default, fully interactive terminal mode,
|
||||
for extremely limited links, ``rnsh`` offers line-interactive mode, allowing you to interact
|
||||
with remote systems, even when link throughput is counted in a few hundreds of bits per second.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
Nomad Network
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
The terminal-based program `Nomad Network <https://github.com/markqvist/nomadnet>`_
|
||||
provides a complete encrypted communications suite built with Reticulum. It features
|
||||
encrypted messaging (both direct and delayed-delivery for offline users), file sharing,
|
||||
and has a built-in text-browser and page server with support for dynamically rendered pages,
|
||||
user authentication and more.
|
||||
|
||||
.. image:: screenshots/nomadnet_3.png
|
||||
:target: https://github.com/markqvist/nomadnet
|
||||
|
||||
`Nomad Network <https://github.com/markqvist/nomadnet>`_ is a user-facing client
|
||||
for the messaging and information-sharing protocol LXMF.
|
||||
|
||||
RNS Page Node
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
`RNS Page Node <https://git.quad4.io/RNS-Things/rns-page-node>`_ is a simple way to serve pages and files to any other Nomad Network compatible client. Drop-in replacement for NomadNet nodes that primarily serve pages and files.
|
||||
|
||||
|
||||
Retipedia
|
||||
^^^^^^^^^
|
||||
|
||||
You can host the entirity of Wikipedia (or any ``.zim``) file to other Nomad Network clients using `Retipedia <https://github.com/RFnexus/Retipedia>`_.
|
||||
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
Sideband
|
||||
^^^^^^^^
|
||||
|
||||
If you would rather use an LXMF client with a graphical user interface, you can take
|
||||
a look at `Sideband <https://unsigned.io/sideband>`_, which is available for Android,
|
||||
Linux, macOS and Windows. Sideband is an advanced LXMF and LXST client, and a multi-purpose Reticulum
|
||||
utility, with features and functionality targeted at advanced users.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/sideband_devices.webp
|
||||
:align: center
|
||||
:target: https://unsigned.io/sideband
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/sideband_devices.png
|
||||
:align: center
|
||||
:target: https://unsigned.io/sideband
|
||||
|
||||
Sideband allows you to communicate with other people or LXMF-compatible
|
||||
systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR
|
||||
Paper Messages, or anything else Reticulum supports.
|
||||
|
||||
It also interoperates with all other LXMF clients, and provides advanced features such as voice messaging,
|
||||
real-time voice calls, file attachments, private telemetry sharing, and a full
|
||||
plugin system for expandability.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
MeshChatX
|
||||
^^^^^^^^
|
||||
|
||||
A `Reticulum MeshChat fork from the future <https://git.quad4.io/RNS-Things/MeshChatX>`_, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ project, and is not affiliated with the original project, but is a much more up-to-date, comprehensive and well-maintained fork.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/meshchatx.webp
|
||||
:align: center
|
||||
:target: https://git.quad4.io/RNS-Things/MeshChatX
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/meshchatx.png
|
||||
:align: center
|
||||
:target: https://git.quad4.io/RNS-Things/MeshChatX
|
||||
|
||||
|
||||
Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
Reticulum Relay Chat
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
`Reticulum Relay Chat <https://rrc.kc1awv.net/>`_ is a live chat system built on top of the Reticulum Network Stack. It exists to let people talk to each other in real time over Reticulum without dragging in message databases, synchronization engines, or architectural commitments they did not ask for.
|
||||
|
||||
The `rrcd <https://github.com/kc1awv/rrcd>`_ program provides a functional, reference RRC hub-server daemon implementation. RRC user clients include `rrc-gui <https://github.com/kc1awv/rrc-gui>`_ and `rrc-web <https://github.com/kc1awv/rrc-web>`_.
|
||||
|
||||
RRC is closer in spirit to IRC than to modern “everything platforms.” You connect, you join a room, you talk, and then you leave. If you were present, you saw the conversation. If you were not, the conversation did not wait for you. This is not an accident. This is the entire design.
|
||||
|
||||
RetiBBS
|
||||
^^^^^^^
|
||||
|
||||
`RetiBBS <https://github.com/kc1awv/RetiBBS>`_ is a bulletin board system implementation for Reticulum networks.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/retibbs.webp
|
||||
:align: center
|
||||
:target: https://github.com/kc1awv/RetiBBS
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/retibbs.png
|
||||
:align: center
|
||||
:target: https://github.com/kc1awv/RetiBBS
|
||||
|
||||
RetiBBS allows users to communicate through message boards in a secure manner.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
RBrowser
|
||||
^^^^^^^^
|
||||
|
||||
The `rBrowser <https://github.com/fr33n0w/rBrowser>`_ program is a cross-platform, standalone, web-based browser for exploring NomadNetwork Nodes over Reticulum Network. It automatically discovers NomadNet nodes through network announces and provides a user-friendly interface for browsing distributed content with Micron markup support.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/rbrowser.webp
|
||||
:align: center
|
||||
:target: https://github.com/fr33n0w/rBrowser
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/rbrowser.png
|
||||
:align: center
|
||||
:target: https://github.com/fr33n0w/rBrowser
|
||||
|
||||
Includes useful features like automatic listening for announce, adding nodes to favorites, browsing and rendering any kind of NomadNet links, downloading files from remote nodes, a unique local NomadNet Search Engine and more.
|
||||
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
Reticulum Network Telephone
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The ``rnphone`` program, included as part of the `LXST <https://github.com/markqvist/LXST>`_ package is a command-line Reticulum telephone utility and daemon, that allows building physical, hardware telephones for LXST and Reticulum, as well as simply performing calls via the command line.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/rnphone.webp
|
||||
:align: center
|
||||
:target: https://github.com/markqvist/LXST
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/rnphone.jpg
|
||||
:align: center
|
||||
:target: https://github.com/markqvist/LXST
|
||||
|
||||
It supports interfacing directly with hardware peripherals such as GPIO keypads and LCD displays, providing a modular system for building secure hardware telephones.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
LXST Phone
|
||||
^^^^^^^^^^
|
||||
|
||||
The `LXST Phone <https://github.com/kc1awv/lxst_phone>`_ program is a cross-platform desktop application for performing LXST voice calls over Reticulum.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/lxst_phone.webp
|
||||
:align: center
|
||||
:target: https://github.com/kc1awv/lxst_phone
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/lxst_phone.png
|
||||
:align: center
|
||||
:target: https://github.com/kc1awv/lxst_phone
|
||||
|
||||
It supports various advanced features such as SAS verification, peer blocking, rate limiting, encrypted call history storage and contact management.
|
||||
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
LXMFy
|
||||
^^^^^
|
||||
|
||||
`LXMFy <https://lxmfy.quad4.io/>`_ is a comprehensive and advanced bot creation framework for LXMF, that allows building any kind of automation or bot system running over LXMF and Reticulum. `Bot implementations exist <https://github.com/lxmfy/awesome-lxmfy-bots>`_ for Home Assistant control, LLM integrations, and various other purposes.
|
||||
|
||||
|
||||
LXMF Interactive Client
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
`LXMF Interactive Client <https://github.com/fr33n0w/lxmf-cli>`_ is a feature-rich, terminal-based LXMF messaging client with many advanced features and an extensible plugin architecture.
|
||||
|
||||
RNS FileSync
|
||||
^^^^^^^^^^^^
|
||||
|
||||
The `RNS FileSync <https://git.quad4.io/RNS-Things/RNS-Filesync>`_ program enables automatic file synchronization between devices without requiring central servers, internet connectivity, or cloud services. It works over any network medium supported by Reticulum, including radio, LoRa, WiFi, or the internet, making it ideal for off-grid, privacy-focused, and resilient file sharing.
|
||||
|
||||
|
||||
Micron Parser JS
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
`Micron Parser JS <https://github.com/RFnexus/micron-parser-js>`_ is the JavaScript-based parser for the Micron markup language, that most web-based Nomad Network browsers use. If you want to make utilities or tools that display Micron pages, this library is essential.
|
||||
|
||||
|
||||
RNMon
|
||||
^^^^^
|
||||
|
||||
`RNMon <https://github.com/lbatalha/rnmon>`_ is a monitoring daemon designed to monitor the status of multiple RNS applications and push the metrics to an InfluxDB instance over the influx line protocol.
|
||||
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
Protocols
|
||||
=========
|
||||
|
||||
A number of standard protocols have emerged through real-world usage and testing in the Reticulum community. While you may sometimes want to use completely custom protocols and implementations when writing Reticulum-based software, using these protocols provides application developers with an easy way to implement advanced functionality quickly and effortlessly. Using them also ensures compatibility and interoperability between many different client applications, creating an open communications ecosystem where users are free to choose the applications that suit their needs, while remaining connected to everyone else.
|
||||
|
||||
LXMF
|
||||
^^^^
|
||||
|
||||
`LXMF <https://github.com/markqvist/lxmf>`_ is a simple and flexible messaging format and delivery protocol that allows a wide variety of applications, while using as little bandwidth as possible. It offers zero-conf message routing, end-to-end encryption and Forward Secrecy, and can be transported over any kind of medium that Reticulum supports.
|
||||
|
||||
LXMF is efficient enough that it can deliver messages over extremely low-bandwidth systems such as packet radio or LoRa. Encrypted LXMF messages can also be encoded as QR-codes or text-based URIs, allowing completely analog paper message transport.
|
||||
|
||||
Using Propagation Nodes, LXMF also offer a way to store and forward messages to users or endpoints that are not directly reachable at the time of message emission.
|
||||
|
||||
LXST
|
||||
^^^^
|
||||
|
||||
`LXST <https://github.com/markqvist/lxst>`_ is a simple and flexible real-time streaming format and delivery protocol that allows a wide variety of applications, while using as little bandwidth as possible. It is built on top of Reticulum and offers zero-conf stream routing, end-to-end encryption and Forward Secrecy, and can be transported over any kind of medium that Reticulum supports. It currently powers real-time voice and telephony applications over Reticulum.
|
||||
|
||||
RRC
|
||||
^^^
|
||||
|
||||
The `Reticulum Relay Chat <https://rrc.kc1awv.net/>`_ protocol, is a live chat system built on top of the Reticulum Network Stack. It exists to provide near real-time group communication without dragging in message history databases, federation machinery, or architectural guilt.
|
||||
|
||||
RRC is intentionally simple. It does not pretend to be email, a mailbox, or a distributed archive. It behaves more like a conversation in a room. If you were there, you heard it. If you were not, you did not. That is not a bug, that is the point.
|
||||
|
||||
Interface Modules & Connectivity Resources
|
||||
==========================================
|
||||
|
||||
This section provides a list of various community-provided interface modules, guides and resources for creating Reticulum networks over special or challenging mediums.
|
||||
|
||||
* Custom interface module for running `RNS over HTTP <https://git.quad4.io/RNS-Things/RNS-over-HTTP>`_
|
||||
* Guide for running `Reticulum over ICMP <https://github.com/matvik22000/rns-over-icmp>`_ using ``PipeInterface``
|
||||
* Guide for running `Reticulum over DNS <https://github.com/markqvist/Reticulum/discussions/1002>`_ with Iodine
|
||||
* Guide for running `Reticulum over HF radio <https://github.com/RFnexus/reticulum-over-hf>`_
|
||||
* `Modem73 <https://github.com/RFnexus/modem73>`_ is a KISS TNC OFDM modem frontend that can be used with Reticulum
|
||||
@@ -16,12 +16,12 @@ Donations are gratefully accepted via the following channels:
|
||||
Monero:
|
||||
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
|
||||
|
||||
Ethereum:
|
||||
0x81F7B979fEa6134bA9FD5c701b3501A2e61E897a
|
||||
|
||||
Bitcoin:
|
||||
3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq
|
||||
bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6
|
||||
|
||||
Ethereum:
|
||||
0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E
|
||||
|
||||
Liberapay:
|
||||
https://liberapay.com/Reticulum/
|
||||
|
||||
@@ -31,17 +31,34 @@ Donations are gratefully accepted via the following channels:
|
||||
Are certain features in the development roadmap are important to you or your
|
||||
organisation? Make them a reality quickly by sponsoring their implementation.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
Provide Feedback
|
||||
================
|
||||
All feedback on the usage, functioning and potential dysfunctioning of any and
|
||||
Feedback on the usage, functioning and potential dysfunctioning of any and
|
||||
all components of the system is very valuable to the continued development and
|
||||
improvement of Reticulum.
|
||||
improvement of Reticulum. But...
|
||||
|
||||
.. warning::
|
||||
|
||||
**Think before you speak**. As time has shown, over 80% of the "feedback",
|
||||
"bug reports" and "advice" the Reticulum project has received has been
|
||||
irrelevant noise, stemming from erroneous assumptions, misunderstanding the
|
||||
foundational functionality or philosophy behind the system, or simply
|
||||
the malinformed (but overly opinionated) personal preferences of individual
|
||||
drive-by architects. This wastes the time of everyone involved.
|
||||
|
||||
The Reticulum project is not a public teahouse for serving the attention
|
||||
needs of random bypassers, but a highly complex system engineered and
|
||||
refined over more than a decade, designed to provide communication and
|
||||
connectivity guarantees in highly adversarial environments.
|
||||
|
||||
If you want to voice your opinion, it better be well-informed, and we
|
||||
expect you to have a comprehensive and solid foundation for your points
|
||||
of view. Everything else will be ignored.
|
||||
|
||||
Absolutely no automated analytics, telemetry, error
|
||||
reporting or statistics is collected and reported by Reticulum under any
|
||||
circumstances, so we rely on old-fashioned human feedback.
|
||||
|
||||
Contribute Code
|
||||
===============
|
||||
Join us on `the GitHub repository <https://github.com/markqvist/reticulum>`_ to
|
||||
report issues, suggest functionality and contribute code to Reticulum.
|
||||
|
||||
@@ -3,65 +3,35 @@
|
||||
***********************
|
||||
Understanding Reticulum
|
||||
***********************
|
||||
This chapter will briefly describe the overall purpose and operating principles of Reticulum.
|
||||
It should give you an overview of how the stack works, and an understanding of how to
|
||||
develop networked applications using Reticulum.
|
||||
This chapter will briefly describe the overall purpose and operating principles of Reticulum. It should give you an overview of how the stack works, and an understanding of how to develop networked applications using Reticulum.
|
||||
|
||||
This chapter is not an exhaustive source of information on Reticulum, at least not yet. Currently,
|
||||
the only complete repository, and final authority on how Reticulum actually functions, is the Python
|
||||
reference implementation and API reference. That being said, this chapter is an essential resource in
|
||||
understanding how Reticulum works from a high-level perspective, along with the general principles of
|
||||
Reticulum, and how to apply them when creating your own networks or software.
|
||||
This chapter is not an exhaustive source of information on Reticulum, at least not yet. Currently, the only complete repository, and final authority on how Reticulum actually functions, is the Python reference implementation and API reference. That being said, this chapter is an essential resource in understanding how Reticulum works from a high-level perspective, along with the general principles of Reticulum, and how to apply them when creating your own networks or software.
|
||||
|
||||
After reading this document, you should be well-equipped to understand how a Reticulum network
|
||||
operates, what it can achieve, and how you can use it yourself. If you want to help out with the
|
||||
development, this is also the place to start, since it will provide a pretty clear overview of the
|
||||
sentiments and the philosophy behind Reticulum, what problems it seeks to solve, and how it
|
||||
approaches those solutions.
|
||||
After reading this chapter, you should be well-equipped to understand how a Reticulum network operates, what it can achieve, and how you can use it yourself. This chapter also seeks to provide an overview of the sentiments and the philosophy behind Reticulum, what problems it seeks to solve, and how it approaches those solutions.
|
||||
|
||||
.. _understanding-motivation:
|
||||
|
||||
Motivation
|
||||
==========
|
||||
|
||||
The primary motivation for designing and implementing Reticulum has been the current lack of
|
||||
reliable, functional and secure minimal-infrastructure modes of digital communication. It is my
|
||||
belief that it is highly desirable to create a reliable and efficient way to set up long-range digital
|
||||
communication networks that can securely allow exchange of information between people and
|
||||
machines, with no central point of authority, control, censorship or barrier to entry.
|
||||
The primary motivation for designing and implementing Reticulum has been the current lack of reliable, functional and secure minimal-infrastructure modes of digital communication. It is my belief that it is highly desirable to create a reliable and efficient way to set up long-range digital communication networks that can securely allow exchange of information between people and machines, with no central point of authority, control, censorship or barrier to entry.
|
||||
|
||||
Almost all of the various networking systems in use today share a common limitation: They
|
||||
require large amounts of coordination and centralised trust and power to function. To join such networks, you need approval
|
||||
of gatekeepers in control. This need for coordination and trust inevitably leads to an environment of
|
||||
central control, where it's very easy for infrastructure operators or governments to control or alter
|
||||
traffic, and censor or persecute unwanted actors. It also makes it completely impossible to freely deploy
|
||||
and use networks at will, like one would use other common tools that enhance individual agency and freedom.
|
||||
Almost all of the various networking systems in use today share a common limitation: They require large amounts of coordination and centralised trust and power to function. To join such networks, you need approval of gatekeepers in control. This need for coordination and trust inevitably leads to an environment of central control, where it's very easy for infrastructure operators or governments to control or alter traffic, and censor or persecute unwanted actors. It also makes it completely impossible to freely deploy and use networks at will, like one would use other common tools that enhance individual agency and freedom.
|
||||
|
||||
Reticulum aims to require as little coordination and trust as possible. It aims to make secure,
|
||||
anonymous and permissionless networking and information exchange a tool that anyone can just pick up and use.
|
||||
Reticulum aims to require as little coordination and trust as possible. It aims to make secure, anonymous and permissionless networking and information exchange a tool that anyone can just pick up and use.
|
||||
|
||||
Since Reticulum is completely medium agnostic, it can be used to build networks on whatever is best
|
||||
suited to the situation, or whatever you have available. In some cases, this might be packet radio
|
||||
links over VHF frequencies, in other cases it might be a 2.4 GHz
|
||||
network using off-the-shelf radios, or it might be using common LoRa development boards.
|
||||
Since Reticulum is completely medium agnostic, it can be used to build networks on whatever is best suited to the situation, or whatever you have available. In some cases, this might be packet radio links over VHF frequencies, in other cases it might be a 2.4 GHz network using off-the-shelf radios, or it might be using common LoRa development boards.
|
||||
|
||||
At the time of release of this document, the fastest and easiest setup for development and testing is using
|
||||
LoRa radio modules with an open source firmware (see the section :ref:`Reference Setup<understanding-referencesystem>`),
|
||||
connected to any kind of computer or mobile device that Reticulum can run on.
|
||||
At the time of release of this document, the fastest and easiest setup for development and testing is using LoRa radio modules with an open source firmware (see the section :ref:`Reference Setup<understanding-referencesystem>`), connected to any kind of computer or mobile device that Reticulum can run on.
|
||||
|
||||
The ultimate aim of Reticulum is to allow anyone to be their own network operator, and to make it
|
||||
cheap and easy to cover vast areas with a myriad of independent, interconnectable and autonomous networks.
|
||||
Reticulum **is not** *one network*, it **is a tool** to build *thousands of networks*. Networks without
|
||||
kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate
|
||||
with each other, and require no central oversight. Networks for human beings. *Networks for the people*.
|
||||
The ultimate aim of Reticulum is to allow anyone to be their own network operator, and to make it cheap and easy to cover vast areas with a myriad of independent, interconnectable and autonomous networks. Reticulum **is not** *one network*, it **is a tool** to build *thousands of networks*. Networks without kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate with each other, and require no central oversight. Networks for human beings. *Networks for the people*.
|
||||
|
||||
.. _understanding-goals:
|
||||
|
||||
Goals
|
||||
=====
|
||||
|
||||
To be as widely usable and efficient to deploy as possible, the following goals have been used to
|
||||
guide the design of Reticulum:
|
||||
To be as widely usable and efficient to deploy as possible, the following goals have been used to guide the design of Reticulum:
|
||||
|
||||
|
||||
* **Fully useable as open source software stack**
|
||||
@@ -84,18 +54,18 @@ guide the design of Reticulum:
|
||||
* **Unlicensed use**
|
||||
Reticulum shall be functional over physical communication mediums that do not require any
|
||||
form of license to use. Reticulum must be designed in a way, so it is usable over ISM radio
|
||||
frequency bands, and can provide functional long distance links in such conditions, for example
|
||||
by connecting a modem to a PMR or CB radio, or by using LoRa or WiFi modules.
|
||||
frequency bands, and can provide functional long distance links in such conditions, for
|
||||
example by connecting a modem to a PMR or CB radio, or by using LoRa or WiFi modules.
|
||||
* **Supplied software**
|
||||
In addition to the core networking stack and API, that allows a developer to build
|
||||
applications with Reticulum, a basic set of Reticulum-based communication tools must be
|
||||
implemented and released along with Reticulum itself. These shall serve both as a
|
||||
functional, basic communication suite, and as an example and learning resource to others wishing
|
||||
to build applications with Reticulum.
|
||||
functional, basic communication suite, and as an example and learning resource to others
|
||||
wishing to build applications with Reticulum.
|
||||
* **Ease of use**
|
||||
The reference implementation of Reticulum is written in Python, to make it easy to use
|
||||
and understand. A programmer with only basic experience should be able to use
|
||||
Reticulum to write networked applications.
|
||||
The reference implementation of Reticulum is written in Python, to make it easy to use and
|
||||
understand. A programmer with only basic experience should be able to use Reticulum to write
|
||||
networked applications.
|
||||
* **Low cost**
|
||||
It shall be as cheap as possible to deploy a communication system based on Reticulum. This
|
||||
should be achieved by using cheap off-the-shelf hardware that potential users might already
|
||||
@@ -107,53 +77,26 @@ guide the design of Reticulum:
|
||||
Introduction & Basic Functionality
|
||||
==================================
|
||||
|
||||
Reticulum is a networking stack suited for high-latency, low-bandwidth links. Reticulum is at its
|
||||
core a *message oriented* system. It is suited for both local point-to-point or point-to-multipoint
|
||||
scenarios where all nodes are within range of each other, as well as scenarios where packets need
|
||||
to be transported over multiple hops in a complex network to reach the recipient.
|
||||
Reticulum is a networking stack suited for high-latency, low-bandwidth links. Reticulum is at its core a *message oriented* system. It is suited for both local point-to-point or point-to-multipoint scenarios where all nodes are within range of each other, as well as scenarios where packets need to be transported over multiple hops in a complex network to reach the recipient.
|
||||
|
||||
Reticulum does away with the idea of addresses and ports known from IP, TCP and UDP. Instead
|
||||
Reticulum uses the singular concept of *destinations*. Any application using Reticulum as its
|
||||
networking stack will need to create one or more destinations to receive data, and know the
|
||||
destinations it needs to send data to.
|
||||
Reticulum does away with the idea of addresses and ports known from IP, TCP and UDP. Instead Reticulum uses the singular concept of *destinations*. Any application using Reticulum as its networking stack will need to create one or more destinations to receive data, and know the destinations it needs to send data to.
|
||||
|
||||
All destinations in Reticulum are *represented* as a 16 byte hash. This hash is derived from truncating a full
|
||||
SHA-256 hash of identifying characteristics of the destination. To users, the destination addresses
|
||||
will be displayed as 16 hexadecimal bytes, like this example: ``<13425ec15b621c1d928589718000d814>``.
|
||||
All destinations in Reticulum are *represented* as a 16 byte hash. This hash is derived from truncating a full SHA-256 hash of identifying characteristics of the destination. To users, the destination addresses will be displayed as 16 hexadecimal bytes, like this example: ``<13425ec15b621c1d928589718000d814>``.
|
||||
|
||||
The truncation size of 16 bytes (128 bits) for destinations has been chosen as a reasonable trade-off
|
||||
between address space
|
||||
and packet overhead. The address space accommodated by this size can support many billions of
|
||||
simultaneously active devices on the same network, while keeping packet overhead low, which is
|
||||
essential on low-bandwidth networks. In the very unlikely case that this address space nears
|
||||
congestion, a one-line code change can upgrade the Reticulum address space all the way up to 256
|
||||
bits, ensuring the Reticulum address space could potentially support galactic-scale networks.
|
||||
This is obviously complete and ridiculous over-allocation, and as such, the current 128 bits should
|
||||
be sufficient, even far into the future.
|
||||
The truncation size of 16 bytes (128 bits) for destinations has been chosen as a reasonable trade-off between address space and packet overhead. The address space accommodated by this size can support many billions of simultaneously active devices on the same network, while keeping packet overhead low, which is essential on low-bandwidth networks. In the very unlikely case that this address space nears congestion, a one-line code change can upgrade the Reticulum address space all the way up to 256 bits, ensuring the Reticulum address space could potentially support galactic-scale networks. This is obviously complete and ridiculous over-allocation, and as such, the current 128 bits should be sufficient, even far into the future.
|
||||
|
||||
By default Reticulum encrypts all data using elliptic curve cryptography and AES. Any packet sent to a
|
||||
destination is encrypted with a per-packet derived key. Reticulum can also set up an encrypted
|
||||
channel to a destination, called a *Link*. Both data sent over Links and single packets offer
|
||||
*Initiator Anonymity*. Links additionally offer *Forward Secrecy* by default, employing an Elliptic Curve
|
||||
Diffie Hellman key exchange on Curve25519 to derive per-link ephemeral keys. Asymmetric, link-less
|
||||
packet communication can also provide forward secrecy, with automatic key ratcheting, by enabling
|
||||
ratchets on a per-destination basis. The multi-hop transport, coordination, verification and reliability
|
||||
layers are fully autonomous and also based on elliptic curve cryptography.
|
||||
By default Reticulum encrypts all data using elliptic curve cryptography and AES. Any packet sent to a destination is encrypted with a per-packet derived key. Reticulum can also set up an encrypted channel to a destination, called a *Link*. Both data sent over Links and single packets offer *Initiator Anonymity*. Links additionally offer *Forward Secrecy* by default, employing an Elliptic Curve Diffie Hellman key exchange on Curve25519 to derive per-link ephemeral keys. Asymmetric, link-less packet communication can also provide forward secrecy, with automatic key ratcheting, by enabling ratchets on a per-destination basis. The multi-hop transport, coordination, verification and reliability layers are fully autonomous and also based on elliptic curve cryptography.
|
||||
|
||||
Reticulum also offers symmetric key encryption for group-oriented communications, as well as
|
||||
unencrypted packets (for local broadcast purposes **only**).
|
||||
Reticulum also offers symmetric key encryption for group-oriented communications, as well as unencrypted packets (for local broadcast purposes **only**).
|
||||
|
||||
Reticulum can connect to a variety of interfaces such as radio modems, data radios and serial ports,
|
||||
and offers the possibility to easily tunnel Reticulum traffic over IP links such as the Internet or
|
||||
private IP networks.
|
||||
Reticulum can connect to a variety of interfaces such as radio modems, data radios and serial ports, and offers the possibility to easily tunnel Reticulum traffic over IP links such as the Internet or private IP networks.
|
||||
|
||||
.. _understanding-destinations:
|
||||
|
||||
Destinations
|
||||
------------
|
||||
|
||||
To receive and send data with the Reticulum stack, an application needs to create one or more
|
||||
destinations. Reticulum uses three different basic destination types, and one special:
|
||||
To receive and send data with the Reticulum stack, an application needs to create one or more destinations. Reticulum uses three different basic destination types, and one special:
|
||||
|
||||
|
||||
* **Single**
|
||||
@@ -166,9 +109,9 @@ destinations. Reticulum uses three different basic destination types, and one sp
|
||||
number of users, or should be readable by anyone. Traffic to a *plain* destination is not encrypted.
|
||||
Generally, *plain* destinations can be used for broadcast information intended to be public.
|
||||
Plain destinations are only reachable directly, and packets addressed to plain destinations are
|
||||
never transported over multiple hops in the network. To be transportable over multiple hops in Reticulum, information
|
||||
*must* be encrypted, since Reticulum uses the per-packet encryption to verify routing paths and
|
||||
keep them alive.
|
||||
never transported over multiple hops in the network. To be transportable over multiple hops in
|
||||
Reticulum, information *must* be encrypted, since Reticulum uses the per-packet encryption to verify
|
||||
routing paths and keep them alive.
|
||||
* **Group**
|
||||
The *group* special destination type, that defines a symmetrically encrypted virtual destination.
|
||||
Data sent to this destination will be encrypted with a symmetric key, and will be readable by
|
||||
@@ -187,16 +130,11 @@ destinations. Reticulum uses three different basic destination types, and one sp
|
||||
Destination Naming
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Destinations are created and named in an easy to understand dotted notation of *aspects*, and
|
||||
represented on the network as a hash of this value. The hash is a SHA-256 truncated to 128 bits. The
|
||||
top level aspect should always be a unique identifier for the application using the destination.
|
||||
The next levels of aspects can be defined in any way by the creator of the application.
|
||||
Destinations are created and named in an easy to understand dotted notation of *aspects*, and represented on the network as a hash of this value. The hash is a SHA-256 truncated to 128 bits. The top level aspect should always be a unique identifier for the application using the destination. The next levels of aspects can be defined in any way by the creator of the application.
|
||||
|
||||
Aspects can be as long and as plentiful as required, and a resulting long destination name will not
|
||||
impact efficiency, as names are always represented as truncated SHA-256 hashes on the network.
|
||||
Aspects can be as long and as plentiful as required, and a resulting long destination name will not impact efficiency, as names are always represented as truncated SHA-256 hashes on the network.
|
||||
|
||||
As an example, a destination for a environmental monitoring application could be made up of the
|
||||
application name, a device type and measurement type, like this:
|
||||
As an example, a destination for a environmental monitoring application could be made up of the application name, a device type and measurement type, like this:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
@@ -206,11 +144,7 @@ application name, a device type and measurement type, like this:
|
||||
full name : environmentlogger.remotesensor.temperature
|
||||
hash : 4faf1b2e0a077e6a9d92fa051f256038
|
||||
|
||||
For the *single* destination, Reticulum will automatically append the associated public key as a
|
||||
destination aspect before hashing. This is done to ensure only the correct destination is reached,
|
||||
since anyone can listen to any destination name. Appending the public key ensures that a given
|
||||
packet is only directed at the destination that holds the corresponding private key to decrypt the
|
||||
packet.
|
||||
For the *single* destination, Reticulum will automatically append the associated public key as a destination aspect before hashing. This is done to ensure only the correct destination is reached, since anyone can listen to any destination name. Appending the public key ensures that a given packet is only directed at the destination that holds the corresponding private key to decrypt the packet.
|
||||
|
||||
**Take note!** There is a very important concept to understand here:
|
||||
|
||||
@@ -219,16 +153,9 @@ packet.
|
||||
* Each destination that does so will still have a unique destination hash, and thus be uniquely
|
||||
addressable, because their public keys will differ.
|
||||
|
||||
In actual use of *single* destination naming, it is advisable not to use any uniquely identifying
|
||||
features in aspect naming. Aspect names should be general terms describing what kind of destination
|
||||
is represented. The uniquely identifying aspect is always achieved by appending the public key,
|
||||
which expands the destination into a uniquely identifiable one. Reticulum does this automatically.
|
||||
In actual use of *single* destination naming, it is advisable not to use any uniquely identifying features in aspect naming. Aspect names should be general terms describing what kind of destination is represented. The uniquely identifying aspect is always achieved by appending the public key, which expands the destination into a uniquely identifiable one. Reticulum does this automatically.
|
||||
|
||||
Any destination on a Reticulum network can be addressed and reached simply by knowing its
|
||||
destination hash (and public key, but if the public key is not known, it can be requested from the
|
||||
network simply by knowing the destination hash). The use of app names and aspects makes it easy to
|
||||
structure Reticulum programs and makes it possible to filter what information and data your program
|
||||
receives.
|
||||
Any destination on a Reticulum network can be addressed and reached simply by knowing its destination hash (and public key, but if the public key is not known, it can be requested from the network simply by knowing the destination hash). The use of app names and aspects makes it easy to structure Reticulum programs and makes it possible to filter what information and data your program receives.
|
||||
|
||||
To recap, the different destination types should be used in the following situations:
|
||||
|
||||
@@ -240,56 +167,30 @@ To recap, the different destination types should be used in the following situat
|
||||
* **Plain**
|
||||
When plain-text communication is desirable, for example when broadcasting information, or for local discovery purposes.
|
||||
|
||||
To communicate with a *single* destination, you need to know its public key. Any method for
|
||||
obtaining the public key is valid, but Reticulum includes a simple mechanism for making other
|
||||
nodes aware of your destinations public key, called the *announce*. It is also possible to request
|
||||
an unknown public key from the network, as all transport instances serve as a distributed ledger
|
||||
of public keys.
|
||||
To communicate with a *single* destination, you need to know its public key. Any method for obtaining the public key is valid, but Reticulum includes a simple mechanism for making other nodes aware of your destinations public key, called the *announce*. It is also possible to request an unknown public key from the network, as all transport instances serve as a distributed ledger of public keys.
|
||||
|
||||
Note that public key information can be shared and verified in other ways than using the
|
||||
built-in *announce* functionality, and that it is therefore not required to use the *announce* and *path request*
|
||||
functionality to obtain public keys. It is by far the easiest though, and should definitely be used
|
||||
if there is not a very good reason for doing it differently.
|
||||
Note that public key information can be shared and verified in other ways than using the built-in *announce* functionality, and that it is therefore not required to use the *announce* and *path request* functionality to obtain public keys. It is by far the easiest though, and should definitely be used if there is not a very good reason for doing it differently.
|
||||
|
||||
.. _understanding-keyannouncements:
|
||||
|
||||
Public Key Announcements
|
||||
------------------------
|
||||
|
||||
An *announce* will send a special packet over any relevant interfaces, containing all needed
|
||||
information about the destination hash and public key, and can also contain some additional,
|
||||
application specific data. The entire packet is signed by the sender to ensure authenticity. It is not
|
||||
required to use the announce functionality, but in many cases it will be the simplest way to share
|
||||
public keys on the network. The announce mechanism also serves to establish end-to-end connectivity
|
||||
to the announced destination, as the announce propagates through the network.
|
||||
An *announce* will send a special packet over any relevant interfaces, containing all needed information about the destination hash and public key, and can also contain some additional, application specific data. The entire packet is signed by the sender to ensure authenticity. It is not required to use the announce functionality, but in many cases it will be the simplest way to share public keys on the network. The announce mechanism also serves to establish end-to-end connectivity to the announced destination, as the announce propagates through the network.
|
||||
|
||||
As an example, an announce in a simple messenger application might contain the following information:
|
||||
|
||||
|
||||
* The announcers destination hash
|
||||
* The announcers public key
|
||||
* Application specific data, in this case the users nickname and availability status
|
||||
* A random blob, making each new announce unique
|
||||
* An Ed25519 signature of the above information, verifying authenticity
|
||||
|
||||
With this information, any Reticulum node that receives it will be able to reconstruct an outgoing
|
||||
destination to securely communicate with that destination. You might have noticed that there is one
|
||||
piece of information lacking to reconstruct full knowledge of the announced destination, and that is
|
||||
the aspect names of the destination. These are intentionally left out to save bandwidth, since they
|
||||
will be implicit in almost all cases. The receiving application will already know them. If a destination
|
||||
name is not entirely implicit, information can be included in the application specific data part that
|
||||
will allow the receiver to infer the naming.
|
||||
With this information, any Reticulum node that receives it will be able to reconstruct an outgoing destination to securely communicate with that destination. You might have noticed that there is one piece of information lacking to reconstruct full knowledge of the announced destination, and that is the aspect names of the destination. These are intentionally left out to save bandwidth, since they will be implicit in almost all cases. The receiving application will already know them. If a destination name is not entirely implicit, information can be included in the application specific data part that will allow the receiver to infer the naming.
|
||||
|
||||
It is important to note that announces will be forwarded throughout the network according to a
|
||||
certain pattern. This will be detailed in the section
|
||||
:ref:`The Announce Mechanism in Detail<understanding-announce>`.
|
||||
It is important to note that announces will be forwarded throughout the network according to a certain pattern. This will be detailed in the section :ref:`The Announce Mechanism in Detail<understanding-announce>`.
|
||||
|
||||
In Reticulum, destinations are allowed to move around the network at will. This is very different from
|
||||
protocols such as IP, where an address is always expected to stay within the network segment it was assigned in.
|
||||
This limitation does not exist in Reticulum, and any destination is *completely portable* over the entire topography
|
||||
of the network, and *can even be moved to other Reticulum networks* than the one it was created in, and
|
||||
still become reachable. To update its reachability, a destination simply needs to send an announce on any
|
||||
networks it is part of. After a short while, it will be globally reachable in the network.
|
||||
In Reticulum, destinations are allowed to move around the network at will. This is very different from protocols such as IP, where an address is always expected to stay within the network segment it was assigned in. This limitation does not exist in Reticulum, and any destination is *completely portable* over the entire topography of the network, and *can even be moved to other Reticulum networks* than the one it was created in, and still become reachable. To update its reachability, a destination simply needs to send an announce on any networks it is part of. After a short while, it will be globally reachable in the network.
|
||||
|
||||
Seeing how *single* destinations are always tied to a private/public key pair leads us to the next topic.
|
||||
|
||||
@@ -298,33 +199,18 @@ Seeing how *single* destinations are always tied to a private/public key pair le
|
||||
Identities
|
||||
----------
|
||||
|
||||
In Reticulum, an *identity* does not necessarily represent a personal identity, but is an abstraction that
|
||||
can represent any kind of *verifiable entity*. This could very well be a person, but it could also be the
|
||||
control interface of a machine, a program, robot, computer, sensor or something else entirely. In
|
||||
general, any kind of agent that can act, or be acted upon, or store or manipulate information, can be
|
||||
represented as an identity. An *identity* can be used to create any number of destinations.
|
||||
In Reticulum, an *identity* does not necessarily represent a personal identity, but is an abstraction that can represent any kind of *verifiable entity*. This could very well be a person, but it could also be the control interface of a machine, a program, robot, computer, sensor or something else entirely. In general, any kind of agent that can act, or be acted upon, or store or manipulate information, can be represented as an identity. An *identity* can be used to create any number of destinations.
|
||||
|
||||
A *single* destination will always have an *identity* tied to it, but not *plain* or *group*
|
||||
destinations. Destinations and identities share a multilateral connection. You can create a
|
||||
destination, and if it is not connected to an identity upon creation, it will just create a new one to use
|
||||
automatically. This may be desirable in some situations, but often you will probably want to create
|
||||
the identity first, and then use it to create new destinations.
|
||||
A *single* destination will always have an *identity* tied to it, but not *plain* or *group* destinations. Destinations and identities share a multilateral connection. You can create a destination, and if it is not connected to an identity upon creation, it will just create a new one to use automatically. This may be desirable in some situations, but often you will probably want to create the identity first, and then use it to create new destinations.
|
||||
|
||||
As an example, we could use an identity to represent the user of a messaging application.
|
||||
Destinations can then be created by this identity to allow communication to reach the user.
|
||||
In all cases it is of great importance to store the private keys associated with any
|
||||
Reticulum Identity securely and privately, since obtaining access to the identity keys equals
|
||||
obtaining access and controlling reachability to any destinations created by that identity.
|
||||
As an example, we could use an identity to represent the user of a messaging application. Destinations can then be created by this identity to allow communication to reach the user. In all cases it is of great importance to store the private keys associated with any Reticulum Identity securely and privately, since obtaining access to the identity keys equals obtaining access and controlling reachability to any destinations created by that identity.
|
||||
|
||||
.. _understanding-gettingfurther:
|
||||
|
||||
Getting Further
|
||||
---------------
|
||||
|
||||
The above functions and principles form the core of Reticulum, and would suffice to create
|
||||
functional networked applications in local clusters, for example over radio links where all interested
|
||||
nodes can directly hear each other. But to be truly useful, we need a way to direct traffic over multiple
|
||||
hops in the network.
|
||||
The above functions and principles form the core of Reticulum, and would suffice to create functional networked applications in local clusters, for example over radio links where all interested nodes can directly hear each other. But to be truly useful, we need a way to direct traffic over multiple hops in the network.
|
||||
|
||||
In the following sections, two concepts that allow this will be introduced, *paths* and *links*.
|
||||
|
||||
@@ -333,16 +219,9 @@ In the following sections, two concepts that allow this will be introduced, *pat
|
||||
Reticulum Transport
|
||||
===================
|
||||
|
||||
The methods of routing used in traditional networks are fundamentally incompatible with the physical medium
|
||||
types and circumstances that Reticulum was designed to handle. These mechanisms mostly assume trust at the physical layer,
|
||||
and often needs a lot more bandwidth than Reticulum can assume is available. Since Reticulum is designed to
|
||||
survive running over open radio spectrum, no such trust can be assumed, and bandwidth is often very limited.
|
||||
The methods of routing used in traditional networks are fundamentally incompatible with the physical medium types and circumstances that Reticulum was designed to handle. These mechanisms mostly assume trust at the physical layer, and often needs a lot more bandwidth than Reticulum can assume is available. Since Reticulum is designed to survive running over open radio spectrum, no such trust can be assumed, and bandwidth is often very limited.
|
||||
|
||||
To overcome such challenges, Reticulum’s *Transport* system uses asymmetric elliptic curve cryptography to
|
||||
implement the concept of *paths* that allow discovery of how to get information closer to a certain
|
||||
destination. It is important to note that no single node in a Reticulum network knows the complete
|
||||
path to a destination. Every Transport node participating in a Reticulum network will only
|
||||
know the most direct way to get a packet one hop closer to it's destination.
|
||||
To overcome such challenges, Reticulum’s *Transport* system uses asymmetric elliptic curve cryptography to implement the concept of *paths* that allow discovery of how to get information closer to a certain destination. It is important to note that no single node in a Reticulum network knows the complete path to a destination. Every Transport node participating in a Reticulum network will only know the most direct way to get a packet one hop closer to it's destination.
|
||||
|
||||
|
||||
.. _understanding-nodetypes:
|
||||
@@ -350,16 +229,11 @@ know the most direct way to get a packet one hop closer to it's destination.
|
||||
Node Types
|
||||
----------
|
||||
|
||||
Currently, Reticulum distinguishes between two types of network nodes. All nodes on a Reticulum network
|
||||
are *Reticulum Instances*, and some are also *Transport Nodes*. If a system running Reticulum is fixed in
|
||||
one place, and is intended to be kept available most of the time, it is a good contender to be a *Transport Node*.
|
||||
Currently, Reticulum distinguishes between two types of network nodes. All nodes on a Reticulum network are *Reticulum Instances*, and some are also *Transport Nodes*. If a system running Reticulum is fixed in one place, and is intended to be kept available most of the time, it is a good contender to be a *Transport Node*.
|
||||
|
||||
Any Reticulum Instance can become a Transport Node by enabling it in the configuration.
|
||||
This distinction is made by the user configuring the node, and is used to determine what nodes on the
|
||||
network will help forward traffic, and what nodes rely on other nodes for wider connectivity.
|
||||
Any Reticulum Instance can become a Transport Node by enabling it in the configuration. This distinction is made by the user configuring the node, and is used to determine what nodes on the network will help forward traffic, and what nodes rely on other nodes for wider connectivity.
|
||||
|
||||
If a node is an *Instance* it should be given the configuration directive ``enable_transport = No``, which
|
||||
is the default setting.
|
||||
If a node is an *Instance* it should be given the configuration directive ``enable_transport = No``, which is the default setting.
|
||||
|
||||
If it is a *Transport Node*, it should be given the configuration directive ``enable_transport = Yes``.
|
||||
|
||||
@@ -369,8 +243,7 @@ If it is a *Transport Node*, it should be given the configuration directive ``en
|
||||
The Announce Mechanism in Detail
|
||||
--------------------------------
|
||||
|
||||
When an *announce* for a destination is transmitted by a Reticulum instance, it will be forwarded by
|
||||
any transport node receiving it, but according to some specific rules:
|
||||
When an *announce* for a destination is transmitted by a Reticulum instance, it will be forwarded by any transport node receiving it, but according to some specific rules:
|
||||
|
||||
|
||||
* | If this exact announce has already been received before, ignore it.
|
||||
@@ -401,42 +274,23 @@ any transport node receiving it, but according to some specific rules:
|
||||
to be transmitted, the newest announce is discarded. If the newest announce contains different
|
||||
application specific data, it will replace the old announce.
|
||||
|
||||
Once an announce has reached a transport node in the network, any other node in direct contact with that
|
||||
transport node will be able to reach the destination the announce originated from, simply by sending a packet
|
||||
addressed to that destination. Any transport node with knowledge of the announce will be able to direct the
|
||||
packet towards the destination by looking up the most efficient next node to the destination.
|
||||
Once an announce has reached a transport node in the network, any other node in direct contact with that transport node will be able to reach the destination the announce originated from, simply by sending a packet addressed to that destination. Any transport node with knowledge of the announce will be able to direct the packet towards the destination by looking up the most efficient next node to the destination.
|
||||
|
||||
According to these rules, an announce will propagate throughout the network in a predictable way,
|
||||
and make the announced destination reachable in a short amount of time. Fast networks that have the
|
||||
capacity to process many announces can reach full convergence very quickly, even when constantly adding
|
||||
new destinations. Slower segments of such networks might take a bit longer to gain full knowledge about
|
||||
the wide and fast networks they are connected to, but can still do so over time, while prioritising full
|
||||
and quickly converging end-to-end connectivity for their local, slower segments.
|
||||
According to these rules, an announce will propagate throughout the network in a predictable way, and make the announced destination reachable in a short amount of time. Fast networks that have the capacity to process many announces can reach full convergence very quickly, even when constantly adding new destinations. Slower segments of such networks might take a bit longer to gain full knowledge about the wide and fast networks they are connected to, but can still do so over time, while prioritising full and quickly converging end-to-end connectivity for their local, slower segments.
|
||||
|
||||
.. tip::
|
||||
Even very slow networks, that simply don't have the capacity to ever reach *full* convergence will generally still be able to reach **any other destination on any connected segments**, since interconnecting transport nodes will prioritize announces into the slower segments that are actually requested by nodes on these.
|
||||
|
||||
Even very slow networks, that simply don't have the capacity to ever reach *full* convergence
|
||||
will generally still be able to reach **any other destination on any connected segments**, since
|
||||
interconnecting transport nodes will prioritize announces into the slower segments that are
|
||||
actually requested by nodes on these.
|
||||
This means that slow, low-capacity or low-resource segments **don't** need to have full network knowledge, since paths can always be recursively resolved from other segments that do have knowledge about them.
|
||||
|
||||
This means that slow, low-capacity or low-resource segments **don't** need to have full network
|
||||
knowledge, since paths can always be recursively resolved from other segments that do have
|
||||
knowledge about them.
|
||||
|
||||
In general, even extremely complex networks, that utilize the maximum 128 hops will converge to full
|
||||
end-to-end connectivity in about one minute, given there is enough bandwidth available to process
|
||||
the required amount of announces.
|
||||
In general, even extremely complex networks, that utilize the maximum 128 hops will converge to full end-to-end connectivity in about one minute, given there is enough bandwidth available to process the required amount of announces.
|
||||
|
||||
.. _understanding-paths:
|
||||
|
||||
Reaching the Destination
|
||||
------------------------
|
||||
|
||||
In networks with changing topology and trustless connectivity, nodes need a way to establish
|
||||
*verified connectivity* with each other. Since the underlying network mediums are assumed to be trustless, Reticulum
|
||||
must provide a way to guarantee that the peer you are communicating with is actually who you
|
||||
expect. Reticulum offers two ways to do this.
|
||||
In networks with changing topology and trustless connectivity, nodes need a way to establish *verified connectivity* with each other. Since the underlying network mediums are assumed to be trustless, Reticulum must provide a way to guarantee that the peer you are communicating with is actually who you expect. Reticulum offers two ways to do this.
|
||||
|
||||
For exchanges of small amounts of information, Reticulum offers the *Packet* API, which works exactly like you would expect - on a per packet level. The following process is employed when sending a packet:
|
||||
|
||||
@@ -496,35 +350,17 @@ For exchanges of larger amounts of data, or when longer sessions of bidirectiona
|
||||
the destination using a Reticulum Identity. This authentication is happening inside the encrypted
|
||||
link, and is only revealed to the verified destination, and no intermediaries.
|
||||
|
||||
In a moment, we will discuss the details of how this methodology is
|
||||
implemented, but let’s first recap what purposes this methodology serves. We
|
||||
first ensure that the node answering our request is actually the one we want to
|
||||
communicate with, and not a malicious actor pretending to be so. At the same
|
||||
time we establish an efficient encrypted channel. The setup of this is
|
||||
relatively cheap in terms of bandwidth, so it can be used just for a short
|
||||
exchange, and then recreated as needed, which will also rotate encryption keys.
|
||||
The link can also be kept alive for longer periods of time, if this is more
|
||||
suitable to the application. The procedure also inserts the *link id* , a hash
|
||||
calculated from the link request packet, into the memory of forwarding nodes,
|
||||
which means that the communicating nodes can thereafter reach each other simply
|
||||
by referring to this *link id*.
|
||||
In a moment, we will discuss the details of how this methodology is implemented, but let’s first recap what purposes this methodology serves. We first ensure that the node answering our request is actually the one we want to communicate with, and not a malicious actor pretending to be so. At the same time we establish an efficient encrypted channel. The setup of this is relatively cheap in terms of bandwidth, so it can be used just for a short exchange, and then recreated as needed, which will also rotate encryption keys. The link can also be kept alive for longer periods of time, if this is more suitable to the application. The procedure also inserts the *link id* , a hash calculated from the link request packet, into the memory of forwarding nodes, which means that the communicating nodes can thereafter reach each other simply by referring to this *link id*.
|
||||
|
||||
The combined bandwidth cost of setting up a link is 3 packets totalling 297 bytes (more info in the
|
||||
:ref:`Binary Packet Format<understanding-packetformat>` section). The amount of bandwidth used on keeping
|
||||
a link open is practically negligible, at 0.45 bits per second. Even on a slow 1200 bits per second packet
|
||||
radio channel, 100 concurrent links will still leave 96% channel capacity for actual data.
|
||||
The combined bandwidth cost of setting up a link is 3 packets totalling 297 bytes (more info in the :ref:`Binary Packet Format<understanding-packetformat>` section). The amount of bandwidth used on keeping a link open is practically negligible, at 0.45 bits per second. Even on a slow 1200 bits per second packet radio channel, 100 concurrent links will still leave 96% channel capacity for actual data.
|
||||
|
||||
|
||||
Link Establishment in Detail
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
After exploring the basics of the announce mechanism, finding a path through the network, and an overview
|
||||
of the link establishment procedure, this section will go into greater detail about the Reticulum link
|
||||
establishment process.
|
||||
After exploring the basics of the announce mechanism, finding a path through the network, and an overview of the link establishment procedure, this section will go into greater detail about the Reticulum link establishment process.
|
||||
|
||||
The *link* in Reticulum terminology should not be viewed as a direct node-to-node link on the
|
||||
physical layer, but as an abstract channel, that can be open for any amount of time, and can span
|
||||
an arbitrary number of hops, where information will be exchanged between two nodes.
|
||||
The *link* in Reticulum terminology should not be viewed as a direct node-to-node link on the physical layer, but as an abstract channel, that can be open for any amount of time, and can span an arbitrary number of hops, where information will be exchanged between two nodes.
|
||||
|
||||
|
||||
* | When a node in the network wants to establish verified connectivity with another node, it
|
||||
@@ -571,49 +407,107 @@ an arbitrary number of hops, where information will be exchanged between two nod
|
||||
that is used to encrypt the channel. Information can now be exchanged reliably and securely.
|
||||
|
||||
.. note::
|
||||
It’s important to note that this methodology ensures that the source of the request does not need to reveal any identifying information about itself. **The link initiator remains completely anonymous**.
|
||||
|
||||
It’s important to note that this methodology ensures that the source of the request does not need to
|
||||
reveal any identifying information about itself. **The link initiator remains completely anonymous**.
|
||||
|
||||
When using *links*, Reticulum will automatically verify all data sent over the link, and can also
|
||||
automate retransmissions if *Resources* are used.
|
||||
When using *links*, Reticulum will automatically verify all data sent over the link, and can also automate retransmissions if *Resources* are used.
|
||||
|
||||
.. _understanding-resources:
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
For exchanging small amounts of data over a Reticulum network, the :ref:`Packet<api-packet>` interface
|
||||
is sufficient, but for exchanging data that would require many packets, an efficient way to coordinate
|
||||
the transfer is needed.
|
||||
For exchanging small amounts of data over a Reticulum network, the :ref:`Packet<api-packet>` interface is sufficient, but for exchanging data that would require many packets, an efficient way to coordinate the transfer is needed.
|
||||
|
||||
This is the purpose of the Reticulum :ref:`Resource<api-resource>`. A *Resource* can automatically
|
||||
handle the reliable transfer of an arbitrary amount of data over an established :ref:`Link<api-link>`.
|
||||
Resources can auto-compress data, will handle breaking the data into individual packets, sequencing
|
||||
the transfer, integrity verification and reassembling the data on the other end.
|
||||
This is the purpose of the Reticulum :ref:`Resource<api-resource>`. A *Resource* can automatically handle the reliable transfer of an arbitrary amount of data over an established :ref:`Link<api-link>`. Resources can auto-compress data, will handle breaking the data into individual packets, sequencing the transfer, integrity verification and reassembling the data on the other end.
|
||||
|
||||
:ref:`Resources<api-resource>` are programmatically very simple to use, and only requires a few lines
|
||||
of codes to reliably transfer any amount of data. They can be used to transfer data stored in memory,
|
||||
or stream data directly from files.
|
||||
:ref:`Resources<api-resource>` are programmatically very simple to use, and only requires a few lines of codes to reliably transfer any amount of data. They can be used to transfer data stored in memory, or stream data directly from files.
|
||||
|
||||
.. _understanding-network_identities:
|
||||
|
||||
Network Identities
|
||||
==================
|
||||
|
||||
In Reticulum, every peer and application utilizes a cryptographic **Identity** to verify authenticity and establish encrypted channels. While standard identities are typically used to represent a single user, device, or service, Reticulum introduces the concept of a **Network Identity** to represent a logical group of nodes or an entire community infrastructure.
|
||||
|
||||
A Network Identity is, at its core, a standard Reticulum Identity keyset. However, its purpose and usage differ from a personal identity. Instead of identifying a single entity, a Network Identity acts as a shared credential that federates multiple independent Transport Instances under a single, verifiable administrative domain.
|
||||
|
||||
|
||||
Conceptual Overview
|
||||
-------------------
|
||||
|
||||
You can think of a standard Reticulum Identity as a self-sovereign, privately created passport for a single person. A Network Identity, conversely, is akin to a cryptographic flag, or a charter that flies over a fleet of ships. It signifies that while the ships may operate independently and be physically distant, they belong to the same organization, follow the same protocols, and are expected to act in concert.
|
||||
|
||||
When you configure a Network Identity on one or more of your nodes, you are effectively declaring that these nodes constitute a specific "network" within a broader Reticulum mesh. This allows other peers to recognize interfaces not just as "a node named Alice", but as "a gateway belonging to The Eastern Ret Of Freedom".
|
||||
|
||||
|
||||
Current Usage
|
||||
-------------
|
||||
|
||||
At present, the primary function of a Network Identity is within the :ref:`Interface Discovery<using-interface_discovery>` system.
|
||||
|
||||
When a Transport Instance broadcasts a discovery announce for an interface, it can optionally sign that announce with a Network Identity, instead of just its local transport identity. Remote peers receiving the announce can then verify the signature. This provides functionality for two important distinctions:
|
||||
|
||||
1. **Authenticity:** It proves that the interface was published by an operator who possesses the private key for that Network Identity.
|
||||
2. **Trust Boundaries:** It allows users to configure their systems to only accept and connect to interfaces that belong to specific Network Identities, effectively creating "whitelisted" zones of trusted infrastructure.
|
||||
|
||||
.. note::
|
||||
If you enable encryption on your discovery announces, the Network Identity is used as the shared secret. Only peers who have been explicitly provided with the Network Identity's full keyset (and have it configured locally) will be able to decrypt and utilize the connection details.
|
||||
|
||||
This functionality will be expanded in the future, so that peers with delegated keys can be allowed to decrypt discovery announces without holding the root network key. Currently, the functionality is sufficient for sharing interface information privately where you control all nodes that must decrypt the discovered interfaces.
|
||||
|
||||
|
||||
Future Implications
|
||||
-------------------
|
||||
|
||||
While the current implementation focuses on interface discovery, the concept of Network Identities serves as the foundational building block for future Reticulum features designed to support large-scale, organic mesh formation.
|
||||
|
||||
As the ecosystem evolves, Network Identities will facilitate:
|
||||
|
||||
* **Distributed Name Resolution:** A system where networks can publish name-to-identity mappings, allowing human-readable names to resolve without centralized servers.
|
||||
* **Service Publishing:** Networks will be able to announce specific capabilities, services, or information endpoints available publicly or to their members.
|
||||
* **Inter-Network Federation:** Trust relationships between different networks, allowing for seamless but managed flow of traffic and information across distinct administrative boundaries.
|
||||
* **Distributed Blackhole Management:** A reputation-based system for blackhole list distribution, where trusted Network Identities can sign and publish lists of blackholed identities. This allows communities to collaboratively enforce security standards and filter spam or malicious identities across the parts of the wider mesh that they are responsible for.
|
||||
|
||||
By adopting the use of Network Identities now, you are preparing your infrastructure to be compatible with this future functionality.
|
||||
|
||||
|
||||
Creating and Using a Network Identity
|
||||
-------------------------------------
|
||||
|
||||
Since a Network Identity is simply a standard Reticulum Identity, you create one using the built-in tools.
|
||||
|
||||
1. **Generate the Identity:**
|
||||
Use the ``rnid`` utility to generate a new identity file that will serve as your Network Identity.
|
||||
|
||||
.. code:: sh
|
||||
|
||||
$ rnid -g ~/.reticulum/storage/identities/my_network
|
||||
|
||||
2. **Distribute the Public Key:**
|
||||
The public key must be distributed to any Transport Instance that needs to verify your network's announces and discovery information. By default, if your node is set up to use a network identity, this happens automatically (using the standard announce mechanism).
|
||||
|
||||
3. **Configure Instances:**
|
||||
In the ``[reticulum]`` section of the configuration file on every node within your network, point the ``network_identity`` option to the file you created.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
network_identity = ~/.reticulum/storage/identities/my_network
|
||||
...
|
||||
|
||||
Once configured, your instances will automatically utilize this identity for signing discovery announces (and potentially decrypting network-private information), presenting a unified front to the wider network.
|
||||
|
||||
.. _understanding-referencesystem:
|
||||
|
||||
Reference Setup
|
||||
======================
|
||||
|
||||
This section will detail a recommended *Reference Setup* for Reticulum. It is important to
|
||||
note that Reticulum is designed to be usable on more or less any computing device, and over more
|
||||
or less any medium that allows you to send and receive data, which satisfies some very low
|
||||
minimum requirements.
|
||||
This section will detail a recommended *Reference Setup* for Reticulum. It is important to note that Reticulum is designed to be usable on more or less any computing device, and over more or less any medium that allows you to send and receive data, which satisfies some very low minimum requirements.
|
||||
|
||||
The communication channel must support at least half-duplex operation, and provide an average
|
||||
throughput of 5 bits per second or greater, and supports a physical layer MTU of 500 bytes. The
|
||||
Reticulum stack should be able to run on more or less any hardware that can provide a Python 3.x
|
||||
runtime environment.
|
||||
The communication channel must support at least half-duplex operation, and provide an average throughput of 5 bits per second or greater, and supports a physical layer MTU of 500 bytes. The Reticulum stack should be able to run on more or less any hardware that can provide a Python 3.x runtime environment.
|
||||
|
||||
That being said, this reference setup has been outlined to provide a common platform for anyone
|
||||
who wants to help in the development of Reticulum, and for everyone who wants to know a
|
||||
recommended setup to get started experimenting. A reference system consists of three parts:
|
||||
That being said, this reference setup has been outlined to provide a common platform for anyone who wants to help in the development of Reticulum, and for everyone who wants to know a recommended setup to get started experimenting. A reference system consists of three parts:
|
||||
|
||||
* **An Interface Device**
|
||||
Which provides access to the physical medium whereupon the communication
|
||||
@@ -625,11 +519,7 @@ recommended setup to get started experimenting. A reference system consists of t
|
||||
* **A Software Stack**
|
||||
The software implementing the Reticulum protocol and applications using it.
|
||||
|
||||
The reference setup can be considered a relatively stable platform to develop on, and also to start
|
||||
building networks or applications on. While details of the implementation might change at the current stage of
|
||||
development, it is the goal to maintain hardware compatibility for as long as entirely possible, and
|
||||
the current reference setup has been determined to provide a functional platform for many years
|
||||
into the future. The current Reference System Setup is as follows:
|
||||
The reference setup can be considered a relatively stable platform to develop on, and also to start building networks or applications on. While details of the implementation might change at the current stage of development, it is the goal to maintain hardware compatibility for as long as entirely possible, and the current reference setup has been determined to provide a functional platform for many years into the future. The current Reference System Setup is as follows:
|
||||
|
||||
|
||||
* **Interface Device**
|
||||
@@ -644,53 +534,34 @@ into the future. The current Reference System Setup is as follows:
|
||||
operating system.
|
||||
|
||||
.. note::
|
||||
To avoid confusion, it is very important to note, that the reference interface device **does not** use the LoRaWAN standard, but uses a custom MAC layer on top of the plain LoRa modulation! As such, you will need a plain LoRa radio module connected to a controller with the correct firmware. Full details on how to get or make such a device is available on the `RNode Page <https://github.com/markqvist/rnode_firmware>`_.
|
||||
|
||||
To avoid confusion, it is very important to note, that the reference interface device **does not**
|
||||
use the LoRaWAN standard, but uses a custom MAC layer on top of the plain LoRa modulation! As such, you will
|
||||
need a plain LoRa radio module connected to a controller with the correct firmware. Full details on how to
|
||||
get or make such a device is available on the `RNode Page <https://github.com/markqvist/rnode_firmware>`_.
|
||||
With the current reference setup, it should be possible to get on a Reticulum network for around 100$ even if you have none of the hardware already, and need to purchase everything.
|
||||
|
||||
With the current reference setup, it should be possible to get on a Reticulum network for around 100$
|
||||
even if you have none of the hardware already, and need to purchase everything.
|
||||
|
||||
This reference setup is of course just a recommendation for getting started easily, and you should
|
||||
tailor it to your own specific needs, or whatever hardware you have available.
|
||||
This reference setup is of course just a recommendation for getting started easily, and you should tailor it to your own specific needs, or whatever hardware you have available.
|
||||
|
||||
.. _understanding-protocolspecifics:
|
||||
|
||||
Protocol Specifics
|
||||
==================
|
||||
|
||||
This chapter will detail protocol specific information that is essential to the implementation of
|
||||
Reticulum, but non-critical in understanding how the protocol works on a general level. It should be
|
||||
treated more as a reference than as essential reading.
|
||||
This chapter will detail protocol specific information that is essential to the implementation of Reticulum, but non-critical in understanding how the protocol works on a general level. It should be treated more as a reference than as essential reading.
|
||||
|
||||
|
||||
Packet Prioritisation
|
||||
---------------------
|
||||
|
||||
Currently, Reticulum is completely priority-agnostic regarding *general* traffic. All traffic is handled
|
||||
on a first-come, first-serve basis. Announce re-transmission and other maintenance traffic is handled
|
||||
according to the re-transmission times and priorities described earlier in this chapter.
|
||||
Currently, Reticulum is completely priority-agnostic regarding *general* traffic. All traffic is handled on a first-come, first-serve basis. Announce re-transmission and other maintenance traffic is handled according to the re-transmission times and priorities described earlier in this chapter.
|
||||
|
||||
|
||||
Interface Access Codes
|
||||
----------------------
|
||||
|
||||
Reticulum can create named virtual networks, and networks that are only accessible by knowing a preshared
|
||||
passphrase. The configuration of this is detailed in the :ref:`Common Interface Options<interfaces-options>`
|
||||
section. To implement this feature, Reticulum uses the concept of Interface Access Codes, that are calculated
|
||||
and verified per-packet.
|
||||
Reticulum can create named virtual networks, and networks that are only accessible by knowing a preshared passphrase. The configuration of this is detailed in the :ref:`Common Interface Options<interfaces-options>` section. To implement this feature, Reticulum uses the concept of Interface Access Codes, that are calculated and verified per-packet.
|
||||
|
||||
An interface with a named virtual network or passphrase authentication enabled will derive a shared Ed25519
|
||||
signing identity, and for every outbound packet generate a signature of the entire packet. This signature is
|
||||
then inserted into the packet as an Interface Access Code before transmission. Depending on the speed and
|
||||
capabilities of the interface, the IFAC can be the full 512-bit Ed25519 signature, or a truncated version.
|
||||
Configured IFAC length can be inspected for all interfaces with the ``rnstatus`` utility.
|
||||
An interface with a named virtual network or passphrase authentication enabled will derive a shared Ed25519 signing identity, and for every outbound packet generate a signature of the entire packet. This signature is then inserted into the packet as an Interface Access Code before transmission. Depending on the speed and capabilities of the interface, the IFAC can be the full 512-bit Ed25519 signature, or a truncated version. Configured IFAC length can be inspected for all interfaces with the ``rnstatus`` utility.
|
||||
|
||||
Upon receipt, the interface will check that the signature matches the expected value, and drop the packet if it
|
||||
does not. This ensures that only packets sent with the correct naming and/or passphrase parameters are allowed to
|
||||
pass onto the network.
|
||||
Upon receipt, the interface will check that the signature matches the expected value, and drop the packet if it does not. This ensures that only packets sent with the correct naming and/or passphrase parameters are allowed to pass onto the network.
|
||||
|
||||
|
||||
.. _understanding-packetformat:
|
||||
@@ -834,14 +705,11 @@ Wire Format
|
||||
Announce Propagation Rules
|
||||
--------------------------
|
||||
|
||||
The following table illustrates the rules for automatically propagating announces
|
||||
from one interface type to another, for all possible combinations. For the purpose
|
||||
of announce propagation, the *Full* and *Gateway* modes are identical.
|
||||
The following table illustrates the rules for automatically propagating announces from one interface type to another, for all possible combinations. For the purpose of announce propagation, the *Full* and *Gateway* modes are identical.
|
||||
|
||||
.. image:: graphics/if_mode_graph_b.png
|
||||
|
||||
See the :ref:`Interface Modes<interfaces-modes>` section for a conceptual overview
|
||||
of the different interface modes, and how they are configured.
|
||||
See the :ref:`Interface Modes<interfaces-modes>` section for a conceptual overview of the different interface modes, and how they are configured.
|
||||
|
||||
..
|
||||
(.. code-block:: text)
|
||||
@@ -871,17 +739,11 @@ of the different interface modes, and how they are configured.
|
||||
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.
|
||||
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.
|
||||
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:
|
||||
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
|
||||
|
||||
@@ -905,12 +767,7 @@ intentionally compromised or weakened clone. The utilised primitives are:
|
||||
|
||||
* SHA-512
|
||||
|
||||
In the default installation configuration, the ``X25519``, ``Ed25519`` and ``AES-256-CBC``
|
||||
primitives are provided by `OpenSSL <https://www.openssl.org/>`_ (via the `PyCA/cryptography <https://github.com/pyca/cryptography>`_
|
||||
package). The hashing functions ``SHA-256`` and ``SHA-512`` are provided by the standard
|
||||
Python `hashlib <https://docs.python.org/3/library/hashlib.html>`_. The ``HKDF``, ``HMAC``,
|
||||
``Token`` primitives, and the ``PKCS7`` padding function are always provided by the
|
||||
following internal implementations:
|
||||
In the default installation configuration, the ``X25519``, ``Ed25519`` and ``AES-256-CBC`` primitives are provided by `OpenSSL <https://www.openssl.org/>`_ (via the `PyCA/cryptography <https://github.com/pyca/cryptography>`_ package). The hashing functions ``SHA-256`` and ``SHA-512`` are provided by the standard Python `hashlib <https://docs.python.org/3/library/hashlib.html>`_. The ``HKDF``, ``HMAC``, ``Token`` primitives, and the ``PKCS7`` padding function are always provided by the following internal implementations:
|
||||
|
||||
- ``RNS/Cryptography/HKDF.py``
|
||||
- ``RNS/Cryptography/HMAC.py``
|
||||
@@ -918,17 +775,9 @@ following internal implementations:
|
||||
- ``RNS/Cryptography/PKCS7.py``
|
||||
|
||||
|
||||
Reticulum also includes a complete implementation of all necessary primitives in pure Python.
|
||||
If OpenSSL & 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.
|
||||
Reticulum also includes a complete implementation of all necessary primitives in pure Python. If OpenSSL & 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.
|
||||
|
||||
Using the normal RNS installation procedures, it is not possible to install Reticulum on a
|
||||
system without the required OpenSSL primitives being available, and if they are not, they will
|
||||
be resolved and installed as a dependency. It is only possible to use the pure-python primitives
|
||||
by manually specifying this, for example by using the ``rnspure`` package.
|
||||
Using the normal RNS installation procedures, it is not possible to install Reticulum on a system without the required OpenSSL primitives being available, and if they are not, they will be resolved and installed as a dependency. It is only possible to use the pure-python primitives by manually specifying this, for example by using the ``rnspure`` package.
|
||||
|
||||
.. warning::
|
||||
If you want to use the internal pure-python primitives, it is **highly advisable** that you
|
||||
|
||||
@@ -338,8 +338,8 @@ Filter output to only show some interfaces:
|
||||
.. code:: text
|
||||
|
||||
usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A]
|
||||
[-l] [-s SORT] [-r] [-j] [-R hash] [-i path]
|
||||
[-w seconds] [-v] [filter]
|
||||
[-l] [-t] [-s SORT] [-r] [-j] [-R hash] [-i path]
|
||||
[-w seconds] [-d] [-D] [-m] [-I seconds] [-v] [filter]
|
||||
|
||||
Reticulum Network Stack Status
|
||||
|
||||
@@ -353,12 +353,19 @@ Filter output to only show some interfaces:
|
||||
-a, --all show all interfaces
|
||||
-A, --announce-stats show announce stats
|
||||
-l, --link-stats show link stats
|
||||
-s SORT, --sort SORT sort interfaces by [rate, traffic, rx, tx, announces, arx, atx, held]
|
||||
-t, --totals display traffic totals
|
||||
-s, --sort SORT sort interfaces by [rate, traffic, rx, tx, rxs, txs,
|
||||
announces, arx, atx, held]
|
||||
-r, --reverse reverse sorting
|
||||
-j, --json output in JSON format
|
||||
-R hash transport identity hash of remote instance to get status from (requires -i)
|
||||
-R hash transport identity hash of remote instance to get status from
|
||||
-i path path to identity used for remote management
|
||||
-w seconds timeout before giving up on remote queries
|
||||
-d, --discovered list discovered interfaces
|
||||
-D show details and config entries for discovered interfaces
|
||||
-m, --monitor continuously monitor status
|
||||
-I, --monitor-interval seconds
|
||||
refresh interval for monitor mode (default: 1)
|
||||
-v, --verbose
|
||||
|
||||
|
||||
@@ -463,6 +470,7 @@ Decrypt a file using the Reticulum Identity it was encrypted for:
|
||||
-B, --base32 Use base32-encoded input and output
|
||||
--version show program's version number and exit
|
||||
|
||||
.. _utility-rnpath:
|
||||
|
||||
The rnpath Utility
|
||||
====================
|
||||
@@ -484,21 +492,23 @@ Resolve path to a destination:
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnpath [-h] [--config CONFIG] [--version] [-t] [-m hops]
|
||||
[-r] [-d] [-D] [-x] [-w seconds] [-R hash] [-i path]
|
||||
[-W seconds] [-j] [-v] [destination]
|
||||
usage: rnpath [-h] [--config CONFIG] [--version] [-t] [-m hops] [-r] [-d] [-D]
|
||||
[-x] [-w seconds] [-R hash] [-i path] [-W seconds] [-b] [-B] [-U]
|
||||
[--duration DURATION] [--reason REASON] [-p] [-j] [-v]
|
||||
[destination] [list_filter]
|
||||
|
||||
Reticulum Path Discovery Utility
|
||||
Reticulum Path Management Utility
|
||||
|
||||
positional arguments:
|
||||
destination hexadecimal hash of the destination
|
||||
list_filter filter for remote blackhole list view
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
--version show program's version number and exit
|
||||
-t, --table show all known paths
|
||||
-m hops, --max hops maximum hops to filter path table by
|
||||
-m, --max hops maximum hops to filter path table by
|
||||
-r, --rates show announce rate info
|
||||
-d, --drop remove the path to a destination
|
||||
-D, --drop-announces drop all queued announces
|
||||
@@ -507,6 +517,13 @@ Resolve path to a destination:
|
||||
-R hash transport identity hash of remote instance to manage
|
||||
-i path path to identity used for remote management
|
||||
-W seconds timeout before giving up on remote queries
|
||||
-b, --blackholed list blackholed identities
|
||||
-B, --blackhole blackhole identity
|
||||
-U, --unblackhole unblackhole identity
|
||||
--duration DURATION duration of blackhole enforcement in hours
|
||||
--reason REASON reason for blackholing identity
|
||||
-p, --blackholed-list
|
||||
view published blackhole list for remote transport instance
|
||||
-j, --json output in JSON format
|
||||
-v, --verbose
|
||||
|
||||
@@ -619,13 +636,20 @@ Or fetch a file from the remote system:
|
||||
|
||||
$ rncp --fetch ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e
|
||||
|
||||
The default identity file is stored in ``~/.reticulum/identities/rncp``, but you can use
|
||||
another one, which will be created if it does not already exist
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rncp ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e -i /path/to/identity
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rncp [-h] [--config path] [-v] [-q] [-S] [-l] [-F] [-f]
|
||||
[-j path] [-b seconds] [-a allowed_hash] [-n] [-p]
|
||||
[-w seconds] [--version] [file] [destination]
|
||||
[-i identity] [-w seconds] [--version] [file] [destination]
|
||||
|
||||
Reticulum File Transfer Utility
|
||||
|
||||
@@ -650,11 +674,27 @@ Or fetch a file from the remote system:
|
||||
-a allowed_hash allow this identity (or add in ~/.rncp/allowed_identities)
|
||||
-n, --no-auth accept requests from anyone
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
-i identity path to identity to use
|
||||
-w seconds sender timeout before giving up
|
||||
-P, --phy-rates display physical layer transfer rates
|
||||
--version show program's version number and exit
|
||||
|
||||
|
||||
The rngit Utility
|
||||
=================
|
||||
|
||||
The ``rngit`` utility provides full Git repository hosting and interaction over Reticulum, as well as many other useful features for software development, collaboration and publishing. It allows you to host Git repositories on Reticulum nodes, interact with remote repositories using standard Git commands through the ``rns://`` URL scheme, and to publish software releases.
|
||||
|
||||
The system consists of two parts: The ``rngit`` node that hosts and manages repositories, and the ``git-remote-rns`` helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: ``rns://DESTINATION_HASH/group/repo``.
|
||||
|
||||
If you set a branch to track a Reticulum remote as the default upstream, you can simply use ``git`` as you normally would; all commands work transparently and as expected.
|
||||
|
||||
.. warning::
|
||||
**The rngit program is a new addition to RNS!** This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible permission system for allowing many users to interact with many different repositories on a single node, ``rngit`` has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.
|
||||
|
||||
For the full documentation on the `rngit` system, see the :ref:`Git Over Reticulum<git-main>` chapter of this manual.
|
||||
|
||||
|
||||
The rnx Utility
|
||||
================
|
||||
|
||||
@@ -727,6 +767,282 @@ another one, which will be created if it does not already exist
|
||||
--version show program's version number and exit
|
||||
|
||||
|
||||
The rnsh Utility
|
||||
================
|
||||
|
||||
The ``rnsh`` utility provides a fully interactive remote shell over Reticulum.
|
||||
It allows you to establish encrypted, authenticated shell sessions on remote
|
||||
systems, complete with terminal emulation, pipe support, and window resizing.
|
||||
|
||||
While the ``rnx`` utility is useful for simple remote command execution and
|
||||
retrieving output, ``rnsh`` provides a complete interactive terminal experience,
|
||||
making it ideal for remote administration and management tasks that require
|
||||
real-time interaction, just like SSH does for IP networks.
|
||||
|
||||
``rnsh`` operates in two modes: a *listener* mode that accepts incoming
|
||||
connections, and an *initiator* mode that connects to a remote listener. Both
|
||||
sides authenticate using Reticulum Identities, ensuring that only authorised
|
||||
peers can establish sessions.
|
||||
|
||||
.. note::
|
||||
``rnsh`` provides a genuine interactive terminal over Reticulum. It supports
|
||||
full terminal emulation including escape sequences, window resizing, signal
|
||||
forwarding, and piping of standard input, output and error streams. This
|
||||
makes it suitable for running text editors, terminal multiplexers, and any
|
||||
other interactive programs on remote systems.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
Start ``rnsh`` in listener mode, accepting connections from specific identities:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
|
||||
|
||||
You can also specify allowed identity hashes (one per line) in the file
|
||||
``~/.rnsh/allowed_identities`` or ``~/.config/rnsh/allowed_identities``, and
|
||||
simply run the program in listener mode:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l
|
||||
|
||||
Connect to a remote listener from another system:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh 7a55144adf826958a9529a3bcf08b149
|
||||
|
||||
Specify a command to run on the remote system, separating ``rnsh`` options from
|
||||
the remote command with ``--``:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh 7a55144adf826958a9529a3bcf08b149 -- top
|
||||
|
||||
Set a default command for the listener, in case the initiator does not supply
|
||||
one, or when remote command execution is disabled:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -- /bin/bash --login
|
||||
|
||||
Use the ``-m`` flag to mirror the exit code of the remote process:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -m 7a55144adf826958a9529a3bcf08b149 -- /usr/local/bin/check-status
|
||||
|
||||
Use the ``-p`` flag to display the identity and destination hash for a listener:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
Listening on : 7a55144adf826958a9529a3bcf08b149
|
||||
|
||||
Use a specific identity file rather than the default:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -i /path/to/identity
|
||||
|
||||
Announce the listener destination on startup, and periodically:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -b 900
|
||||
|
||||
The ``-b`` option specifies the announce period in seconds. Use ``0`` to
|
||||
announce only once at startup.
|
||||
|
||||
**Authentication & Authorisation**
|
||||
|
||||
By default, ``rnsh`` requires that connecting initiators identify themselves
|
||||
with a Reticulum Identity whose hash is present in the list of allowed
|
||||
identities. Allowed identities can be specified on the command line with the
|
||||
``-a`` option, and can be used multiple times:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
|
||||
|
||||
You can also maintain a list of allowed identity hashes in the file
|
||||
``~/.rnsh/allowed_identities`` or ``~/.config/rnsh/allowed_identities``,
|
||||
with one hex hash per line. This file is reloaded every time a new connection
|
||||
is received, so changes take effect immediately without restarting ``rnsh``.
|
||||
|
||||
If you want to accept connections from any identity (for testing or in fully
|
||||
trusted environments), you can disable authentication with the ``-n`` option:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -n
|
||||
|
||||
.. warning::
|
||||
Disabling authentication with ``-n`` means that **any** Reticulum peer that
|
||||
can reach your listener will be able to execute commands on your system. Only
|
||||
use this option if you *really* know what you're doing.
|
||||
|
||||
**Remote Command Control**
|
||||
|
||||
When running in listener mode, ``rnsh`` allows you to control how remote
|
||||
commands are handled:
|
||||
|
||||
- By default, the listener accepts the command sent by the initiator. If the
|
||||
initiator does not supply a command, the listener's default shell is used.
|
||||
|
||||
- Use ``-C`` (``--no-remote-command``) to disable execution of commands received
|
||||
from the initiator. Only the listener's default command (or the command
|
||||
specified after ``--``) will be executed:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -C -- /usr/local/bin/safe-script
|
||||
|
||||
- Use ``-A`` (``--remote-command-as-args``) to append the initiator's command
|
||||
to the listener's default command instead of replacing it. This can be useful
|
||||
for restricting the remote to a specific program while still allowing the
|
||||
initiator to pass arguments:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -A -- /usr/bin/top
|
||||
|
||||
**Service Names**
|
||||
|
||||
When running in listener mode, ``rnsh`` uses a service name to differentiate
|
||||
between multiple listener instances that may share the same identity. By
|
||||
default, the service name is ``default``. You can specify a different service
|
||||
name with the ``-s`` option:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -s monitoring
|
||||
|
||||
This allows you to run multiple listeners on the same node, each with a
|
||||
different service name and purpose.
|
||||
|
||||
**Initiator Options**
|
||||
|
||||
When connecting to a remote listener, several options are available:
|
||||
|
||||
- Use ``-N`` (``--no-id``) to disable sending your identity to the remote
|
||||
listener. Note that the listener must have authentication disabled (``-n``)
|
||||
for the connection to succeed in this case.
|
||||
|
||||
- Use ``-m`` (``--mirror``) to make the initiator return with the exit code of
|
||||
the remote process, rather than always returning ``0``.
|
||||
|
||||
- Use ``-w`` (``--timeout``) to specify the connection and request timeout in
|
||||
seconds. By default, the timeout matches the Reticulum path request timeout.
|
||||
|
||||
**Identity & Destination**
|
||||
|
||||
The default identity file for ``rnsh`` is stored at
|
||||
``~/.reticulum/identities/rnsh``, but you can specify a different one with the
|
||||
``-i`` option, which will be created if it does not already exist:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -i /path/to/identity
|
||||
|
||||
To display the identity and destination information for a listener, use the
|
||||
``-p`` option. When combined with ``-l``, both the identity and the listening
|
||||
destination hash are displayed:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
|
||||
$ rnsh -l -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
Listening on : 7a55144adf826958a9529a3bcf08b149
|
||||
|
||||
**Verbosity**
|
||||
|
||||
Like other Reticulum utilities, ``rnsh`` supports the ``-v`` and ``-q`` flags
|
||||
to increase or decrease logging verbosity. Multiple flags can be specified to
|
||||
further adjust the log level. The default log level is ``INFO`` for listeners
|
||||
and ``ERROR`` for initiators.
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -vv # Listener with debug-level output
|
||||
$ rnsh -q 7a55144adf826958a9529a3bcf08b149 # Quiet initiator
|
||||
|
||||
By default, all log output is routed to ``~/.rnsh/logfile`` for initiators.
|
||||
|
||||
**Escape Sequences**
|
||||
|
||||
During an active ``rnsh`` session, the following escape sequences are
|
||||
available. These are only recognised immediately after a newline character:
|
||||
|
||||
- ``~~`` - Send a literal tilde character
|
||||
- ``~.`` - Terminate the session and exit immediately
|
||||
- ``~L`` - Toggle line-interactive mode
|
||||
- ``~?`` - Display the escape sequence quick reference
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnsh [-h] [--config CONFIG] [--identity IDENTITY] [-v] [-q] [-p]
|
||||
[--version] [-l] [-s SERVICE] [-b PERIOD] [-a HASH] [-n] [-A] [-C]
|
||||
[-N] [-m] [-w SECONDS]
|
||||
[destination]
|
||||
|
||||
Reticulum Remote Shell Utility
|
||||
|
||||
positional arguments:
|
||||
destination hexadecimal hash of the destination to connect to
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config, -c CONFIG path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY
|
||||
path to identity file to use
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
--version show program's version number and exit
|
||||
-l, --listen listen (server) mode; any command specified after --
|
||||
will be used as the default command when the initiator
|
||||
does not provide one or when remote command execution
|
||||
is disabled; if no command is specified, the default
|
||||
shell of the user running rnsh will be used
|
||||
-s, --service SERVICE
|
||||
service name for identity file if not the default
|
||||
-b, --announce PERIOD
|
||||
announce on startup and every PERIOD seconds; specify
|
||||
0 to announce on startup only
|
||||
-a, --allowed HASH allow this identity to connect (may be specified
|
||||
multiple times); allowed identities can also be
|
||||
specified in ~/.rnsh/allowed_identities or
|
||||
~/.config/rnsh/allowed_identities, one hash per line
|
||||
-n, --no-auth disable authentication (allow any identity to connect)
|
||||
-A, --remote-command-as-args
|
||||
concatenate remote command to the argument list of the
|
||||
default program or shell
|
||||
-C, --no-remote-command
|
||||
disable executing command lines received from the
|
||||
remote initiator
|
||||
-N, --no-id disable identity announcement on connect
|
||||
-m, --mirror return with the exit code of the remote process
|
||||
-w, --timeout SECONDS
|
||||
connect and request timeout in seconds
|
||||
|
||||
When specifying a command to execute, separate rnsh options from the command
|
||||
and its arguments with --. For example:
|
||||
|
||||
rnsh -l -- /bin/bash --login
|
||||
rnsh <destination> -- ls -la /tmp
|
||||
|
||||
|
||||
The rnodeconf Utility
|
||||
=====================
|
||||
|
||||
@@ -810,6 +1126,104 @@ to create and provision new :ref:`RNodes<rnode-main>` from any supported hardwar
|
||||
For more information on how to create your own RNodes, please read the :ref:`Creating RNodes<rnode-creating>`
|
||||
section of this manual.
|
||||
|
||||
.. _using-interface_discovery:
|
||||
Discovering Interfaces
|
||||
----------------------
|
||||
|
||||
Reticulum includes built-in functionality for discovering connectable interfaces over Reticulum itself. This is particularly useful in situations where you want to do one or more of the following:
|
||||
|
||||
* Discover connectable entrypoints available on the Internet
|
||||
* Find connectable radio access points in the physical world
|
||||
* Maintain connectivity to RNS instances with unknown or changing IP addresses
|
||||
|
||||
Discovered interfaces can be **auto-connected** by Reticulum, which makes it possible to create setups where an arbitrary interface can act simply as a bootstrap connection, that can be torn down again once more suitable interfaces have been discovered and connected.
|
||||
|
||||
The interface discovery mechanism uses announces sent over Reticulum itself, and supports both publicly readable interfaces and private, encrypted discovery, that can only be decoded by specified *network identities*. It is also possible to specify which network identities should be considered valid sources for discovered interfaces, so that interfaces published by unknown entities are ignored.
|
||||
|
||||
.. note::
|
||||
A *network identity* is a normal Reticulum identity keyset that can be used by
|
||||
one or more transport nodes to identify them as belonging to the same overall
|
||||
network. In the context of interface discovery, this makes it easy to manage
|
||||
connecting to only the particular networks you care about, even if those networks
|
||||
utilize many individual physical transport node.
|
||||
|
||||
This also makes it convenient to auto-connect discovered interfaces only for networks you have some level of trust in.
|
||||
|
||||
For information on how to make your interfaces discoverable, see the :ref:`Discoverable Interfaces<interfaces-discoverable>` chapter of this manual. The current section will focus on how to actually *discover and connect to* interfaces available on the network.
|
||||
|
||||
In its most basic form, enabling interface discovery is as simple as setting ``discover_interfaces`` to ``true`` in your Reticulum config:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
discover_interfaces = yes
|
||||
...
|
||||
|
||||
Once this option is enabled, your RNS instance will start listening for interface discovery announces, and store them for later use or inspection. You can list discovered interfaces with the ``rnstatus`` utility:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnstatus -d
|
||||
|
||||
Name Type Status Last Heard Value Location
|
||||
-------------------------------------------------------------------------
|
||||
Sideband Hub Backbone ✓ Available 1h ago 16 46.2316, 6.0536
|
||||
RNS Amsterdam Backbone ✓ Available 32m ago 16 52.3865, 4.9037
|
||||
|
||||
|
||||
You can view more detailed information about discovered interfaces, including configuration snippets for pasting directly into your ``[interfaces]`` config, by using the ``rnstatus -D`` option:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnstatus -D sideband
|
||||
|
||||
Transport ID : 521c87a83afb8f29e4455e77930b973b
|
||||
Name : Sideband Hub
|
||||
Type : BackboneInterface
|
||||
Status : Available
|
||||
Transport : Enabled
|
||||
Distance : 2 hops
|
||||
Discovered : 9h and 40m ago
|
||||
Last Heard : 1h and 15m ago
|
||||
Location : 46.2316, 6.0536
|
||||
Address : sideband.connect.reticulum.network:7822
|
||||
Stamp Value : 16
|
||||
|
||||
Configuration Entry:
|
||||
[[Sideband Hub]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
remote = sideband.connect.reticulum.network
|
||||
target_port = 7822
|
||||
transport_identity = 521c87a83afb8f29e4455e77930b973b
|
||||
|
||||
In addition to providing local interface discovery information and control, the ``rnstatus`` utility can export discovered interface data in machine-readable JSON format using the ``rnstatus -d --json`` option. This can be useful for exporting the data to external applications such as status pages, access point maps and similar.
|
||||
|
||||
To control what sources are considered valid for discovered sources, additional
|
||||
configuration options can be specified for the interface discovery system.
|
||||
|
||||
* The ``interface_discovery_sources`` option is a list of the network or transport identities from which interfaces will be accepted. If this option is set, all others will be ignored. If this option is not set, discovered interfaces will be accepted from any source, but are still subject to stamp value requirements.
|
||||
|
||||
* The ``required_discovery_value`` options specifies the minimum stamp value required for the interface announce to be considered valid. To make it computationally difficult to spam the network with a large number of defunct or malicious interfaces, each announced interface requires a valid cryptographical stamp, of configurable difficulty value.
|
||||
|
||||
* The ``autoconnect_discovered_interfaces`` value defaults to ``0``, and specifies the maximum number of discovered interfaces that should be auto-connected at any given time. If set to a number greater than ``0``, Reticulum automatically manages discovered interface connections, and will bring discovered interfaces up and down based on availability. You can at any time add discovered interfaces to your configuration manually, to persistently keep them available.
|
||||
|
||||
* The ``network_identity`` option specifies the *network identity* for this RNS instance. This identity is used both to sign (and potentially encrypt) *outgoing* interface discovery announces, and to decrypt incoming discovery information.
|
||||
|
||||
The configuration snippet below contains an example of setting these additional configuration options:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
discover_interfaces = yes
|
||||
interface_discovery_sources = 521c87a83afb8f29e4455e77930b973b
|
||||
required_discovery_value = 16
|
||||
autoconnect_discovered_interfaces = 3
|
||||
network_identity = ~/.reticulum/storage/identities/my_network
|
||||
...
|
||||
|
||||
Remote Management
|
||||
-----------------
|
||||
|
||||
@@ -835,6 +1249,133 @@ in the Reticulum configuration file:
|
||||
|
||||
For a complete example configuration, you can run ``rnsd --exampleconfig``.
|
||||
|
||||
.. _using-blackhole_management:
|
||||
|
||||
Blackhole Management
|
||||
--------------------
|
||||
|
||||
Reticulum networks are fundamentally permissionless and open, allowing anyone with a compatible interface to participate. While this openness is essential for a resilient and decentralized network, it also exposes the network to potential abuse, such as peers flooding the network with excessive announce broadcasts or other forms of resource exhaustion.
|
||||
|
||||
The **Blackhole** system provides tools to help manage this problem. It allows operators and individual users to block specific identities at the Transport layer, preventing them from propagating announces through your node, and for other nodes to reach them through your network.
|
||||
|
||||
.. important::
|
||||
|
||||
There is fundamentally **no way** to *globally* block or censor any identity or destination in Reticulum networks. The blackhole functionality will prevent announces from (and traffic to) all destinations associated with the blackholed identity *on your own network segments only*.
|
||||
|
||||
This provides users and operators with control over what they want to allow *on their own network segments*, but there is no way to globally censor or remove an identity, as long as *someone* is willing to provide transport for it.
|
||||
|
||||
This functionality serves a dual purpose:
|
||||
|
||||
* **For Individual Users:** It offers a simple way to maintain a quiet and efficient local network by manually blocking spammy or unwanted peers.
|
||||
* **For Network Operators:** It enables the creation of federated, community-wide security standards. By publishing and sharing blackhole lists, operators can protect large infrastructures and distribute spam filtering rules across the mesh without manual intervention.
|
||||
|
||||
|
||||
Local Blackhole Management
|
||||
==========================
|
||||
|
||||
The most immediate way to manage unwanted identities is through manual configuration using the ``rnpath`` utility. This allows you to instantly block or unblock specific identities on your local Transport Instance.
|
||||
|
||||
**Blackholing an Identity**
|
||||
|
||||
To block an identity, use the ``-B`` (or ``--blackhole``) flag followed by the identity hash. You can optionally specify a duration and a reason, which are useful for logging and future reference.
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnpath -B 3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o
|
||||
|
||||
You can also add a duration (in hours) and a reason:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnpath -B 3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o --duration 24 --reason "Excessive announces"
|
||||
|
||||
**Lifting Blackholes**
|
||||
|
||||
To remove an identity from the blackhole, use the ``-U`` (or ``--unblackhole``) flag:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnpath -U 3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o
|
||||
|
||||
**Viewing the Blackhole List**
|
||||
|
||||
To see all identities currently blackholed on your local instance, use the ``-b`` (or ``--blackholed``) flag:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnpath -b
|
||||
|
||||
<3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o> blackholed for 23h, 56m (Excessive announces)
|
||||
<399ea050ce0eed1816c300bcb0840938> blackholed indefinitely (Announce spam)
|
||||
<d56a4fa02c0a77b3575935aedd90bdb2> blackholed indefinitely (Announce spam)
|
||||
<2b9ec651326d9bc274119054c70fb75e> blackholed indefinitely (Announce spam)
|
||||
<1178a8f1fad405bf2ad153bf5036bdfd> blackholed indefinitely (Announce spam)
|
||||
|
||||
|
||||
|
||||
Automated List Sourcing
|
||||
=======================
|
||||
|
||||
Manually blocking identities is effective for immediate threats and annoyances, but maintaining an up-to-date blocklist across many nodes on a large network is impractical. Reticulum supports **automated list sourcing**, allowing your node to subscribe to blackhole lists maintained by trusted peers, or a central authority you manage yourself.
|
||||
|
||||
.. warning::
|
||||
**Verify Before Subscribing!** Subscribing to a blackhole source is a powerful action that grants that source the ability to dictate who you can communicate with. Before adding a source to your configuration, verify that the maintainer aligns with your usage policy and values. Blindly subscribing to untrusted lists could inadvertently block legitimate peers or essential services.
|
||||
|
||||
When enabled, your Transport Instance will periodically (approximately once per hour) connect to configured sources, retrieve their latest blackhole lists, and automatically merge them into your local blocklist. This provides "set-and-forget" protection for both individual users and large networks.
|
||||
|
||||
**Configuration**
|
||||
|
||||
To enable automated sourcing, add the ``blackhole_sources`` option to the ``[reticulum]`` section of your configuration file. This option accepts a comma-separated list of Transport Identity hashes that you trust to provide valid blackhole lists.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
# Automatically fetch blackhole lists from these trusted sources
|
||||
blackhole_sources = 521c87a83afb8f29e4455e77930b973b, 68a4aa91ac350c4087564e8a69f84e86
|
||||
|
||||
# Optional update interval, defaults to one hour
|
||||
blackhole_update_interval = 60
|
||||
...
|
||||
|
||||
**How It Works**
|
||||
|
||||
1. When enabled, the ``BlackholeUpdater`` service runs in the background.
|
||||
2. For every identity hash listed in ``blackhole_sources``, it attempts to establish a temporary link to its associated``rnstransport.info.blackhole`` destination.
|
||||
3. It requests the ``/list`` path, which returns a dictionary of blackholed identities and their associated metadata.
|
||||
4. The received list is merged with your local ``blackholed_identities`` database.
|
||||
5. The lists are persisted to disk, ensuring they survive restarts.
|
||||
|
||||
.. note::
|
||||
You can verify the external lists you are subscribed to, and their contents, without importing them by using ``rnpath -p``. See the :ref:`rnpath utility documentation<utility-rnpath>` for details on querying remote blackhole lists.
|
||||
|
||||
|
||||
Publishing Blackhole Lists
|
||||
==========================
|
||||
|
||||
If you are operating a public gateway, a community hub, or simply wish to share your blackhole list with others, you can configure your instance to act as a blackhole list publisher. This allows other nodes to subscribe to *your* definitions of unwanted traffic.
|
||||
|
||||
**Enabling Publishing**
|
||||
|
||||
To publish your local blackhole list, enable the ``publish_blackhole`` option in the ``[reticulum]`` section:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
publish_blackhole = yes
|
||||
...
|
||||
|
||||
When this is enabled, your Transport Instance will register a request handler at ``rnstransport.info.blackhole``. Any peer that connects to this destination and requests ``/list`` will receive the complete set of identities currently present in your local blackhole database.
|
||||
|
||||
**Federation and Trust**
|
||||
|
||||
The blackhole system relies on the trust relationship between the subscriber and the publisher. By subscribing to a source, you are implicitly trusting that source to only block identities that are genuinely detrimental to the network.
|
||||
|
||||
As the ecosystem matures, this system is designed to integrate with **Network Identities**. This allows communities to verify that a published blackhole list is actually provided by a specific network or organization with a certain level of reputation and trustworthiness, adding a layer of cryptographic trust to the federation process. This prevents malicious actors from publishing fake lists intended to censor legitimate traffic.
|
||||
|
||||
For operators, this creates a scalable model where maintaining a single high-quality blocklist can protect thousands of downstream peers, drastically reducing the administrative.
|
||||
|
||||
Improving System Configuration
|
||||
------------------------------
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ Reticulum is a cryptography-based networking stack for building both local and
|
||||
wide-area networks with readily available hardware, that can continue to operate
|
||||
under adverse conditions, such as extremely low bandwidth and very high latency.
|
||||
|
||||
To understand the foundational philosophy and goals of this system, read the
|
||||
:ref:`Zen of Reticulum <zen>`.
|
||||
|
||||
Reticulum allows you to build wide-area networks with off-the-shelf tools, and
|
||||
offers end-to-end encryption, forward secrecy, autoconfiguring cryptographically
|
||||
backed multi-hop transport, efficient addressing, unforgeable packet
|
||||
@@ -40,6 +43,22 @@ real-world use is explored. The API and wire-format can be considered complete a
|
||||
could change if absolutely warranted.
|
||||
|
||||
|
||||
Reference Implementation
|
||||
========================
|
||||
The Python code, for which this documentation is written, and known as the Reticulum Network Stack,
|
||||
is the Reference Implementation of Reticulum. The Reticulum Protocol is defined entirely
|
||||
and authoritatively by this reference implementation, and this manual. It is maintained by Mark Qvist,
|
||||
identified by the Reticulum Identity ``<bc7291552be7a58f361522990465165c>``.
|
||||
|
||||
Compatibility with the Reticulum Protocol is defined as having full interoperability,
|
||||
and sufficient functional parity with this reference implementation. Any specific protocol
|
||||
implementation that achieves this is Reticulum. Any that does not is not Reticulum.
|
||||
|
||||
The reference implementation is licensed under the :ref:`Reticulum License <license>`.
|
||||
|
||||
The Reticulum Protocol was dedicated to the Public Domain in 2016.
|
||||
|
||||
|
||||
What does Reticulum Offer?
|
||||
==========================
|
||||
|
||||
@@ -178,14 +197,7 @@ Reticulum implements a range of generalised interface types that covers the comm
|
||||
|
||||
* Or to quickly create interfaces with custom hardware
|
||||
|
||||
* Anything else using :ref:`custom interface modules<interfaces-custom>` written in Python
|
||||
|
||||
For a full list and more details, see the :ref:`Supported Interfaces<interfaces-main>` chapter.
|
||||
|
||||
|
||||
Caveat Emptor
|
||||
==============
|
||||
Reticulum is an experimental networking stack, and should be considered as
|
||||
such. While it has been built with cryptography best-practices very foremost in
|
||||
mind, it has not yet been externally security audited, and there could very well be
|
||||
privacy-breaking bugs. To be considered secure, Reticulum needs a thorough
|
||||
security review by independent cryptographers and security researchers. If you
|
||||
want to help out with this, or can help sponsor an audit, please do get in touch.
|
||||
@@ -0,0 +1,453 @@
|
||||
.. _zen:
|
||||
|
||||
****************
|
||||
Zen of Reticulum
|
||||
****************
|
||||
|
||||
The Illusion Of The Center
|
||||
==========================
|
||||
|
||||
For the better part of a generation, we have been taught to visualize the digital world through the lens of hierarchy. The mental maps we carry are dominated by a single, misleading image: **The Cloud**.
|
||||
|
||||
We imagine the network as a vast, ethereal space "up there" or "out there". A centralized repository of services and data to which we, the lowly clients, must connect. We build our software with this assumption hardcoded into our logic: *There is a server. The server has the authority. The server knows the way. I must find the server to function*.
|
||||
|
||||
This is the Client-Server mental model, and it is the primary obstacle to understanding Reticulum.
|
||||
|
||||
Fallacy Of The Cloud
|
||||
--------------------
|
||||
|
||||
The first step in the Zen of Reticulum is to realize that *there is no cloud*. There is only other people's computers. When you build for the cloud, you are building *for* a landlord. You are accepting that your application's existence is conditional on the permission, uptime, and continued goodwill of a central authority.
|
||||
|
||||
In Reticulum, you must shift your thinking from "connecting to" to "being among". Reticulum is not a service you subscribe to - *it is a fabric you inhabit*. There is no "up there". There is only *here* and *there*, and the space between them is peer-to-peer.
|
||||
|
||||
Decentralization Or Uncentralizability?
|
||||
---------------------------------------
|
||||
|
||||
It is common to hear the word "decentralized" thrown around in modern tech circles. But often, this is merely a marketing term for "slightly distributed centralization". A blockchain with a few dominant miners, or a federated protocol with a few giant servers. *In practice*, it's still centralized. It simply has a few centers instead of one.
|
||||
|
||||
Reticulum goes further. It wants **Uncentralizability**.
|
||||
|
||||
This is not a wishful political stance, but a foundational mathematical characteristic of the protocol, onto which everything else has been built. Reticulum assumes that every peer on the network is potentially hostile, and every link is potentially compromised. It is designed with no "privileged" nodes. While some nodes may act as Transport Instances - forwarding traffic for others - they do so *blindly*, and they only know about their immediate surroundings, and nothing more. They route based on cryptographic proofs, not on administrative privilege. They cannot see who is talking to whom, nor can they selectively manipulate traffic without breaking their own ability to route entirely.
|
||||
|
||||
The system is designed to make hierarchy structurally impossible. You cannot hijack an address, because there is no central registry to hijack. You cannot block a user, because there is no central switch to flip. You can offer paths through the network, but you can't force anyone to use them.
|
||||
|
||||
Death To The Address
|
||||
--------------------
|
||||
|
||||
To break free of the center, you must also let go of the concept of the "Address".
|
||||
|
||||
In the IP world, an address is a location. It is a coordinate in a *deeply hierarchical* and static grid. If you move your computer to a different house, your address changes. If your router reboots, your address might change. Your *identity* is bound to your *location*, and therefore, it is fragile, and easily controlled.
|
||||
|
||||
Reticulum abolishes this link between *Identity* and *Location*.
|
||||
|
||||
In Reticulum, an address is not a place; it is a **Hash of an Identity**. It is a cryptographic representation of *who* you are, not *where* you are. Because of this, your address is portable. You can take a laptop from a WiFi cafe in Berlin, to a LoRa mesh in the mountains, to a packet radio link on a boat, and your "address" - your *Destination Hash* - never changes.
|
||||
|
||||
The network does not route to a place; it routes to a *person* (or a machine). When you send a packet, you are not targeting a coordinate in a grid; you are encrypting a message for a specific entity. The network dynamically discovers where that entity currently resides, and it does so in a way where no one really knows where that entity is actually located physically.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I am at* ``192.168.1.5``. *Come find me"*.
|
||||
- **The Zen Way:** *"I am* ``<327c1b2f87c9353e01769b01090b18f2>``. *Wherever I am, my peers can reach me"*.
|
||||
|
||||
Once you stop thinking about servers and start thinking about portable identities, where everyone can always reach everyone else directly, the illusion of the center fades away. You realize there *is* no center holding the network together. No coordinators or bureaucrats required. The network is simply the sum of its peers, communicating directly, sovereignly, and without a master.
|
||||
|
||||
|
||||
Physics Of Trust
|
||||
================
|
||||
*Paranoia Is A Great Design Principle*
|
||||
|
||||
If we accept that there is no center - that the network is a chaotic, peer-to-peer mesh - we are forced to confront a terrifying reality: **There is no one guarding the door**.
|
||||
|
||||
In the traditional networking mindset, we rely on the concept of the "trusted core". We assume our local coffee shop WiFi is safe, or that the backbone providers are neutral custodians. We build our security like a castle: strong walls on the outside, soft and trusting on the inside. We use encryption only when we step out into the "wild" internet.
|
||||
|
||||
Hostile Environments
|
||||
--------------------
|
||||
|
||||
The Zen of Reticulum requires you to invert this. You must assume that *every* environment is hostile. This isn't cynicism, just uncaring physics.
|
||||
|
||||
When you transmit information over radio waves, you are shouting into a crowded room. Anyone can listen. When you traverse the internet, your packets pass through routers controlled by strangers, corporations, and state actors. Assuming privacy in this environment without cryptographic protection is not optimism but gross negligence.
|
||||
|
||||
Reticulum is built on the premise that every link is tapped, and every peer is a potential adversary. If your system cannot survive an adversary owning the physical layer, it cannot survive at all.
|
||||
|
||||
But this is the paradox: By assuming the network is hostile, you make it safe. When you accept the dangers for what they are, they become manageable. When you stop trusting the infrastructure and start trusting the math, you eliminate the single point of failure: Human integrity.
|
||||
|
||||
Encryption Is Not A Feature
|
||||
---------------------------
|
||||
|
||||
In the world of TCP/IP, encryption is an afterthought. It is a layer we slap on top of the protocol (HTTPS, TLS) to patch the security holes of the original design. It is a "feature" you sometimes *enable* for "sensitive data". This is fundamentally flawed, since all data is sensitive.
|
||||
|
||||
In Reticulum, encryption is **gravity**.
|
||||
|
||||
It is not optional. It is not a plugin. It is the *fundamental force that allows the network to exist*. If you were to strip the encryption from Reticulum, the routing would break. The Transport system uses cryptographic signatures and entropy to verify paths and pass information. If packets were plaintext, intermediate nodes could not prove that a route was valid, nor could endpoints prevent spoofing or tampering.
|
||||
|
||||
In Reticulum, the entropy of the encrypted packet *is* the routing logic.
|
||||
|
||||
To ask for a version of Reticulum without encryption is like asking for a version of the ocean without liquid. You are not asking for a feature change; you're asking for a different physical universe. We design for a universe where information has mass, structure, and integrity.
|
||||
|
||||
Zero-Trust Architectures
|
||||
------------------------
|
||||
|
||||
We must unlearn our reliance on **Institutional Trust**.
|
||||
|
||||
For decades, we have been trained to trust authorities. We trust a website because a chain of Certificate Authorities (companies we don't know) vouches for it. We trust an app because it is in an app store (run by a corporation we don't control). We trust a message because it comes from a phone number assigned by a telecom. Yet, everything in our digital information sphere today is more untrustworthy and risky than a medieval second-hand underwear market.
|
||||
|
||||
Reticulum replaces institutional trust with **Cryptographic Proof**.
|
||||
|
||||
In Reticulum, you do not trust a node because it has a nice hostname or because it is listed in a directory. You trust it because it holds the private key corresponding to the Destination Hash you are communicating with. This trust is binary, mathematical, and **absolute**. Either the signature matches, or it does not. There is no "maybe".
|
||||
|
||||
This shift moves the power from the institution to the individual. You become the ultimate arbiter of your own trust relationships. You decide which keys to accept, which paths to follow, and which identities to recognize.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I trust this site because the browser says the lock icon is green"*.
|
||||
- **The Zen Way:** *"I trust this destination because I have verified its hash fingerprint out-of-band, and the math confirms the signature"*.
|
||||
|
||||
When you internalize the Physics of Trust, you stop looking for protection from firewalls, VPNs, and Terms of Service agreements. You realize that true security comes from the design of the protocol itself. You can stop trusting the cloud, and you start trusting the code - because you can verify it yourself.
|
||||
|
||||
|
||||
Merits Of Scarcity
|
||||
==================
|
||||
*Every Bit Counts*
|
||||
|
||||
We have grown addicted to abundance. In the modern digital ecosystem, bandwidth is treated as an endless, flat ocean. We stream high-definition video without a thought, we ship entire libraries of code just to render a single button, and we measure performance in gigabits per second. This abundance has hollowed out our craft. When constraints vanish, efficiency dies, and with it, a certain kind of Clarity and Quality.
|
||||
|
||||
Reticulum asks you to step out of the ocean and onto the tightrope.
|
||||
|
||||
The Bandwidth Fallacy
|
||||
---------------------
|
||||
|
||||
The Zen of Reticulum requires the realization that **5 bits per second is a valid speed**.
|
||||
|
||||
To a modern developer, this sounds like paralysis. But there is a profound freedom in limits: When you have a gigabit connection, you can be incredibly sloppy. You can be wasteful. You can push your problems onto the infrastructure. *"It’s slow? Get a faster router"*.
|
||||
|
||||
But on a high-latency, low-bandwidth link (be it a noisy HF radio channel or a tenuous LoRa hop) you cannot push problems anywhere. You must solve them. The network does not negotiate with waste.
|
||||
|
||||
This forces a shift from consumption to interaction. You are no longer, then, consuming a service provided by a fat pipe; you are engaging in a careful negotiation with the physical medium. The medium becomes a partner in the conversation, not just a dumb conduit. You suddenly need to *understand the world to be in it*.
|
||||
|
||||
Cost Of A Byte
|
||||
--------------
|
||||
|
||||
In a scarce economy, a byte is not just data, but energy, time, and space.
|
||||
|
||||
Every byte you transmit consumes battery life on a solar-powered node. It occupies valuable airtime that could have been used by another peer. It represents a measurable slice of the electromagnetic spectrum.
|
||||
|
||||
When you internalize this, you begin to write code differently. You stop asking, "How much data can I send?" and start asking, "What is the *minimum* amount of information required to convey this intent? How can I best utilize my informational entropy?"
|
||||
|
||||
This is where the elegance of Reticulum shines. The protocol is designed to strip away the non-essential. A link establishment takes three very small packets. A destination hash fits in 16 bytes. The overhead is vanishingly small, leaving almost the entire channel for the message itself.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I need to send a status update. I'll send a JSON object with metadata, timestamps, and user profile info (15KB)."*
|
||||
- **The Zen Way:** *"I need to send a status update. I'll send a single byte representing the state code. The context is already known."*
|
||||
|
||||
This is of course optimization, but more importantly, *it is a form of respect*. Efficiency in a shared medium is an act of stewardship. By taking only what you need from the network, you leave room for others. The network listens to those who speak with purpose.
|
||||
|
||||
Flow & Time
|
||||
-----------
|
||||
|
||||
Scarcity also teaches us about time. We have become addicted to the *synchronous* now - the instant ping, the real-time stream. But Reticulum embraces *asynchronous* time.
|
||||
|
||||
When links are intermittent and latency is measured in minutes or hours, "real-time" is an illusion. Reticulum doesn't encourage **Store and Forward** as a mere fallback, but as a primary mode of existence. You write a message, it propagates when it can, and it arrives when it arrives.
|
||||
|
||||
This changes the psychological texture of communication. It removes the anxiety of the immediate response. It allows for contemplation. You are not demanding the recipient's attention *right now*; you are placing a gift in their path, to be found when they are ready.
|
||||
|
||||
By designing for delay, you design for resilience. You are no longer building a house of cards that collapses when a single packet drops. You are building a stone arch that distributes the load *over time*.
|
||||
|
||||
Liberation From Limits
|
||||
----------------------
|
||||
|
||||
There is a strange optimism in scarcity. When you are forced to work within strict constraints, you are forced to prioritize. *You* must decide what truly matters. *That* is the real core of agency.
|
||||
|
||||
In the infinite fantasy world of The Cloud, everything is urgent, so nothing is. In the economy of Reticulum, the cost of transmission forces you to weigh the value of your message. Do you really need to send that heart beat? Is that photo essential?
|
||||
|
||||
When you strip away the noise, what remains is *signal*.
|
||||
|
||||
This discipline creates a different kind of developer. It creates a craftsman who understands that the best code is the code you don't have to write. It creates a user who understands that the most powerful message is the one that is *understood*, not the one that is loudest. In the world of Reticulum, you are not a mere consumer of bandwidth; you are an architect of intent.
|
||||
|
||||
|
||||
Sovereignty Through Infrastructure
|
||||
==================================
|
||||
**Be Your Own Network**
|
||||
|
||||
We live in an era of digital tenancy. We lease our connectivity from ISPs. We rent our storage from cloud providers. We even borrow our identity from social media platforms. We are tenants in a house we did not build, governed by rules we did not write, subject to eviction at the whim of a landlord who has never met us.
|
||||
|
||||
The Zen of Reticulum is the realization that you *can* own the house.
|
||||
|
||||
A Carrier-Grade Fallacy
|
||||
-----------------------
|
||||
|
||||
For decades, we have been gaslit into believing that networking is really not just hard, but impossible. It is presented as a dark art reserved for telcos and billionaires, requiring millions of dollars of fiber optics, climate-controlled data centers, and armies of engineers. We are told that building reliable infrastructure is "too complex" for the individual or small organization.
|
||||
|
||||
This is a big, fat lie.
|
||||
|
||||
Physics is simple. A radio wave needs a transmitter and a receiver. A packet needs a path. The "complexity" of the modern internet is largely bureaucratic - a mountain of billing systems, regulatory capture, and legacy cruft designed to keep the gatekeepers in power.
|
||||
|
||||
Reticulum strips away the bureaucracy. It runs on hardware that costs the price of a dinner. It runs on spectrum that is free to use. It demonstrates that a robust, planetary-scale network does not require a Fortune 500 company. It requires only the will to deploy, and the distributed, uncoordinated efforts of many individuals.
|
||||
|
||||
Personal Infrastructure
|
||||
-----------------------
|
||||
|
||||
This is where the rubber meets the road. You can read about Reticulum, you can understand the theory, but the insights only arrive when you plug in a radio and run a Transport Node. Suddenly, you are no longer a consumer. You're an operator.
|
||||
|
||||
This shift is subtle but profound. When you run your own infrastructure, the network ceases to be a service that is provided *to* you. It becomes a space that you *inhabit*. You become responsible for the flow of information. You gain an intimate understanding of the medium - the way the weather affects the radio waves, the way the topology changes, the way the packets dance through the ether.
|
||||
|
||||
There is a quiet competence that comes from this. You stop asking "Is the internet down?" and start asking "Is *my* links up?" You stop waiting for a technician and start checking the logs. This is a form of strength. To understand the system that carries your words is to be free from the mystery that keeps you dependent.
|
||||
|
||||
The Ability To Disconnect
|
||||
-------------------------
|
||||
|
||||
Why go to the trouble? Why buy the radio, write the config, and leave the Pi running in the corner?
|
||||
|
||||
Because the old, centralized network is fragile. And because most of us doesn't even really want to be there anymore.
|
||||
|
||||
The internet we rely on today is a chain of single points of failure. Cut the undersea cable, and a continent goes dark. Shut down the power grid, and the cloud evaporates. Deprioritize the "wrong" traffic, and the flow of information is strangled.
|
||||
|
||||
Sovereignty is the ability to survive the cut, whether or not that cut was an accident or on purpose.
|
||||
|
||||
When you build your own infrastructure, you build a lifeline. Reticulum is designed to function over media that the traditional internet cannot touch - bare wires, battery-powered radios, ad-hoc WiFi meshes. When the grid fails, or the censors arrive, or the bill goes unpaid, your Reticulum network continues to hum.
|
||||
|
||||
This is not about "dropping out" of society. It is about building a substrate on which an actual *Society* can function.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"My connection is slow. I should call my ISP and complain."*
|
||||
- **The Zen Way:** *"The path is noisy. I will adjust the antenna or find a better route."*
|
||||
|
||||
By taking ownership of the infrastructure, you take ownership of your voice. You stop shouting into someone else's megaphone and start building your own. The network is no longer something that happens to you; it is something you make happen.
|
||||
|
||||
|
||||
Identity and Nomadism
|
||||
=====================
|
||||
**A Fluid Self**
|
||||
|
||||
In the old world, you are defined by your coordinates. If you are at ``34.109.71.5``, you're *here*. If you unplug the cable and walk down the street, you vanish. Your digital self evaporates because it was tethered to the wall. You are a ghost in the endless machinations of gears, levers and transistors, bound to the hardware, and those that own it.
|
||||
|
||||
This creates a subtle, constant anxiety. We are terrified of disconnecting because, in the architecture of the old web, disconnecting is a kind of death.
|
||||
|
||||
The Zen of Reticulum offers a different way to be.
|
||||
|
||||
Portable Existence
|
||||
------------------
|
||||
|
||||
In Reticulum, your identity is not a location, or a username granted by a service. It is a cryptographic key - a complex, unique mathematical signature that exists independently of the physical world. You can carry it only in your mind, if you want to.
|
||||
|
||||
Think of it less like a street address and more like a name. *A true name*.
|
||||
|
||||
If you travel from Berlin to Tokyo, you do not change your name. You are still you. The people who know you can still recognize you. Reticulum applies this principle to the network layer. Your Destination Hash is **invariant**. It travels with you, stored securely on your device, *immutable as a stone*.
|
||||
|
||||
This changes the relationship between you and the machine. You are not "logged into" the network via a specific gateway. You *are* the endpoint. The network does not connect to a place; *it converges on you*.
|
||||
|
||||
Roaming Nodes
|
||||
-------------
|
||||
|
||||
This freedom introduces a new concept of time and space: **Nomadism**.
|
||||
|
||||
Because your identity is portable, your connectivity can be fluid. You can be sitting at a desk connected to a fiber backbone one moment, and walking through a field connected only to a long-range LoRa mesh the next. To the rest of the network, nothing has changed. Your friends do not need to update your contact info. The messages they send do not bounce back. The network senses the shift in the medium and reroutes the flow of data automatically.
|
||||
|
||||
You are no longer a stationary node in a fixed grid. You are a wanderer in a fluid medium.
|
||||
|
||||
The interfaces - whether it is WiFi, Ethernet, Packet Radio, or a physical wire - is merely the clothing your node wears. You change it to suit the environment. Underneath, you remain the same. This is the liberation of the protocol. It treats the physical medium as a transient circumstance, not a definition of self.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"I lost connection. I have to reconnect to the VPN to tell them where I am now."*
|
||||
- **The Zen Way:** *"I moved. The network subtly bends to accomodate this new reality."*
|
||||
|
||||
Announcing Presence
|
||||
-------------------
|
||||
|
||||
How does the network find a wanderer? It listens.
|
||||
|
||||
In the IP world, we query directories. We ask a server, "Where is Mark?" The server checks its database and gives us a coordinate. This means that someone, somewhere, is keeping track of you. It assumes and *requires* surveillance.
|
||||
|
||||
Reticulum replaces surveillance with **Announces**.
|
||||
|
||||
Instead of asking a central authority where you are, you simply state your presence. You broadcast a cryptographic proof: "I am here, and I am who I say I am". This ripples out through the mesh. Your neighbors hear it, update their path tables, and pass it on.
|
||||
|
||||
This is a quiet, organic process. It is the digital equivalent of lighting lanterns in the dark. You do not need to chase the light; you let the light find you. It respects your autonomy. You choose when to announce, how often to speak, and to whom. You also choose when to disappear - for but a moment or perpetually.
|
||||
|
||||
Anchor In The Flow
|
||||
------------------
|
||||
|
||||
There is a deep peace in this nomadism. It teaches you that stability does not come from standing still. Stability comes from *internal coherence*.
|
||||
|
||||
By holding your own private key, you hold your own center of gravity. The world around you; the infrastructure, the topography and the availability of links can all shift chaotically. Storms can knock out towers. Cables can be cut. The internet can go down.
|
||||
|
||||
But as long as you possess your key, you possess your identity. The entire infrastructure can be destroyed and rebuilt, and you are still you. Nothing lasts, yet nothing is lost.
|
||||
|
||||
You become a sovereign entity moving through the noise, connected not by the rigidity of cables, but by the fluidity of recognition. The network becomes a place you inhabit, rather than a utility you subscribe to: You are at home in the ether.
|
||||
|
||||
|
||||
Ethics Of The Tool
|
||||
==================
|
||||
**Technology With Conscience**
|
||||
|
||||
You have unlearned the center. You have accepted the physics of trust. You have embraced the economy of scarcity and the freedom of unbound nomadism. You are standing in a new space. Now, look at the tool in your hand.
|
||||
|
||||
In the old world, we were taught that technology is neutral. We are told that "guns don't kill people, people do", or that a component is just a component, indifferent to what its combinatorial potential is. This is a convenient lie. It serves only to allow the builders to wash their hands of responsibility.
|
||||
|
||||
But we know better now. We know that **architecture is politics**, and *politics is control*. The way you build a system determines how it will be used. If you build a system optimized for mass surveillance, you *will* get a panopticon. If you build a system optimized for centralized control, you *will* get a dictatorship. If you build a system optimized for extraction, you *will* get a parasite.
|
||||
|
||||
The Zen of Reticulum asserts that a tool is never neutral.
|
||||
|
||||
On the very contrary: A tool is intent, **crystallized**.
|
||||
|
||||
The Harm Principle
|
||||
------------------
|
||||
|
||||
Why does the Reticulum License forbid the software from being used in systems designed to harm humans? Is it not just a restriction on freedom?
|
||||
|
||||
It is a restriction on *license*, yes, but it is an expansion of *freedom*.
|
||||
|
||||
Building powerful tools without a moral compass is in no way virtuous or commendable, it is plain and simple irresponsibility.
|
||||
|
||||
A tool that can easily be used to oppress is a real danger to the user. If you build a network that can be turned against you by a tyrant, you are not free. You are merely waiting for the leash to tighten. By encoding the "Harm Principle" into the legal DNA of the reference implementation, we are building a safeguard. We are stating, clearly and immutably, that *this tool* is for **life**, not for death.
|
||||
|
||||
This aligns the software with the interests of humanity. It cements that the network cannot be conscripted into a kill-system, a weaponized drone controller, or a torture device without breaking the license and the law. It is a line drawn in the sand - not by a government or external authority, but by the creators of the tool itself.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** *"It's just software. How people use it is not my problem."*
|
||||
- **The Zen Way:** *"This software is a habitat. I will not allow it to be used to build a cage."*
|
||||
|
||||
It is *your* choice whether to align with this - we are not forcing this stance on anyone. If you choose to align with life over death, with creativity over destruction, we grant you an immensely powerful tool, to own and build with as you please. If you do not, we deny it.
|
||||
|
||||
If you do not like this, we most assuredly do not need you here, and you are on your own.
|
||||
|
||||
Public Domain Protocol
|
||||
----------------------
|
||||
|
||||
This leads to a vital distinction: The difference between the *idea* and the *implementation*.
|
||||
|
||||
The protocol - the mathematical rules of how Reticulum works - is dedicated to the Public Domain. It belongs to humanity. **No one can own it**. Anyone can implement it, improve it, or adapt it. This is the core idea of free communication, which itself must be forever free.
|
||||
|
||||
But the functional, deployed *reference implementation* - the Python code, the maintenance, the years of labor - has a conscience. This distinction is the engine of sustainability. It allows the protocol to be universal, while ensuring that the specific labor of the builders is not hijacked to undermine the foundational intent of the project itself. From this document, it should be very clear what this intent is.
|
||||
|
||||
If you want to build a system with Reticulum that manipulates and damages users for profits or targets missiles, you can use the public domain protocol, and start from scratch. But you cannot take our work. You must do your own. This serves as a pillar of accountability. If you want to build a weapon, *you* go and forge the steel yourself, while the world observes. And when the blood is drawn - it is on **your** hands.
|
||||
|
||||
Preserving Human Agency
|
||||
-----------------------
|
||||
|
||||
We live in an era of predatory extraction. The open-source commons is being scraped, ingested, and regurgitated by machine learning algorithms, whose corporate owners seek to replace the very humans who built those commons. Our code, our words, and our creativity is being used to train systems that are specifically designed to make us obsolete, without offering anything else in return than serfdom and leashes.
|
||||
|
||||
Reticulum stands against this.
|
||||
|
||||
The license protects the software from being used to feed the beast. It draws a hard line: This tool is for *people*. It is for human-to-human connection. It is not a dataset to be strip-mined for the purpose of building a synthetic overlord, puppeteered by a miniscule conglomerate of controllers.
|
||||
|
||||
This is a radical act of preservation. By protecting the code from AI appropriation, we are protecting space for human agency. We are ensuring that there remains a digital realm where the actors are flesh, blood and soul, where decisions are made by minds, not overlords hiding behind models.
|
||||
|
||||
When you use Reticulum, you are using a tool that respects you. It does not see you as a product to be tracked. It does not see your data as fuel for an algorithm. It sees you as a sovereign, equal peer.
|
||||
|
||||
This changes the foundational premise of using the technology. It restores dignity to the interaction. You are not the user of a service; you are a participant in a mutual covenant. The tool aligns with your autonomy, rather than eroding it.
|
||||
|
||||
In this way, ethics is not a restriction, but a foundation. It is the foundation that helps ensure the network will still belong to you tomorrow.
|
||||
|
||||
|
||||
Design Patterns For Post-IP Systems
|
||||
===================================
|
||||
**Practical Philosophy for Developers**
|
||||
|
||||
The philosophy is useless if it cannot be hammered into code. The metaphors we have explored - nomadism, scarcity, trust - are not just poetry, but real-world engineering constraints. When you sit down to write software for Reticulum, these concepts must shape the very structure of your application.
|
||||
|
||||
We are now moving from the *why* to the *how*. This is where the abstract becomes concrete, and where you will see the true depth of the patterns we have been weaving.
|
||||
|
||||
Store & Forward
|
||||
---------------
|
||||
|
||||
The web has trained us to be impatient. We write synchronous code. We fire a request and we wait, blocking the UI, holding our breath. If the response doesn't come in 250 milliseconds, we show a spinner. If it doesn't come in five seconds, we show an error. We treat network connectivity as a binary state: either we are "online" or we are "broken".
|
||||
|
||||
This is brittle. It is a rejection of reality.
|
||||
|
||||
In Reticulum, connectivity is a spectrum, and presence is asynchronous. If at all applicable to your intent, you must design your applications to embrace **Store & Forward**.
|
||||
|
||||
Instead of demanding an immediate answer, your application should act as a patient participant. You create a message for someone or something in the mesh. The network holds it. It carries it from node to node, perhaps over hours or days, waiting for the recipient to appear. When they finally surface, the message is delivered. This requires a shift from "request/response" to "event/handler". How exactly you do this is a challenge for you to solve intelligently within your problem domain, but Reticulum-based systems already exist that does this extremely well, and you can use them for inspiration.
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** ``Connect() -> Send() -> Wait() -> Crash if timeout.``
|
||||
- **The Zen Way:** ``Send() -> Continue living. -> Receive() when it arrives.``
|
||||
|
||||
This changes the user experience profoundly. It removes the anxiety of the loading bar. It creates a sense of continuity. The user is not "waiting for the network"; they are interacting with a persistent log of communication that lives in the network itself.
|
||||
|
||||
Naming Is Power
|
||||
---------------
|
||||
|
||||
In the IP world, we are slaves to the Domain Name System. We rely on a hierarchy of registrars to map human-readable names to machine-readable addresses. This hierarchy is a choke point. If the registrar revokes your domain, or if the DNS server goes down, you vanish.
|
||||
|
||||
Reticulum dissolves this hierarchy with **Hash-based Identity**.
|
||||
|
||||
In this design pattern, a name is not a string you look up; it is a cryptographic destination you verify. When you design for Reticulum, you stop asking the user for a URL and start asking for a Destination or Identity Hash.
|
||||
|
||||
This feels strange at first. A hash like ``<83b7328926fed0d2e6a10a7671f9e237>`` looks alien compared to ``myfriend.com``. But that alienness is the armor. It **cannot** be spoofed. It **cannot** be censored by a registrar. It is **absolute**.
|
||||
|
||||
Designing for this means shifting your UI metaphors. You are no longer browsing a web of pages; you are managing a ledger of keys. You are building an "Address Book" that is actually a keyring. The names are given by the user, and the power stays with them. That hashes look complex is directly analogous to the strengths of the bonds formed by their use. It forces the user to engage in a moment of verification, an out-of-band handshake, which restores the human element of trust that SSL certificates stripped away.
|
||||
|
||||
The Interface Is The Medium
|
||||
---------------------------
|
||||
|
||||
One of the most liberating patterns in Reticulum is **Transport Agnosticism**.
|
||||
|
||||
In traditional networking, your code is often littered with transport logic. "Am I on WiFi? Check bandwidth. Am I on Cellular? Check data plan. Am I on Ethernet?". You are constantly micromanaging the pipe.
|
||||
|
||||
In Reticulum, you write to the API, and the API writes to the medium. You send a packet to a Destination. You do not care if that packet travels over a TCP tunnel, a LoRa radio wave, or a serial wire interface. That is the stack's concern.
|
||||
|
||||
This allows you to write **Universal Applications**.
|
||||
Imagine a messaging app. You write it once. It works on a laptop connected to fiber. It works on a phone in the city using WiFi. And, without a single line of code changed, it works on a device in the wilderness, talking only to other devices via radio.
|
||||
|
||||
The pattern is simple: **Never code to the hardware. Code to the intent.**
|
||||
|
||||
**Consider:**
|
||||
|
||||
- **The Old Way:** ``socket.connect(ip, port)``, and then a whole lot more
|
||||
- **The Zen Way:** ``RNS.Packet(destination, data).send()``
|
||||
|
||||
By abstracting the medium, you make your software immortal to changes in infrastructure. The user might switch from a 4G hotspot to a HF modem tomorrow. Your software doesn't need to know. It simply continues the conversation.
|
||||
|
||||
Emergent Patterns
|
||||
-----------------
|
||||
|
||||
When you combine these patterns - *Store & Forward*, *Hash-based Identity*, and *Transport Agnosticism* - you create software that feels fundamentally different.
|
||||
|
||||
It feels *grounded*. It doesn't flicker when the signal drops. It doesn't panic when the server is down. It has weight. It has persistence. It has *relevance*.
|
||||
|
||||
You are no longer building a "client" that begs a "server" for attention. You are building an autonomous agent that exists within the mesh. It speaks when it needs to, listens when it can, and carries its identity with it wherever it goes.
|
||||
|
||||
This is the culmination of the Zen. The code is not just a set of instructions: It is a behavioral envelope. It is a way of *being* in the network.
|
||||
|
||||
|
||||
Fabric Of The Independent
|
||||
=========================
|
||||
|
||||
We have stripped away the illusions. We have seen that the center is empty, that trust *must* be hard, that resources are finite, and that we must own our infrastructure. We have seen that tools have ethics and that our identity can move fluidly.
|
||||
|
||||
This is a reclaiming of the commons. For too long, we have allowed the most vital substrate of human society - *our ability to speak to one another* - to be colonized by entities that do not share our interests. We have allowed the architecture of our communication to be designed by accountants rather than architects.
|
||||
|
||||
We are taking it back. Not by petitioning the masters, but by building the new world within, over, under and around the shell of the old.
|
||||
|
||||
The Work Is Finished
|
||||
--------------------
|
||||
|
||||
The heavy lifting is done.
|
||||
|
||||
The protocol is in the public domain, a gift to humanity that can never be taken away. The software is written, tested, and running on devices scattered across the globe. The manual lies open before you. The source code for the reference implementation is now distributed on hundreds of thousands of devices across the planet. No one can delete or destroy it. The hardware is accessible and abundant.
|
||||
|
||||
It was a hard road to get here, but we got here. Now, there is no roadmap committee waiting for approval. There is no venture capital dictating the user experience. There is no CEO to sign off on the next feature release.
|
||||
|
||||
There is only you.
|
||||
|
||||
The barrier to entry is no longer complexity: It is the mere habit of dependency. You were conditioned to wait. Wait for the app update. Wait for the ISP to fix the line. Wait for the platform to allow the post. Wait for the government to change the policies. Wait for the likes. Wait for the revolution to be televised.
|
||||
|
||||
The revolution never was televised.
|
||||
|
||||
It is packetized.
|
||||
|
||||
Open Sky
|
||||
--------
|
||||
|
||||
The future of this technology is a construction project.
|
||||
|
||||
It looks like a single node on a windowsill, listening to the static. It looks like a message sent to a neighbor, bypassing the noise of the commercial web. It looks like a community mesh that grows, link by link, hop by hop, carried by hands that care more about connection than profit.
|
||||
|
||||
You have the blueprints. You have the tools. You have the philosophy. The noise of the old world has fallen away, leaving you with the quiet clarity of the open spectrum.
|
||||
|
||||
*Mark, early 2026*
|
||||
@@ -1,5 +1,5 @@
|
||||
const DOCUMENTATION_OPTIONS = {
|
||||
VERSION: '1.0.4',
|
||||
VERSION: '1.3.2',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en" data-content_root="./">
|
||||
<head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html"><link rel="next" title="Git Over Reticulum" href="git.html"><link rel="prev" title="Building Networks" href="networks.html">
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Distributed Development - Reticulum Network Stack 1.3.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
body {
|
||||
--color-code-background: #f2f2f2;
|
||||
--color-code-foreground: #1e1e1e;
|
||||
|
||||
}
|
||||
@media not print {
|
||||
body[data-theme="dark"] {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
--color-background-primary: #202b38;
|
||||
--color-background-secondary: #161f27;
|
||||
--color-foreground-primary: #dbdbdb;
|
||||
--color-foreground-secondary: #a9b1ba;
|
||||
--color-brand-primary: #41adff;
|
||||
--color-background-hover: #161f27;
|
||||
--color-api-name: #ffbe85;
|
||||
--color-api-pre-name: #efae75;
|
||||
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body:not([data-theme="light"]) {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
--color-background-primary: #202b38;
|
||||
--color-background-secondary: #161f27;
|
||||
--color-foreground-primary: #dbdbdb;
|
||||
--color-foreground-secondary: #a9b1ba;
|
||||
--color-brand-primary: #41adff;
|
||||
--color-background-hover: #161f27;
|
||||
--color-api-name: #ffbe85;
|
||||
--color-api-pre-name: #efae75;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style></head>
|
||||
<body>
|
||||
|
||||
<script>
|
||||
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
|
||||
</script>
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="svg-toc" viewBox="0 0 24 24">
|
||||
<title>Contents</title>
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
|
||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-menu" viewBox="0 0 24 24">
|
||||
<title>Menu</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
|
||||
<title>Expand</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun" viewBox="0 0 24 24">
|
||||
<title>Light mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-moon" viewBox="0 0 24 24">
|
||||
<title>Dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun-with-moon" viewBox="0 0 24 24">
|
||||
<title>Auto light/dark, in light mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="icon-custom-derived-from-feather-sun-and-tabler-moon">
|
||||
<path style="opacity: 50%" d="M 5.411 14.504 C 5.471 14.504 5.532 14.504 5.591 14.504 C 3.639 16.319 4.383 19.569 6.931 20.352 C 7.693 20.586 8.512 20.551 9.25 20.252 C 8.023 23.207 4.056 23.725 2.11 21.184 C 0.166 18.642 1.702 14.949 4.874 14.536 C 5.051 14.512 5.231 14.5 5.411 14.5 L 5.411 14.504 Z"/>
|
||||
<line x1="14.5" y1="3.25" x2="14.5" y2="1.25"/>
|
||||
<line x1="14.5" y1="15.85" x2="14.5" y2="17.85"/>
|
||||
<line x1="10.044" y1="5.094" x2="8.63" y2="3.68"/>
|
||||
<line x1="19" y1="14.05" x2="20.414" y2="15.464"/>
|
||||
<line x1="8.2" y1="9.55" x2="6.2" y2="9.55"/>
|
||||
<line x1="20.8" y1="9.55" x2="22.8" y2="9.55"/>
|
||||
<line x1="10.044" y1="14.006" x2="8.63" y2="15.42"/>
|
||||
<line x1="19" y1="5.05" x2="20.414" y2="3.636"/>
|
||||
<circle cx="14.5" cy="9.55" r="3.6"/>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-moon-with-sun" viewBox="0 0 24 24">
|
||||
<title>Auto light/dark, in dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="icon-custom-derived-from-feather-sun-and-tabler-moon">
|
||||
<path d="M 8.282 7.007 C 8.385 7.007 8.494 7.007 8.595 7.007 C 5.18 10.184 6.481 15.869 10.942 17.24 C 12.275 17.648 13.706 17.589 15 17.066 C 12.851 22.236 5.91 23.143 2.505 18.696 C -0.897 14.249 1.791 7.786 7.342 7.063 C 7.652 7.021 7.965 7 8.282 7 L 8.282 7.007 Z"/>
|
||||
<line style="opacity: 50%" x1="18" y1="3.705" x2="18" y2="2.5"/>
|
||||
<line style="opacity: 50%" x1="18" y1="11.295" x2="18" y2="12.5"/>
|
||||
<line style="opacity: 50%" x1="15.316" y1="4.816" x2="14.464" y2="3.964"/>
|
||||
<line style="opacity: 50%" x1="20.711" y1="10.212" x2="21.563" y2="11.063"/>
|
||||
<line style="opacity: 50%" x1="14.205" y1="7.5" x2="13.001" y2="7.5"/>
|
||||
<line style="opacity: 50%" x1="21.795" y1="7.5" x2="23" y2="7.5"/>
|
||||
<line style="opacity: 50%" x1="15.316" y1="10.184" x2="14.464" y2="11.036"/>
|
||||
<line style="opacity: 50%" x1="20.711" y1="4.789" x2="21.563" y2="3.937"/>
|
||||
<circle style="opacity: 50%" cx="18" cy="7.5" r="2.169"/>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-pencil" viewBox="0 0 24 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-pencil-code">
|
||||
<path d="M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4" />
|
||||
<path d="M13.5 6.5l4 4" />
|
||||
<path d="M20 21l2 -2l-2 -2" />
|
||||
<path d="M17 17l-2 2l2 2" />
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-eye" viewBox="0 0 24 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-eye-code">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path
|
||||
d="M11.11 17.958c-3.209 -.307 -5.91 -2.293 -8.11 -5.958c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6c-.21 .352 -.427 .688 -.647 1.008" />
|
||||
<path d="M20 21l2 -2l-2 -2" />
|
||||
<path d="M17 17l-2 2l2 2" />
|
||||
</svg>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation" aria-label="Toggle site navigation sidebar">
|
||||
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc" aria-label="Toggle table of contents sidebar">
|
||||
<label class="overlay sidebar-overlay" for="__navigation"></label>
|
||||
<label class="overlay toc-overlay" for="__toc"></label>
|
||||
|
||||
<a class="skip-to-content muted-link" href="#furo-main-content">Skip to content</a>
|
||||
|
||||
|
||||
|
||||
<div class="page">
|
||||
<header class="mobile-header">
|
||||
<div class="header-left">
|
||||
<label class="nav-overlay-icon" for="__navigation">
|
||||
<span class="icon"><svg><use href="#svg-menu"></use></svg></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
<button class="theme-toggle" aria-label="Toggle Light / Dark / Auto color theme">
|
||||
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
|
||||
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-header-icon" for="__toc">
|
||||
<span class="icon"><svg><use href="#svg-toc"></use></svg></span>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
<aside class="sidebar-drawer">
|
||||
<div class="sidebar-container">
|
||||
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
<div class="sidebar-logo-container">
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
<input type="hidden" name="check_keywords" value="yes">
|
||||
<input type="hidden" name="area" value="default">
|
||||
</form>
|
||||
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
|
||||
<ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="whatis.html">What is Reticulum?</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="gettingstartedfast.html">Getting Started Fast</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="zen.html">Zen of Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="software.html">Programs Using Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="using.html">Using Reticulum on Your System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="understanding.html">Understanding Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Distributed Development</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div class="article-container">
|
||||
<a href="#" class="back-to-top muted-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
|
||||
</svg>
|
||||
<span>Back to top</span>
|
||||
</a>
|
||||
<div class="content-icon-container">
|
||||
<div class="theme-toggle-container theme-toggle-content">
|
||||
<button class="theme-toggle" aria-label="Toggle Light / Dark / Auto color theme">
|
||||
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
|
||||
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-content-icon" for="__toc">
|
||||
<span class="icon"><svg><use href="#svg-toc"></use></svg></span>
|
||||
</label>
|
||||
</div>
|
||||
<article role="main" id="furo-main-content">
|
||||
<section id="distributed-development">
|
||||
<span id="id1"></span><h1>Distributed Development<a class="headerlink" href="#distributed-development" title="Link to this heading">¶</a></h1>
|
||||
<p>This chapter of the manual provides the conceptual basis for understanding <em>why</em> <code class="docutils literal notranslate"><span class="pre">rngit</span></code> exists, what it aims to achieve, and the kinds of spaces it seeks to reestablish. For the practical details of operating the system, refer to the <a class="reference internal" href="git.html#git-main"><span class="std std-ref">Git Over Reticulum</span></a> chapter.</p>
|
||||
<section id="the-original-architecture">
|
||||
<h2>The Original Architecture<a class="headerlink" href="#the-original-architecture" title="Link to this heading">¶</a></h2>
|
||||
<p>When Torvalds created Git in 2005, he designed a tool that reflected a specific philosophy of collaboration. Every copy of a repository would be a complete, sovereign instance. There was no central server, no single point of failure, no gatekeeper. Developers would be able to work independently, exchange patches directly, and maintain their own branches indefinitely. This concept was - and is - both beautiful and revolutionary. It’s execution is peer-to-peer not as a marketing term, but in the most foundational sense: As fundamental, structural reality.</p>
|
||||
<p>Such a design emerged from necessity. The Linux kernel development process operated across geographical boundaries, time zones, and organizational affiliations. Contributors did not “log in” to a shared server to do their work; they maintained their own trees, and the flow of code between these trees was negotiated through patches, reviews, and merge decisions. The architecture of Git mirrored the social architecture of the community: Autonomous, competent, and fundamentally distributed in its technical operation.</p>
|
||||
<p><em>The result of that work is, in the most direct sense, what makes it possible for you to read this today.</em></p>
|
||||
<p>There’s something very important to take note of here: With Git, developers could collaborate effectively and perfectly well without any central server being present, without platform-mediated visibility into each other’s work, and without a centralized authority validating their contributions. They needed <em>only</em> a protocol for exchanging differences and a mechanism for verification of authorship. Everything else - social organization, quality control, release management - was handled by careful <em>human judgment</em> operating on top of the technical substrate.</p>
|
||||
<p>What Git provided was not a development environment, but a <strong>language for versioning</strong>. It specified how to represent history, how to compute differences, how to merge divergent branches. It did not specify who could participate, how they should communicate, or what workflows they should follow. These were left to the competence and discretion of the creators using the system.</p>
|
||||
</section>
|
||||
<section id="the-platform-interregnum">
|
||||
<h2>The Platform Interregnum<a class="headerlink" href="#the-platform-interregnum" title="Link to this heading">¶</a></h2>
|
||||
<p>What followed represents a very familiar pattern: Tools designed to distribute power were re-centralized by platforms that offered convenience in exchange for control. GitHub, GitLab, and similar services reintroduced the centralization that Git had eliminated architecturally. The activity feed replaced durable artifacts with ephemeral notifications. The social graph and open interaction became as important as the code itself, if not more.</p>
|
||||
<p>This re-centralization was not technical, as such. It was <strong>ontological</strong>. When every developer pushes to the same server, when every merge is in theory controllable by a platform, when every issue is tracked in a database controlled by a corporation, the nature of collaboration changes. The platform, and its social dynamics, becomes the ground of reality. The platform mediates not just the technical exchange of information and the programmatics, but the social recognition and codices of contribution, the future archival prospects of the work, and the very identity of the project itself.</p>
|
||||
<p>The consequences extend beyond individual inconvenience. Centralized platforms create single points of failure for entire ecosystem. When a platform changes its terms of service, suspends accounts, removes repositories or ceases operation, entire project histories and community relationships can be disrupted or destroyed. The extractive economics of platform capitalism mean that value created by open-source communities is captured by corporations, while communities remain dependent on infrastructure they do not control. And the surveillance inherent in platform operation means that every action - every commit, every comment, every page view - is logged, analyzed, and potentially monetized or weaponized.</p>
|
||||
<p>More insidiously, platforms have completely reshaped the culture of development itself. They have created what we could call the <strong>Teahouse Developer</strong>: A participant who treats engineering projects as social venues for opinion-sharing rather than sites of disciplined and careful production. These personages have no actual stakes in the projects they act as leeches upon, and only a very base consciousness of the damage they are incurring in order to feed their attention and external validation dependencies.</p>
|
||||
<p>When platforms optimize for engagement, when growth is the only metric, when every user with an opinion must have their voice heard, when a random social process is elevated to higher importance than results, the signal-to-noise ratio collapses catastrophically. Competent engineers find themselves drowning in feedback from the incompetent, managing the emotional needs and dysregulations of drive-by commentators rather than solving technical problems.</p>
|
||||
<p>The platform model is predicated on <strong>unsaturable expansion</strong>. Like almost any industrial system, it cannot function without growth. It pursues no particular aims; it is growth for the sake of growing. There is no saturation point, no concept of “enough”. Every barrier to entry must be put down to the very lowest common denominator, every voice must be amplified, every interaction must be converted into content that feeds the machine. This is fundamentally incompatible with the nature of social beings itself. It is also incompatible with serious engineering, which requires focus, discernment, and the right of people who know better to say “no”.</p>
|
||||
</section>
|
||||
<section id="restoration">
|
||||
<h2>Restoration<a class="headerlink" href="#restoration" title="Link to this heading">¶</a></h2>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> system represents a return to Git’s original architectural principles, fortified with cryptographic networking capabilities that were not available in 2005. The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> system <em>is</em> Git - but running over Reticulum. Welcome back to a world where your work is your own, but where everyone can still reach you - if you want them to.</p>
|
||||
<p>Just as Git eliminated the need for a central version control server, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> eliminates the need for a central hosting platform, “servers” or any kinds of middle-men between the people actually doing the work. By operating over Reticulum, it eliminates the visibility of development activity to platform operators, network observers, state actors and other malicious third-parties.</p>
|
||||
<p>In this model, the repository node is a <strong>sovereign entity</strong>. It is reachable from anywhere in the Reticulum network but owned, operated, and controlled by the developer or community that runs it. It is an actual home for creative output, not an extraction mechanism to which dues are paid. The node operator decides who may contribute, what standards must be met, and which voices are worth listening to. This is not exclusion; it is <strong>discernment</strong>. It is the necessary exercise of judgment that separates engineering from theatrics.</p>
|
||||
<p>I did not create this in a fit of nostalgia. I created it because it is a necessary response to the failures of the centralized model. Git’s technical architecture was - and <em>is</em> - correct. It was the social and economic superstructure built atop it that introduced fragility, exploitation, and environments toxic to actual creativity. By returning to first principles - distributed version control on distributed infrastructure - we recover not just a technical capability, but a mode of collaboration that respects the autonomy of individual developers and the sovereignty of actual communities.</p>
|
||||
</section>
|
||||
<section id="protocols-over-platforms">
|
||||
<h2>Protocols Over Platforms<a class="headerlink" href="#protocols-over-platforms" title="Link to this heading">¶</a></h2>
|
||||
<p>The distinction between platforms and protocols is fundamental to understanding the architecture of sovereignty in networked systems. A platform is a service you access; a protocol is a grammar you speak; actions you live. A platform requires permission to enter, a protocol requires only <em>comprehension</em> to employ. A platform can change its rules, suspend your account, or cease operation entirely, a protocol persists as long as there are participants who <em>understand</em> and <em>use</em> it. A protocol is an <em>idea</em>, a platform is a machine that turns its users into products.</p>
|
||||
<p>Platforms operate on a client-server model that inherently creates power asymmetry. Even when platforms are built atop open-source software, the operational instance remains a black box of corporate control. You <em>may</em> be able to download <em>some</em> of your data, but you cannot download the connections to the people that are the true value-base of the platform, or take them with you if you want to leave.</p>
|
||||
<p>Protocols, by contrast, are agreements. They specify how systems should communicate, but not who may communicate or on what terms. Email is a protocol; Gmail is a platform. HTTP is a protocol; Facebook is a platform. Git is a protocol; GitHub is a platform. The protocol persists regardless of any particular implementation’s success or failure.</p>
|
||||
<p>The power of protocols lies in their <strong>permissionlessness</strong>. Anyone can implement a protocol without approval. Anyone can extend it, fork it, or use it for purposes unforeseen by its creators. This creates resilience: protocols cannot be easily censored, monopolized, or shut down because they exist as shared understanding rather than centralized infrastructure.</p>
|
||||
<p>Reticulum is a protocol in this strict sense. It specifies how packets should be formatted, how paths should be discovered, how encryption should be applied. The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> system extends this protocol approach to development workflows. It is not an external platform that hosts your repositories; it is a protocol for exchanging repository data, release artifacts, and work documents over Reticulum’s encrypted transport. But with a few commands and an old computer, it creates your own infrastructure for hosting repositories, or sharing them with who you choose. <em>That</em> is how tools should function, in case we had forgotten.</p>
|
||||
<p>Unlike platforms, which extract value by creating dependency, there is no entity that can grant or deny you the privilege of running <code class="docutils literal notranslate"><span class="pre">rngit</span></code>. Your Reticulum identity is not endowed by any platform; it is generated locally and certified by its own cryptographic properties. Your repositories are hosted on nodes you control or nodes operated by communities you trust. Your relationships with other developers are peer-to-peer connections established through cryptographic addressing, not social graph connections managed by recommendation algorithms.</p>
|
||||
<p>On a platform, exit means abandonment: you lose your history, your relationships, your visibility. With protocols, exit is just migration. When you change your infrastructure, your identity and your work travel with you. There are no middlemen between you and your collaborators. If push comes to shove, you can write your entire life’s work and connections to an SD card, swim across the lake, and set up camp on the other side.</p>
|
||||
</section>
|
||||
<section id="sovereignty-through-infrastructure">
|
||||
<h2>Sovereignty Through Infrastructure<a class="headerlink" href="#sovereignty-through-infrastructure" title="Link to this heading">¶</a></h2>
|
||||
<p>The concept of sovereignty - supreme authority within a territory - has traditionally been applied to nation-states. But in an age where creative work is conducted through digital infrastructure, sovereignty is essential for individuals and communities. <strong>Creative sovereignty</strong> means having supreme authority over the artifacts you produce, the processes by which you produce them, and the terms under which they are distributed. It means not merely legal ownership of copyright, but operational control of the infrastructure that mediates creation, collaboration, and dissemination.</p>
|
||||
<p>Centralized development platforms strip away most layers of sovereignty. When you host code on a corporate platform, you retain <em>some</em> legal ownership of copyright, but you surrender complete operational control. The platform decides what content is acceptable, who can access it, and how it is presented. They can delete your repository, suspend your account, or change the visibility of your work without consent. In reality, legal ownership becomes meaningless as operational control is ceded.</p>
|
||||
<p>Running your own <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node restores this sovereignty. You control the hardware, the network configuration, the backup strategies, and the access permissions. You decide what constitutes acceptable use, who may contribute, and how contributions are evaluated. Taking this responsibility on yourself is an assertion that your creative work is not a product to be harvested by platform economics, but an autonomous activity to be conducted on your own terms.</p>
|
||||
<p>This sovereignty and responsibility extends to the entry barriers you establish. The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> system allows you to configure access controls that filter participants based on cryptographic identity and demonstrated competence. If, for example, someone cannot navigate a command line, or use Reticulum to submit a patch, they most likely lack the required competence to modify your code. In a world that apparently labels this as “exclusion”, I would simply refer to it as a minimally acceptable level of quality control.</p>
|
||||
<p>Such a stance protects projects from the noise that so often overwhelms and completely dilutes platform-based development, where every user with an opinion believes themselves entitled to attention and access to the decision process.</p>
|
||||
</section>
|
||||
<section id="artifact-centered-workflows">
|
||||
<h2>Artifact-Centered Workflows<a class="headerlink" href="#artifact-centered-workflows" title="Link to this heading">¶</a></h2>
|
||||
<p>Contemporary platform-based development has shifted focus from durable artifacts to ephemeral <em>activity</em>. It does not matter what constitutes this activity, as long as it’s there. The primary interface is not the repository itself, not the produced artifacts, but the activity feed: <em>Notifications</em> of commits, comments, pull requests, and social interactions. Work is measured by velocity, throughput, and the constant stream of updates. This activity-centric model creates constant urgency, discourages discernment, encourages reactive rather than reflective work patterns, and produces vast quantities of ephemeral and useless communication that obscures actual project state and productivity.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> system enables a return to <strong>artifact-centered workflows</strong>, where the focus is on durable, attributable, versioned outputs rather than the stream of notifications surrounding them. The fundamental unit of work is the commit - signed, immutable records of change. The fundamental unit of production is the signed artifact - a self-verifying package of functionality. The fundamental unit of discussion is the work document - a structured, threaded conversation attached to repositories.</p>
|
||||
<p>Artifacts can persist independently of any platform’s continued operation. A commit signed with your Reticulum identity is attributable to you regardless of where it is stored. A release signed with your private key is verifiable as authentic regardless of which network it traverses, and can be verified offline on any system running Reticulum. The work exists as <strong>cryptographic fact</strong>, distributed over the planet, not as database entries in a corporate cloud.</p>
|
||||
<p>Such a shift has real psychological consequences. When work is measured in artifacts rather than activity, the pace changes. There is no need for constant visibility, no pressure to perform busyness. Developers can work deeply, reflectively, and submit complete solutions rather than incremental updates designed to maintain presence in an activity feed. The work becomes <strong>substantial</strong>, in the physical sense of the word, rather than performative.</p>
|
||||
</section>
|
||||
<section id="composable-primitives">
|
||||
<h2>Composable Primitives<a class="headerlink" href="#composable-primitives" title="Link to this heading">¶</a></h2>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> system is not a monolithic application prescribing a specific workflow; it is a collection of <strong>composable primitives</strong> that can be arranged to support diverse creative processes. Understanding these primitives as separate, orthogonal capabilities enables users to construct workflows suited to their specific needs and to recombine these primitives in ways unforeseen by the system’s designers.</p>
|
||||
<p>The core primitives include:</p>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Repository Hosting</strong>: Bare Git repositories served over Reticulum links, accessible via standard Git commands through the <code class="docutils literal notranslate"><span class="pre">rns://</span></code> URL scheme.</p></li>
|
||||
<li><p><strong>Identity-Based Access Control</strong>: Fine-grained permissions managed through cryptographically verifiable identity hashes, configurable at the group, repository, or document level.</p></li>
|
||||
<li><p><strong>Release Distribution</strong>: Cryptographically signed release artifacts with embedded provenance information, verifiable offline and distributable through any Reticulum or physical path.</p></li>
|
||||
<li><p><strong>Work Document Tracking</strong>: Structured, threaded work management attached to repositories, with precise permission controls, and the ability to contain updates or discussions.</p></li>
|
||||
<li><p><strong>Forking and Mirroring</strong>: Automated replication of repositories from any accessible Git URL, with metadata tracking upstream relationships for synchronization.</p></li>
|
||||
<li><p><strong>Nomad Network Integration</strong>: Page node functionality for browsing repository contents, commit history, and release information through the Nomad Network protocol.</p></li>
|
||||
</ul>
|
||||
<p>These primitives can be composed into workflows ranging from single-developer projects to complex multi-organizational collaborations. A solo developer might use only repository hosting and release distribution. A research group might add work document tracking for structured peer review. A software distribution network might combine mirroring with cryptographic release verification to create resilient update channels.</p>
|
||||
<p>The entire system is incredibly light-weight, and can host hundreds of repositories on a Raspberry Pi.</p>
|
||||
<p>Composability is essential because <strong>creative work is diverse</strong>. Software development, academic research, technical writing, hardware design, music production and data analysis all have different requirements for collaboration, review, and distribution. A platform prescribes a single workflow and forces all users to conform. A protocol provides primitives and allows users to construct workflows appropriate to their domain.</p>
|
||||
<p>With <code class="docutils literal notranslate"><span class="pre">rngit</span></code>, you can re-build the system into anything you can imagine. Everything can be modified, extended and hooked into. Adding functionality or automation is never further away than a shell script, a cron-job, or a Python modification of the source.</p>
|
||||
</section>
|
||||
<section id="distribution-without-intermediaries">
|
||||
<h2>Distribution Without Intermediaries<a class="headerlink" href="#distribution-without-intermediaries" title="Link to this heading">¶</a></h2>
|
||||
<p>Creating software is only part of the work. Then comes actually getting it to the people needing to use it. Centralized platforms handle distribution through their own infrastructure: Content delivery networks, central package registries, and download servers accessed through platform-controlled interfaces. This convenience masks a fundamental dependency: Your ability to distribute depends on the platform’s continued operation, their policies regarding your content, and their technical infrastructure’s reach.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> release system enables distribution strategies <strong>decoupled from any single infrastructure provider</strong>. Releases are cryptographically signed using Ed25519 signatures and packaged in signed release manifests (<code class="docutils literal notranslate"><span class="pre">.rsm</span></code> files). These manifests contain embedded signatures for each artifact. The manifest provides full verifiability of all release information, and contains embedded release artifact lists, per-file <code class="docutils literal notranslate"><span class="pre">.rsg</span></code> signatures, origin information, and the creator’s Reticulum Identity. It can also be used to fetch verified updates of the software package over the network, and can always be verified completely offline.</p>
|
||||
<p>Because releases are self-verifying, they can traverse any network or physical path that Reticulum can establish. A release can travel over LoRa radio, be carried on USB drives through areas without internet connectivity, disseminated over a mirror network, or be distributed through store-and-forward mechanisms on intermittent infrastructure. Recipients can verify authenticity regardless of how they obtained the files. This is particularly valuable in low-connectivity environments where Reticulum may be the only available communication channel.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span> <span class="pre">release</span></code> command provides tools for creating, publishing, fetching, and verifying releases. When fetching a release using an <code class="docutils literal notranslate"><span class="pre">.rsm</span></code> manifest, the system validates the manifest signature against the required Reticulum Identity, extracts the origin node and repository path, connects to the origin over Reticulum, retrieves the latest release manifest, and verifies each downloaded artifact against the signatures embedded in the manifest. If any verification fails, the fetch aborts, preventing installation of corrupted or tampered files.</p>
|
||||
<p>This cryptographic verification replaces the trust model of platform distribution. Instead of trusting that a platform has not been compromised, users verify that artifacts match the signatures created by the developer’s identity. It doesn’t matter <em>how</em> they obtained the artifacts, they can <strong>always</strong> be verified. This security model shifts from <strong>institutional trust</strong> (just believe in the goodness of the platform) to <strong>cryptographic proof</strong> (verify the signatures).</p>
|
||||
</section>
|
||||
<section id="long-archive">
|
||||
<h2>Long Archive<a class="headerlink" href="#long-archive" title="Link to this heading">¶</a></h2>
|
||||
<p>Software development is often conceived as an activity of the present only: Solving today’s problems, meeting current deadlines, responding to immediate feedback. But the artifacts produced - code, documentation, releases - have lifespans extending <em>far</em> beyond their creation. They may be used for decades, studied by future developers, depended upon by systems not yet imagined, or preserved as historical records of technological development.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> system is designed with this <strong>extended timeframe</strong> in mind, supporting the creation of archives that are durable, portable, and intelligible across generational timescales. Git repositories are always internally complete; they contain full history and can be migrated to new infrastructure without loss of information. Everything that <code class="docutils literal notranslate"><span class="pre">rngit</span></code> adds on top of this is stored in normal files in standard formats right next to the Git repository folders, not an esoteric database-cluster two thousand kilometers away. Because releases are cryptographically signed, they remain verifiable as authentic regardless of when or where they are retrieved. Because the system operates over Reticulum, it can function over communication mediums that may outlast the internet as we know it.</p>
|
||||
<p>This long-term perspective influences technical decisions. The use of well-established cryptographic primitives ensures that signatures will remain verifiable for centuries. The use of standard formats ensures that repositories will remain readable by future tools. The protocol-based architecture ensures that the system can evolve without losing compatibility with existing data.</p>
|
||||
<p>For critical infrastructure, this archival durability is not optional; it is essential. Communication systems, cryptographic libraries, and safety-critical code must remain available and verifiable for the lifespans of the systems that depend on them. The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> system provides the tools to create such archives: distributed across multiple nodes, cryptographically verified, and independent of any corporate or governmental infrastructure, which as history has shown repeatedly, does <em>not</em> persist.</p>
|
||||
</section>
|
||||
<section id="start-of-the-road">
|
||||
<h2>Start Of The Road<a class="headerlink" href="#start-of-the-road" title="Link to this heading">¶</a></h2>
|
||||
<p>Distributed development and production over Reticulum is a <em>different mode of existence</em> for creative work. It restores the autonomy originally created by Git. It provides local sovereignty over production infrastructure, composability of workflow, and durability of artifact. It lets you filter participation through competence and cryptography rather than incentives of platform operators, raising the quality and enjoyment of work, and protecting the focus of real engineering and creative expression.</p>
|
||||
<p>This is not a system for everyone, and that is the point. It requires investment - in understanding Reticulum, in configuring infrastructure, in establishing workflows. It requires accepting responsibility for your own tools rather than delegating them to platform operators. It requires the discipline to maintain your own node, manage your own backups, and nurture your own community.</p>
|
||||
<p>But for those who make this investment, the returns are substantial. You gain <strong>immunity from platform failure</strong>; your work persists regardless of corporate decisions or service outages. You gain <strong>shelter from surveillance</strong>; your development activity is visible only to those that <em>you</em> choose to involve. You gain <strong>control over process</strong>; you decide how work is conducted, reviewed, and released, unmediated by terms of service, algorithmic feeds and thousands of uninformed and irrelevant opinions.</p>
|
||||
<p>Most importantly, though, you regain the <strong>dignity of craft</strong>. Development becomes an activity conducted among peers, equals among equals, mediated by skill and cryptographic proof rather than corporate permission, producing artifacts that stand as independent testimony to competence, functionality or beauty rather than as content feeding engagement metrics. The <em>work</em> becomes the point. The artifacts become durable. And the network becomes <em>one</em> of the tools you wield in this endeavor.</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
</div>
|
||||
<footer>
|
||||
|
||||
<div class="related-pages">
|
||||
<a class="next-page" href="git.html">
|
||||
<div class="page-info">
|
||||
<div class="context">
|
||||
<span>Next</span>
|
||||
</div>
|
||||
<div class="title">Git Over Reticulum</div>
|
||||
</div>
|
||||
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
|
||||
</a>
|
||||
<a class="prev-page" href="networks.html">
|
||||
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
|
||||
<div class="page-info">
|
||||
<div class="context">
|
||||
<span>Previous</span>
|
||||
</div>
|
||||
|
||||
<div class="title">Building Networks</div>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="bottom-of-page">
|
||||
<div class="left-details">
|
||||
<div class="copyright">
|
||||
Copyright © 2025, Mark Qvist
|
||||
</div>
|
||||
Generated with <a href="https://www.sphinx-doc.org/">Sphinx</a> and
|
||||
<a href="https://github.com/pradyunsg/furo">Furo</a>
|
||||
|
||||
</div>
|
||||
<div class="right-details">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
</div>
|
||||
<aside class="toc-drawer">
|
||||
|
||||
|
||||
<div class="toc-sticky toc-scroll">
|
||||
<div class="toc-title-container">
|
||||
<span class="toc-title">
|
||||
On this page
|
||||
</span>
|
||||
</div>
|
||||
<div class="toc-tree-container">
|
||||
<div class="toc-tree">
|
||||
<ul>
|
||||
<li><a class="reference internal" href="#">Distributed Development</a><ul>
|
||||
<li><a class="reference internal" href="#the-original-architecture">The Original Architecture</a></li>
|
||||
<li><a class="reference internal" href="#the-platform-interregnum">The Platform Interregnum</a></li>
|
||||
<li><a class="reference internal" href="#restoration">Restoration</a></li>
|
||||
<li><a class="reference internal" href="#protocols-over-platforms">Protocols Over Platforms</a></li>
|
||||
<li><a class="reference internal" href="#sovereignty-through-infrastructure">Sovereignty Through Infrastructure</a></li>
|
||||
<li><a class="reference internal" href="#artifact-centered-workflows">Artifact-Centered Workflows</a></li>
|
||||
<li><a class="reference internal" href="#composable-primitives">Composable Primitives</a></li>
|
||||
<li><a class="reference internal" href="#distribution-without-intermediaries">Distribution Without Intermediaries</a></li>
|
||||
<li><a class="reference internal" href="#long-archive">Long Archive</a></li>
|
||||
<li><a class="reference internal" href="#start-of-the-road">Start Of The Road</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
<script src="_static/clipboard.min.js?v=a7894cd8"></script>
|
||||
<script src="_static/copybutton.js?v=f281be69"></script>
|
||||
</body>
|
||||
</html>
|
||||
+23
-35
@@ -3,11 +3,11 @@
|
||||
<head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html"><link rel="next" title="API Reference" href="reference.html"><link rel="prev" title="Support Reticulum" href="support.html">
|
||||
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html"><link rel="next" title="Reticulum License" href="license.html"><link rel="prev" title="Support Reticulum" href="support.html">
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Code Examples - Reticulum Network Stack 1.0.4 documentation</title>
|
||||
<title>Code Examples - Reticulum Network Stack 1.3.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -215,13 +215,18 @@
|
||||
<ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="whatis.html">What is Reticulum?</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="gettingstartedfast.html">Getting Started Fast</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="zen.html">Zen of Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="software.html">Programs Using Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="using.html">Using Reticulum on Your System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="understanding.html">Understanding Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="distributed.html">Distributed Development</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a></li>
|
||||
@@ -259,14 +264,10 @@
|
||||
<article role="main" id="furo-main-content">
|
||||
<section id="code-examples">
|
||||
<span id="examples-main"></span><h1>Code Examples<a class="headerlink" href="#code-examples" title="Link to this heading">¶</a></h1>
|
||||
<p>A number of examples are included in the source distribution of Reticulum.
|
||||
You can use these examples to learn how to write your own programs.</p>
|
||||
<p>A number of examples are included in the source distribution of Reticulum. You can use these examples to learn how to write your own programs.</p>
|
||||
<section id="minimal">
|
||||
<span id="example-minimal"></span><h2>Minimal<a class="headerlink" href="#minimal" title="Link to this heading">¶</a></h2>
|
||||
<p>The <em>Minimal</em> example demonstrates the bare-minimum setup required to connect to
|
||||
a Reticulum network from your program. In about five lines of code, you will
|
||||
have the Reticulum Network Stack initialised, and ready to pass traffic in your
|
||||
program.</p>
|
||||
<p>The <em>Minimal</em> example demonstrates the bare-minimum setup required to connect to a Reticulum network from your program. In about five lines of code, you will have the Reticulum Network Stack initialised, and ready to pass traffic in your program.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
|
||||
<span class="c1"># This RNS example demonstrates a minimal setup, that #</span>
|
||||
<span class="c1"># will start up the Reticulum Network Stack, generate a #</span>
|
||||
@@ -375,9 +376,7 @@ program.</p>
|
||||
</section>
|
||||
<section id="announce">
|
||||
<span id="example-announce"></span><h2>Announce<a class="headerlink" href="#announce" title="Link to this heading">¶</a></h2>
|
||||
<p>The <em>Announce</em> example builds upon the previous example by exploring how to
|
||||
announce a destination on the network, and how to let your program receive
|
||||
notifications about announces from relevant destinations.</p>
|
||||
<p>The <em>Announce</em> example builds upon the previous example by exploring how to announce a destination on the network, and how to let your program receive notifications about announces from relevant destinations.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
|
||||
<span class="c1"># This RNS example demonstrates setting up announce #</span>
|
||||
<span class="c1"># callbacks, which will let an application receive a #</span>
|
||||
@@ -556,8 +555,7 @@ notifications about announces from relevant destinations.</p>
|
||||
</section>
|
||||
<section id="broadcast">
|
||||
<span id="example-broadcast"></span><h2>Broadcast<a class="headerlink" href="#broadcast" title="Link to this heading">¶</a></h2>
|
||||
<p>The <em>Broadcast</em> example explores how to transmit plaintext broadcast messages
|
||||
over the network.</p>
|
||||
<p>The <em>Broadcast</em> example explores how to transmit plaintext broadcast messages over the network.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
|
||||
<span class="c1"># This RNS example demonstrates broadcasting unencrypted #</span>
|
||||
<span class="c1"># information to any listening destinations. #</span>
|
||||
@@ -685,8 +683,7 @@ over the network.</p>
|
||||
</section>
|
||||
<section id="echo">
|
||||
<span id="example-echo"></span><h2>Echo<a class="headerlink" href="#echo" title="Link to this heading">¶</a></h2>
|
||||
<p>The <em>Echo</em> example demonstrates communication between two destinations using
|
||||
the Packet interface.</p>
|
||||
<p>The <em>Echo</em> example demonstrates communication between two destinations using the Packet interface.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
|
||||
<span class="c1"># This RNS example demonstrates a simple client/server #</span>
|
||||
<span class="c1"># echo utility. A client can send an echo request to the #</span>
|
||||
@@ -1025,8 +1022,7 @@ the Packet interface.</p>
|
||||
</section>
|
||||
<section id="link">
|
||||
<span id="example-link"></span><h2>Link<a class="headerlink" href="#link" title="Link to this heading">¶</a></h2>
|
||||
<p>The <em>Link</em> example explores establishing an encrypted link to a remote
|
||||
destination, and passing traffic back and forth over the link.</p>
|
||||
<p>The <em>Link</em> example explores establishing an encrypted link to a remote destination, and passing traffic back and forth over the link.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
|
||||
<span class="c1"># This RNS example demonstrates how to set up a link to #</span>
|
||||
<span class="c1"># a destination, and pass data back and forth over it. #</span>
|
||||
@@ -1323,8 +1319,7 @@ destination, and passing traffic back and forth over the link.</p>
|
||||
</section>
|
||||
<section id="example-identify">
|
||||
<span id="identification"></span><h2>Identification<a class="headerlink" href="#example-identify" title="Link to this heading">¶</a></h2>
|
||||
<p>The <em>Identify</em> example explores identifying an intiator of a link, once
|
||||
the link has been established.</p>
|
||||
<p>The <em>Identify</em> example explores identifying an intiator of a link, once the link has been established.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
|
||||
<span class="c1"># This RNS example demonstrates how to set up a link to #</span>
|
||||
<span class="c1"># a destination, and identify the initiator to it's peer #</span>
|
||||
@@ -1937,8 +1932,7 @@ the link has been established.</p>
|
||||
</section>
|
||||
<section id="channel">
|
||||
<span id="example-channel"></span><h2>Channel<a class="headerlink" href="#channel" title="Link to this heading">¶</a></h2>
|
||||
<p>The <em>Channel</em> example explores using a <code class="docutils literal notranslate"><span class="pre">Channel</span></code> to send structured
|
||||
data between peers of a <code class="docutils literal notranslate"><span class="pre">Link</span></code>.</p>
|
||||
<p>The <em>Channel</em> example explores using a <code class="docutils literal notranslate"><span class="pre">Channel</span></code> to send structured data between peers of a <code class="docutils literal notranslate"><span class="pre">Link</span></code>.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
|
||||
<span class="c1"># This RNS example demonstrates how to set up a link to #</span>
|
||||
<span class="c1"># a destination, and pass structured messages over it #</span>
|
||||
@@ -2334,8 +2328,7 @@ data between peers of a <code class="docutils literal notranslate"><span class="
|
||||
</section>
|
||||
<section id="buffer">
|
||||
<h2>Buffer<a class="headerlink" href="#buffer" title="Link to this heading">¶</a></h2>
|
||||
<p>The <em>Buffer</em> example explores using buffered readers and writers to send
|
||||
binary data between peers of a <code class="docutils literal notranslate"><span class="pre">Link</span></code>.</p>
|
||||
<p>The <em>Buffer</em> example explores using buffered readers and writers to send binary data between peers of a <code class="docutils literal notranslate"><span class="pre">Link</span></code>.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
|
||||
<span class="c1"># This RNS example demonstrates how to set up a link to #</span>
|
||||
<span class="c1"># a destination, and pass binary data over it using a #</span>
|
||||
@@ -2664,9 +2657,7 @@ binary data between peers of a <code class="docutils literal notranslate"><span
|
||||
</section>
|
||||
<section id="filetransfer">
|
||||
<span id="example-filetransfer"></span><h2>Filetransfer<a class="headerlink" href="#filetransfer" title="Link to this heading">¶</a></h2>
|
||||
<p>The <em>Filetransfer</em> example implements a basic file-server program that
|
||||
allow clients to connect and download files. The program uses the Resource
|
||||
interface to efficiently pass files of any size over a Reticulum <a class="reference internal" href="reference.html#api-link"><span class="std std-ref">Link</span></a>.</p>
|
||||
<p>The <em>Filetransfer</em> example implements a basic file-server program that allow clients to connect and download files. The program uses the Resource interface to efficiently pass files of any size over a Reticulum <a class="reference internal" href="reference.html#api-link"><span class="std std-ref">Link</span></a>.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1">##########################################################</span>
|
||||
<span class="c1"># This RNS example demonstrates a simple filetransfer #</span>
|
||||
<span class="c1"># server and client program. The server will serve a #</span>
|
||||
@@ -3276,10 +3267,7 @@ interface to efficiently pass files of any size over a Reticulum <a class="refer
|
||||
</section>
|
||||
<section id="custom-interfaces">
|
||||
<span id="example-custominterface"></span><h2>Custom Interfaces<a class="headerlink" href="#custom-interfaces" title="Link to this heading">¶</a></h2>
|
||||
<p>The <em>ExampleInterface</em> demonstrates creating custom interfaces for Reticulum.
|
||||
Any number of custom interfaces can be loaded and utilised by Reticulum, and
|
||||
will be fully on-par with natively included interfaces, including all supported
|
||||
<a class="reference internal" href="interfaces.html#interfaces-modes"><span class="std std-ref">interface modes</span></a> and <a class="reference internal" href="interfaces.html#interfaces-options"><span class="std std-ref">common configuration options</span></a>.</p>
|
||||
<p>The <em>ExampleInterface</em> demonstrates creating custom interfaces for Reticulum. Any number of custom interfaces can be loaded and utilised by Reticulum, and will be fully on-par with natively included interfaces, including all supported <a class="reference internal" href="interfaces.html#interfaces-modes"><span class="std std-ref">interface modes</span></a> and <a class="reference internal" href="interfaces.html#interfaces-options"><span class="std std-ref">common configuration options</span></a>.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># This example illustrates creating a custom interface</span>
|
||||
<span class="c1"># definition, that can be loaded and used by Reticulum at</span>
|
||||
<span class="c1"># runtime. Any number of custom interfaces can be created</span>
|
||||
@@ -3588,12 +3576,12 @@ will be fully on-par with natively included interfaces, including all supported
|
||||
<footer>
|
||||
|
||||
<div class="related-pages">
|
||||
<a class="next-page" href="reference.html">
|
||||
<a class="next-page" href="license.html">
|
||||
<div class="page-info">
|
||||
<div class="context">
|
||||
<span>Next</span>
|
||||
</div>
|
||||
<div class="title">API Reference</div>
|
||||
<div class="title">Reticulum License</div>
|
||||
</div>
|
||||
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
|
||||
</a>
|
||||
@@ -3660,7 +3648,7 @@ will be fully on-par with natively included interfaces, including all supported
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=71272d9f"></script>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.0.4 documentation</title>
|
||||
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.3.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -215,13 +215,18 @@
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="whatis.html">What is Reticulum?</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="gettingstartedfast.html">Getting Started Fast</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="zen.html">Zen of Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="software.html">Programs Using Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="using.html">Using Reticulum on Your System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="understanding.html">Understanding Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="distributed.html">Distributed Development</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a></li>
|
||||
@@ -291,7 +296,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=71272d9f"></script>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
+38
-11
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#"><link rel="search" title="Search" href="search.html">
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.0.4 documentation</title>
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.3.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -178,7 +178,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -202,7 +202,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -213,13 +213,18 @@
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="whatis.html">What is Reticulum?</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="gettingstartedfast.html">Getting Started Fast</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="zen.html">Zen of Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="software.html">Programs Using Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="using.html">Using Reticulum on Your System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="understanding.html">Understanding Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="distributed.html">Distributed Development</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a></li>
|
||||
@@ -284,15 +289,17 @@
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.RawChannelReader.add_ready_callback">add_ready_callback() (RNS.RawChannelReader method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Resource.advertise">advertise() (RNS.Resource method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Destination.announce">announce() (RNS.Destination method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Reticulum.ANNOUNCE_CAP">ANNOUNCE_CAP (RNS.Reticulum attribute)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Destination.app_and_aspects_from_name">app_and_aspects_from_name() (RNS.Destination static method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Transport.await_path">await_path() (RNS.Transport static method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
</tr></table>
|
||||
@@ -302,6 +309,12 @@
|
||||
<h2>B</h2>
|
||||
<table style="width: 100%" class="indextable genindextable"><tr>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Transport.blackhole_identity">blackhole_identity() (RNS.Transport static method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Reticulum.blackhole_sources">blackhole_sources() (RNS.Reticulum static method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Buffer">Buffer (class in RNS)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
@@ -352,13 +365,15 @@
|
||||
<li><a href="reference.html#RNS.Identity.decrypt">(RNS.Identity method)</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Transport.deregister_announce_handler">deregister_announce_handler() (RNS.Transport static method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Destination.deregister_request_handler">deregister_request_handler() (RNS.Destination method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Destination">Destination (class in RNS)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Reticulum.discovered_interfaces">discovered_interfaces() (RNS.Reticulum static method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
</tr></table>
|
||||
@@ -517,10 +532,12 @@
|
||||
<li><a href="reference.html#RNS.Link.identify">identify() (RNS.Link method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Identity">Identity (class in RNS)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Link.inactive_for">inactive_for() (RNS.Link method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Link.inactive_for">inactive_for() (RNS.Link method)</a>
|
||||
<li><a href="reference.html#RNS.Reticulum.interface_discovery_sources">interface_discovery_sources() (RNS.Reticulum static method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Resource.is_compressed">is_compressed() (RNS.Resource method)</a>
|
||||
</li>
|
||||
@@ -618,13 +635,17 @@
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Packet">Packet (class in RNS)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.PacketReceipt">PacketReceipt (class in RNS)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Transport.PATHFINDER_M">PATHFINDER_M (RNS.Transport attribute)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Packet.PLAIN_MDU">PLAIN_MDU (RNS.Packet attribute)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Identity.pub_to_file">pub_to_file() (RNS.Identity method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Reticulum.publish_blackhole_enabled">publish_blackhole_enabled() (RNS.Reticulum static method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
</tr></table>
|
||||
@@ -669,6 +690,8 @@
|
||||
<li><a href="reference.html#RNS.Transport.request_path">request_path() (RNS.Transport static method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.RequestReceipt">RequestReceipt (class in RNS)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Reticulum.required_discovery_value">required_discovery_value() (RNS.Reticulum static method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Packet.resend">resend() (RNS.Packet method)</a>
|
||||
</li>
|
||||
@@ -771,6 +794,10 @@
|
||||
<section id="U" class="genindex-section">
|
||||
<h2>U</h2>
|
||||
<table style="width: 100%" class="indextable genindextable"><tr>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Transport.unblackhole_identity">unblackhole_identity() (RNS.Transport static method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.MessageBase.unpack">unpack() (RNS.MessageBase method)</a>
|
||||
</li>
|
||||
@@ -819,7 +846,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=71272d9f"></script>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
+174
-438
@@ -3,11 +3,11 @@
|
||||
<head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<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">
|
||||
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html"><link rel="next" title="Zen of Reticulum" href="zen.html"><link rel="prev" title="What is Reticulum?" href="whatis.html">
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Getting Started Fast - Reticulum Network Stack 1.0.4 documentation</title>
|
||||
<title>Getting Started Fast - Reticulum Network Stack 1.3.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -215,13 +215,18 @@
|
||||
<ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="whatis.html">What is Reticulum?</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Getting Started Fast</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="zen.html">Zen of Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="software.html">Programs Using Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="using.html">Using Reticulum on Your System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="understanding.html">Understanding Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="distributed.html">Distributed Development</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a></li>
|
||||
@@ -259,40 +264,27 @@
|
||||
<article role="main" id="furo-main-content">
|
||||
<section id="getting-started-fast">
|
||||
<h1>Getting Started Fast<a class="headerlink" href="#getting-started-fast" title="Link to this heading">¶</a></h1>
|
||||
<p>The best way to get started with the Reticulum Network Stack depends on what
|
||||
you want to do. This guide will outline sensible starting paths for different
|
||||
scenarios.</p>
|
||||
<p>The best way to get started with the Reticulum Network Stack depends on what you want to do. This guide will outline sensible starting paths for different scenarios.</p>
|
||||
<section id="standalone-reticulum-installation">
|
||||
<h2>Standalone Reticulum Installation<a class="headerlink" href="#standalone-reticulum-installation" title="Link to this heading">¶</a></h2>
|
||||
<p>If you simply want to install Reticulum and related utilities on a system,
|
||||
the easiest way is via the <code class="docutils literal notranslate"><span class="pre">pip</span></code> package manager:</p>
|
||||
<p>If you simply want to install Reticulum and related utilities on a system, the easiest way is via the <code class="docutils literal notranslate"><span class="pre">pip</span></code> package manager:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>pip<span class="w"> </span>install<span class="w"> </span>rns
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>If you do not already have pip installed, you can install it using the package manager
|
||||
of your system with a command like <code class="docutils literal notranslate"><span class="pre">sudo</span> <span class="pre">apt</span> <span class="pre">install</span> <span class="pre">python3-pip</span></code>,
|
||||
<code class="docutils literal notranslate"><span class="pre">sudo</span> <span class="pre">pamac</span> <span class="pre">install</span> <span class="pre">python-pip</span></code> or similar.</p>
|
||||
<p>You can also dowload the Reticulum release wheels from GitHub, or other release channels,
|
||||
and install them offline using <code class="docutils literal notranslate"><span class="pre">pip</span></code>:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>pip<span class="w"> </span>install<span class="w"> </span>./rns-1.0.2-py3-none-any.whl
|
||||
<p>If you do not already have pip installed, you can install it using the package manager of your system with a command like <code class="docutils literal notranslate"><span class="pre">sudo</span> <span class="pre">apt</span> <span class="pre">install</span> <span class="pre">python3-pip</span></code>, <code class="docutils literal notranslate"><span class="pre">sudo</span> <span class="pre">pamac</span> <span class="pre">install</span> <span class="pre">python-pip</span></code> or similar.</p>
|
||||
<p>You can also dowload the Reticulum release wheels from GitHub, or other release channels, and install them offline using <code class="docutils literal notranslate"><span class="pre">pip</span></code>:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>pip<span class="w"> </span>install<span class="w"> </span>./rns-1.1.2-py3-none-any.whl
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>On platforms that limit user package installation via <code class="docutils literal notranslate"><span class="pre">pip</span></code>, you may need to manually
|
||||
allow this using the <code class="docutils literal notranslate"><span class="pre">--break-system-packages</span></code> command line flag when installing. This
|
||||
will not actually break any packages, unless you have installed Reticulum directly via
|
||||
your operating system’s package manager.</p>
|
||||
<p>On platforms that limit user package installation via <code class="docutils literal notranslate"><span class="pre">pip</span></code>, you may need to manually allow this using the <code class="docutils literal notranslate"><span class="pre">--break-system-packages</span></code> command line flag when installing. This will not actually break any packages, unless you have installed Reticulum directly via your operating system’s package manager.</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>pip<span class="w"> </span>install<span class="w"> </span>rns<span class="w"> </span>--break-system-packages
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>For more detailed installation instructions, please see the
|
||||
<a class="reference internal" href="#install-guides"><span class="std std-ref">Platform-Specific Install Notes</span></a> section.</p>
|
||||
<p>After installation is complete, it might be helpful to refer to the
|
||||
<a class="reference internal" href="using.html#using-main"><span class="std std-ref">Using Reticulum on Your System</span></a> chapter.</p>
|
||||
<p>For more detailed installation instructions, please see the <a class="reference internal" href="#install-guides"><span class="std std-ref">Platform-Specific Install Notes</span></a> section.</p>
|
||||
<p>After installation is complete, it might be helpful to refer to the <a class="reference internal" href="using.html#using-main"><span class="std std-ref">Using Reticulum on Your System</span></a> chapter.</p>
|
||||
<section id="resolving-dependency-installation-issues">
|
||||
<h3>Resolving Dependency & Installation Issues<a class="headerlink" href="#resolving-dependency-installation-issues" title="Link to this heading">¶</a></h3>
|
||||
<p>On some platforms, there may not be binary packages available for all dependencies, and
|
||||
<code class="docutils literal notranslate"><span class="pre">pip</span></code> installation may fail with an error message. In these cases, the issue can usually
|
||||
be resolved by installing the development essentials packages for your platform:</p>
|
||||
<p>On some platforms, there may not be binary packages available for all dependencies, and <code class="docutils literal notranslate"><span class="pre">pip</span></code> installation may fail with an error message. In these cases, the issue can usually be resolved by installing the development essentials packages for your platform:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="c1"># Debian / Ubuntu / Derivatives</span>
|
||||
sudo<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> </span>build-essential
|
||||
|
||||
@@ -303,204 +295,96 @@ sudo<span class="w"> </span>pamac<span class="w"> </span>install<span class="w">
|
||||
sudo<span class="w"> </span>dnf<span class="w"> </span>groupinstall<span class="w"> </span><span class="s2">"Development Tools"</span><span class="w"> </span><span class="s2">"Development Libraries"</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>With the base development packages installed, <code class="docutils literal notranslate"><span class="pre">pip</span></code> should be able to compile any missing
|
||||
dependencies from source, and complete installation even on platforms that don’t have pre-
|
||||
compiled packages available.</p>
|
||||
<p>With the base development packages installed, <code class="docutils literal notranslate"><span class="pre">pip</span></code> should be able to compile any missing dependencies from source, and complete installation even on platforms that don’t have pre-compiled packages available.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="try-using-a-reticulum-based-program">
|
||||
<h2>Try Using a Reticulum-based Program<a class="headerlink" href="#try-using-a-reticulum-based-program" title="Link to this heading">¶</a></h2>
|
||||
<p>If you simply want to try using a program built with Reticulum, a few different
|
||||
programs exist that allow basic communication and a range of other useful functions,
|
||||
even over extremely low-bandwidth Reticulum networks.</p>
|
||||
<p>These programs will let you get a feel for how Reticulum works. They have been designed
|
||||
to run well over networks based on LoRa or packet radio, but can also be used over fast
|
||||
links, such as local WiFi, wired Ethernet, the Internet, or any combination.</p>
|
||||
<p>As such, it is easy to get started experimenting, without having to set up any radio
|
||||
transceivers or infrastructure just to try it out. Launching the programs on separate
|
||||
devices connected to the same WiFi network is enough to get started, and physical
|
||||
radio interfaces can then be added later.</p>
|
||||
<section id="remote-shell">
|
||||
<h3>Remote Shell<a class="headerlink" href="#remote-shell" title="Link to this heading">¶</a></h3>
|
||||
<p>The <a class="reference external" href="https://github.com/acehoss/rnsh">rnsh</a> program lets you establish fully interactive
|
||||
remote shell sessions over Reticulum. It also allows you to pipe any program to or from a
|
||||
remote system, and is similar to how <code class="docutils literal notranslate"><span class="pre">ssh</span></code> works. The <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> is very efficient, and
|
||||
can facilitate fully interactive shell sessions, even over extremely low-bandwidth links,
|
||||
such as LoRa or packet radio.</p>
|
||||
</section>
|
||||
<section id="nomad-network">
|
||||
<h3>Nomad Network<a class="headerlink" href="#nomad-network" title="Link to this heading">¶</a></h3>
|
||||
<p>The terminal-based program <a class="reference external" href="https://github.com/markqvist/nomadnet">Nomad Network</a>
|
||||
provides a complete encrypted communications suite built with Reticulum. It features
|
||||
encrypted messaging (both direct and delayed-delivery for offline users), file sharing,
|
||||
and has a built-in text-browser and page server with support for dynamically rendered pages,
|
||||
user authentication and more.</p>
|
||||
<a class="reference external image-reference" href="_images/nomadnet_3.png"><img alt="_images/nomadnet_3.png" src="_images/nomadnet_3.png" />
|
||||
</a>
|
||||
<p><a class="reference external" href="https://github.com/markqvist/nomadnet">Nomad Network</a> is a user-facing client
|
||||
for the messaging and information-sharing protocol
|
||||
<a class="reference external" href="https://github.com/markqvist/lxmf">LXMF</a>, another project built with Reticulum.</p>
|
||||
<p>You can install Nomad Network via pip:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install ...</span>
|
||||
<span class="n">pip</span> <span class="n">install</span> <span class="n">nomadnet</span>
|
||||
|
||||
<span class="c1"># ... and run</span>
|
||||
<span class="n">nomadnet</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>If this is the very first time you use <code class="docutils literal notranslate"><span class="pre">pip</span></code> to install a program
|
||||
on your system, you might need to reboot your system for your program to become
|
||||
available. If you get a “command not found” error or similar when running the
|
||||
program, reboot your system and try again. In some cases, you may even need to
|
||||
manually add the <code class="docutils literal notranslate"><span class="pre">pip</span></code> install path to your <code class="docutils literal notranslate"><span class="pre">PATH</span></code> environment variable.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section id="sideband">
|
||||
<h3>Sideband<a class="headerlink" href="#sideband" title="Link to this heading">¶</a></h3>
|
||||
<p>If you would rather use a program with a graphical user interface, you can take
|
||||
a look at <a class="reference external" href="https://unsigned.io/sideband">Sideband</a>, which is available for Android,
|
||||
Linux, macOS and Windows.</p>
|
||||
<a class="reference external image-reference" href="_images/sideband_devices.webp"><img alt="_images/sideband_devices.webp" class="align-center" src="_images/sideband_devices.webp" />
|
||||
</a>
|
||||
<p>Sideband allows you to communicate with other people or LXMF-compatible
|
||||
systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR
|
||||
Paper Messages, or anything else Reticulum supports. It also interoperates with
|
||||
the Nomad Network program.</p>
|
||||
</section>
|
||||
<section id="meshchat">
|
||||
<h3>MeshChat<a class="headerlink" href="#meshchat" title="Link to this heading">¶</a></h3>
|
||||
<p>The <a class="reference external" href="https://github.com/liamcottle/reticulum-meshchat">Reticulum MeshChat</a> application
|
||||
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
|
||||
page browser and other interesting functionality.</p>
|
||||
<a class="reference external image-reference" href="_images/meshchat_1.webp"><img alt="_images/meshchat_1.webp" class="align-center" src="_images/meshchat_1.webp" />
|
||||
</a>
|
||||
<p>Reticulum MeshChat is of course also compatible with Sideband and Nomad Network, or
|
||||
any other LXMF client.</p>
|
||||
</section>
|
||||
<p>If you simply want to try using a program built with Reticulum, a <a class="reference internal" href="software.html#software-main"><span class="std std-ref">range of different programs</span></a> exist that allow basic communication and a various other useful functions, even over extremely low-bandwidth Reticulum networks.</p>
|
||||
</section>
|
||||
<section id="using-the-included-utilities">
|
||||
<h2>Using the Included Utilities<a class="headerlink" href="#using-the-included-utilities" title="Link to this heading">¶</a></h2>
|
||||
<p>Reticulum comes with a range of included utilities that make it easier to
|
||||
manage your network, check connectivity and make Reticulum available to other
|
||||
programs on your system.</p>
|
||||
<p>You can use <code class="docutils literal notranslate"><span class="pre">rnsd</span></code> to run Reticulum as a background or foreground service,
|
||||
and the <code class="docutils literal notranslate"><span class="pre">rnstatus</span></code>, <code class="docutils literal notranslate"><span class="pre">rnpath</span></code> and <code class="docutils literal notranslate"><span class="pre">rnprobe</span></code> utilities to view and query
|
||||
network status and connectivity.</p>
|
||||
<p>To learn more about these utility programs, have a look at the
|
||||
<a class="reference internal" href="using.html#using-main"><span class="std std-ref">Using Reticulum on Your System</span></a> chapter of this manual.</p>
|
||||
<p>Reticulum comes with a range of included utilities that make it easier to manage your network, check connectivity and make Reticulum available to other programs on your system.</p>
|
||||
<p>You can use <code class="docutils literal notranslate"><span class="pre">rnsd</span></code> to run Reticulum as a background or foreground service, and the <code class="docutils literal notranslate"><span class="pre">rnstatus</span></code>, <code class="docutils literal notranslate"><span class="pre">rnpath</span></code> and <code class="docutils literal notranslate"><span class="pre">rnprobe</span></code> utilities to view and query network status and connectivity.</p>
|
||||
<p>To learn more about these utility programs, have a look at the <a class="reference internal" href="using.html#using-main"><span class="std std-ref">Using Reticulum on Your System</span></a> chapter of this manual.</p>
|
||||
</section>
|
||||
<section id="creating-a-network-with-reticulum">
|
||||
<h2>Creating a Network With Reticulum<a class="headerlink" href="#creating-a-network-with-reticulum" title="Link to this heading">¶</a></h2>
|
||||
<p>To create a network, you will need to specify one or more <em>interfaces</em> for
|
||||
Reticulum to use. This is done in the Reticulum configuration file, which by
|
||||
default is located at <code class="docutils literal notranslate"><span class="pre">~/.reticulum/config</span></code>. You can get an example
|
||||
configuration file with all options via <code class="docutils literal notranslate"><span class="pre">rnsd</span> <span class="pre">--exampleconfig</span></code>.</p>
|
||||
<p>When Reticulum is started for the first time, it will create a default
|
||||
configuration file, with one active interface. This default interface uses
|
||||
your existing Ethernet and WiFi networks (if any), and only allows you to
|
||||
communicate with other Reticulum peers within your local broadcast domains.</p>
|
||||
<p>To communicate further, you will have to add one or more interfaces. The default
|
||||
configuration includes a number of examples, ranging from using TCP over the
|
||||
internet, to LoRa and Packet Radio interfaces.</p>
|
||||
<p>With Reticulum, you only need to configure what interfaces you want to communicate
|
||||
over. There is no need to configure address spaces, subnets, routing tables,
|
||||
or other things you might be used to from other network types.</p>
|
||||
<p>Once Reticulum knows which interfaces it should use, it will automatically
|
||||
discover topography and configure transport of data to any destinations it
|
||||
knows about.</p>
|
||||
<p>In situations where you already have an established WiFi or Ethernet network, and
|
||||
many devices that want to utilise the same external Reticulum network paths (for example over
|
||||
LoRa), it will often be sufficient to let one system act as a Reticulum gateway, by
|
||||
adding any external interfaces to the configuration of this system, and then enabling transport on it. Any
|
||||
other device on your local WiFi will then be able to connect to this wider Reticulum
|
||||
network just using the default (<a class="reference internal" href="interfaces.html#interfaces-auto"><span class="std std-ref">AutoInterface</span></a>) configuration.</p>
|
||||
<p>Possibly, the examples in the config file are enough to get you started. If
|
||||
you want more information, you can read the <a class="reference internal" href="networks.html#networks-main"><span class="std std-ref">Building Networks</span></a>
|
||||
and <a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">Interfaces</span></a> chapters of this manual.</p>
|
||||
<p>To create a network, you will need to specify one or more <em>interfaces</em> for Reticulum to use. This is done in the Reticulum configuration file, which by default is located at <code class="docutils literal notranslate"><span class="pre">~/.reticulum/config</span></code>. You can get an example configuration file with all options via <code class="docutils literal notranslate"><span class="pre">rnsd</span> <span class="pre">--exampleconfig</span></code>.</p>
|
||||
<p>When Reticulum is started for the first time, it will create a default configuration file, with one active interface. This default interface uses your existing Ethernet and WiFi networks (if any), and only allows you to communicate with other Reticulum peers within your local broadcast domains.</p>
|
||||
<p>To communicate further, you will have to add one or more interfaces. The default configuration includes a number of examples, ranging from using TCP over the internet, to LoRa and Packet Radio interfaces.</p>
|
||||
<p>With Reticulum, you only need to configure what interfaces you want to communicate over. There is no need to configure address spaces, subnets, routing tables, or other things you might be used to from other network types.</p>
|
||||
<p>Once Reticulum knows which interfaces it should use, it will automatically discover topography and configure transport of data to any destinations it knows about.</p>
|
||||
<p>In situations where you already have an established WiFi or Ethernet network, and many devices that want to utilise the same external Reticulum network paths (for example over LoRa), it will often be sufficient to let one system act as a Reticulum gateway, by adding any external interfaces to the configuration of this system, and then enabling transport on it. Any other device on your local WiFi will then be able to connect to this wider Reticulum network just using the default (<a class="reference internal" href="interfaces.html#interfaces-auto"><span class="std std-ref">AutoInterface</span></a>) configuration.</p>
|
||||
<p>Possibly, the examples in the config file are enough to get you started. If you want more information, you can read the <a class="reference internal" href="networks.html#networks-main"><span class="std std-ref">Building Networks</span></a> and <a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">Interfaces</span></a> chapters of this manual, but most importantly, start with reading the next section, <a class="reference internal" href="#bootstrapping-connectivity"><span class="std std-ref">Bootstrapping Connectivity</span></a>, as this provides the most essential understanding of how to ensure reliable connectivity with a minimum of maintenance.</p>
|
||||
</section>
|
||||
<section id="connecting-reticulum-instances-over-the-internet">
|
||||
<h2>Connecting Reticulum Instances Over the Internet<a class="headerlink" href="#connecting-reticulum-instances-over-the-internet" title="Link to this heading">¶</a></h2>
|
||||
<p>Reticulum currently offers three interfaces suitable for connecting instances over the Internet: <a class="reference internal" href="interfaces.html#interfaces-backbone"><span class="std std-ref">Backbone</span></a>, <a class="reference internal" href="interfaces.html#interfaces-tcps"><span class="std std-ref">TCP</span></a>
|
||||
and <a class="reference internal" href="interfaces.html#interfaces-i2p"><span class="std std-ref">I2P</span></a>. Each interface offers a different set of features, and Reticulum
|
||||
users should carefully choose the interface which best suites their needs.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">TCPServerInterface</span></code> allows users to host an instance accessible over TCP/IP. This
|
||||
method is generally faster, lower latency, and more energy efficient than using <code class="docutils literal notranslate"><span class="pre">I2PInterface</span></code>,
|
||||
however it also leaks more data about the server host.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code> is a very fast and efficient interface type available on POSIX operating
|
||||
systems, designed to handle many hundreds of connections simultaneously with low memory, processing
|
||||
and I/O overhead. It is fully compatible with the TCP-based interface types.</p>
|
||||
<p>TCP connections reveal the IP address of both your instance and the server to anyone who can
|
||||
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
|
||||
inspecting your packets may be able to record packet metadata like time of transmission and packet size.
|
||||
Even though Reticulum encrypts traffic, TCP does not, so an adversary may be able to use
|
||||
packet inspection to learn that a system is running Reticulum, and what other IP addresses connect to it.
|
||||
Hosting a publicly reachable instance over TCP also requires a publicly reachable IP address,
|
||||
which most Internet connections don’t offer anymore.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">I2PInterface</span></code> routes messages through the <a class="reference external" href="https://geti2p.net/en/">Invisible Internet Protocol
|
||||
(I2P)</a>. To use this interface, users must also run an I2P daemon in
|
||||
parallel to <code class="docutils literal notranslate"><span class="pre">rnsd</span></code>. For always-on I2P nodes it is recommended to use <a class="reference external" href="https://i2pd.website/">i2pd</a>.</p>
|
||||
<p>By default, I2P will encrypt and mix all traffic sent over the Internet, and
|
||||
hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node
|
||||
will also relay other I2P user’s encrypted packets, which will use extra
|
||||
bandwidth and compute power, but also makes timing attacks and other forms of
|
||||
deep-packet-inspection much more difficult.</p>
|
||||
<p>I2P also allows users to host globally available Reticulum instances from non-public IP’s and behind firewalls and NAT.</p>
|
||||
<p>In general it is recommended to use an I2P node if you want to host a publicly accessible
|
||||
instance, while preserving anonymity. If you care more about performance, and a slightly
|
||||
easier setup, use TCP.</p>
|
||||
</section>
|
||||
<section id="connect-to-the-public-testnet">
|
||||
<h2>Connect to the Public Testnet<a class="headerlink" href="#connect-to-the-public-testnet" title="Link to this heading">¶</a></h2>
|
||||
<p>An experimental public testnet has been made accessible by volunteers in the community. You
|
||||
can find interface definitions for adding to your <code class="docutils literal notranslate"><span class="pre">.reticulum/config</span></code> file on the
|
||||
<a class="reference external" href="https://reticulum.network/connect.html">Reticulum Website</a> or the
|
||||
<a class="reference external" href="https://github.com/markqvist/Reticulum/wiki/Community-Node-List">Community Wiki</a></p>
|
||||
<p>You can connect your devices or instances to one or more of these to gain access to any
|
||||
Reticulum networks they are physically connected to. Simply add one or more interface
|
||||
snippets to your config file in the <code class="docutils literal notranslate"><span class="pre">[interface]</span></code> section, like in the example below:</p>
|
||||
<div class="highlight-ini notranslate"><div class="highlight"><pre><span></span><span class="c1"># TCP/IP interface to the BetweenTheBorders Hub (community-provided)</span>
|
||||
<span class="k">[[RNS Testnet BetweenTheBorders]]</span>
|
||||
<span class="w"> </span><span class="na">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">TCPClientInterface</span>
|
||||
<span class="w"> </span><span class="na">enabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">yes</span>
|
||||
<span class="w"> </span><span class="na">target_host</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">reticulum.betweentheborders.com</span>
|
||||
<span class="w"> </span><span class="na">target_port</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">4242</span>
|
||||
</pre></div>
|
||||
<section id="bootstrapping-connectivity">
|
||||
<span id="id1"></span><h2>Bootstrapping Connectivity<a class="headerlink" href="#bootstrapping-connectivity" title="Link to this heading">¶</a></h2>
|
||||
<p>Reticulum is not a service you subscribe to, nor is it a single global network you “join”. It is a <em>networking stack</em>; a toolkit for building communications systems that align with your specific values, requirements, and operational environment. The way you choose to connect to other Reticulum peers is entirely your own choice.</p>
|
||||
<p>One of the most powerful aspects of Reticulum is that it provides a multitude of tools to establish, maintain, and optimize connectivity. You can use these tools in isolation or combine them in complex configurations to achieve a vast array of goals.</p>
|
||||
<p>Whether your aim is to create a completely private, air-gapped network for your family; to build a resilient community mesh that survives infrastructure collapse; to connect far and wide to as many nodes as possible; or simply to maintain a reliable, encrypted link to a specific organization you care about, Reticulum provides the mechanisms to make it happen.</p>
|
||||
<p>There is no “right” or “wrong” way to build a Reticulum network, and you don’t need to be a network engineer just to get started. If the information flows in the way you intend, and your privacy and security requirements are met, your configuration is a success. Reticulum is designed to make the most challenging and difficult scenarios attainable, even when other networking technologies fail.</p>
|
||||
<section id="finding-your-way">
|
||||
<h3>Finding Your Way<a class="headerlink" href="#finding-your-way" title="Link to this heading">¶</a></h3>
|
||||
<p>When you first start using Reticulum, you need a way to obtain connectivity with the peers you want to communicate with - the process of <em>bootstrapping connectivity</em>.</p>
|
||||
<div class="admonition important">
|
||||
<p class="admonition-title">Important</p>
|
||||
<p>A common mistake in modern networking is the reliance on a few centralized, hard-coded entrypoints. If every user simply connects to the same list of public IP addresses found on a website, the network becomes brittle, centralized, and ultimately fails to deliver on the promise of decentralization and resilience. You have a responsibility here.</p>
|
||||
</div>
|
||||
<p>Reticulum encourages the approach of <em>organic growth</em>. Instead of relying on permanent static connections to distant servers, you can use temporary bootstrap connections to continously <em>discover</em> more relevant or local infrastructure. Once discovered, your system can automatically form stronger, more direct links to these peers, and discard the temporary bootstrap links. This results in a web of connections that are geographically relevant, resilient and efficient.</p>
|
||||
<p>It <em>is</em> possible to simply add a few public entrypoints to the <code class="docutils literal notranslate"><span class="pre">[interfaces]</span></code> section of your Reticulum configuration and be connected, but a better option is to enable <a class="reference internal" href="using.html#using-interface-discovery"><span class="std std-ref">interface discovery</span></a> and either manually select relevant, local interfaces, or enable discovered interface auto-connection.</p>
|
||||
<p>A relevant option in this context is the <a class="reference internal" href="interfaces.html#interfaces-options"><span class="std std-ref">bootstrap only</span></a> interface option. This is an automated tool for better distributing connectivity. By enabling interface discovery and auto-connection, and marking an interface as <code class="docutils literal notranslate"><span class="pre">bootstrap_only</span></code>, you tell Reticulum to use that interface primarliy to find connectivity options, and then disconnect it once sufficient entrypoints have been discovered. This helps create a network topology that favors locality and resilience over the simple centralization caused by using only a few static entrypoints.</p>
|
||||
<p>Good places to find interface definitions for bootstrapping connectivity are websites like
|
||||
<a class="reference external" href="https://directory.rns.recipes/">directory.rns.recipes</a> and <a class="reference external" href="https://rmap.world/">rmap.world</a>.</p>
|
||||
</section>
|
||||
<section id="build-personal-infrastructure">
|
||||
<h3>Build Personal Infrastructure<a class="headerlink" href="#build-personal-infrastructure" title="Link to this heading">¶</a></h3>
|
||||
<p>You do not need a datacenter to be a meaningful part of the Reticulum ecosystem. In fact, the most important nodes in the network are often the smallest ones.</p>
|
||||
<p>We strongly encourage everyone, even home users, to think in terms of building <strong>personal infrastructure</strong>. Don’t connect every phone, tablet, and computer in your house directly to a public internet gateway. Instead, repurpose an old computer, a Raspberry Pi, or a supported router to act as your own, personal <strong>Transport Node</strong>:</p>
|
||||
<ul class="simple">
|
||||
<li><p>Your local Transport Node sits in your home, connected to your WiFi and perhaps a radio interface (like an RNode).</p></li>
|
||||
<li><p>You configure this node with a <code class="docutils literal notranslate"><span class="pre">bootstrap_only</span></code> interface (perhaps a TCP tunnel to a wider network) and enable interface discovery.</p></li>
|
||||
<li><p>While you sleep, work, or cook, your node listens to the network. It discovers other local community members, validates their Network Identities, and automatically establishes direct links.</p></li>
|
||||
<li><p>Your personal devices now connect to your <em>local</em> node, which is integrated into a living, breathing local mesh. Your traffic flows through local paths provided by other real people in the community rather than bouncing off a distant server.</p></li>
|
||||
</ul>
|
||||
<p><strong>Don’t wait for others to build the networks you want to see</strong>. Every network is important, perhaps even most so those that support individual families and persons. Once enough of this personal, local infrastructure exist, connecting them directly to each other, without traversing the public Internet, becomes inevitable.</p>
|
||||
</section>
|
||||
<section id="mixing-strategies">
|
||||
<h3>Mixing Strategies<a class="headerlink" href="#mixing-strategies" title="Link to this heading">¶</a></h3>
|
||||
<p>There is no requirement to commit to a single strategy. The most robust setups often mix static, dynamic, and discovered interfaces.</p>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Static Interfaces:</strong> You maintain a permanent interface to a trusted friend or organization using a static configuration.</p></li>
|
||||
<li><p><strong>Bootstrap Links:</strong> You connect a <code class="docutils literal notranslate"><span class="pre">bootstrap_only</span></code> interface to a public gateway on the Internet to scan for new connectable peers or to regain connectivity if your other interfaces fail.</p></li>
|
||||
<li><p><strong>Local Wide-Area Connectivity:</strong> You run a <code class="docutils literal notranslate"><span class="pre">RNodeInterface</span></code> on a shared frequency, giving you completely self-sovereign and private wide-area access to both your own network and other Reticulum peers globally, without any “service providers” being able to control or monitor how you interact with people.</p></li>
|
||||
</ul>
|
||||
<p>By combining these methods, you create a system that is secure against single points of failure, adaptable to changing network conditions, and better integrated into your physical and social reality.</p>
|
||||
</section>
|
||||
<section id="network-health-responsibility">
|
||||
<h3>Network Health & Responsibility<a class="headerlink" href="#network-health-responsibility" title="Link to this heading">¶</a></h3>
|
||||
<p>As you participate in the wider networks you discover and build, you will inevitably encounter peers that are misconfigured, malicious, or simply broken. To protect your resources and those of your local peers, you can utilize the <a class="reference internal" href="using.html#using-blackhole-management"><span class="std std-ref">Blackhole Management</span></a> system.</p>
|
||||
<p>Whether you manually block a spamming identity or subscribe to a blackhole list maintained by a trusted Network Identity, these tools help ensure that <em>your</em> transport capacity is used for what <em>you</em> consider legitimate communication. This keeps your local segment efficient and contributes to the health of the wider network.</p>
|
||||
</section>
|
||||
<section id="contributing-to-the-global-ret">
|
||||
<h3>Contributing to the Global Ret<a class="headerlink" href="#contributing-to-the-global-ret" title="Link to this heading">¶</a></h3>
|
||||
<p>If you have the means to host a stable node with a public IP address, consider becoming a <a class="reference internal" href="#hosting-entrypoints"><span class="std std-ref">Public Entrypoint</span></a>. By <a class="reference internal" href="interfaces.html#interfaces-discoverable"><span class="std std-ref">publishing your interface as discoverable</span></a>, you provide a potential connection point for others, helping the network grow and reach new areas.</p>
|
||||
<p>For guidelines on how to properly configure a public entrypoint, refer to the <a class="reference internal" href="#hosting-entrypoints"><span class="std std-ref">Hosting Public Entrypoints</span></a> section.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="connect-to-the-distributed-backbone">
|
||||
<h2>Connect to the Distributed Backbone<a class="headerlink" href="#connect-to-the-distributed-backbone" title="Link to this heading">¶</a></h2>
|
||||
<p>A global, distributed backbone of Reticulum Transport Nodes is being run by volunteers from around the world. This network constitutes a heterogenous collection of both public and private nodes that form an uncoordinated, voluntary inter-networking backbone that currently provides global transport and internetworking capabilities for Reticulum.</p>
|
||||
<p>As a good starting point, you can find interface definitions for connecting your own networks to this backbone on websites such as <a class="reference external" href="https://directory.rns.recipes/">directory.rns.recipes</a> and <a class="reference external" href="https://rmap.world/">rmap.world</a>.</p>
|
||||
<div class="admonition tip">
|
||||
<p class="admonition-title">Tip</p>
|
||||
<p>Ideally, set up a Reticulum Transport Node that your own devices can reach locally, and then
|
||||
connect that transport node to a couple of public entrypoints. This will provide efficient
|
||||
connections and redundancy in case any of them go down.</p>
|
||||
</div>
|
||||
<p>Many other Reticulum instances are connecting to this testnet, and you can also join it
|
||||
via other entry points if you know them. There is absolutely no control over the network
|
||||
topography, usage or what types of instances connect. It will also occasionally be used
|
||||
to test various failure scenarios, and there are no availability or service guarantees.
|
||||
Expect weird things to happen on this network, as people experiment and try out things.</p>
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Warning</p>
|
||||
<p>It probably goes without saying, but <em>don’t use the testnet entry-points as
|
||||
hardcoded or default interfaces in any applications you ship to users</em>. When
|
||||
shipping applications, the best practice is to provide your own default
|
||||
connectivity solutions, if needed and applicable, or in most cases, simply
|
||||
leave it up to the user which networks to connect to, and how.</p>
|
||||
<p>Don’t rely on just a single connection to the distributed backbone for everyday use. It is much better to have several redundant connections configured, and enable the interface discovery options, so your nodes can continously discover peering opportunities as the network evolves. Refer to the <a class="reference internal" href="#bootstrapping-connectivity"><span class="std std-ref">Bootstrapping Connectivity</span></a> section to understand the options.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section id="hosting-public-entrypoints">
|
||||
<h2>Hosting Public Entrypoints<a class="headerlink" href="#hosting-public-entrypoints" title="Link to this heading">¶</a></h2>
|
||||
<p>If you want to host a public (or private) entry-point to a Reticulum network over the
|
||||
Internet, this section offers some helpful pointers. You will need a machine, physical or
|
||||
virtual with a public IP address, that can be reached by other devices on the Internet.</p>
|
||||
<p>The most efficient and performant way to host a connectable entry-point supporting many
|
||||
users is to use the <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code>. This interface type is fully compatible with
|
||||
the <code class="docutils literal notranslate"><span class="pre">TCPClientInterface</span></code> and <code class="docutils literal notranslate"><span class="pre">TCPServerInterface</span></code> types, but much faster and uses
|
||||
less system resources, allowing your device to handle thousands of connections even on
|
||||
small systems.</p>
|
||||
<p>It is also important to set your connectable interface to <code class="docutils literal notranslate"><span class="pre">gateway</span></code> mode, since this
|
||||
will greatly improve network convergence time and path resolution for anyone connecting
|
||||
to your entry-point.</p>
|
||||
<span id="hosting-entrypoints"></span><h2>Hosting Public Entrypoints<a class="headerlink" href="#hosting-public-entrypoints" title="Link to this heading">¶</a></h2>
|
||||
<p>If you want to help build a strong global interconnection backbone, you can host a public (or private) entry-point to a Reticulum network over the Internet. This section offers some helpful pointers. Once you have set up your public entrypoint, it is a great idea to <a class="reference internal" href="interfaces.html#interfaces-discoverable"><span class="std std-ref">make it discoverable over Reticulum</span></a>.</p>
|
||||
<p>You will need a machine, physical or virtual with a public IP address, that can be reached by other devices on the Internet.</p>
|
||||
<p>The most efficient and performant way to host a connectable entry-point supporting many users is to use the <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code>. This interface type is fully compatible with the <code class="docutils literal notranslate"><span class="pre">TCPClientInterface</span></code> and <code class="docutils literal notranslate"><span class="pre">TCPServerInterface</span></code> types, but much faster and uses less system resources, allowing your device to handle thousands of connections even on small systems.</p>
|
||||
<p>It is also important to set your connectable interface to <code class="docutils literal notranslate"><span class="pre">gateway</span></code> mode, since this will greatly improve network convergence time and path resolution for anyone connecting to your entry-point.</p>
|
||||
<div class="highlight-ini notranslate"><div class="highlight"><pre><span></span><span class="c1"># This example demonstrates a backbone interface</span>
|
||||
<span class="c1"># configured for acting as a gateway for users to</span>
|
||||
<span class="c1"># connect to either a public or private network</span>
|
||||
@@ -511,10 +395,16 @@ to your entry-point.</p>
|
||||
<span class="w"> </span><span class="na">mode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">gateway</span>
|
||||
<span class="w"> </span><span class="na">listen_on</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">0.0.0.0</span>
|
||||
<span class="w"> </span><span class="na">port</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">4242</span>
|
||||
|
||||
<span class="w"> </span><span class="c1"># On publicly available interfaces, it is</span>
|
||||
<span class="w"> </span><span class="c1"># essential to configure sensible announce</span>
|
||||
<span class="w"> </span><span class="c1"># rate targets.</span>
|
||||
<span class="w"> </span><span class="na">announce_rate_target</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">3600</span>
|
||||
<span class="w"> </span><span class="na">announce_rate_penalty</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">3600</span>
|
||||
<span class="w"> </span><span class="na">announce_rate_grace</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">6</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>If instead you want to make a private entry-point from the Internet, you can use the
|
||||
<a class="reference internal" href="interfaces.html#interfaces-options"><span class="std std-ref">IFAC name and passphrase options</span></a> to secure your interface with a network name and passphrase.</p>
|
||||
<p>If instead you want to make a private entry-point from the Internet, you can use the <a class="reference internal" href="interfaces.html#interfaces-options"><span class="std std-ref">IFAC name and passphrase options</span></a> to secure your interface with a network name and passphrase.</p>
|
||||
<div class="highlight-ini notranslate"><div class="highlight"><pre><span></span><span class="c1"># A private entry-point requiring a pre-shared</span>
|
||||
<span class="c1"># network name and passphrase to connect to.</span>
|
||||
|
||||
@@ -528,119 +418,51 @@ to your entry-point.</p>
|
||||
<span class="w"> </span><span class="na">passphrase</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">2owjajquafIanPecAc</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>If you are hosting an entry-point on an operating system that does not support
|
||||
<code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code>, you can use <code class="docutils literal notranslate"><span class="pre">TCPServerInterface</span></code> instead, although it will
|
||||
not be as performant.</p>
|
||||
<p>If you are hosting an entry-point on an operating system that does not support <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code>, you can use <code class="docutils literal notranslate"><span class="pre">TCPServerInterface</span></code> instead, although it will not be as performant.</p>
|
||||
</section>
|
||||
<section id="connecting-reticulum-instances-over-the-internet">
|
||||
<h2>Connecting Reticulum Instances Over the Internet<a class="headerlink" href="#connecting-reticulum-instances-over-the-internet" title="Link to this heading">¶</a></h2>
|
||||
<p>Reticulum currently offers three interfaces suitable for connecting instances over the Internet: <a class="reference internal" href="interfaces.html#interfaces-backbone"><span class="std std-ref">Backbone</span></a>, <a class="reference internal" href="interfaces.html#interfaces-tcps"><span class="std std-ref">TCP</span></a> and <a class="reference internal" href="interfaces.html#interfaces-i2p"><span class="std std-ref">I2P</span></a>. Each interface offers a different set of features, and Reticulum users should carefully choose the interface which best suites their needs.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">TCPServerInterface</span></code> allows users to host an instance accessible over TCP/IP. This method is generally faster, lower latency, and more energy efficient than using <code class="docutils literal notranslate"><span class="pre">I2PInterface</span></code>, however it also leaks more data about the server host.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code> is a very fast and efficient interface type available on POSIX operating systems, designed to handle thousands of connections simultaneously with low memory, processing and I/O overhead. It is fully compatible with the TCP-based interface types.</p>
|
||||
<p>TCP connections reveal the IP address of both your instance and the server to anyone who can inspect the connection. Someone could use this information to determine your location or identity. Adversaries inspecting your packets may be able to record packet metadata like time of transmission and packet size. Even though Reticulum encrypts traffic, TCP does not, so an adversary may be able to use packet inspection to learn that a system is running Reticulum, and what other IP addresses connect to it. Hosting a publicly reachable instance over TCP also requires a publicly reachable IP address, which most Internet connections don’t offer anymore.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">I2PInterface</span></code> routes messages through the <a class="reference external" href="https://geti2p.net/en/">Invisible Internet Protocol (I2P)</a>. To use this interface, users must also run an I2P daemon in parallel to <code class="docutils literal notranslate"><span class="pre">rnsd</span></code>. For always-on I2P nodes it is recommended to use <a class="reference external" href="https://i2pd.website/">i2pd</a>.</p>
|
||||
<p>By default, I2P will encrypt and mix all traffic sent over the Internet, and hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node will also relay other I2P user’s encrypted packets, which will use extra bandwidth and compute power, but also makes timing attacks and other forms of deep-packet-inspection much more difficult.</p>
|
||||
<p>I2P also allows users to host globally available Reticulum instances from non-public IP’s and behind firewalls and NAT.</p>
|
||||
<p>In general it is recommended to use an I2P node if you want to host a publicly accessible instance, while preserving anonymity. If you care more about performance, and a slightly easier setup, use TCP.</p>
|
||||
</section>
|
||||
<section id="adding-radio-interfaces">
|
||||
<h2>Adding Radio Interfaces<a class="headerlink" href="#adding-radio-interfaces" title="Link to this heading">¶</a></h2>
|
||||
<p>Once you have Reticulum installed and working, you can add radio interfaces with
|
||||
any compatible hardware you have available. Reticulum supports a wide range of radio
|
||||
hardware, and if you already have any available, it is very likely that it will
|
||||
work with Reticulum. For information on how to configure this, see the
|
||||
<a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">Interfaces</span></a> section of this manual.</p>
|
||||
<p>If you do not already have transceiver hardware available, you can easily and
|
||||
cheaply build an <a class="reference internal" href="hardware.html#rnode-main"><span class="std std-ref">RNode</span></a>, which is a general-purpose long-range
|
||||
digital radio transceiver, that integrates easily with Reticulum.</p>
|
||||
<p>To build one yourself requires installing a custom firmware on a supported LoRa
|
||||
development board with an auto-install script. Please see the <a class="reference internal" href="hardware.html#hardware-main"><span class="std std-ref">Communications Hardware</span></a>
|
||||
chapter for a guide. If you prefer purchasing a ready-made unit, you can refer to the
|
||||
<span class="xref std std-ref">list of suppliers</span>. For more information on RNode, you can also
|
||||
refer to these additional external resources:</p>
|
||||
<ul class="simple">
|
||||
<li><p><a class="reference external" href="https://unsigned.io/how-to-make-your-own-rnodes/">How To Make Your Own RNodes</a></p></li>
|
||||
<li><p><a class="reference external" href="https://unsigned.io/installing-rnode-firmware-on-supported-devices/">Installing RNode Firmware on Compatible LoRa Devices</a></p></li>
|
||||
<li><p><a class="reference external" href="https://unsigned.io/private-messaging-over-lora/">Private, Secure and Uncensorable Messaging Over a LoRa Mesh</a></p></li>
|
||||
<li><p><a class="reference external" href="https://github.com/markqvist/RNode_Firmware/">RNode Firmware</a></p></li>
|
||||
</ul>
|
||||
<p>If you have communications hardware that is not already supported by any of the
|
||||
<a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">existing interface types</span></a>, but you think would be suitable for use with Reticulum,
|
||||
you are welcome to head over to the <a class="reference external" href="https://github.com/markqvist/Reticulum/discussions">GitHub discussion pages</a>
|
||||
and propose adding an interface for the hardware.</p>
|
||||
<p>Once you have Reticulum installed and working, you can add radio interfaces with any compatible hardware you have available. Reticulum supports a wide range of radio hardware, and if you already have any available, it is very likely that it will work with Reticulum. For information on how to configure this, see the <a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">Interfaces</span></a> section of this manual.</p>
|
||||
<p>If you do not already have transceiver hardware available, you can easily and cheaply build an <a class="reference internal" href="hardware.html#rnode-main"><span class="std std-ref">RNode</span></a>, which is a general-purpose long-range digital radio transceiver, that integrates easily with Reticulum.</p>
|
||||
<p>To build one yourself requires installing a custom firmware on a supported LoRa development board with an auto-install script or web-based flasher. Please see the <a class="reference internal" href="hardware.html#hardware-main"><span class="std std-ref">Communications Hardware</span></a> chapter for a guide. If you prefer purchasing a ready-made unit, you can refer to the <span class="xref std std-ref">list of suppliers</span>.</p>
|
||||
<p>Other radio-based hardware interfaces are being developed and made available by the broader Reticulum community. You can find more information on such topics over Reticulum-based information sharing systems.</p>
|
||||
<p>If you have communications hardware that is not already supported by any of the <a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">existing interface types</span></a>, it is easy to write (and potentially publish) a <a class="reference internal" href="interfaces.html#interfaces-custom"><span class="std std-ref">custom interface module</span></a> that makes it compatible with Reticulum.</p>
|
||||
</section>
|
||||
<section id="creating-and-using-custom-interfaces">
|
||||
<h2>Creating and Using Custom Interfaces<a class="headerlink" href="#creating-and-using-custom-interfaces" title="Link to this heading">¶</a></h2>
|
||||
<p>While Reticulum includes a flexible and broad range of built-in interfaces, these
|
||||
will not cover every conceivable type of communications hardware that Reticulum
|
||||
can potentially use to communicate.</p>
|
||||
<p>It is therefore possible to easily write your own interface modules, that can be
|
||||
loaded at run-time and used on-par with any of the built-in interface types.</p>
|
||||
<p>For more information on this subject, and code examples to build on, please see
|
||||
the <a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">Configuring Interfaces</span></a> chapter.</p>
|
||||
<p>While Reticulum includes a flexible and broad range of built-in interfaces, these will not cover every conceivable type of communications hardware that Reticulum can potentially use to communicate.</p>
|
||||
<p>It is therefore possible to easily write your own interface modules, that can be loaded at run-time and used on-par with any of the built-in interface types.</p>
|
||||
<p>For more information on this subject, and code examples to build on, please see the <a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">Configuring Interfaces</span></a> chapter.</p>
|
||||
</section>
|
||||
<section id="develop-a-program-with-reticulum">
|
||||
<h2>Develop a Program with Reticulum<a class="headerlink" href="#develop-a-program-with-reticulum" title="Link to this heading">¶</a></h2>
|
||||
<p>If you want to develop programs that use Reticulum, the easiest way to get
|
||||
started is to install the latest release of Reticulum via pip:</p>
|
||||
<p>If you want to develop programs that use Reticulum, the easiest way to get started is to install the latest release of Reticulum via pip:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">pip</span> <span class="n">install</span> <span class="n">rns</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The above command will install Reticulum and dependencies, and you will be
|
||||
ready to import and use RNS in your own programs. The next step will most
|
||||
likely be to look at some <a class="reference internal" href="examples.html#examples-main"><span class="std std-ref">Example Programs</span></a>.</p>
|
||||
<p>The entire Reticulum API is documented in the <a class="reference internal" href="reference.html#api-main"><span class="std std-ref">API Reference</span></a>
|
||||
chapter of this manual.</p>
|
||||
</section>
|
||||
<section id="participate-in-reticulum-development">
|
||||
<h2>Participate in Reticulum Development<a class="headerlink" href="#participate-in-reticulum-development" title="Link to this heading">¶</a></h2>
|
||||
<p>If you want to participate in the development of Reticulum and associated
|
||||
utilities, you’ll want to get the latest source from GitHub. In that case,
|
||||
don’t use pip, but try this recipe:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install dependencies</span>
|
||||
pip<span class="w"> </span>install<span class="w"> </span>cryptography<span class="w"> </span>pyserial
|
||||
|
||||
<span class="c1"># Clone repository</span>
|
||||
git<span class="w"> </span>clone<span class="w"> </span>https://github.com/markqvist/Reticulum.git
|
||||
|
||||
<span class="c1"># Move into Reticulum folder and symlink library to examples folder</span>
|
||||
<span class="nb">cd</span><span class="w"> </span>Reticulum
|
||||
ln<span class="w"> </span>-s<span class="w"> </span>../RNS<span class="w"> </span>./Examples/
|
||||
|
||||
<span class="c1"># Run an example</span>
|
||||
python<span class="w"> </span>Examples/Echo.py<span class="w"> </span>-s
|
||||
|
||||
<span class="c1"># Unless you've manually created a config file, Reticulum will do so now,</span>
|
||||
<span class="c1"># and immediately exit. Make any necessary changes to the file:</span>
|
||||
nano<span class="w"> </span>~/.reticulum/config
|
||||
|
||||
<span class="c1"># ... and launch the example again.</span>
|
||||
python<span class="w"> </span>Examples/Echo.py<span class="w"> </span>-s
|
||||
|
||||
<span class="c1"># You can now repeat the process on another computer,</span>
|
||||
<span class="c1"># and run the same example with -h to get command line options.</span>
|
||||
python<span class="w"> </span>Examples/Echo.py<span class="w"> </span>-h
|
||||
|
||||
<span class="c1"># Run the example in client mode to "ping" the server.</span>
|
||||
<span class="c1"># Replace the hash below with the actual destination hash of your server.</span>
|
||||
python<span class="w"> </span>Examples/Echo.py<span class="w"> </span>174a64852a75682259ad8b921b8bf416
|
||||
|
||||
<span class="c1"># Have a look at another example</span>
|
||||
python<span class="w"> </span>Examples/Filetransfer.py<span class="w"> </span>-h
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>When you have experimented with the basic examples, it’s time to go read the
|
||||
<a class="reference internal" href="understanding.html#understanding-main"><span class="std std-ref">Understanding Reticulum</span></a> chapter. Before submitting
|
||||
your first pull request, it is probably a good idea to introduce yourself on
|
||||
the <a class="reference external" href="https://github.com/markqvist/Reticulum/discussions">disucssion forum on GitHub</a>,
|
||||
or ask one of the developers or maintainers for a good place to start.</p>
|
||||
<p>The above command will install Reticulum and dependencies, and you will be ready to import and use RNS in your own programs. The next step will most likely be to look at some <a class="reference internal" href="examples.html#examples-main"><span class="std std-ref">Example Programs</span></a>.</p>
|
||||
<p>The entire Reticulum API is documented in the <a class="reference internal" href="reference.html#api-main"><span class="std std-ref">API Reference</span></a> chapter of this manual. Before diving in, it’s probably a good idea to read this manual in full, but at least start with the <a class="reference internal" href="understanding.html#understanding-main"><span class="std std-ref">Understanding Reticulum</span></a> chapter.</p>
|
||||
</section>
|
||||
<section id="platform-specific-install-notes">
|
||||
<span id="install-guides"></span><h2>Platform-Specific Install Notes<a class="headerlink" href="#platform-specific-install-notes" title="Link to this heading">¶</a></h2>
|
||||
<p>Some platforms require a slightly different installation procedure, or have
|
||||
various quirks that are worth being aware of. These are listed here.</p>
|
||||
<p>Some platforms require a slightly different installation procedure, or have various quirks that are worth being aware of. These are listed here.</p>
|
||||
<section id="android">
|
||||
<h3>Android<a class="headerlink" href="#android" title="Link to this heading">¶</a></h3>
|
||||
<p>Reticulum can be used on Android in different ways. The easiest way to get
|
||||
started is using an app like <a class="reference external" href="https://unsigned.io/sideband">Sideband</a>.</p>
|
||||
<p>For more control and features, you can use Reticulum and related programs via
|
||||
the <a class="reference external" href="https://termux.com/">Termux app</a>, at the time of writing available on
|
||||
<a class="reference external" href="https://f-droid.org">F-droid</a>.</p>
|
||||
<p>Termux is a terminal emulator and Linux environment for Android based devices,
|
||||
which includes the ability to use many different programs and libraries,
|
||||
including Reticulum.</p>
|
||||
<p>To use Reticulum within the Termux environment, you will need to install
|
||||
<code class="docutils literal notranslate"><span class="pre">python</span></code> and the <code class="docutils literal notranslate"><span class="pre">python-cryptography</span></code> library using <code class="docutils literal notranslate"><span class="pre">pkg</span></code>, the package-manager
|
||||
build into Termux. After that, you can use <code class="docutils literal notranslate"><span class="pre">pip</span></code> to install Reticulum.</p>
|
||||
<p>Reticulum can be used on Android in different ways. The easiest way to get started is using an app like <a class="reference external" href="https://unsigned.io/sideband">Sideband</a>.</p>
|
||||
<p>For more control and features, you can use Reticulum and related programs via the <a class="reference external" href="https://termux.com/">Termux app</a>, at the time of writing available on <a class="reference external" href="https://f-droid.org">F-droid</a>.</p>
|
||||
<p>Termux is a terminal emulator and Linux environment for Android based devices, which includes the ability to use many different programs and libraries, including Reticulum.</p>
|
||||
<p>To use Reticulum within the Termux environment, you will need to install <code class="docutils literal notranslate"><span class="pre">python</span></code> and the <code class="docutils literal notranslate"><span class="pre">python-cryptography</span></code> library using <code class="docutils literal notranslate"><span class="pre">pkg</span></code>, the package-manager build into Termux. After that, you can use <code class="docutils literal notranslate"><span class="pre">pip</span></code> to install Reticulum.</p>
|
||||
<p>From within Termux, execute the following:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="c1"># First, make sure indexes and packages are up to date.</span>
|
||||
pkg<span class="w"> </span>update
|
||||
@@ -656,9 +478,7 @@ pip<span class="w"> </span>install<span class="w"> </span>wheel<span class="w">
|
||||
pip<span class="w"> </span>install<span class="w"> </span>rns
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>If for some reason the <code class="docutils literal notranslate"><span class="pre">python-cryptography</span></code> package is not available for
|
||||
your platform via the Termux package manager, you can attempt to build it
|
||||
locally on your device using the following command:</p>
|
||||
<p>If for some reason the <code class="docutils literal notranslate"><span class="pre">python-cryptography</span></code> package is not available for your platform via the Termux package manager, you can attempt to build it locally on your device using the following command:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="c1"># First, make sure indexes and packages are up to date.</span>
|
||||
pkg<span class="w"> </span>update
|
||||
pkg<span class="w"> </span>upgrade
|
||||
@@ -683,15 +503,11 @@ pip<span class="w"> </span>install<span class="w"> </span>cryptography
|
||||
pip<span class="w"> </span>install<span class="w"> </span>rns
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>It is also possible to include Reticulum in apps compiled and distributed as
|
||||
Android APKs. A detailed tutorial and example source code will be included
|
||||
here at a later point. Until then you can use the <a class="reference external" href="https://github.com/markqvist/sideband">Sideband source code</a> as an example and starting point.</p>
|
||||
<p>It is also possible to include Reticulum in apps compiled and distributed as Android APKs. A detailed tutorial and example source code will be included here at a later point. Until then you can use the <a class="reference external" href="https://github.com/markqvist/sideband">Sideband source code</a> as an example and starting point.</p>
|
||||
</section>
|
||||
<section id="arm64">
|
||||
<h3>ARM64<a class="headerlink" href="#arm64" title="Link to this heading">¶</a></h3>
|
||||
<p>On some architectures, including ARM64, not all dependencies have precompiled
|
||||
binaries. On such systems, you may need to install <code class="docutils literal notranslate"><span class="pre">python3-dev</span></code> (or similar) before
|
||||
installing Reticulum or programs that depend on Reticulum.</p>
|
||||
<p>On some architectures, including ARM64, not all dependencies have precompiled binaries. On such systems, you may need to install <code class="docutils literal notranslate"><span class="pre">python3-dev</span></code> (or similar) before installing Reticulum or programs that depend on Reticulum.</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install Python and development packages</span>
|
||||
sudo<span class="w"> </span>apt<span class="w"> </span>update
|
||||
sudo<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> </span>python3<span class="w"> </span>python3-pip<span class="w"> </span>python3-dev
|
||||
@@ -705,11 +521,7 @@ on your system locally.</p>
|
||||
</section>
|
||||
<section id="debian-bookworm">
|
||||
<h3>Debian Bookworm<a class="headerlink" href="#debian-bookworm" title="Link to this heading">¶</a></h3>
|
||||
<p>On versions of Debian released after April 2023, it is no longer possible by default
|
||||
to use <code class="docutils literal notranslate"><span class="pre">pip</span></code> to install packages onto your system. Unfortunately, you will need to
|
||||
use the replacement <code class="docutils literal notranslate"><span class="pre">pipx</span></code> command instead, which places installed packages in an
|
||||
isolated environment. This should not negatively affect Reticulum, but will not work
|
||||
for including and using Reticulum in your own scripts and programs.</p>
|
||||
<p>On versions of Debian released after April 2023, it is no longer possible by default to use <code class="docutils literal notranslate"><span class="pre">pip</span></code> to install packages onto your system. Unfortunately, you will need to use the replacement <code class="docutils literal notranslate"><span class="pre">pipx</span></code> command instead, which places installed packages in an isolated environment. This should not negatively affect Reticulum, but will not work for including and using Reticulum in your own scripts and programs.</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install pipx</span>
|
||||
sudo<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> </span>pipx
|
||||
|
||||
@@ -720,37 +532,25 @@ pipx<span class="w"> </span>ensurepath
|
||||
pipx<span class="w"> </span>install<span class="w"> </span>rns
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Alternatively, you can restore normal behaviour to <code class="docutils literal notranslate"><span class="pre">pip</span></code> by creating or editing
|
||||
the configuration file located at <code class="docutils literal notranslate"><span class="pre">~/.config/pip/pip.conf</span></code>, and adding the
|
||||
following section:</p>
|
||||
<p>Alternatively, you can restore normal behaviour to <code class="docutils literal notranslate"><span class="pre">pip</span></code> by creating or editing the configuration file located at <code class="docutils literal notranslate"><span class="pre">~/.config/pip/pip.conf</span></code>, and adding the following section:</p>
|
||||
<div class="highlight-ini notranslate"><div class="highlight"><pre><span></span><span class="k">[global]</span>
|
||||
<span class="na">break-system-packages</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">true</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>For a one-shot installation of Reticulum, without globally enabling the <code class="docutils literal notranslate"><span class="pre">break-system-packages</span></code>
|
||||
option, you can use the following command:</p>
|
||||
<p>For a one-shot installation of Reticulum, without globally enabling the <code class="docutils literal notranslate"><span class="pre">break-system-packages</span></code> option, you can use the following command:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>pip<span class="w"> </span>install<span class="w"> </span>rns<span class="w"> </span>--break-system-packages
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">--break-system-packages</span></code> directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing <code class="docutils literal notranslate"><span class="pre">pip</span></code> packages user- and system-wide. While this <em>could</em> in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">--break-system-packages</span></code> directive is a somewhat misleading choice of words. Setting it will of course not break any system packages, but will simply allow installing <code class="docutils literal notranslate"><span class="pre">pip</span></code> packages user- and system-wide. While this <em>could</em> in rare cases lead to version conflicts, it does not generally pose any problems, especially not in the case of installing Reticulum.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section id="macos">
|
||||
<h3>MacOS<a class="headerlink" href="#macos" title="Link to this heading">¶</a></h3>
|
||||
<p>To install Reticulum on macOS, you will need to have Python and the <code class="docutils literal notranslate"><span class="pre">pip</span></code> package
|
||||
manager installed.</p>
|
||||
<p>Systems running macOS can vary quite widely in whether or not Python is pre-installed,
|
||||
and if it is, which version is installed, and whether the <code class="docutils literal notranslate"><span class="pre">pip</span></code> package manager is
|
||||
also installed and set up. If in doubt, you can <a class="reference external" href="https://www.python.org/downloads/">download and install</a>
|
||||
Python manually.</p>
|
||||
<p>When Python and <code class="docutils literal notranslate"><span class="pre">pip</span></code> is available on your system, simply open a terminal window
|
||||
and use one of the following commands:</p>
|
||||
<p>To install Reticulum on macOS, you will need to have Python and the <code class="docutils literal notranslate"><span class="pre">pip</span></code> package manager installed.</p>
|
||||
<p>Systems running macOS can vary quite widely in whether or not Python is pre-installed, and if it is, which version is installed, and whether the <code class="docutils literal notranslate"><span class="pre">pip</span></code> package manager is also installed and set up. If in doubt, you can <a class="reference external" href="https://www.python.org/downloads/">download and install</a> Python manually.</p>
|
||||
<p>When Python and <code class="docutils literal notranslate"><span class="pre">pip</span></code> is available on your system, simply open a terminal window and use one of the following commands:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install Reticulum and utilities with pip:</span>
|
||||
pip3<span class="w"> </span>install<span class="w"> </span>rns
|
||||
|
||||
@@ -761,16 +561,9 @@ pip3<span class="w"> </span>install<span class="w"> </span>rns<span class="w"> <
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">--break-system-packages</span></code> directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing <code class="docutils literal notranslate"><span class="pre">pip</span></code> packages user- and system-wide. While this <em>could</em> in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">--break-system-packages</span></code> directive is a somewhat misleading choice of words. Setting it will of course not break any system packages, but will simply allow installing <code class="docutils literal notranslate"><span class="pre">pip</span></code> packages user- and system-wide. While this <em>could</em> in rare cases lead to version conflicts, it does not generally pose any problems, especially not in the case of installing Reticulum.</p>
|
||||
</div>
|
||||
<p>Additionally, some version combinations of macOS and Python require you to
|
||||
manually add your installed <code class="docutils literal notranslate"><span class="pre">pip</span></code> packages directory to your <cite>PATH</cite> environment
|
||||
variable, before you can use installed commands in your terminal. Usually, adding
|
||||
the following line to your shell init script (for example <code class="docutils literal notranslate"><span class="pre">~/.zshrc</span></code>) will be enough:</p>
|
||||
<p>Additionally, some version combinations of macOS and Python require you to manually add your installed <code class="docutils literal notranslate"><span class="pre">pip</span></code> packages directory to your <cite>PATH</cite> environment variable, before you can use installed commands in your terminal. Usually, adding the following line to your shell init script (for example <code class="docutils literal notranslate"><span class="pre">~/.zshrc</span></code>) will be enough:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="nb">export</span><span class="w"> </span><span class="nv">PATH</span><span class="o">=</span><span class="nv">$PATH</span>:~/Library/Python/3.9/bin
|
||||
</pre></div>
|
||||
</div>
|
||||
@@ -778,18 +571,12 @@ the following line to your shell init script (for example <code class="docutils
|
||||
</section>
|
||||
<section id="openwrt">
|
||||
<h3>OpenWRT<a class="headerlink" href="#openwrt" title="Link to this heading">¶</a></h3>
|
||||
<p>On OpenWRT systems with sufficient storage and memory, you can install
|
||||
Reticulum and related utilities using the <cite>opkg</cite> package manager and <cite>pip</cite>.</p>
|
||||
<p>On OpenWRT systems with sufficient storage and memory, you can install Reticulum and related utilities using the <cite>opkg</cite> package manager and <cite>pip</cite>.</p>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>At the time of releasing this manual, work is underway to create pre-built
|
||||
Reticulum packages for OpenWRT, with full configuration, service
|
||||
and <code class="docutils literal notranslate"><span class="pre">uci</span></code> integration. Please see the <a class="reference external" href="https://github.com/gretel/feed-reticulum">feed-reticulum</a>
|
||||
and <a class="reference external" href="https://github.com/gretel/reticulum-openwrt">reticulum-openwrt</a>
|
||||
repositories for more information.</p>
|
||||
<p>At the time of releasing this manual, work is underway to create pre-built Reticulum packages for OpenWRT, with full configuration, service and <code class="docutils literal notranslate"><span class="pre">uci</span></code> integration. Please see the <a class="reference external" href="https://github.com/gretel/feed-reticulum">feed-reticulum</a> and <a class="reference external" href="https://github.com/gretel/reticulum-openwrt">reticulum-openwrt</a> repositories for more information.</p>
|
||||
</div>
|
||||
<p>To install Reticulum on OpenWRT, first log into a command line session, and
|
||||
then use the following instructions:</p>
|
||||
<p>To install Reticulum on OpenWRT, first log into a command line session, and then use the following instructions:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install dependencies</span>
|
||||
opkg<span class="w"> </span>install<span class="w"> </span>python3<span class="w"> </span>python3-pip<span class="w"> </span>python3-cryptography<span class="w"> </span>python3-pyserial
|
||||
|
||||
@@ -802,28 +589,14 @@ rnsd<span class="w"> </span>-vvv
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>The above instructions have been verified and tested on OpenWRT 21.02 only.
|
||||
It is likely that other versions may require slightly altered installation
|
||||
commands or package names. You will also need enough free space in your
|
||||
overlay FS, and enough free RAM to actually run Reticulum and any related
|
||||
programs and utilities.</p>
|
||||
<p>The above instructions have been verified and tested on OpenWRT 21.02 only. It is likely that other versions may require slightly altered installation commands or package names. You will also need enough free space in your overlay FS, and enough free RAM to actually run Reticulum and any related programs and utilities.</p>
|
||||
</div>
|
||||
<p>Depending on your device configuration, you may need to adjust firewall rules
|
||||
for Reticulum connectivity to and from your device to work. Until proper
|
||||
packaging is ready, you will also need to manually create a service or startup
|
||||
script to automatically laucnh Reticulum at boot time.</p>
|
||||
<p>Please also note that the <cite>AutoInterface</cite> requires link-local IPv6 addresses
|
||||
to be enabled for any Ethernet and WiFi devices you intend to use. If <code class="docutils literal notranslate"><span class="pre">ip</span> <span class="pre">a</span></code>
|
||||
shows an address starting with <code class="docutils literal notranslate"><span class="pre">fe80::</span></code> for the device in question,
|
||||
<code class="docutils literal notranslate"><span class="pre">AutoInterface</span></code> should work for that device.</p>
|
||||
<p>Depending on your device configuration, you may need to adjust firewall rules for Reticulum connectivity to and from your device to work. Until proper packaging is ready, you will also need to manually create a service or startup script to automatically laucnh Reticulum at boot time.</p>
|
||||
<p>Please also note that the <cite>AutoInterface</cite> requires link-local IPv6 addresses to be enabled for any Ethernet and WiFi devices you intend to use. If <code class="docutils literal notranslate"><span class="pre">ip</span> <span class="pre">a</span></code> shows an address starting with <code class="docutils literal notranslate"><span class="pre">fe80::</span></code> for the device in question, <code class="docutils literal notranslate"><span class="pre">AutoInterface</span></code> should work for that device.</p>
|
||||
</section>
|
||||
<section id="raspberry-pi">
|
||||
<h3>Raspberry Pi<a class="headerlink" href="#raspberry-pi" title="Link to this heading">¶</a></h3>
|
||||
<p>It is currently recommended to use a 64-bit version of the Raspberry Pi OS
|
||||
if you want to run Reticulum on Raspberry Pi computers, since 32-bit versions
|
||||
don’t always have packages available for some dependencies. If Python and the
|
||||
<cite>pip</cite> package manager is not already installed, do that first, and then
|
||||
install Reticulum using <cite>pip</cite>.</p>
|
||||
<p>It is currently recommended to use a 64-bit version of the Raspberry Pi OS if you want to run Reticulum on Raspberry Pi computers, since 32-bit versions don’t always have packages available for some dependencies. If Python and the <cite>pip</cite> package manager is not already installed, do that first, and then install Reticulum using <cite>pip</cite>.</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install dependencies</span>
|
||||
sudo<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> </span>python3<span class="w"> </span>python3-pip<span class="w"> </span>python3-cryptography<span class="w"> </span>python3-pyserial
|
||||
|
||||
@@ -833,21 +606,13 @@ pip<span class="w"> </span>install<span class="w"> </span>rns<span class="w"> </
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">--break-system-packages</span></code> directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing <code class="docutils literal notranslate"><span class="pre">pip</span></code> packages user- and system-wide. While this <em>could</em> in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">--break-system-packages</span></code> directive is a somewhat misleading choice of words. Setting it will of course not break any system packages, but will simply allow installing <code class="docutils literal notranslate"><span class="pre">pip</span></code> packages user- and system-wide. While this <em>could</em> in rare cases lead to version conflicts, it does not generally pose any problems, especially not in the case of installing Reticulum.</p>
|
||||
</div>
|
||||
<p>While it is possible to install and run Reticulum on 32-bit Rasperry Pi OSes,
|
||||
it will require manually configuring and installing required build dependencies,
|
||||
and is not detailed in this manual.</p>
|
||||
<p>While it is possible to install and run Reticulum on 32-bit Rasperry Pi OSes, it will require manually configuring and installing required build dependencies, and is not detailed in this manual.</p>
|
||||
</section>
|
||||
<section id="risc-v">
|
||||
<h3>RISC-V<a class="headerlink" href="#risc-v" title="Link to this heading">¶</a></h3>
|
||||
<p>On some architectures, including RISC-V, not all dependencies have precompiled
|
||||
binaries. On such systems, you may need to install <code class="docutils literal notranslate"><span class="pre">python3-dev</span></code> (or similar) before
|
||||
installing Reticulum or programs that depend on Reticulum.</p>
|
||||
<p>On some architectures, including RISC-V, not all dependencies have precompiled binaries. On such systems, you may need to install <code class="docutils literal notranslate"><span class="pre">python3-dev</span></code> (or similar) before installing Reticulum or programs that depend on Reticulum.</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install Python and development packages</span>
|
||||
sudo<span class="w"> </span>apt<span class="w"> </span>update
|
||||
sudo<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> </span>python3<span class="w"> </span>python3-pip<span class="w"> </span>python3-dev
|
||||
@@ -856,16 +621,11 @@ sudo<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> <
|
||||
python3<span class="w"> </span>-m<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>rns
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>With these packages installed, <code class="docutils literal notranslate"><span class="pre">pip</span></code> will be able to build any missing dependencies
|
||||
on your system locally.</p>
|
||||
<p>With these packages installed, <code class="docutils literal notranslate"><span class="pre">pip</span></code> will be able to build any missing dependencies on your system locally.</p>
|
||||
</section>
|
||||
<section id="ubuntu-lunar">
|
||||
<h3>Ubuntu Lunar<a class="headerlink" href="#ubuntu-lunar" title="Link to this heading">¶</a></h3>
|
||||
<p>On versions of Ubuntu released after April 2023, it is no longer possible by default
|
||||
to use <code class="docutils literal notranslate"><span class="pre">pip</span></code> to install packages onto your system. Unfortunately, you will need to
|
||||
use the replacement <code class="docutils literal notranslate"><span class="pre">pipx</span></code> command instead, which places installed packages in an
|
||||
isolated environment. This should not negatively affect Reticulum, but will not work
|
||||
for including and using Reticulum in your own scripts and programs.</p>
|
||||
<p>On versions of Ubuntu released after April 2023, it is no longer possible by default to use <code class="docutils literal notranslate"><span class="pre">pip</span></code> to install packages onto your system. Unfortunately, you will need to use the replacement <code class="docutils literal notranslate"><span class="pre">pipx</span></code> command instead, which places installed packages in an isolated environment. This should not negatively affect Reticulum, but will not work for including and using Reticulum in your own scripts and programs.</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install pipx</span>
|
||||
sudo<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> </span>pipx
|
||||
|
||||
@@ -876,44 +636,30 @@ pipx<span class="w"> </span>ensurepath
|
||||
pipx<span class="w"> </span>install<span class="w"> </span>rns
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Alternatively, you can restore normal behaviour to <code class="docutils literal notranslate"><span class="pre">pip</span></code> by creating or editing
|
||||
the configuration file located at <code class="docutils literal notranslate"><span class="pre">~/.config/pip/pip.conf</span></code>, and adding the
|
||||
following section:</p>
|
||||
<p>Alternatively, you can restore normal behaviour to <code class="docutils literal notranslate"><span class="pre">pip</span></code> by creating or editing the configuration file located at <code class="docutils literal notranslate"><span class="pre">~/.config/pip/pip.conf</span></code>, and adding the following section:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[global]
|
||||
break-system-packages = true
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>For a one-shot installation of Reticulum, without globally enabling the <code class="docutils literal notranslate"><span class="pre">break-system-packages</span></code>
|
||||
option, you can use the following command:</p>
|
||||
<p>For a one-shot installation of Reticulum, without globally enabling the <code class="docutils literal notranslate"><span class="pre">break-system-packages</span></code> option, you can use the following command:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>pip install rns --break-system-packages
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">--break-system-packages</span></code> directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing <code class="docutils literal notranslate"><span class="pre">pip</span></code> packages user- and system-wide. While this <em>could</em> in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">--break-system-packages</span></code> directive is a somewhat misleading choice of words. Setting it will of course not break any system packages, but will simply allow installing <code class="docutils literal notranslate"><span class="pre">pip</span></code> packages user- and system-wide. While this <em>could</em> in rare cases lead to version conflicts, it does not generally pose any problems, especially not in the case of installing Reticulum.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section id="windows">
|
||||
<h3>Windows<a class="headerlink" href="#windows" title="Link to this heading">¶</a></h3>
|
||||
<p>On Windows operating systems, the easiest way to install Reticulum is by using the
|
||||
<code class="docutils literal notranslate"><span class="pre">pip</span></code> package manager from the command line (either the command prompt or Windows
|
||||
Powershell).</p>
|
||||
<p>If you don’t already have Python installed, <a class="reference external" href="https://www.python.org/downloads/">download and install Python</a>.
|
||||
At the time of publication of this manual, the recommended version is <a class="reference external" href="https://www.python.org/downloads/release/python-3127">Python 3.12.7</a>.</p>
|
||||
<p><strong>Important!</strong> When asked by the installer, make sure to add the Python program to
|
||||
your PATH environment variables. If you don’t do this, you will not be able to
|
||||
use the <code class="docutils literal notranslate"><span class="pre">pip</span></code> installer, or run the included Reticulum utility programs (such as
|
||||
<code class="docutils literal notranslate"><span class="pre">rnsd</span></code> and <code class="docutils literal notranslate"><span class="pre">rnstatus</span></code>) from the command line.</p>
|
||||
<p>On Windows operating systems, the easiest way to install Reticulum is by using the <code class="docutils literal notranslate"><span class="pre">pip</span></code> package manager from the command line (either the command prompt or Windows Powershell).</p>
|
||||
<p>If you don’t already have Python installed, <a class="reference external" href="https://www.python.org/downloads/">download and install Python</a>. At the time of publication of this manual, the recommended version is <a class="reference external" href="https://www.python.org/downloads/release/python-3127">Python 3.12.7</a>.</p>
|
||||
<p><strong>Important!</strong> When asked by the installer, make sure to add the Python program to your PATH environment variables. If you don’t do this, you will not be able to use the <code class="docutils literal notranslate"><span class="pre">pip</span></code> installer, or run the included Reticulum utility programs (such as <code class="docutils literal notranslate"><span class="pre">rnsd</span></code> and <code class="docutils literal notranslate"><span class="pre">rnstatus</span></code>) from the command line.</p>
|
||||
<p>After installing Python, open the command prompt or Windows Powershell, and type:</p>
|
||||
<div class="highlight-shell notranslate"><div class="highlight"><pre><span></span>pip<span class="w"> </span>install<span class="w"> </span>rns
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>You can now use Reticulum and all included utility programs directly from your
|
||||
preferred command line interface.</p>
|
||||
<p>You can now use Reticulum and all included utility programs directly from your preferred command line interface.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="pure-python-reticulum">
|
||||
@@ -925,19 +671,8 @@ do not support <a class="reference external" href="https://github.com/pyca/crypt
|
||||
important that you read and understand the <a class="reference internal" href="understanding.html#understanding-primitives"><span class="std std-ref">Cryptographic Primitives</span></a>
|
||||
section of this manual.</p>
|
||||
</div>
|
||||
<p>In some rare cases, and on more obscure system types, it is not possible to
|
||||
install one or more dependencies. In such situations,
|
||||
you can use the <code class="docutils literal notranslate"><span class="pre">rnspure</span></code> package instead of the <code class="docutils literal notranslate"><span class="pre">rns</span></code> package, or use <code class="docutils literal notranslate"><span class="pre">pip</span></code>
|
||||
with the <code class="docutils literal notranslate"><span class="pre">--no-dependencies</span></code> command-line option. The <code class="docutils literal notranslate"><span class="pre">rnspure</span></code>
|
||||
package requires no external dependencies for installation. Please note that the
|
||||
actual contents of the <code class="docutils literal notranslate"><span class="pre">rns</span></code> and <code class="docutils literal notranslate"><span class="pre">rnspure</span></code> packages are <em>completely identical</em>.
|
||||
The only difference is that the <code class="docutils literal notranslate"><span class="pre">rnspure</span></code> package lists no dependencies required
|
||||
for installation.</p>
|
||||
<p>No matter how Reticulum is installed and started, it will load external dependencies
|
||||
only if they are <em>needed</em> and <em>available</em>. If for example you want to use Reticulum
|
||||
on a system that cannot support <code class="docutils literal notranslate"><span class="pre">pyserial</span></code>, it is perfectly possible to do so using
|
||||
the <cite>rnspure</cite> package, but Reticulum will not be able to use serial-based interfaces.
|
||||
All other available modules will still be loaded when needed.</p>
|
||||
<p>In some rare cases, and on more obscure system types, it is not possible to install one or more dependencies. In such situations, you can use the <code class="docutils literal notranslate"><span class="pre">rnspure</span></code> package instead of the <code class="docutils literal notranslate"><span class="pre">rns</span></code> package, or use <code class="docutils literal notranslate"><span class="pre">pip</span></code> with the <code class="docutils literal notranslate"><span class="pre">--no-dependencies</span></code> command-line option. The <code class="docutils literal notranslate"><span class="pre">rnspure</span></code> package requires no external dependencies for installation. Please note that the actual contents of the <code class="docutils literal notranslate"><span class="pre">rns</span></code> and <code class="docutils literal notranslate"><span class="pre">rnspure</span></code> packages are <em>completely identical</em>. The only difference is that the <code class="docutils literal notranslate"><span class="pre">rnspure</span></code> package lists no dependencies required for installation.</p>
|
||||
<p>No matter how Reticulum is installed and started, it will load external dependencies only if they are <em>needed</em> and <em>available</em>. If for example you want to use Reticulum on a system that cannot support <code class="docutils literal notranslate"><span class="pre">pyserial</span></code>, it is perfectly possible to do so using the <cite>rnspure</cite> package, but Reticulum will not be able to use serial-based interfaces. All other available modules will still be loaded when needed.</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -946,12 +681,12 @@ All other available modules will still be loaded when needed.</p>
|
||||
<footer>
|
||||
|
||||
<div class="related-pages">
|
||||
<a class="next-page" href="using.html">
|
||||
<a class="next-page" href="zen.html">
|
||||
<div class="page-info">
|
||||
<div class="context">
|
||||
<span>Next</span>
|
||||
</div>
|
||||
<div class="title">Using Reticulum on Your System</div>
|
||||
<div class="title">Zen of Reticulum</div>
|
||||
</div>
|
||||
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
|
||||
</a>
|
||||
@@ -1000,22 +735,23 @@ All other available modules will still be loaded when needed.</p>
|
||||
<li><a class="reference internal" href="#resolving-dependency-installation-issues">Resolving Dependency & Installation Issues</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#try-using-a-reticulum-based-program">Try Using a Reticulum-based Program</a><ul>
|
||||
<li><a class="reference internal" href="#remote-shell">Remote Shell</a></li>
|
||||
<li><a class="reference internal" href="#nomad-network">Nomad Network</a></li>
|
||||
<li><a class="reference internal" href="#sideband">Sideband</a></li>
|
||||
<li><a class="reference internal" href="#meshchat">MeshChat</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#try-using-a-reticulum-based-program">Try Using a Reticulum-based Program</a></li>
|
||||
<li><a class="reference internal" href="#using-the-included-utilities">Using the Included Utilities</a></li>
|
||||
<li><a class="reference internal" href="#creating-a-network-with-reticulum">Creating a Network With Reticulum</a></li>
|
||||
<li><a class="reference internal" href="#connecting-reticulum-instances-over-the-internet">Connecting Reticulum Instances Over the Internet</a></li>
|
||||
<li><a class="reference internal" href="#connect-to-the-public-testnet">Connect to the Public Testnet</a></li>
|
||||
<li><a class="reference internal" href="#bootstrapping-connectivity">Bootstrapping Connectivity</a><ul>
|
||||
<li><a class="reference internal" href="#finding-your-way">Finding Your Way</a></li>
|
||||
<li><a class="reference internal" href="#build-personal-infrastructure">Build Personal Infrastructure</a></li>
|
||||
<li><a class="reference internal" href="#mixing-strategies">Mixing Strategies</a></li>
|
||||
<li><a class="reference internal" href="#network-health-responsibility">Network Health & Responsibility</a></li>
|
||||
<li><a class="reference internal" href="#contributing-to-the-global-ret">Contributing to the Global Ret</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#connect-to-the-distributed-backbone">Connect to the Distributed Backbone</a></li>
|
||||
<li><a class="reference internal" href="#hosting-public-entrypoints">Hosting Public Entrypoints</a></li>
|
||||
<li><a class="reference internal" href="#connecting-reticulum-instances-over-the-internet">Connecting Reticulum Instances Over the Internet</a></li>
|
||||
<li><a class="reference internal" href="#adding-radio-interfaces">Adding Radio Interfaces</a></li>
|
||||
<li><a class="reference internal" href="#creating-and-using-custom-interfaces">Creating and Using Custom Interfaces</a></li>
|
||||
<li><a class="reference internal" href="#develop-a-program-with-reticulum">Develop a Program with Reticulum</a></li>
|
||||
<li><a class="reference internal" href="#participate-in-reticulum-development">Participate in Reticulum Development</a></li>
|
||||
<li><a class="reference internal" href="#platform-specific-install-notes">Platform-Specific Install Notes</a><ul>
|
||||
<li><a class="reference internal" href="#android">Android</a></li>
|
||||
<li><a class="reference internal" href="#arm64">ARM64</a></li>
|
||||
@@ -1040,7 +776,7 @@ All other available modules will still be loaded when needed.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=71272d9f"></script>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Communications Hardware - Reticulum Network Stack 1.0.4 documentation</title>
|
||||
<title>Communications Hardware - Reticulum Network Stack 1.3.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.0.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -215,13 +215,18 @@
|
||||
<ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="whatis.html">What is Reticulum?</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="gettingstartedfast.html">Getting Started Fast</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="zen.html">Zen of Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="software.html">Programs Using Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="using.html">Using Reticulum on Your System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="understanding.html">Understanding Reticulum</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="distributed.html">Distributed Development</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a></li>
|
||||
@@ -671,7 +676,7 @@ can be used with Reticulum. This includes virtual software modems such as
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=71272d9f"></script>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
+221
-22
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Reticulum Network Stack 1.0.4 documentation</title>
|
||||
<title>Reticulum Network Stack 1.3.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 1.0.4 documentation</div></a>
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.0.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -215,13 +215,18 @@
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="whatis.html">What is Reticulum?</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="gettingstartedfast.html">Getting Started Fast</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="zen.html">Zen of Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="software.html">Programs Using Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="using.html">Using Reticulum on Your System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="understanding.html">Understanding Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="distributed.html">Distributed Development</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a></li>
|
||||
@@ -270,10 +275,10 @@ to participate in the development of Reticulum itself.</p>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="whatis.html">What is Reticulum?</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="whatis.html#current-status">Current Status</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="whatis.html#reference-implementation">Reference Implementation</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="whatis.html#what-does-reticulum-offer">What does Reticulum Offer?</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="whatis.html#where-can-reticulum-be-used">Where can Reticulum be Used?</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="whatis.html#interface-types-and-devices">Interface Types and Devices</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="whatis.html#caveat-emptor">Caveat Emptor</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="gettingstartedfast.html">Getting Started Fast</a><ul>
|
||||
@@ -281,22 +286,23 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#resolving-dependency-installation-issues">Resolving Dependency & Installation Issues</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#try-using-a-reticulum-based-program">Try Using a Reticulum-based Program</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#remote-shell">Remote Shell</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#nomad-network">Nomad Network</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#sideband">Sideband</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#meshchat">MeshChat</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#try-using-a-reticulum-based-program">Try Using a Reticulum-based Program</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#using-the-included-utilities">Using the Included Utilities</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#creating-a-network-with-reticulum">Creating a Network With Reticulum</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#connecting-reticulum-instances-over-the-internet">Connecting Reticulum Instances Over the Internet</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#connect-to-the-public-testnet">Connect to the Public Testnet</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#bootstrapping-connectivity">Bootstrapping Connectivity</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#finding-your-way">Finding Your Way</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#build-personal-infrastructure">Build Personal Infrastructure</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#mixing-strategies">Mixing Strategies</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#network-health-responsibility">Network Health & Responsibility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#contributing-to-the-global-ret">Contributing to the Global Ret</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#connect-to-the-distributed-backbone">Connect to the Distributed Backbone</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#hosting-public-entrypoints">Hosting Public Entrypoints</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#connecting-reticulum-instances-over-the-internet">Connecting Reticulum Instances Over the Internet</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#adding-radio-interfaces">Adding Radio Interfaces</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#creating-and-using-custom-interfaces">Creating and Using Custom Interfaces</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#develop-a-program-with-reticulum">Develop a Program with Reticulum</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#participate-in-reticulum-development">Participate in Reticulum Development</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#platform-specific-install-notes">Platform-Specific Install Notes</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#android">Android</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#arm64">ARM64</a></li>
|
||||
@@ -312,6 +318,88 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#pure-python-reticulum">Pure-Python Reticulum</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="zen.html">Zen of Reticulum</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="zen.html#the-illusion-of-the-center">The Illusion Of The Center</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#fallacy-of-the-cloud">Fallacy Of The Cloud</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#decentralization-or-uncentralizability">Decentralization Or Uncentralizability?</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#death-to-the-address">Death To The Address</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="zen.html#physics-of-trust">Physics Of Trust</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#hostile-environments">Hostile Environments</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#encryption-is-not-a-feature">Encryption Is Not A Feature</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#zero-trust-architectures">Zero-Trust Architectures</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="zen.html#merits-of-scarcity">Merits Of Scarcity</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#the-bandwidth-fallacy">The Bandwidth Fallacy</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#cost-of-a-byte">Cost Of A Byte</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#flow-time">Flow & Time</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#liberation-from-limits">Liberation From Limits</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="zen.html#sovereignty-through-infrastructure">Sovereignty Through Infrastructure</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#a-carrier-grade-fallacy">A Carrier-Grade Fallacy</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#personal-infrastructure">Personal Infrastructure</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#the-ability-to-disconnect">The Ability To Disconnect</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="zen.html#identity-and-nomadism">Identity and Nomadism</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#portable-existence">Portable Existence</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#roaming-nodes">Roaming Nodes</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#announcing-presence">Announcing Presence</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#anchor-in-the-flow">Anchor In The Flow</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="zen.html#ethics-of-the-tool">Ethics Of The Tool</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#the-harm-principle">The Harm Principle</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#public-domain-protocol">Public Domain Protocol</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#preserving-human-agency">Preserving Human Agency</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="zen.html#design-patterns-for-post-ip-systems">Design Patterns For Post-IP Systems</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#store-forward">Store & Forward</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#naming-is-power">Naming Is Power</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#the-interface-is-the-medium">The Interface Is The Medium</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#emergent-patterns">Emergent Patterns</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="zen.html#fabric-of-the-independent">Fabric Of The Independent</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#the-work-is-finished">The Work Is Finished</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="zen.html#open-sky">Open Sky</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="software.html">Programs Using Reticulum</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="software.html#programs-utilities">Programs & Utilities</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#remote-shell">Remote Shell</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#nomad-network">Nomad Network</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#rns-page-node">RNS Page Node</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#retipedia">Retipedia</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#sideband">Sideband</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#meshchatx">MeshChatX</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#reticulum-relay-chat">Reticulum Relay Chat</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#retibbs">RetiBBS</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#rbrowser">RBrowser</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#reticulum-network-telephone">Reticulum Network Telephone</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#lxst-phone">LXST Phone</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#lxmfy">LXMFy</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#lxmf-interactive-client">LXMF Interactive Client</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#rns-filesync">RNS FileSync</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#micron-parser-js">Micron Parser JS</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#rnmon">RNMon</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="software.html#protocols">Protocols</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#lxmf">LXMF</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#id16">LXST</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#rrc">RRC</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="software.html#interface-modules-connectivity-resources">Interface Modules & Connectivity Resources</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="using.html">Using Reticulum on Your System</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="using.html#configuration-data">Configuration & Data</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="using.html#included-utility-programs">Included Utility Programs</a><ul>
|
||||
@@ -321,11 +409,20 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnpath-utility">The rnpath Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnprobe-utility">The rnprobe Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rncp-utility">The rncp Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rngit-utility">The rngit Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnx-utility">The rnx Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnsh-utility">The rnsh Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnodeconf-utility">The rnodeconf Utility</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="using.html#discovering-interfaces">Discovering Interfaces</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="using.html#remote-management">Remote Management</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="using.html#blackhole-management">Blackhole Management</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#local-blackhole-management">Local Blackhole Management</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#automated-list-sourcing">Automated List Sourcing</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#publishing-blackhole-lists">Publishing Blackhole Lists</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="using.html#improving-system-configuration">Improving System Configuration</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#fixed-serial-port-names">Fixed Serial Port Names</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#reticulum-as-a-system-service">Reticulum as a System Service</a></li>
|
||||
@@ -350,6 +447,13 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l3"><a class="reference internal" href="understanding.html#resources">Resources</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="understanding.html#network-identities">Network Identities</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="understanding.html#conceptual-overview">Conceptual Overview</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="understanding.html#current-usage">Current Usage</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="understanding.html#future-implications">Future Implications</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="understanding.html#creating-and-using-a-network-identity">Creating and Using a Network Identity</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="understanding.html#reference-setup">Reference Setup</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="understanding.html#protocol-specifics">Protocol Specifics</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="understanding.html#packet-prioritisation">Packet Prioritisation</a></li>
|
||||
@@ -394,18 +498,113 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#pipe-interface">Pipe Interface</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#kiss-interface">KISS Interface</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#ax-25-kiss-interface">AX.25 KISS Interface</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#discoverable-interfaces">Discoverable Interfaces</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#enabling-discovery">Enabling Discovery</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#discovery-parameters">Discovery Parameters</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#interface-modes">Interface Modes</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#security-considerations">Security Considerations</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#example-configuration">Example Configuration</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#common-interface-options">Common Interface Options</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#interface-modes">Interface Modes</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#interfaces-modes">Interface Modes</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#announce-rate-control">Announce Rate Control</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#new-destination-rate-limiting">New Destination Rate Limiting</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#path-request-burst-control">Path Request Burst Control</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="networks.html#concepts-overview">Concepts & Overview</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="networks.html#example-scenarios">Example Scenarios</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="networks.html#interconnected-lora-sites">Interconnected LoRa Sites</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="networks.html#bridging-over-the-internet">Bridging Over the Internet</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="networks.html#growth-and-convergence">Growth and Convergence</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="networks.html#concepts-overview">Concepts & Overview</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="networks.html#introductory-considerations">Introductory Considerations</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="networks.html#destinations-not-addresses">Destinations, Not Addresses</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="networks.html#transport-nodes-and-instances">Transport Nodes and Instances</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="networks.html#trustless-networking">Trustless Networking</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="networks.html#heterogeneous-connectivity">Heterogeneous Connectivity</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="distributed.html">Distributed Development</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="distributed.html#the-original-architecture">The Original Architecture</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="distributed.html#the-platform-interregnum">The Platform Interregnum</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="distributed.html#restoration">Restoration</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="distributed.html#protocols-over-platforms">Protocols Over Platforms</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="distributed.html#sovereignty-through-infrastructure">Sovereignty Through Infrastructure</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="distributed.html#artifact-centered-workflows">Artifact-Centered Workflows</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="distributed.html#composable-primitives">Composable Primitives</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="distributed.html#distribution-without-intermediaries">Distribution Without Intermediaries</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="distributed.html#long-archive">Long Archive</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="distributed.html#start-of-the-road">Start Of The Road</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Git Over Reticulum</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#the-rngit-utility">The rngit Utility</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#repository-creation-management">Repository Creation & Management</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#creating-empty-repositories">Creating Empty Repositories</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#forking-repositories">Forking Repositories</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#mirroring-repositories">Mirroring Repositories</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#automatic-mirror-synchronization">Automatic Mirror Synchronization</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#manual-synchronization">Manual Synchronization</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#git-configuration-parameters">Git Configuration Parameters</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#repository-structure">Repository Structure</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#configuration">Configuration</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#permissions">Permissions</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#permission-types">Permission Types</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#permission-hierarchy">Permission Hierarchy</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#configuration-methods">Configuration Methods</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#work-document-permissions">Work Document Permissions</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#creator-permissions">Creator Permissions</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#permission-examples">Permission Examples</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#permission-short-forms">Permission Short Forms</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#permission-configuration-locations">Permission Configuration Locations</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#remote-permission-management">Remote Permission Management</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#managing-group-permissions">Managing Group Permissions</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#managing-repository-permissions">Managing Repository Permissions</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#permission-validation">Permission Validation</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#identity-destination-aliases">Identity & Destination Aliases</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#serving-pages-over-nomad-network">Serving Pages Over Nomad Network</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#enabling-the-git-page-node">Enabling the Git Page Node</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#accessing-repository-pages">Accessing Repository Pages</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#formatting-syntax-highlighting">Formatting & Syntax Highlighting</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#customizing-templates">Customizing Templates</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#repository-statistics">Repository Statistics</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#configuration-example">Configuration Example</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#verified-releases">Verified Releases</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#obtaining-verified-releases">Obtaining Verified Releases</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#offline-verification">Offline Verification</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#creating-signed-releases">Creating Signed Releases</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#release-management">Release Management</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#the-release-workflow">The Release Workflow</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#release-storage-structure">Release Storage & Structure</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#command-line-interaction">Command-Line Interaction</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#work-documents">Work Documents</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#working-with-work-documents">Working With Work Documents</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#proposing-work-documents">Proposing Work Documents</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#state-management">State Management</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#managing-work-document-permissions">Managing Work Document Permissions</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#cryptographic-attribution">Cryptographic Attribution</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#commit-signing">Commit Signing</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#prerequisites">Prerequisites</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#id1">Configuration</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#author-identity-binding">Author Identity Binding</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#signing-commits">Signing Commits</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="git.html#validating-commit-signatures">Validating Commit Signatures</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -413,7 +612,6 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="support.html#donations">Donations</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="support.html#provide-feedback">Provide Feedback</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="support.html#contribute-code">Contribute Code</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a><ul>
|
||||
@@ -430,6 +628,7 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l2"><a class="reference internal" href="examples.html#custom-interfaces">Custom Interfaces</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="toctree-wrapper compound">
|
||||
@@ -520,7 +719,7 @@ to participate in the development of Reticulum itself.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=71272d9f"></script>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user