Compare commits

...

264 Commits

Author SHA1 Message Date
Mark Qvist 95502e2c21 Prepare release 2026-05-14 01:56:30 +02:00
Mark Qvist 3dd4145e62 Updated changelog 2026-05-14 01:53:33 +02:00
Mark Qvist 1d7ddc3f8a Implemented rngit work document signing 2026-05-14 01:51:22 +02:00
Mark Qvist d731b4396c Repo page rendering 2026-05-14 00:32:22 +02:00
Mark Qvist c186a1f6b0 Updated version 2026-05-14 00:16:33 +02:00
Mark Qvist a049ec8b7b Updated changelog 2026-05-14 00:16:28 +02:00
Mark Qvist 4c93f6c7f4 Added local URL resolution to repo frontpage markdown readme renderer 2026-05-13 23:41:07 +02:00
Mark Qvist 35c7a89b19 Fixed typo 2026-05-13 22:58:50 +02:00
Mark Qvist c86b9c9703 Fixed missing none check in interface discovery sanitizer thanks to PAzter1101 2026-05-13 10:34:58 +02:00
Mark Qvist 64ebdd0ee3 Cleanup 2026-05-13 01:19:51 +02:00
Mark Qvist 9179b914d5 Added embedded message signing, validation and viewing to rnid 2026-05-13 01:14:41 +02:00
Mark Qvist eb5d46b20b Added file decryption for multiple file path inputs and shell expansions to rnid 2026-05-12 23:20:28 +02:00
Mark Qvist 54c36f515b Added file encryption for multiple file path inputs and shell expansions to rnid 2026-05-12 23:14:01 +02:00
Mark Qvist 5c5668a4fc Added signature creation for multiple file path inputs and shell expansions to rnid 2026-05-12 23:09:50 +02:00
Mark Qvist eeefb60c89 Added signature validation of multiple file path inputs and shell expansions to rnid 2026-05-12 23:00:06 +02:00
Mark Qvist 018df10a26 Fixed rngit remote helper hanging on startup if no client config had been created previously, and RNS loglevel was configured at debug or higher 2026-05-12 22:21:53 +02:00
Mark Qvist 93ead77435 Added workdoc downloads 2026-05-12 21:47:10 +02:00
Mark Qvist bd0e1ad0ca Better workdoc page handling 2026-05-12 21:05:15 +02:00
Mark Qvist d0ceeacb37 Allow setting title on workdoc edit 2026-05-12 15:04:02 +02:00
Mark Qvist 7d5fb6a13f Cleanup 2026-05-11 23:31:25 +02:00
Mark Qvist 855ef7bfd1 Base256 encoding 2026-05-11 23:22:13 +02:00
Mark Qvist 323890021a Better remote monitor loop 2026-05-11 00:20:02 +02:00
Mark Qvist e004e7592b Added lock to interface discovery 2026-05-10 00:29:48 +02:00
Mark Qvist 0ebec014e5 Improved release page 2026-05-10 00:26:55 +02:00
Mark Qvist 1b624cc0e2 Updated manual 2026-05-09 19:20:38 +02:00
Mark Qvist e8d161c0d5 Yes, that was indeed a bit overkill 2026-05-09 19:17:38 +02:00
Mark Qvist e5c7dd7ec7 Prepare release 2026-05-09 18:59:29 +02:00
Mark Qvist 7d6ed59e6e Added hex/b32/b64 output to rnid rsg signature generator 2026-05-09 18:34:28 +02:00
Mark Qvist 11e4e7953a Consistency 2026-05-09 15:11:33 +02:00
Mark Qvist a5b292ee81 Dreaming of a universe without escape characters 2026-05-09 14:58:43 +02:00
Mark Qvist d619bafb8d People use tabs, I guess 2026-05-09 13:51:55 +02:00
Mark Qvist 0119a589dc Improved transport jobs error handling 2026-05-09 13:32:32 +02:00
Mark Qvist b7346bed4d Fixed announce processing edge case where path was cleaned while waiting for announce rebroadcast 2026-05-09 13:29:31 +02:00
Mark Qvist fcea57cb8e Added burst filter to rnstatus 2026-05-09 13:28:49 +02:00
Mark Qvist 8d8af5e60a Improved git command timeout logging 2026-05-09 12:51:28 +02:00
Mark Qvist 1a732ac1c1 Adjusted logging 2026-05-09 12:35:39 +02:00
Mark Qvist f827d945be Implemented path request ingress burst control and egress limiting 2026-05-09 04:43:22 +02:00
Mark Qvist e03c4ee455 Added path request burst control to manual 2026-05-09 03:21:09 +02:00
Mark Qvist 35e7ccb773 Fixed invalid handling of corrupted discovery file 2026-05-09 02:52:01 +02:00
Mark Qvist a932a10492 Inherit egress and PR burst settings from parent interface 2026-05-09 02:27:31 +02:00
Mark Qvist c5108c3a19 Added path request frequency sorting to rnstatus 2026-05-09 01:45:04 +02:00
Mark Qvist 767782e425 Cleanup 2026-05-09 01:27:22 +02:00
Mark Qvist 60c440a3b6 Transport logic for path request ingress and egress control 2026-05-09 01:14:40 +02:00
Mark Qvist 6551a25877 Cleanup 2026-05-09 01:10:49 +02:00
Mark Qvist 70db2c5369 Updated log levels 2026-05-09 01:08:19 +02:00
Mark Qvist 8ed31d0dc8 Added path request frequency monitoring support to interfaces subsystem 2026-05-09 00:51:44 +02:00
Mark Qvist ef1ecb35e1 Fixed formatting 2026-05-09 00:50:19 +02:00
Mark Qvist 6768f10631 Improved discovery persist error handling 2026-05-09 00:26:42 +02:00
Mark Qvist fee6a53473 Added path request frequency display to rnstatus 2026-05-09 00:05:39 +02:00
Mark Qvist bbfa3b0aa0 Use validation functions canonically from util 2026-05-08 20:03:48 +02:00
Mark Qvist 325ae654ef Template rendering sequence 2026-05-08 18:24:30 +02:00
Mark Qvist 8655a4fb37 Cleaned up error messages 2026-05-08 18:18:28 +02:00
Mark Qvist b30d272ee6 Ensure non-corrupting stats writes 2026-05-08 17:37:32 +02:00
Mark Qvist cc90ac2853 Fixed workdoc limit 2026-05-08 17:27:56 +02:00
Mark Qvist 55473f39cb Improved rngit error logging 2026-05-08 17:25:46 +02:00
Mark Qvist 6d73881b07 Ensure error return consistency 2026-05-08 17:08:27 +02:00
Mark Qvist d107cd4b42 Cleanup 2026-05-08 17:00:49 +02:00
Mark Qvist 33247e21b2 Added AutoInterface per-peer announce rate display to rnstatus 2026-05-08 16:48:50 +02:00
Mark Qvist 6bdc769af3 Ensure SHA validation is canonical 2026-05-08 16:22:21 +02:00
Mark Qvist e923ccbf1b Improved ref name validation in rngit 2026-05-08 16:07:16 +02:00
Mark Qvist d402ee33a2 Formatting and cleanup 2026-05-08 12:00:39 +02:00
Mark Qvist d8d420745f Removed programs from docs using non-verified/LLM-generated implementations of Reticulum 2026-05-08 11:33:51 +02:00
Mark Qvist 524f2068cd Fixed regression in link close handling in rnstatus and rnpath remote management 2026-05-08 02:47:43 +02:00
Mark Qvist 5db089ff19 Updated version 2026-05-08 02:15:28 +02:00
Mark Qvist 08d6780c73 Tuned default IC params. Show burst status in rnstatus. 2026-05-08 01:13:49 +02:00
Mark Qvist ca3f0bba6d Cleanup 2026-05-08 00:28:02 +02:00
Mark Qvist 830327e4a2 IC default config 2026-05-08 00:26:01 +02:00
Mark Qvist f96409dfa9 IC config stuff 2026-05-08 00:11:18 +02:00
Mark Qvist 18e2da7d2b Updated manual 2026-05-07 21:08:24 +02:00
Mark Qvist dfd046afb6 Fixed f-string for old snakes 2026-05-07 20:59:44 +02:00
Mark Qvist 63d7f1e295 Fixed page formatting 2026-05-07 20:49:20 +02:00
Mark Qvist 9d076d6a19 Prepare release 2026-05-07 20:07:21 +02:00
Mark Qvist c6fa33a8aa Prepare release 2026-05-07 20:05:44 +02:00
Mark Qvist 37fa4392a5 Fixed signature validation display for offline rsg validation with hex-based required signer identity 2026-05-07 19:44:53 +02:00
Mark Qvist 90c88ade00 Fixed signature validation display for offline rsg validation with hex-based required signer identity 2026-05-07 19:32:55 +02:00
Mark Qvist bb08f63a9f Improved releases page rendering 2026-05-07 19:06:05 +02:00
Mark Qvist bdfad57d3f Added identity retain on use to rnid 2026-05-07 18:40:45 +02:00
Mark Qvist 7ceb2d2078 Added ability to retain identity data based on identity hash 2026-05-07 18:40:28 +02:00
Mark Qvist 304acdd0c1 Added ability to query network for raw identities to rnid 2026-05-07 18:15:31 +02:00
Mark Qvist 8b6609c588 Improved rnid options and control flow 2026-05-07 17:59:31 +02:00
Mark Qvist 8a1d3aedd4 Updated version 2026-05-07 17:23:07 +02:00
Mark Qvist d49f100edd Print help if no args 2026-05-07 17:23:01 +02:00
Mark Qvist 83d9ee1c5f Refactored rnid 2026-05-07 17:19:46 +02:00
Mark Qvist b527c59735 Cleanup 2026-05-07 15:50:07 +02:00
Mark Qvist 23498a7a0a Refactoring work for rnid 2026-05-07 15:46:17 +02:00
Mark Qvist ac2cf79451 Refactoring work for rnid 2026-05-07 15:31:14 +02:00
Mark Qvist 42b7426ed8 Refactoring work for rnid 2026-05-07 15:25:20 +02:00
Mark Qvist 928c02099b Refactoring work for rnid 2026-05-07 14:11:49 +02:00
Mark Qvist c0ae63e27a Fixed invalid processing order for inline markdown conversion 2026-05-07 02:18:11 +02:00
Mark Qvist 62532e1c54 Clean weird markdown output in API reference 2026-05-07 02:16:41 +02:00
Mark Qvist 3136b53277 Clean weird markdown output in API reference 2026-05-07 02:12:35 +02:00
Mark Qvist 9352cff870 Cleanup 2026-05-07 02:03:37 +02:00
Mark Qvist 9e5fd0f079 Cleanup 2026-05-07 02:01:42 +02:00
Mark Qvist 1d37ba4780 Updated readme 2026-05-07 01:36:53 +02:00
Mark Qvist 134c1fb6ac Updated readme 2026-05-07 01:35:43 +02:00
Mark Qvist 24df04f304 Cleanup 2026-05-07 01:31:18 +02:00
Mark Qvist 26595bb25a Added support for local URL scope mapping in markdown converter 2026-05-07 01:29:41 +02:00
Mark Qvist 5ee7dcf5a3 Cleanup 2026-05-07 00:44:11 +02:00
Mark Qvist 8b2ba9907f Added work document permissions control logic and CLI interaction to rngit. Added ability to create comments/updates on work documents from allowed identities. 2026-05-07 00:42:37 +02:00
Mark Qvist d1c59ef3b6 Prepare workdoc permissions management 2026-05-06 22:18:08 +02:00
Mark Qvist 2dd23b15a8 Added docs permissions resolver 2026-05-06 22:11:19 +02:00
Mark Qvist 93ad11f193 Consistency 2026-05-06 22:10:53 +02:00
Mark Qvist ec27d8bfde Added markdown manual build 2026-05-06 21:18:01 +02:00
Mark Qvist 4d6e164d62 Cleanup 2026-05-06 21:12:34 +02:00
Mark Qvist d82ffce504 Fixed markdown-to-micron formatting and syntax highlighting being weird in some cases 2026-05-06 21:11:49 +02:00
Mark Qvist 7ecd435911 Updated docs 2026-05-06 20:12:06 +02:00
Mark Qvist 49f56e7d0d Added markdown manual 2026-05-06 19:25:28 +02:00
Mark Qvist b8d6a14599 Display help if no operation 2026-05-06 19:12:26 +02:00
Mark Qvist 9c166936ad Added outbound announce frequency per client display to rnstatus 2026-05-06 19:06:16 +02:00
Mark Qvist 69db87cc24 Cleaned up f-strings for Android build compat 2026-05-06 18:59:17 +02:00
Mark Qvist 5d86232fbe Added detection of yggdrasil addresses to auto-connect handler 2026-05-06 04:57:20 +02:00
Mark Qvist 607e80bc82 Improved autoconnect logging 2026-05-06 04:46:23 +02:00
Mark Qvist f9625b2b88 Improved interface discovery data sanitization 2026-05-06 04:24:07 +02:00
Mark Qvist 3d8079c02b Added announce rate control defaults configuration options 2026-05-06 03:29:40 +02:00
Mark Qvist 5c05a7fa58 Updated docs 2026-05-06 03:25:21 +02:00
Mark Qvist 2fa959a560 Fixed time formatter 2026-05-06 01:49:19 +02:00
Mark Qvist c39494d9fa Improved logging performance 2026-05-06 01:03:43 +02:00
Mark Qvist a3cd1ea83d Improved shutdown handling 2026-05-05 23:42:00 +02:00
Mark Qvist d4ddf6bb13 Improved workdoc sorting 2026-05-05 21:19:16 +02:00
Mark Qvist 8661a3886b Prepare release 2026-05-05 20:01:08 +02:00
Mark Qvist 2ddbef70fe Improved markdown, micron and syntax highlight rendering consistency and accuracy 2026-05-05 19:54:39 +02:00
Mark Qvist bb051e5a11 Added markdown handling to markdown-to-micron converter 2026-05-05 19:09:31 +02:00
Mark Qvist 080085e813 Cleanup 2026-05-05 18:25:48 +02:00
Mark Qvist 85454b1f25 Updated version 2026-05-05 18:14:32 +02:00
Mark Qvist 3f5653f650 Added admin permission type in rngit 2026-05-05 18:12:42 +02:00
Mark Qvist b1357eb146 Updated documentation 2026-05-05 17:43:51 +02:00
Mark Qvist 7731e799f4 Implemented rngit work doc management 2026-05-05 17:40:57 +02:00
Mark Qvist 15320e4d2c Added interact permission to rngit 2026-05-05 12:41:09 +02:00
Mark Qvist 78596b687a Cleanup 2026-05-05 11:08:05 +02:00
Mark Qvist 729dc8dc11 Updated readme 2026-05-05 02:33:06 +02:00
Mark Qvist 3c08eb8122 Updated readme 2026-05-05 02:32:13 +02:00
Mark Qvist 9d12c86ac8 Updated readme 2026-05-05 02:29:52 +02:00
Mark Qvist 3bd573688c Updated readme 2026-05-05 02:29:09 +02:00
Mark Qvist 07ff87974e Prepare release 2026-05-05 01:19:43 +02:00
Mark Qvist e8fa92950d Fixed missing unquote 2026-05-05 01:18:07 +02:00
Mark Qvist ab6532742e Prepare release 2026-05-05 01:00:51 +02:00
Mark Qvist 4e583770e5 Updated docs 2026-05-05 00:57:26 +02:00
Mark Qvist f9b6dc2ab8 Added transfer progress to release artifact uploads for rngit 2026-05-04 23:55:03 +02:00
Mark Qvist 1c2bc0c7b8 Added file downloads to rngit 2026-05-04 22:49:56 +02:00
Mark Qvist 05760f914c Added latest release meta-tag support 2026-05-04 21:17:31 +02:00
Mark Qvist 3f6e8605af Cleanup 2026-05-04 20:58:49 +02:00
Mark Qvist b6bfd1655c Updated version 2026-05-04 20:53:31 +02:00
Mark Qvist 8cbd0e22ff Added artifact file serving to rngit 2026-05-04 20:48:20 +02:00
Mark Qvist 15ec64e974 Added rngit release management 2026-05-04 20:14:39 +02:00
Mark Qvist 3de16e085e Added releases to rngit page node 2026-05-04 20:13:35 +02:00
Mark Qvist 4cbd4ed60c Added basic release management scaffold 2026-05-04 15:28:28 +02:00
Mark Qvist b8fbd616e5 Added release permission to rngit 2026-05-04 14:10:23 +02:00
Mark Qvist f8a79d2f51 Catch tunnel synthesis errors and log 2026-05-04 12:56:31 +02:00
Mark Qvist 0218ff4e26 Cleanup 2026-05-04 02:08:31 +02:00
Mark Qvist 1f3ce7e78f Prepare release 2026-05-04 01:37:51 +02:00
Mark Qvist 9009e1d232 Handle empty data in rngit page server 2026-05-04 01:25:45 +02:00
Mark Qvist cc73b2c2b9 Fixed escape 2026-05-04 01:13:25 +02:00
Mark Qvist dbf19ed054 Fixed missing tag subs 2026-05-04 00:28:02 +02:00
Mark Qvist a1cff4e8ab Added raw table formatter 2026-05-04 00:18:06 +02:00
Mark Qvist c9822968c8 Updated docs for rngit 2026-05-03 21:05:06 +02:00
Mark Qvist 8acabd95b5 Updated stats page 2026-05-03 19:55:10 +02:00
Mark Qvist 49f6a6924d Added iconset configuration 2026-05-03 19:32:13 +02:00
Mark Qvist 8d73265cf4 Yeah, that'll probably work better 2026-05-03 19:22:19 +02:00
Mark Qvist fceb7d18d7 Added thanks function to rngit pages 2026-05-03 19:19:00 +02:00
Mark Qvist 337007cf70 Added ability to ignore identities for rngit stats collector 2026-05-03 18:49:27 +02:00
Mark Qvist 4733d6d75a Strip trailing whitespace from templates 2026-05-03 18:34:58 +02:00
Mark Qvist c8235544e8 Added stats recording configuration option. Improved default config file info. 2026-05-03 17:36:37 +02:00
Mark Qvist 3d1111ff02 Enabled templating system for all pages. Improved rendering consistency. 2026-05-03 17:12:36 +02:00
Mark Qvist 83c9f2b10a Made blobs renderable by adding rendering controls and rendering support for renderable file types using the built-in rendering of flow of the markdown renderer and micron's own rendering in micron-rendering clients. Reeeeeendeeeeer. 2026-05-03 16:05:45 +02:00
Mark Qvist 734eb53aa7 Updated docs 2026-05-03 01:53:26 +02:00
Mark Qvist 6d39cb8e7c Updated docs 2026-05-03 01:52:47 +02:00
Mark Qvist 3c3f38b239 Fixed missing linebreak 2026-05-03 01:47:46 +02:00
Mark Qvist 86d52d3884 Added stats page for repositories to rngit 2026-05-03 01:43:47 +02:00
Mark Qvist 6782672cb8 Added stats method to rngit node 2026-05-03 01:40:35 +02:00
Mark Qvist 7fada7e5ab Stats page link on repo page 2026-05-02 23:33:55 +02:00
Mark Qvist 4380026a4e Added basic scaffold for stats page to rngit 2026-05-02 23:12:29 +02:00
Mark Qvist 5143ea3d02 Added stats permission to rngit 2026-05-02 23:01:32 +02:00
Mark Qvist 4802bcd829 Added basic view/fetch/push stats to rngit 2026-05-02 22:50:20 +02:00
Mark Qvist 6038096b95 Updated readme 2026-05-02 20:00:40 +02:00
Mark Qvist 2acfc31350 Updated readme 2026-05-02 20:00:14 +02:00
Mark Qvist 2742e5253f Updated readme 2026-05-02 19:54:51 +02:00
Mark Qvist 46f2e994b9 Updated readme 2026-05-02 19:54:07 +02:00
Mark Qvist 2c97a20c12 Updated readme 2026-05-02 19:45:19 +02:00
Mark Qvist 9be10ebd47 Added micron readme 2026-05-02 19:43:51 +02:00
Mark Qvist 93cbfe7f7e Added support for readme files in micron format to rngit 2026-05-02 19:38:50 +02:00
Mark Qvist 4589de2115 Added RNS git URL to repo page 2026-05-02 19:26:57 +02:00
Mark Qvist 662054ae25 Cleanup 2026-05-02 19:21:23 +02:00
Mark Qvist 3cf186f3cb Handle link conversion in isolation 2026-05-02 19:18:10 +02:00
Mark Qvist 7a91c82e4b Changed substitution order for link conversion 2026-05-02 19:04:48 +02:00
Mark Qvist 72aace40d3 Fixed markdown-to-micron link rendering 2026-05-02 18:48:46 +02:00
Mark Qvist 0c9a65b5f1 Cleanup 2026-05-02 18:43:25 +02:00
Mark Qvist ea749499c3 Cleanup 2026-05-02 18:38:36 +02:00
Mark Qvist 828cbe7f20 Syntax highlighting for rngit 2026-05-02 18:27:23 +02:00
Mark Qvist 1d8d547872 Improved rngit page rendering 2026-05-02 15:16:50 +02:00
Mark Qvist 16c53221e3 Improved rngit page rendering 2026-05-02 14:51:51 +02:00
Mark Qvist 74936010c4 Improved rngit page rendering 2026-05-02 14:30:45 +02:00
Mark Qvist f3245e1d65 Improved rngit page rendering 2026-05-02 14:10:52 +02:00
Mark Qvist 1f74570ed9 Improved rngit page rendering 2026-05-02 13:50:12 +02:00
Mark Qvist 88d1b7d2d1 Improved rngit page rendering 2026-05-02 13:38:01 +02:00
Mark Qvist fb5dcf0631 Improved rngit page rendering 2026-05-02 13:12:07 +02:00
Mark Qvist a23086d3fc Improved rngit page rendering 2026-05-02 13:11:52 +02:00
Mark Qvist a4cbcbca97 Improved rngit page rendering 2026-05-02 11:45:56 +02:00
Mark Qvist 9dd008d42b Improved rngit page rendering 2026-05-02 02:00:16 +02:00
Mark Qvist 76fa07cb90 Updated version 2026-05-02 01:06:04 +02:00
Mark Qvist 35d72f27ed Added nomadnet page server to rngit 2026-05-02 01:02:19 +02:00
Mark Qvist 852891c779 Basic git page node scaffolding 2026-05-01 18:13:05 +02:00
Mark Qvist f4aa7dc389 Added rngit create permission 2026-05-01 17:33:12 +02:00
Mark Qvist d7c3859f61 Prepare release 2026-04-28 21:54:18 +02:00
Mark Qvist 85d77c10a1 Improved rngit pull efficiency 2026-04-28 21:47:59 +02:00
Mark Qvist 95222c7793 Prepare release 2026-04-28 19:25:42 +02:00
Mark Qvist 0a18b47e8c Cleanup 2026-04-28 19:22:10 +02:00
Mark Qvist 70f5126499 Added rngit client-side handling for direct ref updates 2026-04-28 19:09:45 +02:00
Mark Qvist b60eab0fcf Added rngit server-side handling for direct ref updates 2026-04-28 19:07:02 +02:00
Mark Qvist 17310fc294 Prepared rngit push protocol extension 2026-04-28 18:11:01 +02:00
Mark Qvist 9c892dc1a4 Prepared rngit push protocol extension 2026-04-28 18:05:24 +02:00
Mark Qvist c596dab806 Improved rngit ref exclusion logic 2026-04-28 17:58:28 +02:00
Mark Qvist fcb590e661 Updated changelog 2026-04-28 16:44:15 +02:00
Mark Qvist 328017cca0 Reset progress counters on multi-segment resources 2026-04-28 16:28:34 +02:00
Mark Qvist 63dba562ae Fixed missing cascade of progress callback set after resource creation 2026-04-28 16:27:58 +02:00
Mark Qvist cf20f26098 Prepare release 2026-04-28 15:55:51 +02:00
Mark Qvist e1e6063d17 Cleanup 2026-04-28 15:46:04 +02:00
Mark Qvist ccbbe6f2f8 Added base256 map 2026-04-28 14:38:32 +02:00
Mark Qvist 55c95bf59a Added --print-identity option to rngit 2026-04-27 11:44:57 +02:00
Mark Qvist 043a5dc4e7 Added rnsh to documentation 2026-04-27 00:42:15 +02:00
Mark Qvist 32a1cdf494 Credit Aaron Heise for original rnsh program 2026-04-27 00:12:27 +02:00
Mark Qvist f924086198 Refactored rnsh to use argparse 2026-04-27 00:06:33 +02:00
Mark Qvist 6abb31e469 Added rnsh to included utilities 2026-04-26 22:24:00 +02:00
Mark Qvist 3eee369704 Added rnsh entrypoint 2026-04-26 22:22:13 +02:00
Mark Qvist 695d4d8684 Improved link teardown on SIGINT/SIGTERM 2026-04-26 17:07:43 +02:00
Mark Qvist 015692d51e Tear down active and pending links before interface detach 2026-04-26 11:30:22 +02:00
Mark Qvist 86004a89e5 Cleanup 2026-04-26 11:11:20 +02:00
Mark Qvist 86031ef3f8 Added path request and link establishment status output to git operations 2026-04-26 10:59:17 +02:00
Mark Qvist 034239daf3 Cleanup 2026-04-26 01:19:29 +02:00
Mark Qvist a7b0f9924e Track local ref SHAs on pull for incremental bundle generation on remote 2026-04-26 01:18:31 +02:00
Mark Qvist a1d35b34b9 Cleanup 2026-04-26 00:52:57 +02:00
Mark Qvist 8d7e337dff Updated readme 2026-04-26 00:48:32 +02:00
Mark Qvist de7e0996ce Track remote refs on list-for-pull for push bundle exclusion 2026-04-26 00:47:16 +02:00
Mark Qvist 7377b69144 Updated readme 2026-04-26 00:43:08 +02:00
Mark Qvist c933cfdaa3 Cleanup 2026-04-25 23:22:39 +02:00
Mark Qvist 726185cee2 Cleanup 2026-04-25 23:16:59 +02:00
Mark Qvist de1000bfda Added outbound transfer progress to git helper 2026-04-25 19:31:11 +02:00
Mark Qvist 555e8c0376 Updated readme 2026-04-25 18:59:02 +02:00
Mark Qvist d836de3fe7 Updated readme 2026-04-25 18:58:27 +02:00
Mark Qvist 6ade1269ea Updated docs 2026-04-25 18:56:33 +02:00
Mark Qvist a8b519e06e Fixed typos. Fixed missing lock. 2026-04-25 18:45:21 +02:00
Mark Qvist 7d502306ea Cleanup 2026-04-25 18:02:40 +02:00
Mark Qvist e9fa57c660 Updated readme 2026-04-25 18:00:24 +02:00
Mark Qvist 7d4ab17f0d Updated version 2026-04-25 17:58:12 +02:00
Mark Qvist d532902320 Added Git over RNS shell entrypoints 2026-04-25 17:57:15 +02:00
Mark Qvist e592244443 Cleanup 2026-04-25 17:56:54 +02:00
Mark Qvist c1def5da19 Allow setting logfile destination before RNS init 2026-04-25 17:55:04 +02:00
Mark Qvist 6a7f081f12 Added Reticulum Git Node utility as part of included utility programs. Added git remote helper to interact with git repositories over Reticulum. 2026-04-25 17:53:33 +02:00
Mark Qvist 11555198eb Updated readme 2026-04-24 12:43:49 +02:00
Mark Qvist 6c77e27a50 Updated manual 2026-04-23 02:14:23 +02:00
Mark Qvist 17e8159fd8 Improved ratchet cleaning 2026-04-23 01:16:43 +02:00
Mark Qvist c71f5d8c5e Improved ratchet cleaning. Added inbound packet wait during transport core initialization. 2026-04-23 01:06:19 +02:00
Mark Qvist 31cc9fc7d1 Added LocalInterface client TX hold on client app sleep on Android 2026-04-23 01:04:32 +02:00
Mark Qvist 1d2421b0af Added AutoInterface filters for rmnet interfaces on Android 2026-04-23 01:04:01 +02:00
Mark Qvist a5df765951 Added LocalInterface client TX hold on client app sleep on Android 2026-04-23 01:03:20 +02:00
Mark Qvist 622019ee06 Updated manual 2026-04-22 14:40:16 +02:00
Mark Qvist 45e12cc668 Prepare release 2026-04-22 13:51:09 +02:00
Mark Qvist a21024a57e Prepare release 2026-04-22 13:48:02 +02:00
Mark Qvist c175491bb0 Updated version 2026-04-22 12:50:02 +02:00
Mark Qvist 09b0469faf Fixed bz2 decompression bomb vulnerability in Resource transfer assembly and Buffer StreamDataMessage unpacking. 2026-04-22 12:43:16 +02:00
Mark Qvist 3d63bbf4bf Fixed typo 2026-04-22 12:39:36 +02:00
Mark Qvist 56d5d01497 Updated changelog 2026-04-21 18:57:31 +02:00
Mark Qvist a70bd44426 Prepare release 2026-04-21 18:54:31 +02:00
Mark Qvist 8c082b2fcc Fixed path state potentially being applied before path table entry exists. 2026-04-21 18:49:03 +02:00
Mark Qvist 1732cac806 Updated makefile 2026-04-21 17:10:27 +02:00
108 changed files with 27731 additions and 1429 deletions
+253
View File
@@ -1,3 +1,256 @@
### 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**
+1 -1
View File
@@ -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
+14 -4
View File
@@ -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,7 +51,7 @@ 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
@@ -61,9 +62,18 @@ release: test remove_symlinks build_sdist build_wheel build_pure_wheel documenta
debug: remove_symlinks build_wheel build_pure_wheel create_symlinks
upload:
@echo Ready to publish release, hit enter to continue
upload: upload-rns upload-rnspure
upload-rns:
@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:
@echo Ready to publish rnspure release, hit enter to continue
@read VOID
@echo Uploading to PyPi...
twine upload dist/rnspure-*.whl
@echo Release published
+8 -5
View File
@@ -98,12 +98,12 @@ If you want to quickly get an idea of what Reticulum can do, take a look at the
[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
@@ -184,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
@@ -275,7 +277,7 @@ to find interface definitions for initial connectivity to the global distributed
***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:
```
@@ -378,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)
+267
View File
@@ -0,0 +1,267 @@
>> 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 3.
>> 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](https://reticulum.network/manual/software.html) section of the manual, or the following resources:
• [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 `B333rnphone`b 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.
• [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 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](https://markqvist.github.io/Reticulum/manual/hardware.html#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](https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
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](https://markqvist.github.io/Reticulum/manual/using.html#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](https://markqvist.github.io/Reticulum/manual/interfaces.html) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
>> 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](https://markqvist.github.io/Reticulum/manual/using.html#included-utility-programs) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
• 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
All tools, including `B333rnx`b and `B333rncp`b, work reliably and well even over very low-bandwidth links like LoRa or Packet Radio. For full-featured remote shells over Reticulum, also have a look at the [rnsh](https://github.com/acehoss/rnsh) program.
>> 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](https://markqvist.github.io/Reticulum/manual/interfaces.html#custom-interfaces).
Currently, the following built-in interfaces are supported:
• Any Ethernet device
• LoRa using [RNode](https://unsigned.io/rnode/)
• 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](https://github.com/pyca/cryptography)
• [pyserial](https://github.com/pyserial/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 [pyserial](https://github.com/pyserial/pyserial), 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](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
***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
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 **is not** Reticulum, and possibly an intentionally compromised or weakened clone. The utilised primitives are:
• Reticulum Identity Keys are 512-bit Curve25519 keysets
• A 256-bit Ed25519 key for signatures
• 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](RNS/Cryptography/HKDF.py)
• [HMAC.py](RNS/Cryptography/HMAC.py)
• [Token.py](RNS/Cryptography/Token.py)
• [PKCS7.py](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#file-curve25519-py) by [Nicko van Someren](https://gist.github.com/nickovs), *Public Domain*
• [I2Plib](https://github.com/l-n-s/i2plib) by [Viktor Villainov](https://github.com/l-n-s)
• [PySerial](https://github.com/pyserial/pyserial) by Chris Liechti, *BSD License*
• [Configobj](https://github.com/DiffSK/configobj) by Michael Foord, Nicola Larosa, Rob Dennis & Eli Courtwright, *BSD License*
• [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
View File
@@ -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):
-1
View File
@@ -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):
+4
View File
@@ -62,3 +62,7 @@ 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")
else: return hashlib.file_digest(file, "sha256").digest()
+94 -46
View File
@@ -6,6 +6,7 @@ import random
import threading
import ipaddress
import subprocess
from threading import Lock
from .vendor import umsgpack as msgpack
NAME = 0xFF
@@ -86,6 +87,7 @@ class InterfaceAnnouncer():
RNS.trace_exception(e)
def sanitize(self, in_str):
if in_str == None: return None
sanitized = in_str.replace("\n", "")
sanitized = sanitized.replace("\r", "")
sanitized = sanitized.strip()
@@ -200,6 +202,15 @@ class InterfaceAnnounceHandler:
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()
@@ -234,10 +245,24 @@ class InterfaceAnnounceHandler:
info = None
unpacked = msgpack.unpackb(packed)
if INTERFACE_TYPE in unpacked:
interface_type = unpacked[INTERFACE_TYPE]
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": unpacked[NAME] or f"Discovered {interface_type}",
"name": name or f"Discovered {interface_type}",
"received": time.time(),
"stamp": stamp,
"value": value,
@@ -248,12 +273,8 @@ class InterfaceAnnounceHandler:
"longitude": unpacked[LONGITUDE],
"height": unpacked[HEIGHT]}
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")
if IFAC_NETNAME in unpacked: info["ifac_netname"] = unpacked[IFAC_NETNAME]
if IFAC_NETKEY in unpacked: info["ifac_netkey"] = unpacked[IFAC_NETKEY]
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()
@@ -355,6 +376,8 @@ class InterfaceDiscovery():
AUTOCONNECT_TYPES = ["BackboneInterface", "TCPServerInterface"]
DISCOVERABLE_TYPES = ["BackboneInterface", "TCPServerInterface", "I2PInterface", "RNodeInterface", "WeaveInterface", "KISSInterface"]
discovery_lock = Lock()
def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None, discover_interfaces=True):
if not required_value: required_value = InterfaceAnnouncer.DEFAULT_STAMP_VALUE
@@ -382,10 +405,13 @@ class InterfaceDiscovery():
discovery_sources = RNS.Reticulum.interface_discovery_sources()
for filename in os.listdir(self.storagepath):
try:
filepath = os.path.join(self.storagepath, filename)
with open(filepath, "rb") as f: info = msgpack.unpackb(f.read())
with self.discovery_lock:
filepath = os.path.join(self.storagepath, filename)
with open(filepath, "rb") as f: info = msgpack.unpackb(f.read())
should_remove = False
heard_delta = now-info["last_heard"]
info["name"] = InterfaceAnnounceHandler.sanitize_name(info["name"])
if heard_delta > self.THRESHOLD_REMOVE: should_remove = True
elif discovery_sources and not "network_id" in info: should_remove = True
@@ -414,8 +440,8 @@ class InterfaceDiscovery():
if should_append: discovered_interfaces.append(info)
except Exception as e:
RNS.log(f"Error while loading discovered interface data: {e}", RNS.LOG_ERROR)
RNS.log(f"The interface data file {os.path.join(self.storagepath, filename)} may be corrupt", RNS.LOG_ERROR)
RNS.log(f"Error while loading discovered interface data: {e}", RNS.LOG_WARNING)
RNS.log(f"The interface data file {os.path.join(self.storagepath, filename)} may be corrupt", RNS.LOG_WARNING)
RNS.trace_exception(e)
discovered_interfaces.sort(key=lambda info: (info["status_code"], info["value"], info["last_heard"]), reverse=True)
@@ -433,41 +459,45 @@ class InterfaceDiscovery():
filename = RNS.hexrep(discovery_hash, delimit=False)
filepath = os.path.join(self.storagepath, filename)
RNS.log(f"Discovered {interface_type} {hops} hop{ms} away with stamp value {value}: {name}", RNS.LOG_DEBUG)
if not os.path.isfile(filepath):
try:
with open(filepath, "wb") as f:
info["discovered"] = info["received"]
info["last_heard"] = info["received"]
info["heard_count"] = 0
f.write(msgpack.packb(info))
except Exception as e:
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
return
with self.discovery_lock:
if not os.path.isfile(filepath):
try:
with open(filepath, "wb") as f:
info["discovered"] = info["received"]
info["last_heard"] = info["received"]
info["heard_count"] = 0
f.write(msgpack.packb(info))
except Exception as e:
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
return
else:
discovered = None
heard_count = None
try:
with open(filepath, "rb") as f:
last_info = msgpack.unpackb(f.read())
discovered = last_info["discovered"]
heard_count = last_info["heard_count"]
else:
discovered = None
heard_count = None
try:
try:
with open(filepath, "rb") as f:
last_info = msgpack.unpackb(f.read())
discovered = last_info["discovered"]
heard_count = last_info["heard_count"]
if discovered == None: discovered = info["discovered"]
if heard_count == None: heard_count = 0
except Exception as e: RNS.log(f"Error while reading existing data for discovered interface, re-creating data", RNS.LOG_ERROR)
with open(filepath, "wb") as f:
info["discovered"] = discovered
info["last_heard"] = info["received"]
info["heard_count"] = heard_count+1
f.write(msgpack.packb(info))
if discovered == None: discovered = info["received"]
if heard_count == None: heard_count = 0
except Exception as e:
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
return
with open(filepath, "wb") as f:
info["discovered"] = discovered
info["last_heard"] = info["received"]
info["heard_count"] = heard_count+1
f.write(msgpack.packb(info))
except Exception as e:
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
return
except Exception as e:
RNS.log(f"Error processing discovered interface data: {e}", RNS.LOG_ERROR)
@@ -489,7 +519,7 @@ class InterfaceDiscovery():
threading.Thread(target=self.__monitor_job, daemon=True).start()
def __monitor_job(self):
while self.monitoring_autoconnects:
while self.monitoring_autoconnects and RNS.Transport._should_run:
time.sleep(self.monitor_interval)
detached_interfaces = []
online_interfaces = 0
@@ -616,8 +646,11 @@ class InterfaceDiscovery():
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"]
RNS.log(f"Auto-connecting discovered {interface_type} {interface_name}")
config_entry = info["config_entry"]
interface_config = {}
interface_config["name"] = f"{interface_name}"
@@ -632,9 +665,15 @@ class InterfaceDiscovery():
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"]
RNS.Reticulum.get_instance()._add_interface(interface, ifac_netname=ifac_netname, ifac_netkey=ifac_netkey, configured_bitrate=5E6)
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:
@@ -733,6 +772,10 @@ def is_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
@@ -740,3 +783,8 @@ def is_hostname(hostname):
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")
+58 -20
View File
@@ -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
@@ -279,6 +280,18 @@ class Identity:
return True
return False
@staticmethod
def _retain_identity(identity_hash):
try:
retained = False
for destination_hash in Identity.known_destinations:
if identity_hash == Identity.truncated_hash(Identity.known_destinations[destination_hash][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():
@@ -321,7 +334,7 @@ class Identity:
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)
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:
@@ -400,7 +413,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():
@@ -429,35 +442,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):
@@ -482,7 +502,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
@@ -530,7 +550,7 @@ class Identity:
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)
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):
@@ -568,9 +588,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)
@@ -578,11 +598,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
@@ -649,6 +669,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
@@ -688,13 +724,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):
"""
@@ -841,7 +879,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
@@ -854,14 +892,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")
+6 -1
View File
@@ -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
@@ -539,6 +539,11 @@ class AutoInterface(Interface):
spawned_interface.ic_new_time = self.ic_new_time
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
spawned_interface.egress_control = self.egress_control
spawned_interface.ec_pr_freq = self.ec_pr_freq
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
spawned_interface.parent_interface = self
spawned_interface.bitrate = self.bitrate
+82 -13
View File
@@ -156,6 +156,58 @@ class BackboneInterface(Interface):
else:
raise SystemError("Insufficient parameters to create listener")
__last_ic_burst_check = 0
__last_ic_burst_state = False
@property
def ic_burst_active(self):
if time.time() > self.__last_ic_burst_check + 2:
self.__last_ic_burst_state = any(i.ic_burst_active for i in self.spawned_interfaces)
return self.__last_ic_burst_state
@ic_burst_active.setter
def ic_burst_active(self, value): pass
__ic_burst_activated_check = 0
__ic_burst_activated = 0
@property
def ic_burst_activated(self):
if time.time() > self.__ic_burst_activated_check + 2:
activated = [i.ic_burst_activated for i in self.spawned_interfaces if i.ic_burst_active]
if activated: self.__ic_burst_activated = min(activated)
return self.__ic_burst_activated
@ic_burst_activated.setter
def ic_burst_activated(self, value): pass
__last_ic_pr_burst_check = 0
__last_ic_pr_burst_state = False
@property
def ic_pr_burst_active(self):
if time.time() > self.__last_ic_pr_burst_check + 2:
self.__last_ic_pr_burst_state = any(i.ic_pr_burst_active for i in self.spawned_interfaces)
return self.__last_ic_pr_burst_state
@ic_pr_burst_active.setter
def ic_pr_burst_active(self, value): pass
__ic_pr_burst_activated_check = 0
__ic_pr_burst_activated = 0
@property
def ic_pr_burst_activated(self):
if time.time() > self.__ic_pr_burst_activated_check + 2:
activated = [i.ic_pr_burst_activated for i in self.spawned_interfaces if i.ic_pr_burst_active]
if activated: self.__ic_pr_burst_activated = min(activated)
return self.__ic_pr_burst_activated
@ic_pr_burst_activated.setter
def ic_pr_burst_activated(self, value): pass
@staticmethod
def start():
if not BackboneInterface._job_active: threading.Thread(target=BackboneInterface.__job, daemon=True).start()
@@ -196,17 +248,17 @@ class BackboneInterface(Interface):
@staticmethod
def register_in(fileno):
if fileno < 0:
RNS.log(f"Attempt to register invalid file descriptor {fileno}", RNS.LOG_ERROR)
RNS.log(f"Attempt to register invalid file descriptor {fileno}", RNS.LOG_WARNING)
return
try: BackboneInterface.epoll.register(fileno, select.EPOLLIN)
except Exception as e:
RNS.log(f"An error occurred while registering EPOLL_IN for file descriptor {fileno}: {e}", RNS.LOG_ERROR)
RNS.log(f"An error occurred while registering EPOLL_IN for file descriptor {fileno}: {e}", RNS.LOG_WARNING)
@staticmethod
def deregister_fileno(fileno):
if fileno < 0:
RNS.log(f"Attempt to deregister invalid file descriptor {fileno}", RNS.LOG_ERROR)
RNS.log(f"Attempt to deregister invalid file descriptor {fileno}", RNS.LOG_WARNING)
return
try: BackboneInterface.epoll.unregister(fileno)
@@ -288,7 +340,7 @@ class BackboneInterface(Interface):
except Exception as e: RNS.log(f"Error while removing spawned interface from {pif}: {e}", RNS.LOG_ERROR)
try: client_socket.close()
except Exception as e: RNS.log(f"Error while closing socket for {spawned_interface}: {e}", RNS.LOG_ERROR)
except Exception as e: RNS.log(f"Error while closing socket for {spawned_interface}: {e}", RNS.LOG_WARNING)
spawned_interface.receive(b"")
spawned_interface.transmit_buffer = spawned_interface.transmit_buffer[written:]
@@ -320,18 +372,24 @@ class BackboneInterface(Interface):
elif fileno in BackboneInterface.listener_filenos:
owner_interface, server_socket = BackboneInterface.listener_filenos[fileno]
if fileno == server_socket.fileno() and (event & select.EPOLLIN):
client_socket, address = server_socket.accept()
client_socket.setblocking(0)
if not owner_interface.incoming_connection(client_socket):
try:
client_socket, address = server_socket.accept()
client_socket.setblocking(0)
if not owner_interface.incoming_connection(client_socket):
try: client_socket.close()
except Exception as e: RNS.log(f"Error while closing socket for failed incoming connection: {e}", RNS.LOG_WARNING)
except:
RNS.log(f"Accepting socket failed for incoming connection: {e}", RNS.LOG_WARNING)
try: client_socket.close()
except Exception as e: RNS.log(f"Error while closing socket for failed incoming connection: {e}", RNS.LOG_ERROR)
except Exception as e: RNS.log(f"Error while closing socket for failed incoming socket accept: {e}", RNS.LOG_WARNING)
elif fileno == server_socket.fileno() and (event & select.EPOLLHUP):
try: BackboneInterface.deregister_fileno(fileno)
except Exception as e: RNS.log(f"Error while deregistering listener file descriptor {fileno}: {e}", RNS.LOG_ERROR)
try: server_socket.close()
except Exception as e: RNS.log(f"Error while closing listener socket for {server_socket}: {e}", RNS.LOG_ERROR)
except Exception as e: RNS.log(f"Error while closing listener socket for {server_socket}: {e}", RNS.LOG_WARNING)
except Exception as e:
RNS.log(f"BackboneInterface error: {e}", RNS.LOG_ERROR)
@@ -356,6 +414,11 @@ class BackboneInterface(Interface):
spawned_interface.ic_new_time = self.ic_new_time
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
spawned_interface.egress_control = self.egress_control
spawned_interface.ec_pr_freq = self.ec_pr_freq
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
spawned_interface.socket = socket
spawned_interface.target_ip = socket.getpeername()[0]
@@ -408,6 +471,12 @@ class BackboneInterface(Interface):
def sent_announce(self, from_spawned=False):
if from_spawned: self.oa_freq_deque.append(time.time())
def received_path_request(self, from_spawned=False):
if from_spawned: self.ip_freq_deque.append(time.time())
def sent_path_request(self, from_spawned=False):
if from_spawned: self.op_freq_deque.append(time.time())
def process_outgoing(self, data):
pass
@@ -578,8 +647,8 @@ class BackboneClientInterface(Interface):
except Exception as e:
if initial:
RNS.log("Initial connection for "+str(self)+" could not be established: "+str(e), RNS.LOG_ERROR)
RNS.log("Leaving unconnected and retrying connection in "+str(BackboneClientInterface.RECONNECT_WAIT)+" seconds.", RNS.LOG_ERROR)
RNS.log("Initial connection for "+str(self)+" could not be established: "+str(e), RNS.LOG_WARNING)
RNS.log("Leaving unconnected and retrying connection in "+str(BackboneClientInterface.RECONNECT_WAIT)+" seconds.", RNS.LOG_WARNING)
return False
else:
@@ -602,7 +671,7 @@ class BackboneClientInterface(Interface):
attempts += 1
if self.max_reconnect_tries != None and attempts > self.max_reconnect_tries:
RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_ERROR)
RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_WARNING)
self.teardown()
break
@@ -669,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:
+11
View File
@@ -957,6 +957,11 @@ class I2PInterface(Interface):
spawned_interface.ic_new_time = self.ic_new_time
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
spawned_interface.egress_control = self.egress_control
spawned_interface.ec_pr_freq = self.ec_pr_freq
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
spawned_interface.parent_interface = self
spawned_interface.online = True
@@ -1003,6 +1008,12 @@ class I2PInterface(Interface):
def sent_announce(self, from_spawned=False):
if from_spawned: self.oa_freq_deque.append(time.time())
def received_path_request(self, from_spawned=False):
if from_spawned: self.ip_freq_deque.append(time.time())
def sent_path_request(self, from_spawned=False):
if from_spawned: self.op_freq_deque.append(time.time())
def detach(self):
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
self.i2p.stop()
+120 -28
View File
@@ -55,8 +55,15 @@ class Interface:
# How many samples to use for announce
# frequency calculations
IA_FREQ_SAMPLES = 128
OA_FREQ_SAMPLES = 128
IA_FREQ_SAMPLES = 48
OA_FREQ_SAMPLES = 48
IP_FREQ_SAMPLES = 48
OP_FREQ_SAMPLES = 48
AR_MINFREQ_HZ = 0.1
PR_MINFREQ_HZ = 0.1
AR_FREQ_DECAY = 1/AR_MINFREQ_HZ
PR_FREQ_DECAY = 1/PR_MINFREQ_HZ
# Maximum amount of ingress limited announces
# to hold at any given time.
@@ -66,12 +73,22 @@ class Interface:
# considered to be newly created. Two
# hours by default.
IC_NEW_TIME = 2*60*60
IC_BURST_FREQ_NEW = 6
IC_BURST_FREQ = 35
IC_BURST_HOLD = 1*60
IC_BURST_FREQ_NEW = 3
IC_BURST_FREQ = 10
IC_PR_BURST_FREQ_NEW = 3
IC_PR_BURST_FREQ = 8
IC_BURST_HOLD = 15
IC_BURST_PENALTY = 15
IC_HELD_RELEASE_INTERVAL = 2
IC_DEQUE_MIN_SAMPLE = 32
IC_HELD_RELEASE_INTERVAL = 5
IC_DEQUE_MIN_SAMPLE = 2
IC_BURST_MIN_SAMPLES = 6
EC_PR_FREQ = 5
EGRESS_CONTROL = False
# Default announce rate targets
DEFAULT_AR_TARGET = 3600
DEFAULT_AR_PENALTY = 0
DEFAULT_AR_GRACE = 5
AUTOCONFIGURE_MTU = False
FIXED_MTU = False
@@ -85,28 +102,38 @@ class Interface:
self.bitrate = 62500
self.HW_MTU = None
self.supports_discovery = False
self.discoverable = False
self.last_discovery_announce = 0
self.bootstrap_only = False
self.parent_interface = None
self.spawned_interfaces = None
self.tunnel_id = None
self.ingress_control = True
self.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"))
@@ -122,7 +149,7 @@ class Interface:
if self.ic_burst_active:
if ia_freq < freq_threshold and time.time() > self.ic_burst_activated+self.ic_burst_hold:
self.ic_burst_active = False
if len(self.ia_freq_deque) >= self.IC_BURST_MIN_SAMPLES: self.ic_burst_active = False
return True
@@ -137,6 +164,37 @@ class Interface:
else: return False
def should_ingress_limit_pr(self):
if self.ingress_control:
freq_threshold = self.ic_pr_burst_freq_new if self.age() < self.ic_new_time else self.ic_pr_burst_freq
ip_freq = self.incoming_pr_frequency()
if self.ic_pr_burst_active:
if ip_freq < freq_threshold and time.time() > self.ic_pr_burst_activated+self.ic_burst_hold:
self.ic_pr_burst_active = False
return True
else:
if ip_freq > freq_threshold:
self.ic_pr_burst_active = True
self.ic_pr_burst_activated = time.time()
return True
else: return False
else: return False
def should_egress_limit_pr(self):
if self.egress_control:
freq_threshold = self.ec_pr_freq
op_freq = self.outgoing_pr_frequency()
if op_freq > freq_threshold:
if len(self.op_freq_deque) >= self.IC_BURST_MIN_SAMPLES: return True
return False
def optimise_mtu(self):
if self.AUTOCONFIGURE_MTU:
if self.bitrate >= 1_000_000_000:
@@ -162,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
@@ -208,12 +266,23 @@ class Interface:
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.sent_announce(from_spawned=True)
def received_path_request(self, from_spawned=False):
self.ip_freq_deque.append(time.time())
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.received_path_request(from_spawned=True)
def sent_path_request(self, from_spawned=False):
self.op_freq_deque.append(time.time())
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.sent_path_request(from_spawned=True)
def incoming_announce_frequency(self):
n = len(self.ia_freq_deque)
if not n > self.IC_DEQUE_MIN_SAMPLE: return 0
else:
oldest = self.ia_freq_deque[0]
span = time.time() - oldest
if span > self.AR_FREQ_DECAY: self.ia_freq_deque.popleft()
if span <= 0: return 0
hz = n / span
return hz
@@ -224,6 +293,29 @@ class Interface:
else:
oldest = self.oa_freq_deque[0]
span = time.time() - oldest
if span > self.AR_FREQ_DECAY: self.oa_freq_deque.popleft()
if span <= 0: return 0
hz = n / span
return hz
def incoming_pr_frequency(self):
n = len(self.ip_freq_deque)
if not n > self.IC_DEQUE_MIN_SAMPLE: return 0
else:
oldest = self.ip_freq_deque[0]
span = time.time() - oldest
if span > self.PR_FREQ_DECAY: self.ip_freq_deque.popleft()
if span <= 0: return 0
hz = n / span
return hz
def outgoing_pr_frequency(self):
n = len(self.op_freq_deque)
if not len(self.op_freq_deque) > 1: return 0
else:
oldest = self.op_freq_deque[0]
span = time.time() - oldest
if span > self.PR_FREQ_DECAY: self.op_freq_deque.popleft()
if span <= 0: return 0
hz = n / span
return hz
+44 -11
View File
@@ -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""
@@ -461,6 +488,12 @@ class LocalServerInterface(Interface):
def sent_announce(self, from_spawned=False):
if from_spawned: self.oa_freq_deque.append(time.time())
def received_path_request(self, from_spawned=False):
if from_spawned: self.ip_freq_deque.append(time.time())
def sent_path_request(self, from_spawned=False):
if from_spawned: self.op_freq_deque.append(time.time())
def __str__(self):
if self.socket_path: return "Shared Instance["+str(self.socket_path.replace("\0", ""))+"]"
else: return "Shared Instance["+str(self.bind_port)+"]"
+6
View File
@@ -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
+12 -1
View File
@@ -403,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
@@ -589,6 +589,11 @@ class TCPServerInterface(Interface):
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
spawned_interface.egress_control = self.egress_control
spawned_interface.ec_pr_freq = self.ec_pr_freq
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
spawned_interface.target_ip = handler.client_address[0]
spawned_interface.target_port = str(handler.client_address[1])
spawned_interface.parent_interface = self
@@ -634,6 +639,12 @@ class TCPServerInterface(Interface):
def sent_announce(self, from_spawned=False):
if from_spawned: self.oa_freq_deque.append(time.time())
def received_path_request(self, from_spawned=False):
if from_spawned: self.ip_freq_deque.append(time.time())
def sent_path_request(self, from_spawned=False):
if from_spawned: self.op_freq_deque.append(time.time())
def process_outgoing(self, data):
pass
+2 -2
View File
@@ -1319,11 +1319,11 @@ class Link:
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)
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)
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
+5 -4
View File
@@ -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
@@ -293,7 +294,7 @@ class Packet:
if RNS.Transport.outbound(self): return self.receipt
else:
RNS.log("No interfaces could process the outbound packet", RNS.LOG_DEBUG)
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
+61 -46
View File
@@ -194,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"
@@ -222,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)
@@ -234,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
@@ -360,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
@@ -386,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
@@ -397,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]
@@ -413,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
@@ -433,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)
@@ -453,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:
@@ -469,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:
@@ -530,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()
@@ -572,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()
@@ -585,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()
@@ -610,7 +612,7 @@ class Resource:
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:
@@ -630,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
@@ -642,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)
@@ -659,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()
@@ -679,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:
@@ -737,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):
@@ -749,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:
@@ -965,8 +974,8 @@ class Resource:
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
@@ -1011,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:
@@ -1050,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):
@@ -1059,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)
@@ -1069,7 +1077,13 @@ class Resource:
Cancels transferring the resource.
"""
if self.next_segment: self.next_segment.cancel()
if self.status < Resource.COMPLETE:
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:
@@ -1107,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):
"""
+178 -26
View File
@@ -103,12 +103,8 @@ class Reticulum:
LINK_MTU_DISCOVERY = True
"""
Whether automatic link MTU discovery is enabled by default in this
release. Link MTU discovery significantly increases throughput over
fast links, but requires all intermediary hops to also support it.
Support for this feature was added in RNS version 0.9.0. This option
will become enabled by default in the near future. Please update your
RNS instances.
Whether automatic link MTU discovery is enabled by default. Link MTU
discovery significantly increases throughput over fast links.
"""
MAX_QUEUED_ANNOUNCES = 16384
@@ -186,15 +182,14 @@ class Reticulum:
# out cleanup operations.
if not Reticulum.__exit_handler_ran:
Reticulum.__exit_handler_ran = True
if not Reticulum.__interface_detach_ran:
RNS.Transport.detach_interfaces()
if not Reticulum.__interface_detach_ran: RNS.Transport.detach_interfaces()
RNS.Transport.exit_handler()
RNS.Identity.exit_handler()
if RNS.Profiler.ran():
RNS.Profiler.results()
if RNS.Profiler.ran(): RNS.Profiler.results()
RNS.loglevel = -1
RNS.loglevel = RNS.LOG_NONE
RNS._detach_stdout()
@staticmethod
def sigint_handler(signal, frame):
@@ -241,7 +236,7 @@ class Reticulum:
if logdest == RNS.LOG_FILE:
RNS.logdest = RNS.LOG_FILE
RNS.logfile = Reticulum.configdir+"/logfile"
RNS.logfile = RNS.logfile or Reticulum.configdir+"/logfile"
elif callable(logdest):
RNS.logdest = RNS.LOG_CALLBACK
RNS.logcall = logdest
@@ -254,19 +249,33 @@ class Reticulum:
Reticulum.blackholepath = Reticulum.configdir+"/storage/blackhole"
Reticulum.interfacepath = Reticulum.configdir+"/interfaces"
Reticulum.__network_identity = None
Reticulum.__transport_enabled = False
Reticulum.__link_mtu_discovery = Reticulum.LINK_MTU_DISCOVERY
Reticulum.__remote_management_enabled = False
Reticulum.__use_implicit_proof = True
Reticulum.__allow_probes = False
Reticulum.__discovery_enabled = False
Reticulum.__discover_interfaces = False
Reticulum.__network_identity = None
Reticulum.__transport_enabled = False
Reticulum.__link_mtu_discovery = Reticulum.LINK_MTU_DISCOVERY
Reticulum.__remote_management_enabled = False
Reticulum.__use_implicit_proof = True
Reticulum.__allow_probes = False
Reticulum.__discovery_enabled = False
Reticulum.__discover_interfaces = False
Reticulum.__autoconnect_discovered_interfaces = False
Reticulum.__required_discovery_value = None
Reticulum.__publish_blackhole = False
Reticulum.__blackhole_sources = []
Reticulum.__interface_sources = []
Reticulum.__required_discovery_value = None
Reticulum.__publish_blackhole = False
Reticulum.__blackhole_sources = []
Reticulum.__interface_sources = []
Reticulum.__default_ar_target = None
Reticulum.__default_ar_penalty = None
Reticulum.__default_ar_grace = None
Reticulum.__ic_max_held_announces = None
Reticulum.__ic_burst_hold = None
Reticulum.__ic_burst_freq_new = None
Reticulum.__ic_burst_freq = None
Reticulum.__ic_pr_burst_freq_new = None
Reticulum.__ic_pr_burst_freq = None
Reticulum.__ic_new_time = None
Reticulum.__ic_burst_penalty = None
Reticulum.__ic_held_release_interval = None
Reticulum.__ec_pr_freq = None
Reticulum.__egress_control = None
Reticulum.panic_on_interface_error = False
@@ -325,6 +334,7 @@ class Reticulum:
RNS.log(f"Configuration loaded from {self.configpath}", RNS.LOG_VERBOSE)
RNS.Identity.load_known_destinations()
if not self.is_connected_to_shared_instance: RNS.Identity._clean_ratchets()
RNS.Transport.start(self)
if self.use_af_unix:
@@ -354,7 +364,6 @@ class Reticulum:
def __start_jobs(self):
if self.jobs_thread == None:
RNS.Identity._clean_ratchets()
self.jobs_thread = threading.Thread(target=self.__jobs)
self.jobs_thread.daemon = True
self.jobs_thread.start()
@@ -584,6 +593,64 @@ class Reticulum:
if option == "autoconnect_discovered_interfaces":
v = self.config["reticulum"].as_int(option)
if v > 0: Reticulum.__autoconnect_discovered_interfaces = v
if option == "default_ar_target":
v = self.config["reticulum"].as_int(option)
if v == 0: Reticulum.__default_ar_target = None
elif v > 0: Reticulum.__default_ar_target = v
if option == "default_ar_penalty":
v = self.config["reticulum"].as_int(option)
if v >= 0: Reticulum.__default_ar_penalty = v
if option == "default_ar_grace":
v = self.config["reticulum"].as_int(option)
if v >= 0: Reticulum.__default_ar_grace = v
if option == "ic_max_held_announces":
v = self.config["reticulum"].as_int(option)
if v >= 0: Reticulum.__ic_max_held_announces = v
if option == "ic_burst_hold":
v = self.config["reticulum"].as_float(option)
if v >= 0: Reticulum.__ic_burst_hold = v
if option == "ic_burst_freq_new":
v = self.config["reticulum"].as_float(option)
if v >= 0: Reticulum.__ic_burst_freq_new = v
if option == "ic_burst_freq":
v = self.config["reticulum"].as_float(option)
if v >= 0: Reticulum.__ic_burst_freq = v
if option == "ic_pr_burst_freq_new":
v = self.config["reticulum"].as_float(option)
if v >= 0: Reticulum.__ic_pr_burst_freq_new = v
if option == "ic_pr_burst_freq":
v = self.config["reticulum"].as_float(option)
if v >= 0: Reticulum.__ic_pr_burst_freq = v
if option == "ec_pr_freq":
v = self.config["reticulum"].as_float(option)
if v >= 0: Reticulum.__ec_pr_freq = v
if option == "egress_control":
v = self.config["reticulum"].as_bool(option)
if v >= 0: Reticulum.__egress_control = v
if option == "ic_new_time":
v = self.config["reticulum"].as_float(option)
if v >= 0: Reticulum.__ic_new_time = v
if option == "ic_burst_penalty":
v = self.config["reticulum"].as_float(option)
if v >= 0: Reticulum.__ic_burst_penalty = v
if option == "ic_held_release_interval":
v = self.config["reticulum"].as_float(option)
if v >= 0: Reticulum.__ic_held_release_interval = v
if RNS.compiled: RNS.log("Reticulum running in compiled mode", RNS.LOG_DEBUG)
else: RNS.log("Reticulum running in interpreted mode", RNS.LOG_DEBUG)
@@ -672,6 +739,8 @@ class Reticulum:
ingress_control = True
if "ingress_control" in c: ingress_control = c.as_bool("ingress_control")
egress_control = None
if "egress_control" in c: egress_control = c.as_bool("egress_control")
ic_max_held_announces = None
if "ic_max_held_announces" in c: ic_max_held_announces = c.as_int("ic_max_held_announces")
ic_burst_hold = None
@@ -680,6 +749,12 @@ class Reticulum:
if "ic_burst_freq_new" in c: ic_burst_freq_new = c.as_float("ic_burst_freq_new")
ic_burst_freq = None
if "ic_burst_freq" in c: ic_burst_freq = c.as_float("ic_burst_freq")
ic_pr_burst_freq_new = None
if "ic_pr_burst_freq_new" in c: ic_pr_burst_freq_new = c.as_float("ic_pr_burst_freq_new")
ic_pr_burst_freq = None
if "ic_pr_burst_freq" in c: ic_pr_burst_freq = c.as_float("ic_pr_burst_freq")
ec_pr_freq = None
if "ec_pr_freq" in c: ec_pr_freq = c.as_float("ec_pr_freq")
ic_new_time = None
if "ic_new_time" in c: ic_new_time = c.as_float("ic_new_time")
ic_burst_penalty = None
@@ -724,6 +799,11 @@ class Reticulum:
ignore_config_warnings = False
if "ignore_config_warnings" in c: ignore_config_warnings = c.as_bool("ignore_config_warnings")
if Reticulum.transport_enabled():
if announce_rate_target == None: announce_rate_target = self._default_ar_target()
if announce_rate_penalty == None: announce_rate_penalty = self._default_ar_penalty()
if announce_rate_grace == None: announce_rate_grace = self._default_ar_grace()
discoverable = False
discovery_announce_interval = None
discovery_stamp_value = None
@@ -800,10 +880,14 @@ class Reticulum:
interface.announce_rate_grace = announce_rate_grace
interface.announce_rate_penalty = announce_rate_penalty
interface.ingress_control = ingress_control
if egress_control != None: interface.egress_control = egress_control
if ic_max_held_announces != None: interface.ic_max_held_announces = ic_max_held_announces
if ic_burst_hold != None: interface.ic_burst_hold = ic_burst_hold
if ic_burst_freq_new != None: interface.ic_burst_freq_new = ic_burst_freq_new
if ic_burst_freq != None: interface.ic_burst_freq = ic_burst_freq
if ic_pr_burst_freq_new != None: interface.ic_pr_burst_freq_new = ic_pr_burst_freq_new
if ic_pr_burst_freq != None: interface.ic_pr_burst_freq = ic_pr_burst_freq
if ec_pr_freq != None: interface.ec_pr_freq = ec_pr_freq
if ic_new_time != None: interface.ic_new_time = ic_new_time
if ic_burst_penalty != None: interface.ic_burst_penalty = ic_burst_penalty
if ic_held_release_interval != None: interface.ic_held_release_interval = ic_held_release_interval
@@ -996,6 +1080,48 @@ class Reticulum:
RNS.Transport.interfaces.append(interface)
interface.final_init()
def _default_ar_target(self):
return self.__default_ar_target or RNS.Interfaces.Interface.Interface.DEFAULT_AR_TARGET
def _default_ar_penalty(self):
return self.__default_ar_penalty or RNS.Interfaces.Interface.Interface.DEFAULT_AR_PENALTY
def _default_ar_grace(self):
return self.__default_ar_grace or RNS.Interfaces.Interface.Interface.DEFAULT_AR_GRACE
def _default_ic_max_held_announces(self):
return self.__ic_max_held_announces or RNS.Interfaces.Interface.Interface.MAX_HELD_ANNOUNCES
def _default_ic_burst_hold(self):
return self.__ic_burst_hold or RNS.Interfaces.Interface.Interface.IC_BURST_HOLD
def _default_ic_burst_freq_new(self):
return self.__ic_burst_freq_new or RNS.Interfaces.Interface.Interface.IC_BURST_FREQ_NEW
def _default_ic_burst_freq(self):
return self.__ic_burst_freq or RNS.Interfaces.Interface.Interface.IC_BURST_FREQ
def _default_ic_pr_burst_freq_new(self):
return self.__ic_pr_burst_freq_new or RNS.Interfaces.Interface.Interface.IC_PR_BURST_FREQ_NEW
def _default_ic_pr_burst_freq(self):
return self.__ic_pr_burst_freq or RNS.Interfaces.Interface.Interface.IC_PR_BURST_FREQ
def _default_ec_pr_freq(self):
return self.__ec_pr_freq or RNS.Interfaces.Interface.Interface.EC_PR_FREQ
def _default_egress_control(self):
return self.__egress_control or RNS.Interfaces.Interface.Interface.EGRESS_CONTROL
def _default_ic_new_time(self):
return self.__ic_new_time or RNS.Interfaces.Interface.Interface.IC_NEW_TIME
def _default_ic_burst_penalty(self):
return self.__ic_burst_penalty or RNS.Interfaces.Interface.Interface.IC_BURST_PENALTY
def _default_ic_held_release_interval(self):
return self.__ic_held_release_interval or RNS.Interfaces.Interface.Interface.IC_HELD_RELEASE_INTERVAL
def _should_persist_data(self, background=False):
if time.time() > self.last_data_persist+Reticulum.GRACIOUS_PERSIST_INTERVAL:
def job(): self.__persist_data(background=background)
@@ -1048,7 +1174,7 @@ class Reticulum:
self.config.write()
def rpc_loop(self):
while True:
while RNS.Transport._should_run:
try:
rpc_connection = self.rpc_listener.accept()
call = rpc_connection.recv()
@@ -1094,6 +1220,11 @@ class Reticulum:
elif operation == "retain": rpc_connection.send(self._retain_destination_data(destination_hash))
elif operation == "unretain": rpc_connection.send(self._unretain_destination_data(destination_hash))
if "identity_data" in call:
operation = call["identity_data"]
identity_hash = call["identity_hash"]
if operation == "retain": rpc_connection.send(self._retain_identity(identity_hash))
rpc_connection.close()
except Exception as e:
@@ -1128,6 +1259,18 @@ class Reticulum:
else: return RNS.Identity._unretain_destination_data(destination_hash)
def _retain_identity(self, identity_hash):
if type(identity_hash) != bytes or len(identity_hash) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
raise TypeError("Cannot retain identity, not a valid identity hash")
if self.is_connected_to_shared_instance:
rpc_connection = self.get_rpc_client()
rpc_connection.send({"identity_data": "retain", "identity_hash": identity_hash})
response = rpc_connection.recv()
return response
else: return RNS.Identity._retain_identity(identity_hash)
def get_interface_stats(self):
if self.is_connected_to_shared_instance:
rpc_connection = self.get_rpc_client()
@@ -1279,7 +1422,16 @@ class Reticulum:
ifstats["txb"] = interface.txb
ifstats["incoming_announce_frequency"] = interface.incoming_announce_frequency()
ifstats["outgoing_announce_frequency"] = interface.outgoing_announce_frequency()
ifstats["incoming_pr_frequency"] = interface.incoming_pr_frequency()
ifstats["outgoing_pr_frequency"] = interface.outgoing_pr_frequency()
ifstats["announce_rate_target"] = interface.announce_rate_target
ifstats["announce_rate_penalty"] = interface.announce_rate_penalty
ifstats["announce_rate_grace"] = interface.announce_rate_grace
ifstats["held_announces"] = len(interface.held_announces)
ifstats["burst_active"] = interface.ic_burst_active
ifstats["burst_activated"] = interface.ic_burst_activated
ifstats["pr_burst_active"] = interface.ic_pr_burst_active
ifstats["pr_burst_activated"] = interface.ic_pr_burst_activated
ifstats["status"] = interface.online
ifstats["mode"] = interface.mode
+321 -214
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -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"))]))
+674
View File
@@ -0,0 +1,674 @@
#!/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.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 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
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
[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()
+412
View File
@@ -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",
}
+40
View File
@@ -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
+758
View File
@@ -0,0 +1,758 @@
# 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
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:
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}"
text = text.replace('`', '')
return f"`!`[{text}`{url}]`!"
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
stripped = text
stripped = re.sub(r'`[FB][0-9a-fA-F]{3}', '', stripped)
stripped = re.sub(r'`[!*_]', '', stripped)
stripped = re.sub(r'`f`b', '', stripped)
if len(stripped) <= width - 1: return text
truncated = stripped[:width - 1] + ""
return truncated
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)
+929 -554
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -59,7 +59,8 @@ def connect_remote(destination_hash, auth_identity, timeout, no_output = False,
remote_identity = RNS.Identity.recall(destination_hash)
def remote_link_closed(link):
if link.teardown_reason == RNS.Link.TIMEOUT:
if link.teardown_reason == RNS.Link.INITIATOR_CLOSED: return
elif link.teardown_reason == RNS.Link.TIMEOUT:
if not no_output:
print(output_rst_str, end="")
print("The link timed out, exiting now")
@@ -536,9 +537,8 @@ def pretty_date(time=False):
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 < 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"
+41
View File
@@ -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__
+1
View File
@@ -0,0 +1 @@
__version__ = "0.2.0"
+93
View File
@@ -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
+60
View File
@@ -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)
+59
View File
@@ -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())
+484
View File
@@ -0,0 +1,484 @@
# 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
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
+229
View File
@@ -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)
+46
View File
@@ -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))
+785
View File
@@ -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()
+149
View File
@@ -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)
+201
View File
@@ -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
+174
View File
@@ -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()
+441
View File
@@ -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)
+134 -48
View File
@@ -60,8 +60,11 @@ def size_str(num, suffix='B'):
request_result = None
request_concluded = False
first_remote_req = True
remote_destination = None
remote_link = None
def get_remote_status(destination_hash, include_lstats, identity, no_output=False, timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
global request_result, request_concluded
global request_result, request_concluded, first_remote_req, remote_destination, remote_link
link_count = None
if not RNS.Transport.has_path(destination_hash):
@@ -81,7 +84,8 @@ def get_remote_status(destination_hash, include_lstats, identity, no_output=Fals
remote_identity = RNS.Identity.recall(destination_hash)
def remote_link_closed(link):
if link.teardown_reason == RNS.Link.TIMEOUT:
if link.teardown_reason == RNS.Link.INITIATOR_CLOSED: return
elif link.teardown_reason == RNS.Link.TIMEOUT:
if not no_output:
print("\r \r", end="")
print("The link timed out, exiting now")
@@ -107,44 +111,50 @@ def get_remote_status(destination_hash, include_lstats, identity, no_output=Fals
response = request_receipt.response
if isinstance(response, list):
status = response[0]
if len(response) > 1:
link_count = response[1]
else:
link_count = None
if len(response) > 1: link_count = response[1]
else: link_count = None
request_result = (status, link_count)
request_concluded = True
def remote_link_established(link):
if not no_output:
global first_remote_req
if not no_output and first_remote_req:
print("\r \r", end="")
print("Sending request...", end=" ")
sys.stdout.flush()
link.identify(identity)
link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed)
first_remote_req = False
if not no_output:
if not remote_link and not no_output:
print("\r \r", end="")
print("Establishing link with remote transport instance...", end=" ")
sys.stdout.flush()
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
link = RNS.Link(remote_destination)
link.set_link_established_callback(remote_link_established)
link.set_link_closed_callback(remote_link_closed)
if not remote_destination:
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
if remote_link and remote_link.status == RNS.Link.ACTIVE:
request_concluded = False
remote_link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed)
while not request_concluded:
time.sleep(0.1)
else:
remote_link = RNS.Link(remote_destination)
remote_link.set_link_established_callback(remote_link_established)
remote_link.set_link_closed_callback(remote_link_closed)
while not request_concluded: time.sleep(0.1)
if request_result != None:
print("\r \r", end="")
return request_result
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, lstats=False, sorting=None, sort_reverse=False,
remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, must_exit=True, rns_instance=None,
traffic_totals=False, discovered_interfaces=False, config_entries=False):
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, pstats=False, lstats=False, sorting=None,
sort_reverse=False, remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, must_exit=True,
rns_instance=None, traffic_totals=False, discovered_interfaces=False, config_entries=False, burst_filter=False):
if remote: require_shared = False
else: require_shared = True
@@ -300,28 +310,22 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
if remote:
try:
if management_identity is None:
raise ValueError("Remote management requires an identity file. Use -i to specify the path to a management identity.")
if management_identity is None: raise ValueError("Remote management requires an identity file. Use -i to specify the path to a management identity.")
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(remote) != dest_len:
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
if len(remote) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try:
identity_hash = bytes.fromhex(remote)
destination_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.remote.management", identity_hash)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
identity = RNS.Identity.from_file(os.path.expanduser(management_identity))
if identity == None:
raise ValueError("Could not load management identity from "+str(management_identity))
if identity == None: raise ValueError("Could not load management identity from "+str(management_identity))
try:
remote_status = get_remote_status(destination_hash, lstats, identity, no_output=json, timeout=remote_timeout)
if remote_status != None:
stats, link_count = remote_status
except Exception as e:
raise e
if remote_status != None: stats, link_count = remote_status
except Exception as e: raise e
except Exception as e:
print(str(e))
@@ -375,6 +379,10 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
interfaces.sort(key=lambda i: i["incoming_announce_frequency"], reverse=not sort_reverse)
if sorting == "atx":
interfaces.sort(key=lambda i: i["outgoing_announce_frequency"], reverse=not sort_reverse)
if sorting == "prx":
interfaces.sort(key=lambda i: i["incoming_pr_frequency"], reverse=not sort_reverse)
if sorting == "ptx":
interfaces.sort(key=lambda i: i["outgoing_pr_frequency"], reverse=not sort_reverse)
if sorting == "held":
interfaces.sort(key=lambda i: i["held_announces"], reverse=not sort_reverse)
@@ -393,7 +401,18 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
):
if not (name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False)):
if name_filter == None or name_filter.lower() in name.lower():
if name_filter == None and burst_filter == None: show_if = True
elif not burst_filter:
if not name_filter or name_filter.lower() in name.lower(): show_if = True
else: show_if = False
elif burst_filter:
burst_act = True if ("burst_active" in ifstat and "pr_burst_active" in ifstat) and (ifstat["burst_active"] or ifstat["pr_burst_active"]) else False
nfilt = name_filter.lower() in name.lower() if name_filter else False
if burst_act or nfilt: show_if = True
else: show_if = False
else: show_if = True
if show_if:
print("")
if ifstat["status"]: ss = "Up"
@@ -533,18 +552,80 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
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
@@ -607,9 +688,11 @@ def main(must_exit=True, rns_instance=None):
parser.add_argument("-a", "--all", action="store_true", help="show all interfaces", default=False)
parser.add_argument("-A", "--announce-stats", action="store_true", help="show announce stats", default=False)
parser.add_argument("-P", "--pr-stats", action="store_true", help="show path request stats", default=False)
parser.add_argument("-l", "--link-stats", action="store_true", help="show link stats", default=False)
parser.add_argument("-B", "--burst", action="store_true", help="only show interfaces with active bursts", default=False)
parser.add_argument("-t", "--totals", action="store_true", help="display traffic totals", default=False)
parser.add_argument("-s", "--sort", action="store", help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, announces, arx, atx, held]", default=None, type=str)
parser.add_argument("-s", "--sort", action="store", help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, announces, arx, atx, prx, ptx, held]", default=None, type=str)
parser.add_argument("-r", "--reverse", action="store_true", help="reverse sorting", default=False)
parser.add_argument("-j", "--json", action="store_true", help="output in JSON format", default=False)
parser.add_argument("-R", action="store", metavar="hash", help="transport identity hash of remote instance to get status from", default=None, type=str)
@@ -637,15 +720,16 @@ def main(must_exit=True, rns_instance=None):
exit(1)
while True:
st = time.time()
buffer = io.StringIO()
old_stdout = sys.stdout
sys.stdout = buffer
try:
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json,
astats=args.announce_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse, remote=args.R,
management_identity=args.i, remote_timeout=args.w, must_exit=False, rns_instance=reticulum, traffic_totals=args.totals,
discovered_interfaces=args.discovered, config_entries=args.D)
astats=args.announce_stats, pstats=args.pr_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse,
remote=args.R, management_identity=args.i, remote_timeout=args.w, must_exit=False, rns_instance=reticulum,
traffic_totals=args.totals, discovered_interfaces=args.discovered, config_entries=args.D, burst_filter=args.burst)
finally:
sys.stdout = old_stdout
@@ -653,14 +737,16 @@ def main(must_exit=True, rns_instance=None):
output = buffer.getvalue()
print("\033[H\033[2J", end="")
print(output, end="", flush=True)
time.sleep(args.monitor_interval)
td = time.time()-st
sleeptime = max(args.monitor_interval-td, 0.2)
time.sleep(sleeptime)
else:
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json,
astats=args.announce_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse, remote=args.R,
management_identity=args.i, remote_timeout=args.w, must_exit=must_exit, rns_instance=rns_instance, traffic_totals=args.totals,
discovered_interfaces=args.discovered, config_entries=args.D)
astats=args.announce_stats, pstats=args.pr_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse,
remote=args.R, management_identity=args.i, remote_timeout=args.w, must_exit=must_exit, rns_instance=rns_instance,
traffic_totals=args.totals, discovered_interfaces=args.discovered, config_entries=args.D, burst_filter=args.burst)
except KeyboardInterrupt:
print("")
+125 -117
View File
@@ -94,22 +94,14 @@ _always_override_destination = False
logging_lock = threading.Lock()
def loglevelname(level):
if (level == LOG_CRITICAL):
return "[Critical]"
if (level == LOG_ERROR):
return "[Error] "
if (level == LOG_WARNING):
return "[Warning] "
if (level == LOG_NOTICE):
return "[Notice] "
if (level == LOG_INFO):
return "[Info] "
if (level == LOG_VERBOSE):
return "[Verbose] "
if (level == LOG_DEBUG):
return "[Debug] "
if (level == LOG_EXTREME):
return "[Extra] "
if (level == LOG_CRITICAL): return "[Critical]"
if (level == LOG_ERROR): return "[Error] "
if (level == LOG_WARNING): return "[Warning] "
if (level == LOG_NOTICE): return "[Notice] "
if (level == LOG_INFO): return "[Info] "
if (level == LOG_VERBOSE): return "[Verbose] "
if (level == LOG_DEBUG): return "[Debug] "
if (level == LOG_EXTREME): return "[Extra] "
return "Unknown"
@@ -127,18 +119,16 @@ 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())+"] "+loglevelname(level)+" "+msg
else: logstring = "["+timestamp_str(time.time())+"] "+msg
with logging_lock:
if (logdest == LOG_STDOUT or _always_override_destination or _override_destination):
@@ -149,14 +139,10 @@ def log(msg, level=3, _override_destination = False, pt=False):
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:
@@ -166,8 +152,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)
@@ -186,14 +171,11 @@ def trace_exception(e):
log(exception_info, LOG_ERROR)
def hexrep(data, delimit=True):
try:
iter(data)
except TypeError:
data = [data]
try: iter(data)
except TypeError: data = [data]
delimiter = ":"
if not delimit:
delimiter = ""
if not delimit: delimiter = ""
hexrep = delimiter.join("{:02x}".format(c) for c in data)
return hexrep
@@ -216,23 +198,24 @@ def prettysize(num, suffix='B'):
for unit in units:
if abs(num) < 1000.0:
if unit == "":
return "%.0f %s%s" % (num, unit, suffix)
else:
return "%.2f %s%s" % (num, unit, suffix)
if unit == "": return "%.0f %s%s" % (num, unit, suffix)
else: return "%.2f %s%s" % (num, unit, suffix)
num /= 1000.0
return "%.2f%s%s" % (num, last_unit, suffix)
def prettyfrequency(hz, suffix="Hz"):
def prettyfrequency(hz, suffix="Hz", d=2, lpf=False):
if hz == 0: return "0 Hz"
num = hz*1e6
units = ["µ", "m", "", "K","M","G","T","P","E","Z"]
if not lpf: num = hz*1e6
else: num = hz
if not lpf: units = ["µ", "m", "", "K","M","G","T","P","E","Z"]
else: units = ["", "K","M","G","T","P","E","Z"]
last_unit = "Y"
for unit in units:
if abs(num) < 1000.0:
return "%.2f %s%s" % (num, unit, suffix)
if d == 2: return "%.2f %s%s" % (num, unit, suffix)
else: return "%s %s%s" % (str(round(num,d)), unit, suffix)
num /= 1000.0
return "%.2f%s%s" % (num, last_unit, suffix)
@@ -247,8 +230,7 @@ def prettydistance(m, suffix="m"):
if unit == "m": divisor = 10
if unit == "c": divisor = 100
if abs(num) < divisor:
return "%.2f %s%s" % (num, unit, suffix)
if abs(num) < divisor: return "%.2f %s%s" % (num, unit, suffix)
num /= divisor
return "%.2f %s%s" % (num, last_unit, suffix)
@@ -265,10 +247,8 @@ def prettytime(time, verbose=False, compact=False):
time %= 3600
minutes = int(time // 60)
time %= 60
if compact:
seconds = int(time)
else:
seconds = round(time, 2)
if compact: seconds = int(time)
else: seconds = round(time, 2)
ss = "" if seconds == 1 else "s"
sm = "" if minutes == 1 else "s"
@@ -297,22 +277,16 @@ def prettytime(time, verbose=False, compact=False):
tstr = ""
for c in components:
i += 1
if i == 1:
pass
elif i < len(components):
tstr += ", "
elif i == len(components):
tstr += " and "
if i == 1: pass
elif i < len(components): tstr += ", "
elif i == len(components): tstr += " and "
tstr += c
if tstr == "":
return "0s"
if tstr == "": return "0s"
else:
if not neg:
return tstr
else:
return f"-{tstr}"
if not neg: return tstr
else: return f"-{tstr}"
def prettyshorttime(time, verbose=False, compact=False):
neg = False
@@ -324,10 +298,8 @@ def prettyshorttime(time, verbose=False, compact=False):
seconds = int(time // 1e6); time %= 1e6
milliseconds = int(time // 1e3); time %= 1e3
if compact:
microseconds = int(time)
else:
microseconds = round(time, 2)
if compact: microseconds = int(time)
else: microseconds = round(time, 2)
ss = "" if seconds == 1 else "s"
sms = "" if milliseconds == 1 else "s"
@@ -351,22 +323,16 @@ def prettyshorttime(time, verbose=False, compact=False):
tstr = ""
for c in components:
i += 1
if i == 1:
pass
elif i < len(components):
tstr += ", "
elif i == len(components):
tstr += " and "
if i == 1: pass
elif i < len(components): tstr += ", "
elif i == len(components): tstr += " and "
tstr += c
if tstr == "":
return "0us"
if tstr == "": return "0us"
else:
if not neg:
return tstr
else:
return f"-{tstr}"
if not neg: return tstr
else: return f"-{tstr}"
def phyparams():
print("Required Physical Layer MTU : "+str(Reticulum.MTU)+" bytes")
@@ -377,8 +343,7 @@ def phyparams():
print("Link Public Key Size : "+str(Link.ECPUBSIZE*8)+" bits")
print("Link Private Key Size : "+str(Link.KEYSIZE*8)+" bits")
def panic():
os._exit(255)
def panic(): os._exit(255)
exit_called = False
def exit(code=0):
@@ -388,6 +353,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 = {}
@@ -395,8 +364,7 @@ class Profiler:
@staticmethod
def get_profiler(tag=None, super_tag=None):
if tag in Profiler.profilers:
return Profiler.profilers[tag]
if tag in Profiler.profilers: return Profiler.profilers[tag]
else:
profiler = Profiler(tag, super_tag)
Profiler.profilers[tag] = profiler
@@ -408,13 +376,14 @@ class Profiler:
self.pause_started = None
self.tag = tag
self.super_tag = super_tag
if self.super_tag in Profiler.profilers:
self.super_profiler = Profiler.profilers[self.super_tag]
self.pause_super = self.super_profiler.pause
self.resume_super = self.super_profiler.resume
else:
def noop(self=None):
pass
def noop(self=None): pass
self.super_profiler = None
self.pause_super = noop
self.resume_super = noop
@@ -424,8 +393,7 @@ class Profiler:
tag = self.tag
super_tag = self.super_tag
thread_ident = threading.get_ident()
if not tag in Profiler.tags:
Profiler.tags[tag] = {"threads": {}, "super": super_tag}
if not tag in Profiler.tags: Profiler.tags[tag] = {"threads": {}, "super": super_tag}
if not thread_ident in Profiler.tags[tag]["threads"]:
Profiler.tags[tag]["threads"][thread_ident] = {"current_start": None, "captures": []}
@@ -461,8 +429,7 @@ class Profiler:
self.resume_super()
@staticmethod
def ran():
return Profiler._ran
def ran(): return Profiler._ran
@staticmethod
def results():
@@ -479,41 +446,35 @@ class Profiler:
sample_count = len(thread_captures)
if sample_count > 1:
thread_results = {
"count": sample_count,
"mean": mean(thread_captures),
"median": median(thread_captures),
"stdev": stdev(thread_captures)
}
thread_results = { "count": sample_count,
"mean": mean(thread_captures),
"median": median(thread_captures),
"stdev": stdev(thread_captures) }
elif sample_count == 1:
thread_results = {
"count": sample_count,
"mean": mean(thread_captures),
"median": median(thread_captures),
"stdev": None
}
thread_results = { "count": sample_count,
"mean": mean(thread_captures),
"median": median(thread_captures),
"stdev": None }
tag_captures.extend(thread_captures)
sample_count = len(tag_captures)
if sample_count > 1:
tag_results = {
"name": tag,
"super": tag_entry["super"],
"count": len(tag_captures),
"mean": mean(tag_captures),
"median": median(tag_captures),
"stdev": stdev(tag_captures)
}
tag_results = { "name": tag,
"super": tag_entry["super"],
"count": len(tag_captures),
"mean": mean(tag_captures),
"median": median(tag_captures),
"stdev": stdev(tag_captures) }
elif sample_count == 1:
tag_results = {
"name": tag,
"super": tag_entry["super"],
"count": len(tag_captures),
"mean": mean(tag_captures),
"median": median(tag_captures),
"stdev": None
}
tag_results = { "name": tag,
"super": tag_entry["super"],
"count": len(tag_captures),
"mean": mean(tag_captures),
"median": median(tag_captures),
"stdev": None }
results[tag] = tag_results
@@ -545,4 +506,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
View File
@@ -1 +1 @@
__version__ = "1.1.7"
__version__ = "1.2.6"
+7
View File
@@ -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.
+125
View File
@@ -0,0 +1,125 @@
#!/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'<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):
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}`"
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 -1
View File
@@ -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: 93f6eab163a291fbdb28b0b7666c1971
config: 6d7f4aac8313ba495ab156ec11ab15c0
tags: 645f666f9bcd5a90fca523b33c5a78b7
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

@@ -244,12 +244,12 @@ to your entry-point.
listen_on = 0.0.0.0
port = 4242
# On publicly available interfaces, it can be
# a good idea to configure sensible announce
# On publicly available interfaces, it is
# essential to configure sensible announce
# rate targets.
announce_rate_target = 3600
announce_rate_penalty = 3600
announce_rate_grace = 12
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.
+576
View File
@@ -0,0 +1,576 @@
.. _git-main:
******************
Git Over Reticulum
******************
A set of utilities for distributed collaborative software development and publishing is included in RNS.
The system consists of two parts: The ``rngit`` node that hosts 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.
The rngit Utility
=================
The ``rngit`` utility provides full Git repository hosting and interaction over Reticulum. It allows you to host and manage Git repositories and releases on Reticulum nodes, and to interact with remote repositories using standard Git commands through the ``rns://`` URL scheme.
**Usage Examples**
Run ``rngit`` to start a repository node:
.. code:: text
$ rngit
[Notice] Starting Reticulum Git Node...
[Notice] Reticulum Git Node listening on <0d7334d411d00120cbad24edf355fdd2>
On the first run, ``rngit`` will create a default configuration file. You will then need to edit this, to point to your repository locations, configure access permissions, and perform any other necessary configuration.
View your identity and destination hashes:
.. code:: text
$ rngit --print-identity
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
If the page node is enabled, the output will also include the Nomad Network destination hash.
You can run ``rngit`` in service mode with logging to file:
.. code:: text
$ rngit -s
Clone a repository from a remote ``rngit`` node:
.. code:: text
$ git clone rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
Add a Reticulum remote to an existing repository:
.. code:: text
$ git remote add some_remote rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
Push changes to the Reticulum remote:
.. code:: text
$ git push some_remote master
Get changes from a remote repository:
.. code:: text
$ git pull rns_remote master
**All Command-Line Options (rngit)**
.. code:: text
usage: rngit.py [-h] [--config CONFIG] [--rnsconfig RNSCONFIG] [-s] [-i] [-v]
[-q] [--version]
Reticulum Git Repository Node
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-p, --print-identity print identity and destination info and exit
-s, --service rngit is running as a service and should log to file
-i, --interactive drop into interactive shell after initialisation
-v, --verbose increase verbosity
-q, --quiet decrease verbosity
--version show program's version number and exit
**All Command-Line Options (git-remote-rns)**
The ``git-remote-rns`` helper is automatically invoked by Git when interacting with ``rns://`` URLs. It is not typically run directly by users, but accepts the following environment variables for configuration:
- ``RNGIT_CONFIG`` - Path to alternative client configuration directory
- ``RNS_CONFIG`` - Path to alternative Reticulum configuration directory
The client configuration file is located at ``~/.rngit/client_config`` and allows adjusting parameters such as the reference batch size for transfers.
Repository Structure
====================
The ``rngit`` node organizes repositories into groups. Each group is a directory containing bare Git repositories. The repository path format is ``group_name/repo_name``. For example, a repository at ``/var/git/public/myrepo`` would be accessible as ``public/myrepo`` via the URL ``rns://DESTINATION_HASH/public/myrepo``.
**Configuration**
The ``rngit`` node configuration file is located at ``~/.rngit/config`` (or ``/etc/rngit/config`` for system-wide installations). The default configuration includes:
- Repository group paths defining where to find bare repositories
- Access permissions for groups and individual repositories
- Announce intervals for network visibility
- Optional statistics recording for repository activity
Access permissions can be configured at the group level in the config file, or per-repository using ``.allowed`` files. Permissions use the format ``permission:target`` where permission is ``r`` (read), ``w`` (write), ``rw`` (read/write), ``c`` (create) or ``s`` (stats) and target is ``all``, ``none``, or a specific identity hash.
The ``s`` (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set ``record_stats = yes`` in the ``[rngit]`` section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to ``stats_ignore_identities``.
Repository-specific ``.allowed`` files can be static text files or executable scripts that output permission rules to stdout. A ``group.allowed`` file in a repository group directory applies to all repositories within that group.
Serving Pages Over Nomad Network
================================
In addition to providing Git repository access via the Git remote helper protocol, ``rngit`` can also run a `Nomad Network <https://github.com/markqvist/nomadnet>`_ compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.
When enabled, the page node provides a complete interface to your repositories, with automatic Markdown to Micron conversion, syntax-highlighted code browsing, and detailed commit, diff and statistics views.
**Enabling the Git Page Node**
To enable the page node, add the following to your ``~/.rngit/config`` file:
.. code:: text
[pages]
serve_nomadnet = yes
When the page node is enabled, ``rngit`` will listen on a Nomad Network node destination in addition to the Git repository destination. You can view the destination hash by running:
.. code:: text
$ rngit --print-identity
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
Nomad Network Destination : <50824b711717f97c2fb1166ceddd5ea9>
**Accessing Repository Pages**
Once the page node is running, you can access it from any Nomad Network client by connecting to the Nomad Network destination. The page node provides the following views:
- **Front Page** - Lists all repository groups accessible to your identity
- **Group Page** - Shows all repositories within a group
- **Repository Page** - Displays repository overview, description and README
- **Releases** - List of releases for the repository, with information and downloads
- **File Browser** - Browse directory trees and view and download file contents
- **Commits View** - View commit history with pagination
- **Commit Details** - Detailed commit information with file changes and diffs
- **Refs View** - List branches and tags
- **Statistics** - Activity charts showing views, fetches and pushes over time
All pages respect the same permission system used for Git access. If an identity does not have read access to a repository, they will not be able to view its pages.
Formatting & Syntax Highlighting
================================
If the ``pygments`` Python module is installed on your system, the page node will automatically apply syntax highlighting to code files. The highlighting supports a wide range of programming languages and uses a color theme optimized for terminal display.
To enable syntax highlighting, install pygments:
.. code:: text
pip install pygments
**Markdown & Micron Support**
README files and other Markdown documents are automatically converted to Micron markup for display in Nomad Network clients. You can also write your README files directly in Micron, in which case they will display and render as such in any Nomad Network client. The file browser also supports viewing both rendered and raw Markdown and Micron documents.
Code blocks in Markdown can include language hints for syntax highlighting:
.. code:: text
```python
def hello_world():
print("Hello, Reticulum!")
```
Customizing Templates
=====================
The page node uses a template system that allows complete customization of the generated pages. Templates are stored in the ``~/.rngit/templates/`` directory as Micron files.
The following template files are supported:
- ``base.mu`` - Base template wrapping all pages
- ``front.mu`` - Front page listing all groups
- ``group.mu`` - Group page listing repositories
- ``repo.mu`` - Repository overview page
- ``releases.mu`` - Release list page
- ``release.mu`` - Release details page
- ``tree.mu`` - File browser pages
- ``blob.mu`` - File content display
- ``commits.mu`` - Commit history listing
- ``commit.mu`` - Individual commit detail page
- ``refs.mu`` - Branches and tags listing
- ``stats.mu`` - Statistics page
Templates can include the following variables:
- ``{PAGE_CONTENT}`` - The main content of the page (required)
- ``{NODE_NAME}`` - The configured node name
- ``{NAVIGATION}`` - Breadcrumb navigation links
- ``{VERSION}`` - The rngit version number
- ``{GEN_TIME}`` - Page generation time
**Dynamic Templates**
Templates can be made executable to generate dynamic content. If a template file has the executable bit set, it will be executed and its stdout used as the template content.
**Icon Sets**
By default, the page node uses Nerd Font icons. If you prefer simpler icons or your terminal does not support Nerd Fonts, you can enable Unicode icons instead:
.. code:: text
[pages]
serve_nomadnet = yes
unicode_icons = yes
**Repository Statistics**
When statistics recording is enabled (see the ``record_stats`` configuration option), the page node can display activity charts for each repository. The statistics page shows:
- Total and peak views, fetches and pushes
- Daily activity charts over a 90-day period
- Combined activity visualization
To view statistics, a user must have the ``s`` (stats) permission for the repository. See the Access Configuration section for details on setting permissions.
**Repository Thanks**
The page node includes a "Thanks" feature that allows users to express appreciation for a repository. On each repository page, a "Thanks" link is displayed showing the current thanks count. Clicking this link registers a thank you for the repository.
**Configuration Example**
A complete page node configuration might look like this:
.. code:: text
[rngit]
node_name = My Git Node
announce_interval = 360
record_stats = yes
[repositories]
public = /var/git/public
internal = /var/git/internal
[access]
public = r:all
internal = rw:9710b86ba12c42d1d8f30f74fe509286
[pages]
serve_nomadnet = yes
unicode_icons = no
Release Management
==================
In addition to hosting Git repositories, ``rngit`` provides a complete release management system. This allows you to publish versioned releases with associated artifacts, release notes and metadata. Releases are managed through the ``rngit release`` subcommand, and are also viewable through the Nomad Network page interface.
**The Release Workflow**
Creating a release involves specifying a Git tag and a directory containing build artifacts or other files to distribute. The ``rngit`` client will open your configured ``$EDITOR`` to compose release notes, then upload all artifacts to the remote repository node.
To create a release, specify the tag name and path to artifacts:
.. code:: text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo create v1.2.0:./dist
This will:
1. Verify that the tag ``v1.2.0`` exists in the repository
2. Open your editor to write release notes
3. Upload all files from the ``./dist`` directory
4. Publish the release
If no ``$EDITOR`` environment variable is set, ``rngit`` will try to use ``nano``, ``vim`` or ``vi``. The editor will show a template with instructions. Lines starting with ``#`` will be ignored, and if the remaining content is empty after stripping comments, the release creation will be cancelled.
**Release Storage & Structure**
Releases are stored on the node in a directory named ``repo_name.releases`` next to the bare repository. Each release is a subdirectory containing:
- ``META`` - Release metadata in ConfigObj format
- ``RELEASE.md`` or ``RELEASE.mu`` - Release notes
- ``artifacts/`` - All uploaded files
- ``THANKS`` - Appreciation count from users
**Listing Releases**
To view all releases for a repository:
.. code:: text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list
Tag Status Created Objs Notes
------------------------------------------------------------------
v1.2.0 published 2025-01-15 14:32 3 Another release
v1.1.0 published 2024-12-03 09:15 2 Bug fix release
v1.0.0 published 2024-10-20 16:45 2 Initial release
**Viewing Release Details**
To see full information about a specific release:
.. code:: text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo view v1.2.0
Release : 0.9.2
Status : published
Created : 2026-05-04 23:53:09
Thanks : 5
Release Notes
=============
Version 1.2.0 release notes...
Artifacts (4)
=============
- myapp-1.2.0.tar.gz (1.5 MB)
- myapp-1.2.0.zip (1.6 MB)
- checksums.txt (256 B)
**Deleting Releases**
To remove a release:
.. code:: text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo delete v1.2.0
Are you sure you want to delete release 'v1.2.0'? [y/N]: y
Release v1.2.0 deleted
**Requirements & Validation**
- The specified tag must exist in the remote repository
- You must have ``release`` permission for the repository
- The target artifacts directory must exist and contain at least one file
- Release notes cannot be empty
**Permissions**
Release management requires the ``release`` permission, configured the same way as other repository permissions. In the config file or ``.allowed`` files, use ``rel:target`` to grant release management rights:
.. code:: text
# In .allowed file or config
rel:all # Allow everyone
rel:9710b86... # Allow specific identity
rel:none # Deny everyone
**Nomad Network Interface**
When the Nomad Network page node is enabled, releases are displayed on a dedicated releases page for each repository. Each release is listed with its tag, creation date, artifact count and a preview of the release notes. Clicking a release shows the full details including formatted release notes and a listing of all artifacts with their sizes.
Only releases with ``published`` status are visible through the Nomad Network interface. Draft releases (if supported in future implementations) would only be visible through the command-line interface.
**All Command-Line Options (rngit release)**
.. code:: text
usage: rngit release [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
[-i PATH] [-v] [-q] [--version]
[repository] [operation] [target]
Reticulum Git Release Manager
positional arguments:
repository URL of remote repository
operation list, view, create or delete
target tag and path to release artifacts directory
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-i, --identity PATH path to release identity
-v, --verbose
-q, --quiet
--version show program's version number and exit
.. raw:: latex
\newpage
Work Documents
==============
In addition to releases, ``rngit`` provides a work document management system for tracking tasks, investigations, issues and progress related to repositories. Work documents are stored as structured msgpack data and support threaded updates and comments.
**Listing Work Documents**
To view work documents for a repository:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list
Active documents
=================
ID Title Author Created Comments
---------------------------------------------------------------------------
1 Implemented new feature 9710b86ba12c4f2e… 2025-01-15 14:32 3
2 Fixed bug in parser 8f3a21c9d84e927b… 2025-01-14 09:15 1
Use ``--scope completed`` to view completed work documents, or ``--scope all`` to see both active and completed.
**Viewing a Work Document**
To view a specific work document with all its comments:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo view -d 1
Implement new feature (active #1)
=================================
Author : 9710b86ba12c42d1d8f30f74fe509286
Status : active
Created : 2026-05-05 15:11:11
Edited : 2026-05-05 18:22:11
Format : markdown
Updates : 0
This work document tracks the implementation of the new feature...
Updates
=======
#1 by 9710b86ba12c42d1d8f30f74fe509286 at 2026-05-05 15:38:37
-------------------------------------------------------------
Initial analysis complete
**Creating Work Documents**
To create a new work document:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo create --title "Investigate performance issue"
This will open your configured ``$EDITOR`` to compose the document content. Save and exit to create the document, or save an empty document to cancel.
**Editing Work Documents**
To edit an existing work document:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo edit -d 1
This fetches the current content, opens it in your editor, and sends any changes back to the node.
**Adding Comments**
To add an update to a work document:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo update -d 1
This opens your editor to compose the update.
**Completing Work Documents**
To mark a work document as completed (moving it from ``active`` to ``completed``):
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo complete -d 1
Work document #1 completed
**Activating Work Documents**
To mark a work document as active (moving it from ``completed`` to ``active``):
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo activate -d 1
Work document #1 activated
**Deleting Work Documents**
To delete a work document and all its comments:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo delete -id 1
Are you sure you want to delete active work document #1? [y/N]: y
Work document #1 deleted
**Permissions**
Users can view work documents and updates if the have ``read`` permission for the repository. If users have ``read`` and ``interact``, they can also post updates/comments on existing work documents. Work document management requires having ``write`` and ``interact`` permission to the repository. These permissions are configured the same way as any other repository permissions. In the config file or ``.allowed`` files, use ``i:target`` to grant work document interaction rights:
.. code:: text
# In .allowed file or config
i:all # Allow everyone
i:9710b86... # Allow specific identity
i:none # Deny everyone
**Author Verification**
Users can only edit or delete work documents and updates they created. The author is cryptographically verified from the interacting link's ``remote_identity``.
**Storage Format**
Work documents are stored in a ``repo_name.work`` directory next to the repository, containing:
- ``active/`` - Active work documents
- ``completed/`` - Completed work documents
Each document is a numbered directory containing:
- ``root`` - The work document content and metadata (msgpack format)
- ``N`` - Numbered comment files (msgpack format)
**Nomad Network Interface**
When the Nomad Network page node is enabled, work documents are viewable through the web interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.
**All Command-Line Options (rngit work)**
.. code:: text
usage: rngit work [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
[-i PATH] [--scope SCOPE] [-t TITLE] [-d ID] [-v]
[-q] [--version]
[repository] [operation]
Reticulum Git Work Document Manager
positional arguments:
repository URL of remote repository
operation list, view, create, edit, delete, update or complete
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-i, --identity PATH path to identity
--scope SCOPE document scope: active, completed or all
-t, --title TITLE document title for create
-d, --id ID document ID
-v, --verbose
-q, --quiet
--version show program's version number and exit
+1
View File
@@ -27,6 +27,7 @@ to participate in the development of Reticulum itself.
hardware
interfaces
networks
git
support
examples
license
+96 -20
View File
@@ -1293,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
@@ -1315,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:
@@ -1352,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
@@ -1364,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
@@ -1402,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.
+1 -51
View File
@@ -110,7 +110,7 @@ plugin system for expandability.
MeshChatX
^^^^^^^^
A `Reticulum MeshChat fork from the future <https://git.quad4.io/RNS-Things/MeshChatX>`_, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original Reticulum MeshChat project, and is not affiliated with the original project.
A `Reticulum MeshChat fork from the future <https://git.quad4.io/RNS-Things/MeshChatX>`_, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ project, and is not affiliated with the original project, but is a much more up-to-date, comprehensive and well-maintained fork.
.. only:: html
@@ -127,56 +127,6 @@ A `Reticulum MeshChat fork from the future <https://git.quad4.io/RNS-Things/Mesh
Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security.
.. raw:: latex
\newpage
MeshChat
^^^^^^^^
The `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ application
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
page browser and other interesting functionality.
.. only:: html
.. image:: screenshots/meshchat_1.webp
:align: center
:target: https://github.com/liamcottle/reticulum-meshchat
.. only:: latex
.. image:: screenshots/meshchat_1.png
:align: center
:target: https://github.com/liamcottle/reticulum-meshchat
Reticulum MeshChat is of course also compatible with Sideband and Nomad Network, or
any other LXMF client.
Columba
^^^^^^^
`Columba <https://github.com/torlando-tech/columba/>`_ is a simple and familiar LXMF
messaging app Android, built with a native Android interface and Material Design 3.
.. only:: html
.. image:: screenshots/columba.webp
:align: center
:width: 25%
:target: https://github.com/torlando-tech/columba/
.. only:: latex
.. image:: screenshots/columba.png
:align: center
:width: 25%
:target: https://github.com/torlando-tech/columba/
While still in early and very active development, it is of course also compatible
with all other LXMF clients, and allows you to message seamlessly with anyone else
using LXMF.
.. raw:: latex
\newpage
+4
View File
@@ -31,6 +31,10 @@ Donations are gratefully accepted via the following channels:
Are certain features in the development roadmap are important to you or your
organisation? Make them a reality quickly by sponsoring their implementation.
.. raw:: latex
\newpage
Provide Feedback
================
Feedback on the usage, functioning and potential dysfunctioning of any and
+291
View File
@@ -680,6 +680,21 @@ another one, which will be created if it does not already exist
--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
================
@@ -752,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
=====================
+1 -1
View File
@@ -1,5 +1,5 @@
const DOCUMENTATION_OPTIONS = {
VERSION: '1.1.7',
VERSION: '1.2.6',
LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',
+5 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Code Examples - Reticulum Network Stack 1.1.7 documentation</title>
<title>Code Examples - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -3663,7 +3664,7 @@ will be fully on-par with natively included interfaces, including all supported
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+5 -4
View File
@@ -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.1.7 documentation</title>
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -294,7 +295,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+7 -4
View File
@@ -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.1.7 documentation</title>
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -178,7 +178,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -202,7 +202,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -220,6 +220,7 @@
<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="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>
@@ -638,6 +639,8 @@
<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>
@@ -836,7 +839,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+8 -7
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Getting Started Fast - Reticulum Network Stack 1.1.7 documentation</title>
<title>Getting Started Fast - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -445,12 +446,12 @@ to your entry-point.</p>
<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 can be</span>
<span class="w"> </span><span class="c1"># a good idea to configure sensible announce</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">12</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
@@ -966,7 +967,7 @@ All other available modules will still be loaded when needed.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+792
View File
@@ -0,0 +1,792 @@
<!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="Support Reticulum" href="support.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>Git Over Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
<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.2.6 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.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
<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="#">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="git-over-reticulum">
<span id="git-main"></span><h1>Git Over Reticulum<a class="headerlink" href="#git-over-reticulum" title="Link to this heading"></a></h1>
<p>A set of utilities for distributed collaborative software development and publishing is included in RNS.</p>
<p>The system consists of two parts: The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node that hosts repositories, and the <code class="docutils literal notranslate"><span class="pre">git-remote-rns</span></code> 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: <code class="docutils literal notranslate"><span class="pre">rns://DESTINATION_HASH/group/repo</span></code>.</p>
<p>If you set a branch to track a Reticulum remote as the default upstream, you can simply use <code class="docutils literal notranslate"><span class="pre">git</span></code> as you normally would; all commands work transparently and as expected.</p>
<div class="admonition warning">
<p class="admonition-title">Warning</p>
<p><strong>The rngit program is a new addition to RNS!</strong> 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, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.</p>
</div>
<section id="the-rngit-utility">
<h2>The rngit Utility<a class="headerlink" href="#the-rngit-utility" title="Link to this heading"></a></h2>
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> utility provides full Git repository hosting and interaction over Reticulum. It allows you to host and manage Git repositories and releases on Reticulum nodes, and to interact with remote repositories using standard Git commands through the <code class="docutils literal notranslate"><span class="pre">rns://</span></code> URL scheme.</p>
<p><strong>Usage Examples</strong></p>
<p>Run <code class="docutils literal notranslate"><span class="pre">rngit</span></code> to start a repository node:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit
[Notice] Starting Reticulum Git Node...
[Notice] Reticulum Git Node listening on &lt;0d7334d411d00120cbad24edf355fdd2&gt;
</pre></div>
</div>
<p>On the first run, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> will create a default configuration file. You will then need to edit this, to point to your repository locations, configure access permissions, and perform any other necessary configuration.</p>
<p>View your identity and destination hashes:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit --print-identity
Git Peer Identity : &lt;959e10e5efc1bd9d97a4083babe51dea&gt;
Repository Node Identity : &lt;153cb870b4665b8c1c348896292b0bad&gt;
Repositories Destination : &lt;0d7334d411d00120cbad24edf355fdd2&gt;
</pre></div>
</div>
<p>If the page node is enabled, the output will also include the Nomad Network destination hash.</p>
<p>You can run <code class="docutils literal notranslate"><span class="pre">rngit</span></code> in service mode with logging to file:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit -s
</pre></div>
</div>
<p>Clone a repository from a remote <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ git clone rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
</pre></div>
</div>
<p>Add a Reticulum remote to an existing repository:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ git remote add some_remote rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
</pre></div>
</div>
<p>Push changes to the Reticulum remote:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ git push some_remote master
</pre></div>
</div>
<p>Get changes from a remote repository:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ git pull rns_remote master
</pre></div>
</div>
<p><strong>All Command-Line Options (rngit)</strong></p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rngit.py [-h] [--config CONFIG] [--rnsconfig RNSCONFIG] [-s] [-i] [-v]
[-q] [--version]
Reticulum Git Repository Node
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-p, --print-identity print identity and destination info and exit
-s, --service rngit is running as a service and should log to file
-i, --interactive drop into interactive shell after initialisation
-v, --verbose increase verbosity
-q, --quiet decrease verbosity
--version show program&#39;s version number and exit
</pre></div>
</div>
<p><strong>All Command-Line Options (git-remote-rns)</strong></p>
<p>The <code class="docutils literal notranslate"><span class="pre">git-remote-rns</span></code> helper is automatically invoked by Git when interacting with <code class="docutils literal notranslate"><span class="pre">rns://</span></code> URLs. It is not typically run directly by users, but accepts the following environment variables for configuration:</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">RNGIT_CONFIG</span></code> - Path to alternative client configuration directory</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">RNS_CONFIG</span></code> - Path to alternative Reticulum configuration directory</p></li>
</ul>
<p>The client configuration file is located at <code class="docutils literal notranslate"><span class="pre">~/.rngit/client_config</span></code> and allows adjusting parameters such as the reference batch size for transfers.</p>
</section>
<section id="repository-structure">
<h2>Repository Structure<a class="headerlink" href="#repository-structure" title="Link to this heading"></a></h2>
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node organizes repositories into groups. Each group is a directory containing bare Git repositories. The repository path format is <code class="docutils literal notranslate"><span class="pre">group_name/repo_name</span></code>. For example, a repository at <code class="docutils literal notranslate"><span class="pre">/var/git/public/myrepo</span></code> would be accessible as <code class="docutils literal notranslate"><span class="pre">public/myrepo</span></code> via the URL <code class="docutils literal notranslate"><span class="pre">rns://DESTINATION_HASH/public/myrepo</span></code>.</p>
<p><strong>Configuration</strong></p>
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node configuration file is located at <code class="docutils literal notranslate"><span class="pre">~/.rngit/config</span></code> (or <code class="docutils literal notranslate"><span class="pre">/etc/rngit/config</span></code> for system-wide installations). The default configuration includes:</p>
<ul class="simple">
<li><p>Repository group paths defining where to find bare repositories</p></li>
<li><p>Access permissions for groups and individual repositories</p></li>
<li><p>Announce intervals for network visibility</p></li>
<li><p>Optional statistics recording for repository activity</p></li>
</ul>
<p>Access permissions can be configured at the group level in the config file, or per-repository using <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files. Permissions use the format <code class="docutils literal notranslate"><span class="pre">permission:target</span></code> where permission is <code class="docutils literal notranslate"><span class="pre">r</span></code> (read), <code class="docutils literal notranslate"><span class="pre">w</span></code> (write), <code class="docutils literal notranslate"><span class="pre">rw</span></code> (read/write), <code class="docutils literal notranslate"><span class="pre">c</span></code> (create) or <code class="docutils literal notranslate"><span class="pre">s</span></code> (stats) and target is <code class="docutils literal notranslate"><span class="pre">all</span></code>, <code class="docutils literal notranslate"><span class="pre">none</span></code>, or a specific identity hash.</p>
<p>The <code class="docutils literal notranslate"><span class="pre">s</span></code> (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set <code class="docutils literal notranslate"><span class="pre">record_stats</span> <span class="pre">=</span> <span class="pre">yes</span></code> in the <code class="docutils literal notranslate"><span class="pre">[rngit]</span></code> section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to <code class="docutils literal notranslate"><span class="pre">stats_ignore_identities</span></code>.</p>
<p>Repository-specific <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files can be static text files or executable scripts that output permission rules to stdout. A <code class="docutils literal notranslate"><span class="pre">group.allowed</span></code> file in a repository group directory applies to all repositories within that group.</p>
</section>
<section id="serving-pages-over-nomad-network">
<h2>Serving Pages Over Nomad Network<a class="headerlink" href="#serving-pages-over-nomad-network" title="Link to this heading"></a></h2>
<p>In addition to providing Git repository access via the Git remote helper protocol, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> can also run a <a class="reference external" href="https://github.com/markqvist/nomadnet">Nomad Network</a> compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.</p>
<p>When enabled, the page node provides a complete interface to your repositories, with automatic Markdown to Micron conversion, syntax-highlighted code browsing, and detailed commit, diff and statistics views.</p>
<p><strong>Enabling the Git Page Node</strong></p>
<p>To enable the page node, add the following to your <code class="docutils literal notranslate"><span class="pre">~/.rngit/config</span></code> file:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[pages]
serve_nomadnet = yes
</pre></div>
</div>
<p>When the page node is enabled, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> will listen on a Nomad Network node destination in addition to the Git repository destination. You can view the destination hash by running:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit --print-identity
Git Peer Identity : &lt;959e10e5efc1bd9d97a4083babe51dea&gt;
Repository Node Identity : &lt;153cb870b4665b8c1c348896292b0bad&gt;
Repositories Destination : &lt;0d7334d411d00120cbad24edf355fdd2&gt;
Nomad Network Destination : &lt;50824b711717f97c2fb1166ceddd5ea9&gt;
</pre></div>
</div>
<p><strong>Accessing Repository Pages</strong></p>
<p>Once the page node is running, you can access it from any Nomad Network client by connecting to the Nomad Network destination. The page node provides the following views:</p>
<ul class="simple">
<li><p><strong>Front Page</strong> - Lists all repository groups accessible to your identity</p></li>
<li><p><strong>Group Page</strong> - Shows all repositories within a group</p></li>
<li><p><strong>Repository Page</strong> - Displays repository overview, description and README</p></li>
<li><p><strong>Releases</strong> - List of releases for the repository, with information and downloads</p></li>
<li><p><strong>File Browser</strong> - Browse directory trees and view and download file contents</p></li>
<li><p><strong>Commits View</strong> - View commit history with pagination</p></li>
<li><p><strong>Commit Details</strong> - Detailed commit information with file changes and diffs</p></li>
<li><p><strong>Refs View</strong> - List branches and tags</p></li>
<li><p><strong>Statistics</strong> - Activity charts showing views, fetches and pushes over time</p></li>
</ul>
<p>All pages respect the same permission system used for Git access. If an identity does not have read access to a repository, they will not be able to view its pages.</p>
</section>
<section id="formatting-syntax-highlighting">
<h2>Formatting &amp; Syntax Highlighting<a class="headerlink" href="#formatting-syntax-highlighting" title="Link to this heading"></a></h2>
<p>If the <code class="docutils literal notranslate"><span class="pre">pygments</span></code> Python module is installed on your system, the page node will automatically apply syntax highlighting to code files. The highlighting supports a wide range of programming languages and uses a color theme optimized for terminal display.</p>
<p>To enable syntax highlighting, install pygments:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>pip install pygments
</pre></div>
</div>
<p><strong>Markdown &amp; Micron Support</strong></p>
<p>README files and other Markdown documents are automatically converted to Micron markup for display in Nomad Network clients. You can also write your README files directly in Micron, in which case they will display and render as such in any Nomad Network client. The file browser also supports viewing both rendered and raw Markdown and Micron documents.</p>
<p>Code blocks in Markdown can include language hints for syntax highlighting:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>```python
def hello_world():
print(&quot;Hello, Reticulum!&quot;)
```
</pre></div>
</div>
</section>
<section id="customizing-templates">
<h2>Customizing Templates<a class="headerlink" href="#customizing-templates" title="Link to this heading"></a></h2>
<p>The page node uses a template system that allows complete customization of the generated pages. Templates are stored in the <code class="docutils literal notranslate"><span class="pre">~/.rngit/templates/</span></code> directory as Micron files.</p>
<p>The following template files are supported:</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">base.mu</span></code> - Base template wrapping all pages</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">front.mu</span></code> - Front page listing all groups</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">group.mu</span></code> - Group page listing repositories</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">repo.mu</span></code> - Repository overview page</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">releases.mu</span></code> - Release list page</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">release.mu</span></code> - Release details page</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">tree.mu</span></code> - File browser pages</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">blob.mu</span></code> - File content display</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">commits.mu</span></code> - Commit history listing</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">commit.mu</span></code> - Individual commit detail page</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">refs.mu</span></code> - Branches and tags listing</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">stats.mu</span></code> - Statistics page</p></li>
</ul>
<p>Templates can include the following variables:</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">{PAGE_CONTENT}</span></code> - The main content of the page (required)</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">{NODE_NAME}</span></code> - The configured node name</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">{NAVIGATION}</span></code> - Breadcrumb navigation links</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">{VERSION}</span></code> - The rngit version number</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">{GEN_TIME}</span></code> - Page generation time</p></li>
</ul>
<p><strong>Dynamic Templates</strong></p>
<p>Templates can be made executable to generate dynamic content. If a template file has the executable bit set, it will be executed and its stdout used as the template content.</p>
<p><strong>Icon Sets</strong></p>
<p>By default, the page node uses Nerd Font icons. If you prefer simpler icons or your terminal does not support Nerd Fonts, you can enable Unicode icons instead:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[pages]
serve_nomadnet = yes
unicode_icons = yes
</pre></div>
</div>
<p><strong>Repository Statistics</strong></p>
<p>When statistics recording is enabled (see the <code class="docutils literal notranslate"><span class="pre">record_stats</span></code> configuration option), the page node can display activity charts for each repository. The statistics page shows:</p>
<ul class="simple">
<li><p>Total and peak views, fetches and pushes</p></li>
<li><p>Daily activity charts over a 90-day period</p></li>
<li><p>Combined activity visualization</p></li>
</ul>
<p>To view statistics, a user must have the <code class="docutils literal notranslate"><span class="pre">s</span></code> (stats) permission for the repository. See the Access Configuration section for details on setting permissions.</p>
<p><strong>Repository Thanks</strong></p>
<p>The page node includes a “Thanks” feature that allows users to express appreciation for a repository. On each repository page, a “Thanks” link is displayed showing the current thanks count. Clicking this link registers a thank you for the repository.</p>
<p><strong>Configuration Example</strong></p>
<p>A complete page node configuration might look like this:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[rngit]
node_name = My Git Node
announce_interval = 360
record_stats = yes
[repositories]
public = /var/git/public
internal = /var/git/internal
[access]
public = r:all
internal = rw:9710b86ba12c42d1d8f30f74fe509286
[pages]
serve_nomadnet = yes
unicode_icons = no
</pre></div>
</div>
</section>
<section id="release-management">
<h2>Release Management<a class="headerlink" href="#release-management" title="Link to this heading"></a></h2>
<p>In addition to hosting Git repositories, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> provides a complete release management system. This allows you to publish versioned releases with associated artifacts, release notes and metadata. Releases are managed through the <code class="docutils literal notranslate"><span class="pre">rngit</span> <span class="pre">release</span></code> subcommand, and are also viewable through the Nomad Network page interface.</p>
<p><strong>The Release Workflow</strong></p>
<p>Creating a release involves specifying a Git tag and a directory containing build artifacts or other files to distribute. The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> client will open your configured <code class="docutils literal notranslate"><span class="pre">$EDITOR</span></code> to compose release notes, then upload all artifacts to the remote repository node.</p>
<p>To create a release, specify the tag name and path to artifacts:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo create v1.2.0:./dist
</pre></div>
</div>
<p>This will:</p>
<ol class="arabic simple">
<li><p>Verify that the tag <code class="docutils literal notranslate"><span class="pre">v1.2.0</span></code> exists in the repository</p></li>
<li><p>Open your editor to write release notes</p></li>
<li><p>Upload all files from the <code class="docutils literal notranslate"><span class="pre">./dist</span></code> directory</p></li>
<li><p>Publish the release</p></li>
</ol>
<p>If no <code class="docutils literal notranslate"><span class="pre">$EDITOR</span></code> environment variable is set, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> will try to use <code class="docutils literal notranslate"><span class="pre">nano</span></code>, <code class="docutils literal notranslate"><span class="pre">vim</span></code> or <code class="docutils literal notranslate"><span class="pre">vi</span></code>. The editor will show a template with instructions. Lines starting with <code class="docutils literal notranslate"><span class="pre">#</span></code> will be ignored, and if the remaining content is empty after stripping comments, the release creation will be cancelled.</p>
<p><strong>Release Storage &amp; Structure</strong></p>
<p>Releases are stored on the node in a directory named <code class="docutils literal notranslate"><span class="pre">repo_name.releases</span></code> next to the bare repository. Each release is a subdirectory containing:</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">META</span></code> - Release metadata in ConfigObj format</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">RELEASE.md</span></code> or <code class="docutils literal notranslate"><span class="pre">RELEASE.mu</span></code> - Release notes</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">artifacts/</span></code> - All uploaded files</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">THANKS</span></code> - Appreciation count from users</p></li>
</ul>
<p><strong>Listing Releases</strong></p>
<p>To view all releases for a repository:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list
Tag Status Created Objs Notes
------------------------------------------------------------------
v1.2.0 published 2025-01-15 14:32 3 Another release
v1.1.0 published 2024-12-03 09:15 2 Bug fix release
v1.0.0 published 2024-10-20 16:45 2 Initial release
</pre></div>
</div>
<p><strong>Viewing Release Details</strong></p>
<p>To see full information about a specific release:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo view v1.2.0
Release : 0.9.2
Status : published
Created : 2026-05-04 23:53:09
Thanks : 5
Release Notes
=============
Version 1.2.0 release notes...
Artifacts (4)
=============
- myapp-1.2.0.tar.gz (1.5 MB)
- myapp-1.2.0.zip (1.6 MB)
- checksums.txt (256 B)
</pre></div>
</div>
<p><strong>Deleting Releases</strong></p>
<p>To remove a release:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo delete v1.2.0
Are you sure you want to delete release &#39;v1.2.0&#39;? [y/N]: y
Release v1.2.0 deleted
</pre></div>
</div>
<p><strong>Requirements &amp; Validation</strong></p>
<ul class="simple">
<li><p>The specified tag must exist in the remote repository</p></li>
<li><p>You must have <code class="docutils literal notranslate"><span class="pre">release</span></code> permission for the repository</p></li>
<li><p>The target artifacts directory must exist and contain at least one file</p></li>
<li><p>Release notes cannot be empty</p></li>
</ul>
<p><strong>Permissions</strong></p>
<p>Release management requires the <code class="docutils literal notranslate"><span class="pre">release</span></code> permission, configured the same way as other repository permissions. In the config file or <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files, use <code class="docutils literal notranslate"><span class="pre">rel:target</span></code> to grant release management rights:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span># In .allowed file or config
rel:all # Allow everyone
rel:9710b86... # Allow specific identity
rel:none # Deny everyone
</pre></div>
</div>
<p><strong>Nomad Network Interface</strong></p>
<p>When the Nomad Network page node is enabled, releases are displayed on a dedicated releases page for each repository. Each release is listed with its tag, creation date, artifact count and a preview of the release notes. Clicking a release shows the full details including formatted release notes and a listing of all artifacts with their sizes.</p>
<p>Only releases with <code class="docutils literal notranslate"><span class="pre">published</span></code> status are visible through the Nomad Network interface. Draft releases (if supported in future implementations) would only be visible through the command-line interface.</p>
<p><strong>All Command-Line Options (rngit release)</strong></p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rngit release [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
[-i PATH] [-v] [-q] [--version]
[repository] [operation] [target]
Reticulum Git Release Manager
positional arguments:
repository URL of remote repository
operation list, view, create or delete
target tag and path to release artifacts directory
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-i, --identity PATH path to release identity
-v, --verbose
-q, --quiet
--version show program&#39;s version number and exit
</pre></div>
</div>
</section>
<section id="work-documents">
<h2>Work Documents<a class="headerlink" href="#work-documents" title="Link to this heading"></a></h2>
<p>In addition to releases, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> provides a work document management system for tracking tasks, investigations, issues and progress related to repositories. Work documents are stored as structured msgpack data and support threaded updates and comments.</p>
<p><strong>Listing Work Documents</strong></p>
<p>To view work documents for a repository:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list
Active documents
=================
ID Title Author Created Comments
---------------------------------------------------------------------------
1 Implemented new feature 9710b86ba12c4f2e… 2025-01-15 14:32 3
2 Fixed bug in parser 8f3a21c9d84e927b… 2025-01-14 09:15 1
</pre></div>
</div>
<p>Use <code class="docutils literal notranslate"><span class="pre">--scope</span> <span class="pre">completed</span></code> to view completed work documents, or <code class="docutils literal notranslate"><span class="pre">--scope</span> <span class="pre">all</span></code> to see both active and completed.</p>
<p><strong>Viewing a Work Document</strong></p>
<p>To view a specific work document with all its comments:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo view -d 1
Implement new feature (active #1)
=================================
Author : 9710b86ba12c42d1d8f30f74fe509286
Status : active
Created : 2026-05-05 15:11:11
Edited : 2026-05-05 18:22:11
Format : markdown
Updates : 0
This work document tracks the implementation of the new feature...
Updates
=======
#1 by 9710b86ba12c42d1d8f30f74fe509286 at 2026-05-05 15:38:37
-------------------------------------------------------------
Initial analysis complete
</pre></div>
</div>
<p><strong>Creating Work Documents</strong></p>
<p>To create a new work document:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo create --title &quot;Investigate performance issue&quot;
</pre></div>
</div>
<p>This will open your configured <code class="docutils literal notranslate"><span class="pre">$EDITOR</span></code> to compose the document content. Save and exit to create the document, or save an empty document to cancel.</p>
<p><strong>Editing Work Documents</strong></p>
<p>To edit an existing work document:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo edit -d 1
</pre></div>
</div>
<p>This fetches the current content, opens it in your editor, and sends any changes back to the node.</p>
<p><strong>Adding Comments</strong></p>
<p>To add an update to a work document:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo update -d 1
</pre></div>
</div>
<p>This opens your editor to compose the update.</p>
<p><strong>Completing Work Documents</strong></p>
<p>To mark a work document as completed (moving it from <code class="docutils literal notranslate"><span class="pre">active</span></code> to <code class="docutils literal notranslate"><span class="pre">completed</span></code>):</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo complete -d 1
Work document #1 completed
</pre></div>
</div>
<p><strong>Activating Work Documents</strong></p>
<p>To mark a work document as active (moving it from <code class="docutils literal notranslate"><span class="pre">completed</span></code> to <code class="docutils literal notranslate"><span class="pre">active</span></code>):</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo activate -d 1
Work document #1 activated
</pre></div>
</div>
<p><strong>Deleting Work Documents</strong></p>
<p>To delete a work document and all its comments:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo delete -id 1
Are you sure you want to delete active work document #1? [y/N]: y
Work document #1 deleted
</pre></div>
</div>
<p><strong>Permissions</strong></p>
<p>Users can view work documents and updates if the have <code class="docutils literal notranslate"><span class="pre">read</span></code> permission for the repository. If users have <code class="docutils literal notranslate"><span class="pre">read</span></code> and <code class="docutils literal notranslate"><span class="pre">interact</span></code>, they can also post updates/comments on existing work documents. Work document management requires having <code class="docutils literal notranslate"><span class="pre">write</span></code> and <code class="docutils literal notranslate"><span class="pre">interact</span></code> permission to the repository. These permissions are configured the same way as any other repository permissions. In the config file or <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files, use <code class="docutils literal notranslate"><span class="pre">i:target</span></code> to grant work document interaction rights:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span># In .allowed file or config
i:all # Allow everyone
i:9710b86... # Allow specific identity
i:none # Deny everyone
</pre></div>
</div>
<p><strong>Author Verification</strong></p>
<p>Users can only edit or delete work documents and updates they created. The author is cryptographically verified from the interacting links <code class="docutils literal notranslate"><span class="pre">remote_identity</span></code>.</p>
<p><strong>Storage Format</strong></p>
<p>Work documents are stored in a <code class="docutils literal notranslate"><span class="pre">repo_name.work</span></code> directory next to the repository, containing:</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">active/</span></code> - Active work documents</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">completed/</span></code> - Completed work documents</p></li>
</ul>
<p>Each document is a numbered directory containing:</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">root</span></code> - The work document content and metadata (msgpack format)</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">N</span></code> - Numbered comment files (msgpack format)</p></li>
</ul>
<p><strong>Nomad Network Interface</strong></p>
<p>When the Nomad Network page node is enabled, work documents are viewable through the web interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.</p>
<p><strong>All Command-Line Options (rngit work)</strong></p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rngit work [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
[-i PATH] [--scope SCOPE] [-t TITLE] [-d ID] [-v]
[-q] [--version]
[repository] [operation]
Reticulum Git Work Document Manager
positional arguments:
repository URL of remote repository
operation list, view, create, edit, delete, update or complete
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-i, --identity PATH path to identity
--scope SCOPE document scope: active, completed or all
-t, --title TITLE document title for create
-d, --id ID document ID
-v, --verbose
-q, --quiet
--version show program&#39;s version number and exit
</pre></div>
</div>
</section>
</section>
</article>
</div>
<footer>
<div class="related-pages">
<a class="next-page" href="support.html">
<div class="page-info">
<div class="context">
<span>Next</span>
</div>
<div class="title">Support 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 &#169; 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="#">Git Over Reticulum</a><ul>
<li><a class="reference internal" href="#the-rngit-utility">The rngit Utility</a></li>
<li><a class="reference internal" href="#repository-structure">Repository Structure</a></li>
<li><a class="reference internal" href="#serving-pages-over-nomad-network">Serving Pages Over Nomad Network</a></li>
<li><a class="reference internal" href="#formatting-syntax-highlighting">Formatting &amp; Syntax Highlighting</a></li>
<li><a class="reference internal" href="#customizing-templates">Customizing Templates</a></li>
<li><a class="reference internal" href="#release-management">Release Management</a></li>
<li><a class="reference internal" href="#work-documents">Work Documents</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
<script src="_static/clipboard.min.js?v=a7894cd8"></script>
<script src="_static/copybutton.js?v=f281be69"></script>
</body>
</html>
+5 -4
View File
@@ -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.1.7 documentation</title>
<title>Communications Hardware - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -674,7 +675,7 @@ can be used with Reticulum. This includes virtual software modems such as
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+19 -7
View File
@@ -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.1.7 documentation</title>
<title>Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="#"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="#"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -377,8 +378,6 @@ to participate in the development of Reticulum itself.</p>
<li class="toctree-l3"><a class="reference internal" href="software.html#retipedia">Retipedia</a></li>
<li class="toctree-l3"><a class="reference internal" href="software.html#sideband">Sideband</a></li>
<li class="toctree-l3"><a class="reference internal" href="software.html#meshchatx">MeshChatX</a></li>
<li class="toctree-l3"><a class="reference internal" href="software.html#meshchat">MeshChat</a></li>
<li class="toctree-l3"><a class="reference internal" href="software.html#columba">Columba</a></li>
<li class="toctree-l3"><a class="reference internal" href="software.html#reticulum-relay-chat">Reticulum Relay Chat</a></li>
<li class="toctree-l3"><a class="reference internal" href="software.html#retibbs">RetiBBS</a></li>
<li class="toctree-l3"><a class="reference internal" href="software.html#rbrowser">RBrowser</a></li>
@@ -393,7 +392,7 @@ to participate in the development of Reticulum itself.</p>
</li>
<li class="toctree-l2"><a class="reference internal" href="software.html#protocols">Protocols</a><ul>
<li class="toctree-l3"><a class="reference internal" href="software.html#lxmf">LXMF</a></li>
<li class="toctree-l3"><a class="reference internal" href="software.html#id17">LXST</a></li>
<li class="toctree-l3"><a class="reference internal" href="software.html#id16">LXST</a></li>
<li class="toctree-l3"><a class="reference internal" href="software.html#rrc">RRC</a></li>
</ul>
</li>
@@ -409,7 +408,9 @@ 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>
@@ -508,6 +509,7 @@ to participate in the development of Reticulum itself.</p>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#interfaces-modes">Interface Modes</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#announce-rate-control">Announce Rate Control</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#new-destination-rate-limiting">New Destination Rate Limiting</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#path-request-burst-control">Path Request Burst Control</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a><ul>
@@ -521,6 +523,16 @@ to participate in the development of Reticulum itself.</p>
</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-structure">Repository Structure</a></li>
<li class="toctree-l2"><a class="reference internal" href="git.html#serving-pages-over-nomad-network">Serving Pages Over Nomad Network</a></li>
<li class="toctree-l2"><a class="reference internal" href="git.html#formatting-syntax-highlighting">Formatting &amp; Syntax Highlighting</a></li>
<li class="toctree-l2"><a class="reference internal" href="git.html#customizing-templates">Customizing Templates</a></li>
<li class="toctree-l2"><a class="reference internal" href="git.html#release-management">Release Management</a></li>
<li class="toctree-l2"><a class="reference internal" href="git.html#work-documents">Work Documents</a></li>
</ul>
</li>
<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>
@@ -631,7 +643,7 @@ to participate in the development of Reticulum itself.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+111 -22
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Configuring Interfaces - Reticulum Network Stack 1.1.7 documentation</title>
<title>Configuring Interfaces - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">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="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>
@@ -1457,11 +1458,13 @@ please see the <a class="reference internal" href="understanding.html#understand
<section id="announce-rate-control">
<span id="interfaces-announcerates"></span><h2>Announce Rate Control<a class="headerlink" href="#announce-rate-control" title="Link to this heading"></a></h2>
<p>The built-in announce control mechanisms and the default <code class="docutils literal notranslate"><span class="pre">announce_cap</span></code>
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
<code class="docutils literal notranslate"><span class="pre">announce_rate_target</span></code>, <code class="docutils literal notranslate"><span class="pre">announce_rate_grace</span></code> and <code class="docutils literal notranslate"><span class="pre">announce_rate_penalty</span></code>
options, this can be done on a per-interface basis, and moderates the <em>rate at
which received announces are re-broadcasted to other interfaces</em>.</p>
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.</p>
<p>Using the <code class="docutils literal notranslate"><span class="pre">announce_rate_target</span></code>, <code class="docutils literal notranslate"><span class="pre">announce_rate_grace</span></code> and <code class="docutils literal notranslate"><span class="pre">announce_rate_penalty</span></code>
options, this can be done on a per-interface basis, or by setting instance-wide defaults.
When configured, this moderates the <em>rate at which received announces are
re-broadcasted to other interfaces</em>.</p>
<blockquote>
<div><ul>
<li><div class="line-block">
@@ -1488,18 +1491,31 @@ destination in question will only have its announces propagated every
</li>
</ul>
</div></blockquote>
<p>You can also configure default announce rate parameters for all interfaces that
do not have these parameters set explicitly by setting the <code class="docutils literal notranslate"><span class="pre">default_ar_target</span></code>
<code class="docutils literal notranslate"><span class="pre">default_ar_penalty</span></code> and <code class="docutils literal notranslate"><span class="pre">default_ar_grace</span></code> options in the <code class="docutils literal notranslate"><span class="pre">[reticulum]</span></code>
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.</p>
<p>For auto-connected interfaces, sensible default announce rate control parameters
will <strong>always</strong> 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.</p>
<p>These mechanisms, in conjunction with the <code class="docutils literal notranslate"><span class="pre">annouce_cap</span></code> 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.</p>
<p>Current statistics and information about announce rates can be viewed using the
<code class="docutils literal notranslate"><span class="pre">rnpath</span> <span class="pre">-r</span></code> command.</p>
<p>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 will be for your destinations to make it into slower networks, or networks that
are many hops away.</p>
<p>Statistics and information about announce rates can be viewed using the
<code class="docutils literal notranslate"><span class="pre">rnpath</span> <span class="pre">-r</span></code> and <code class="docutils literal notranslate"><span class="pre">rnstatus</span> <span class="pre">-A</span></code> commands.</p>
<p>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.</p>
<p>If youre designing an application where you think you need to annonuce more
often than once an hour, youre most likely doing something wrong.</p>
<p>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 <em>co-exist</em> and interconnect.</p>
</section>
<section id="new-destination-rate-limiting">
@@ -1517,11 +1533,14 @@ also means, that should a node decide to connect to a public interface, announce
a large amount of bogus destinations, and then disconnect, these destination will
never make it into path tables and waste network bandwidth on retransmitted
announces.</p>
<p><strong>Its important to note</strong> that the ingress control works at the level of <em>individual
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>Its important to remember that the ingress control works at the level of <em>individual
sub-interfaces</em>. As an example, this means that one client on a <a class="reference internal" href="#interfaces-tcps"><span class="std std-ref">TCP Server Interface</span></a>
cannot disrupt processing of incoming announces for other connected clients on the same
<a class="reference internal" href="#interfaces-tcps"><span class="std std-ref">TCP Server Interface</span></a>. All other clients on the same interface will still have new announces
processed without interruption.</p>
<a class="reference internal" href="#interfaces-tcps"><span class="std std-ref">TCP Server Interface</span></a>. All other clients on the same interface
will still have new announces processed without interruption.</p>
</div>
<p>By default, Reticulum will handle this automatically, and ingress announce
control will be enabled on interface where it is sensible to do so. It should
generally not be neccessary to modify the ingress control configuration,
@@ -1530,8 +1549,7 @@ but all the parameters are exposed for configuration if needed.</p>
<div><ul>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ingress_control</span></code> option tells Reticulum whether or not
to enable announce ingress control on the interface. Defaults to
<code class="docutils literal notranslate"><span class="pre">True</span></code>.</div>
to enable ingress control on the interface. Defaults to <code class="docutils literal notranslate"><span class="pre">True</span></code>.</div>
</div>
</li>
<li><div class="line-block">
@@ -1586,6 +1604,76 @@ to <code class="docutils literal notranslate"><span class="pre">30</span></code>
</li>
</ul>
</div></blockquote>
<p>All of the above settings can be configured both as instance-wide defaults
under the <code class="docutils literal notranslate"><span class="pre">[reticulum]</span></code> section of the configuration file, or on a per-
interface basis under the relevant interface configuration section.</p>
</section>
<section id="path-request-burst-control">
<h2>Path Request Burst Control<a class="headerlink" href="#path-request-burst-control" title="Link to this heading"></a></h2>
<p>In addition the announce controls for newly created destination, Reticulum will also
monitor incoming path request activity, and enforce burst controls if per-client rates
exceed configured limits. Once path request burst control is activated on an
interface, path requests will no longer be propagated further on the network.
As with announce burst control, this happens on a per sub-interface basis. One
client connecting to a public gateway will not be able to disrupt path request
processing for other clients.</p>
<div class="admonition warning">
<p class="admonition-title">Warning</p>
<p>Applications that send large amounts of unnecessary path requests will very
quickly get rate limited by transport nodes, and the entire system they are
running on will not be able to resolve any paths on the network, until the
burst subsides and hold period expires. <strong>Do not</strong> write applications like
this. Only request paths for destinations you need to communicate with.</p>
</div>
<p>By default, Reticulum will handle this automatically, and ingress path request
control will be enabled on interface where it is sensible to do so. It should
generally not be neccessary to modify the ingress control configuration,
but all the parameters are exposed for configuration if needed.</p>
<blockquote>
<div><ul>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ingress_control</span></code> option tells Reticulum whether or not
to enable ingress control on the interface. Defaults to <code class="docutils literal notranslate"><span class="pre">True</span></code>.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ic_new_time</span></code> option configures how long (in seconds) an
interface is considered newly spawned. Defaults to <code class="docutils literal notranslate"><span class="pre">2*60*60</span></code> seconds. This
option is useful on publicly accessible interfaces that spawn new
sub-interfaces when a new client connects.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ic_pr_burst_freq_new</span></code> option sets the maximum path request
ingress frequency for newly spawned interfaces. Defaults to <code class="docutils literal notranslate"><span class="pre">3</span></code>
path requests per second.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ic_pr_burst_freq</span></code> option sets the maximum path request
ingress frequency for other interfaces. Defaults to <code class="docutils literal notranslate"><span class="pre">8</span></code> path requests
per second.</div>
</div>
<blockquote>
<div><p><em>If an interface exceeds its burst frequency, incoming path requests
from that system will not traverse the network further.</em></p>
</div></blockquote>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">egress_control</span></code> option enables hard-limiting path request egress
control per-interface. Defaults to <code class="docutils literal notranslate"><span class="pre">False</span></code></div>
</div>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ec_pr_freq</span></code> option sets the hard limit for outbound path requests
per second on a given interface.</div>
</div>
</li>
</ul>
</div></blockquote>
<p>All of the above settings can be configured both as instance-wide defaults
under the <code class="docutils literal notranslate"><span class="pre">[reticulum]</span></code> section of the configuration file, or on a per-
interface basis under the relevant interface configuration section.</p>
</section>
</section>
@@ -1673,6 +1761,7 @@ to <code class="docutils literal notranslate"><span class="pre">30</span></code>
<li><a class="reference internal" href="#interfaces-modes">Interface Modes</a></li>
<li><a class="reference internal" href="#announce-rate-control">Announce Rate Control</a></li>
<li><a class="reference internal" href="#new-destination-rate-limiting">New Destination Rate Limiting</a></li>
<li><a class="reference internal" href="#path-request-burst-control">Path Request Burst Control</a></li>
</ul>
</li>
</ul>
@@ -1684,7 +1773,7 @@ to <code class="docutils literal notranslate"><span class="pre">30</span></code>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+5 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Reticulum License - Reticulum Network Stack 1.1.7 documentation</title>
<title>Reticulum License - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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 current current-page"><a class="current reference internal" href="#">Reticulum License</a></li>
@@ -343,7 +344,7 @@ SOFTWARE.
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+8 -7
View File
@@ -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="Support Reticulum" href="support.html"><link rel="prev" title="Configuring Interfaces" href="interfaces.html">
<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="Configuring Interfaces" href="interfaces.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>Building Networks - Reticulum Network Stack 1.1.7 documentation</title>
<title>Building Networks - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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 current current-page"><a class="current reference internal" href="#">Building Networks</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>
@@ -593,12 +594,12 @@ differently than a mobile device roaming between radio cells.</p>
<footer>
<div class="related-pages">
<a class="next-page" href="support.html">
<a class="next-page" href="git.html">
<div class="page-info">
<div class="context">
<span>Next</span>
</div>
<div class="title">Support Reticulum</div>
<div class="title">Git Over Reticulum</div>
</div>
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
</a>
@@ -662,7 +663,7 @@ differently than a mobile device roaming between radio cells.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
Binary file not shown.
+22 -10
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>API Reference - Reticulum Network Stack 1.1.7 documentation</title>
<title>API Reference - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -303,12 +304,8 @@ the default value.</p>
<dl class="py attribute">
<dt class="sig sig-object py" id="RNS.Reticulum.LINK_MTU_DISCOVERY">
<span class="sig-name descname"><span class="pre">LINK_MTU_DISCOVERY</span></span><em class="property"><span class="w"> </span><span class="p"><span class="pre">=</span></span><span class="w"> </span><span class="pre">True</span></em><a class="headerlink" href="#RNS.Reticulum.LINK_MTU_DISCOVERY" title="Link to this definition"></a></dt>
<dd><p>Whether automatic link MTU discovery is enabled by default in this
release. Link MTU discovery significantly increases throughput over
fast links, but requires all intermediary hops to also support it.
Support for this feature was added in RNS version 0.9.0. This option
will become enabled by default in the near future. Please update your
RNS instances.</p>
<dd><p>Whether automatic link MTU discovery is enabled by default. Link MTU
discovery significantly increases throughput over fast links.</p>
</dd></dl>
<dl class="py attribute">
@@ -642,6 +639,20 @@ communication for the identity. Be very careful with this method.</p>
</dl>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="RNS.Identity.pub_to_file">
<span class="sig-name descname"><span class="pre">pub_to_file</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">path</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.pub_to_file" title="Link to this definition"></a></dt>
<dd><p>Saves the public identity to a file.</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
<dd class="field-odd"><p><strong>path</strong> The full path specifying where to save the identity.</p>
</dd>
<dt class="field-even">Returns<span class="colon">:</span></dt>
<dd class="field-even"><p>True if the file was saved, otherwise False.</p>
</dd>
</dl>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="RNS.Identity.get_private_key">
<span class="sig-name descname"><span class="pre">get_private_key</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.get_private_key" title="Link to this definition"></a></dt>
@@ -2305,6 +2316,7 @@ will announce it.</p>
<li><a class="reference internal" href="#RNS.Identity.from_bytes"><code class="docutils literal notranslate"><span class="pre">from_bytes()</span></code></a></li>
<li><a class="reference internal" href="#RNS.Identity.from_file"><code class="docutils literal notranslate"><span class="pre">from_file()</span></code></a></li>
<li><a class="reference internal" href="#RNS.Identity.to_file"><code class="docutils literal notranslate"><span class="pre">to_file()</span></code></a></li>
<li><a class="reference internal" href="#RNS.Identity.pub_to_file"><code class="docutils literal notranslate"><span class="pre">pub_to_file()</span></code></a></li>
<li><a class="reference internal" href="#RNS.Identity.get_private_key"><code class="docutils literal notranslate"><span class="pre">get_private_key()</span></code></a></li>
<li><a class="reference internal" href="#RNS.Identity.get_public_key"><code class="docutils literal notranslate"><span class="pre">get_public_key()</span></code></a></li>
<li><a class="reference internal" href="#RNS.Identity.load_private_key"><code class="docutils literal notranslate"><span class="pre">load_private_key()</span></code></a></li>
@@ -2472,7 +2484,7 @@ will announce it.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+5 -4
View File
@@ -8,7 +8,7 @@
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<meta name="robots" content="noindex" />
<title>Search - Reticulum Network Stack 1.1.7 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<title>Search - Reticulum Network Stack 1.2.6 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="#" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -302,7 +303,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
File diff suppressed because one or more lines are too long
+9 -30
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Programs Using Reticulum - Reticulum Network Stack 1.1.7 documentation</title>
<title>Programs Using Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -327,31 +328,11 @@ plugin system for expandability.</p>
</section>
<section id="meshchatx">
<h3>MeshChatX<a class="headerlink" href="#meshchatx" title="Link to this heading"></a></h3>
<p>A <a class="reference external" href="https://git.quad4.io/RNS-Things/MeshChatX">Reticulum MeshChat fork from the future</a>, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original Reticulum MeshChat project, and is not affiliated with the original project.</p>
<p>A <a class="reference external" href="https://git.quad4.io/RNS-Things/MeshChatX">Reticulum MeshChat fork from the future</a>, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original <a class="reference external" href="https://github.com/liamcottle/reticulum-meshchat">Reticulum MeshChat</a> project, and is not affiliated with the original project, but is a much more up-to-date, comprehensive and well-maintained fork.</p>
<a class="reference external image-reference" href="https://git.quad4.io/RNS-Things/MeshChatX"><img alt="_images/meshchatx.webp" class="align-center" src="_images/meshchatx.webp" />
</a>
<p>Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security.</p>
</section>
<section id="meshchat">
<h3>MeshChat<a class="headerlink" href="#meshchat" title="Link to this heading"></a></h3>
<p>The <a class="reference external" href="https://github.com/liamcottle/reticulum-meshchat">Reticulum MeshChat</a> application
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
page browser and other interesting functionality.</p>
<a class="reference external image-reference" href="https://github.com/liamcottle/reticulum-meshchat"><img alt="_images/meshchat_1.webp" class="align-center" src="_images/meshchat_1.webp" />
</a>
<p>Reticulum MeshChat is of course also compatible with Sideband and Nomad Network, or
any other LXMF client.</p>
</section>
<section id="columba">
<h3>Columba<a class="headerlink" href="#columba" title="Link to this heading"></a></h3>
<p><a class="reference external" href="https://github.com/torlando-tech/columba/">Columba</a> is a simple and familiar LXMF
messaging app Android, built with a native Android interface and Material Design 3.</p>
<a class="reference external image-reference" href="https://github.com/torlando-tech/columba/"><img alt="_images/columba.webp" class="align-center" src="_images/columba.webp" style="width: 25%;" />
</a>
<p>While still in early and very active development, it is of course also compatible
with all other LXMF clients, and allows you to message seamlessly with anyone else
using LXMF.</p>
</section>
<section id="reticulum-relay-chat">
<h3>Reticulum Relay Chat<a class="headerlink" href="#reticulum-relay-chat" title="Link to this heading"></a></h3>
<p><a class="reference external" href="https://rrc.kc1awv.net/">Reticulum Relay Chat</a> is a live chat system built on top of the Reticulum Network Stack. It exists to let people talk to each other in real time over Reticulum without dragging in message databases, synchronization engines, or architectural commitments they did not ask for.</p>
@@ -416,8 +397,8 @@ using LXMF.</p>
<p>LXMF is efficient enough that it can deliver messages over extremely low-bandwidth systems such as packet radio or LoRa. Encrypted LXMF messages can also be encoded as QR-codes or text-based URIs, allowing completely analog paper message transport.</p>
<p>Using Propagation Nodes, LXMF also offer a way to store and forward messages to users or endpoints that are not directly reachable at the time of message emission.</p>
</section>
<section id="id17">
<h3>LXST<a class="headerlink" href="#id17" title="Link to this heading"></a></h3>
<section id="id16">
<h3>LXST<a class="headerlink" href="#id16" title="Link to this heading"></a></h3>
<p><a class="reference external" href="https://github.com/markqvist/lxst">LXST</a> is a simple and flexible real-time streaming format and delivery protocol that allows a wide variety of applications, while using as little bandwidth as possible. It is built on top of Reticulum and offers zero-conf stream routing, end-to-end encryption and Forward Secrecy, and can be transported over any kind of medium that Reticulum supports. It currently powers real-time voice and telephony applications over Reticulum.</p>
</section>
<section id="rrc">
@@ -501,8 +482,6 @@ using LXMF.</p>
<li><a class="reference internal" href="#retipedia">Retipedia</a></li>
<li><a class="reference internal" href="#sideband">Sideband</a></li>
<li><a class="reference internal" href="#meshchatx">MeshChatX</a></li>
<li><a class="reference internal" href="#meshchat">MeshChat</a></li>
<li><a class="reference internal" href="#columba">Columba</a></li>
<li><a class="reference internal" href="#reticulum-relay-chat">Reticulum Relay Chat</a></li>
<li><a class="reference internal" href="#retibbs">RetiBBS</a></li>
<li><a class="reference internal" href="#rbrowser">RBrowser</a></li>
@@ -517,7 +496,7 @@ using LXMF.</p>
</li>
<li><a class="reference internal" href="#protocols">Protocols</a><ul>
<li><a class="reference internal" href="#lxmf">LXMF</a></li>
<li><a class="reference internal" href="#id17">LXST</a></li>
<li><a class="reference internal" href="#id16">LXST</a></li>
<li><a class="reference internal" href="#rrc">RRC</a></li>
</ul>
</li>
@@ -533,7 +512,7 @@ using LXMF.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+8 -7
View File
@@ -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="Code Examples" href="examples.html"><link rel="prev" title="Building Networks" href="networks.html">
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html"><link rel="next" title="Code Examples" href="examples.html"><link rel="prev" title="Git Over Reticulum" href="git.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>Support Reticulum - Reticulum Network Stack 1.1.7 documentation</title>
<title>Support Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="git.html">Git Over Reticulum</a></li>
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">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>
@@ -327,14 +328,14 @@ circumstances, so we rely on old-fashioned human feedback.</p>
</div>
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
</a>
<a class="prev-page" href="networks.html">
<a class="prev-page" href="git.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 class="title">Git Over Reticulum</div>
</div>
</a>
@@ -381,7 +382,7 @@ circumstances, so we rely on old-fashioned human feedback.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+5 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Understanding Reticulum - Reticulum Network Stack 1.1.7 documentation</title>
<title>Understanding Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -1336,7 +1337,7 @@ those risks are acceptable to you.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+244 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Using Reticulum on Your System - Reticulum Network Stack 1.1.7 documentation</title>
<title>Using Reticulum on Your System - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -850,6 +851,17 @@ options:
</pre></div>
</div>
</section>
<section id="the-rngit-utility">
<h3>The rngit Utility<a class="headerlink" href="#the-rngit-utility" title="Link to this heading"></a></h3>
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> 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 <code class="docutils literal notranslate"><span class="pre">rns://</span></code> URL scheme, and to publish software releases.</p>
<p>The system consists of two parts: The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node that hosts and manages repositories, and the <code class="docutils literal notranslate"><span class="pre">git-remote-rns</span></code> 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: <code class="docutils literal notranslate"><span class="pre">rns://DESTINATION_HASH/group/repo</span></code>.</p>
<p>If you set a branch to track a Reticulum remote as the default upstream, you can simply use <code class="docutils literal notranslate"><span class="pre">git</span></code> as you normally would; all commands work transparently and as expected.</p>
<div class="admonition warning">
<p class="admonition-title">Warning</p>
<p><strong>The rngit program is a new addition to RNS!</strong> 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, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.</p>
</div>
<p>For the full documentation on the <cite>rngit</cite> system, see the <a class="reference internal" href="git.html#git-main"><span class="std std-ref">Git Over Reticulum</span></a> chapter of this manual.</p>
</section>
<section id="the-rnx-utility">
<h3>The rnx Utility<a class="headerlink" href="#the-rnx-utility" title="Link to this heading"></a></h3>
<p>The <code class="docutils literal notranslate"><span class="pre">rnx</span></code> utility is a basic remote command execution program. It allows you to
@@ -909,6 +921,232 @@ optional arguments:
</pre></div>
</div>
</section>
<section id="the-rnsh-utility">
<h3>The rnsh Utility<a class="headerlink" href="#the-rnsh-utility" title="Link to this heading"></a></h3>
<p>The <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> 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.</p>
<p>While the <code class="docutils literal notranslate"><span class="pre">rnx</span></code> utility is useful for simple remote command execution and
retrieving output, <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> 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.</p>
<p><code class="docutils literal notranslate"><span class="pre">rnsh</span></code> operates in two modes: a <em>listener</em> mode that accepts incoming
connections, and an <em>initiator</em> mode that connects to a remote listener. Both
sides authenticate using Reticulum Identities, ensuring that only authorised
peers can establish sessions.</p>
<div class="admonition note">
<p class="admonition-title">Note</p>
<p><code class="docutils literal notranslate"><span class="pre">rnsh</span></code> 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.</p>
</div>
<p><strong>Usage Examples</strong></p>
<p>Start <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> in listener mode, accepting connections from specific identities:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
</pre></div>
</div>
<p>You can also specify allowed identity hashes (one per line) in the file
<code class="docutils literal notranslate"><span class="pre">~/.rnsh/allowed_identities</span></code> or <code class="docutils literal notranslate"><span class="pre">~/.config/rnsh/allowed_identities</span></code>, and
simply run the program in listener mode:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l
</pre></div>
</div>
<p>Connect to a remote listener from another system:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh 7a55144adf826958a9529a3bcf08b149
</pre></div>
</div>
<p>Specify a command to run on the remote system, separating <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> options from
the remote command with <code class="docutils literal notranslate"><span class="pre">--</span></code>:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh 7a55144adf826958a9529a3bcf08b149 -- top
</pre></div>
</div>
<p>Set a default command for the listener, in case the initiator does not supply
one, or when remote command execution is disabled:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -- /bin/bash --login
</pre></div>
</div>
<p>Use the <code class="docutils literal notranslate"><span class="pre">-m</span></code> flag to mirror the exit code of the remote process:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -m 7a55144adf826958a9529a3bcf08b149 -- /usr/local/bin/check-status
</pre></div>
</div>
<p>Use the <code class="docutils literal notranslate"><span class="pre">-p</span></code> flag to display the identity and destination hash for a listener:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -p
Identity : &lt;984b74a3f768bef236af4371e6f248cd&gt;
Listening on : 7a55144adf826958a9529a3bcf08b149
</pre></div>
</div>
<p>Use a specific identity file rather than the default:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -i /path/to/identity
</pre></div>
</div>
<p>Announce the listener destination on startup, and periodically:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -b 900
</pre></div>
</div>
<p>The <code class="docutils literal notranslate"><span class="pre">-b</span></code> option specifies the announce period in seconds. Use <code class="docutils literal notranslate"><span class="pre">0</span></code> to
announce only once at startup.</p>
<p><strong>Authentication &amp; Authorisation</strong></p>
<p>By default, <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> 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
<code class="docutils literal notranslate"><span class="pre">-a</span></code> option, and can be used multiple times:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
</pre></div>
</div>
<p>You can also maintain a list of allowed identity hashes in the file
<code class="docutils literal notranslate"><span class="pre">~/.rnsh/allowed_identities</span></code> or <code class="docutils literal notranslate"><span class="pre">~/.config/rnsh/allowed_identities</span></code>,
with one hex hash per line. This file is reloaded every time a new connection
is received, so changes take effect immediately without restarting <code class="docutils literal notranslate"><span class="pre">rnsh</span></code>.</p>
<p>If you want to accept connections from any identity (for testing or in fully
trusted environments), you can disable authentication with the <code class="docutils literal notranslate"><span class="pre">-n</span></code> option:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -n
</pre></div>
</div>
<div class="admonition warning">
<p class="admonition-title">Warning</p>
<p>Disabling authentication with <code class="docutils literal notranslate"><span class="pre">-n</span></code> means that <strong>any</strong> Reticulum peer that
can reach your listener will be able to execute commands on your system. Only
use this option if you <em>really</em> know what youre doing.</p>
</div>
<p><strong>Remote Command Control</strong></p>
<p>When running in listener mode, <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> allows you to control how remote
commands are handled:</p>
<ul class="simple">
<li><p>By default, the listener accepts the command sent by the initiator. If the
initiator does not supply a command, the listeners default shell is used.</p></li>
<li><p>Use <code class="docutils literal notranslate"><span class="pre">-C</span></code> (<code class="docutils literal notranslate"><span class="pre">--no-remote-command</span></code>) to disable execution of commands received
from the initiator. Only the listeners default command (or the command
specified after <code class="docutils literal notranslate"><span class="pre">--</span></code>) will be executed:</p></li>
</ul>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -C -- /usr/local/bin/safe-script
</pre></div>
</div>
<ul class="simple">
<li><p>Use <code class="docutils literal notranslate"><span class="pre">-A</span></code> (<code class="docutils literal notranslate"><span class="pre">--remote-command-as-args</span></code>) to append the initiators command
to the listeners 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:</p></li>
</ul>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -A -- /usr/bin/top
</pre></div>
</div>
<p><strong>Service Names</strong></p>
<p>When running in listener mode, <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> uses a service name to differentiate
between multiple listener instances that may share the same identity. By
default, the service name is <code class="docutils literal notranslate"><span class="pre">default</span></code>. You can specify a different service
name with the <code class="docutils literal notranslate"><span class="pre">-s</span></code> option:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -s monitoring
</pre></div>
</div>
<p>This allows you to run multiple listeners on the same node, each with a
different service name and purpose.</p>
<p><strong>Initiator Options</strong></p>
<p>When connecting to a remote listener, several options are available:</p>
<ul class="simple">
<li><p>Use <code class="docutils literal notranslate"><span class="pre">-N</span></code> (<code class="docutils literal notranslate"><span class="pre">--no-id</span></code>) to disable sending your identity to the remote
listener. Note that the listener must have authentication disabled (<code class="docutils literal notranslate"><span class="pre">-n</span></code>)
for the connection to succeed in this case.</p></li>
<li><p>Use <code class="docutils literal notranslate"><span class="pre">-m</span></code> (<code class="docutils literal notranslate"><span class="pre">--mirror</span></code>) to make the initiator return with the exit code of
the remote process, rather than always returning <code class="docutils literal notranslate"><span class="pre">0</span></code>.</p></li>
<li><p>Use <code class="docutils literal notranslate"><span class="pre">-w</span></code> (<code class="docutils literal notranslate"><span class="pre">--timeout</span></code>) to specify the connection and request timeout in
seconds. By default, the timeout matches the Reticulum path request timeout.</p></li>
</ul>
<p><strong>Identity &amp; Destination</strong></p>
<p>The default identity file for <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> is stored at
<code class="docutils literal notranslate"><span class="pre">~/.reticulum/identities/rnsh</span></code>, but you can specify a different one with the
<code class="docutils literal notranslate"><span class="pre">-i</span></code> option, which will be created if it does not already exist:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -i /path/to/identity
</pre></div>
</div>
<p>To display the identity and destination information for a listener, use the
<code class="docutils literal notranslate"><span class="pre">-p</span></code> option. When combined with <code class="docutils literal notranslate"><span class="pre">-l</span></code>, both the identity and the listening
destination hash are displayed:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -p
Identity : &lt;984b74a3f768bef236af4371e6f248cd&gt;
$ rnsh -l -p
Identity : &lt;984b74a3f768bef236af4371e6f248cd&gt;
Listening on : 7a55144adf826958a9529a3bcf08b149
</pre></div>
</div>
<p><strong>Verbosity</strong></p>
<p>Like other Reticulum utilities, <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> supports the <code class="docutils literal notranslate"><span class="pre">-v</span></code> and <code class="docutils literal notranslate"><span class="pre">-q</span></code> flags
to increase or decrease logging verbosity. Multiple flags can be specified to
further adjust the log level. The default log level is <code class="docutils literal notranslate"><span class="pre">INFO</span></code> for listeners
and <code class="docutils literal notranslate"><span class="pre">ERROR</span></code> for initiators.</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -vv # Listener with debug-level output
$ rnsh -q 7a55144adf826958a9529a3bcf08b149 # Quiet initiator
</pre></div>
</div>
<p>By default, all log output is routed to <code class="docutils literal notranslate"><span class="pre">~/.rnsh/logfile</span></code> for initiators.</p>
<p><strong>Escape Sequences</strong></p>
<p>During an active <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> session, the following escape sequences are
available. These are only recognised immediately after a newline character:</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">~~</span></code> - Send a literal tilde character</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">~.</span></code> - Terminate the session and exit immediately</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">~L</span></code> - Toggle line-interactive mode</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">~?</span></code> - Display the escape sequence quick reference</p></li>
</ul>
<p><strong>All Command-Line Options</strong></p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>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&#39;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 &lt;destination&gt; -- ls -la /tmp
</pre></div>
</div>
</section>
<section id="the-rnodeconf-utility">
<h3>The rnodeconf Utility<a class="headerlink" href="#the-rnodeconf-utility" title="Link to this heading"></a></h3>
<p>The <code class="docutils literal notranslate"><span class="pre">rnodeconf</span></code> utility allows you to inspect and configure existing <a class="reference internal" href="hardware.html#rnode-main"><span class="std std-ref">RNodes</span></a>, and
@@ -1363,7 +1601,9 @@ systemctl --user enable rnsd.service
<li><a class="reference internal" href="#the-rnpath-utility">The rnpath Utility</a></li>
<li><a class="reference internal" href="#the-rnprobe-utility">The rnprobe Utility</a></li>
<li><a class="reference internal" href="#the-rncp-utility">The rncp Utility</a></li>
<li><a class="reference internal" href="#the-rngit-utility">The rngit Utility</a></li>
<li><a class="reference internal" href="#the-rnx-utility">The rnx Utility</a></li>
<li><a class="reference internal" href="#the-rnsh-utility">The rnsh Utility</a></li>
<li><a class="reference internal" href="#the-rnodeconf-utility">The rnodeconf Utility</a></li>
</ul>
</li>
@@ -1395,7 +1635,7 @@ systemctl --user enable rnsd.service
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+5 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>What is Reticulum? - Reticulum Network Stack 1.1.7 documentation</title>
<title>What is Reticulum? - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -503,7 +504,7 @@ network, and vice versa.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
+5 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Zen of Reticulum - Reticulum Network Stack 1.1.7 documentation</title>
<title>Zen of Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.7 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.7 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -222,6 +222,7 @@
<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="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>
@@ -675,7 +676,7 @@ Imagine a messaging app. You write it once. It works on a laptop connected to fi
</aside>
</div>
</div><script src="_static/documentation_options.js?v=370aedac"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
# An Explanation of Reticulum for Human Beings
+684
View File
@@ -0,0 +1,684 @@
# 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.
## 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:
```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.
You can also dowload the Reticulum release wheels from GitHub, or other release channels,
and install them offline using `pip`:
```shell
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 systems package manager.
```shell
pip install rns --break-system-packages
```
For more detailed installation instructions, please see the
[Platform-Specific Install Notes](#install-guides) section.
After installation is complete, it might be helpful to refer to the
[Using Reticulum on Your System](using.md#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:
```shell
# Debian / Ubuntu / Derivatives
sudo apt install build-essential
# Arch / Manjaro / Derivatives
sudo pamac install base-devel
# 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 dont have pre-
compiled packages available.
## Try Using a Reticulum-based Program
If you simply want to try using a program built with Reticulum, a [range of different
programs](software.md#software-main) exist that allow basic communication and a various other useful functions,
even over extremely low-bandwidth Reticulum networks.
## 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.
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
[Using Reticulum on Your System](using.md#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`.
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.
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.
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 ([AutoInterface](interfaces.md#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 [Building Networks](networks.md#networks-main)
and [Interfaces](interfaces.md#interfaces-main) chapters of this manual, but most importantly,
start with reading the next section, [Bootstrapping Connectivity](#bootstrapping-connectivity),
as this provides the most essential understanding of how to ensure reliable
connectivity with a minimum of maintenance.
## Bootstrapping Connectivity
Reticulum is not a service you subscribe to, nor is it a single global network you “join”. It is a *networking stack*; a toolkit for building communications systems that align with your specific values, requirements, and operational environment. The way you choose to connect to other Reticulum peers is entirely your own choice.
One of the most powerful aspects of Reticulum is that it provides a multitude of tools to establish, maintain, and optimize connectivity. You can use these tools in isolation or combine them in complex configurations to achieve a vast array of goals.
Whether your aim is to create a completely private, air-gapped network for your family; to build a resilient community mesh that survives infrastructure collapse; to connect far and wide to as many nodes as possible; or simply to maintain a reliable, encrypted link to a specific organization you care about, Reticulum provides the mechanisms to make it happen.
There is no “right” or “wrong” way to build a Reticulum network, and you dont 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 [interface discovery](using.md#using-interface-discovery) and either manually select relevant, local interfaces, or enable discovered interface auto-connection.
A relevant option in this context is the [bootstrap only](interfaces.md#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**. Dont 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.
**Dont 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 [Blackhole Management](using.md#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 [Public Entrypoint](#hosting-entrypoints). By [publishing your interface as discoverable](interfaces.md#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 [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/).
## Hosting Public Entrypoints
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 [make it discoverable over Reticulum](interfaces.md#interfaces-discoverable).
You will need a machine, physical or virtual with a public IP address, that can be reached by other devices on the Internet.
The most efficient and performant way to host a connectable entry-point supporting many
users is to use the `BackboneInterface`. This interface type is fully compatible with
the `TCPClientInterface` and `TCPServerInterface` types, but much faster and uses
less system resources, allowing your device to handle thousands of connections even on
small systems.
It is also important to set your connectable interface to `gateway` mode, since this
will greatly improve network convergence time and path resolution for anyone connecting
to your entry-point.
```ini
# This example demonstrates a backbone interface
# configured for acting as a gateway for users to
# connect to either a public or private network
[[Public Gateway]]
type = BackboneInterface
enabled = yes
mode = gateway
listen_on = 0.0.0.0
port = 4242
# 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
[IFAC name and passphrase options](interfaces.md#interfaces-options) to secure your interface with a network name and passphrase.
```ini
# A private entry-point requiring a pre-shared
# network name and passphrase to connect to.
[[Private Gateway]]
type = BackboneInterface
enabled = yes
mode = gateway
listen_on = 0.0.0.0
port = 4242
network_name = private_ret
passphrase = 2owjajquafIanPecAc
```
If you are hosting an entry-point on an operating system that does not support
`BackboneInterface`, you can use `TCPServerInterface` instead, although it will
not be as performant.
## Connecting Reticulum Instances Over the Internet
Reticulum currently offers three interfaces suitable for connecting instances over the Internet: [Backbone](interfaces.md#interfaces-backbone), [TCP](interfaces.md#interfaces-tcps)
and [I2P](interfaces.md#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 dont 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 users 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 IPs 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
[Interfaces](interfaces.md#interfaces-main) section of this manual.
If you do not already have transceiver hardware available, you can easily and
cheaply build an [RNode](hardware.md#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 or web-based flasher.
Please see the [Communications Hardware](hardware.md#hardware-main) chapter for a guide.
If you prefer purchasing a ready-made unit, you can refer to the
list of suppliers.
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
[existing interface types](interfaces.md#interfaces-main), it is easy to write (and potentially
publish) a [custom interface module](interfaces.md#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.
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 [Configuring Interfaces](interfaces.md#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:
```default
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 [Example Programs](examples.md#examples-main).
The entire Reticulum API is documented in the [API Reference](reference.md#api-main)
chapter of this manual. Before diving in, its probably a good idea to read
this manual in full, but at least start with the [Understanding Reticulum](understanding.md#understanding-main) chapter.
## 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.
### 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).
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.
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:
```shell
# First, make sure indexes and packages are up to date.
pkg update
pkg upgrade
# Then install python and the cryptography library.
pkg install python python-cryptography
# Make sure pip is up to date, and install the wheel module.
pip install wheel pip --upgrade
# 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:
```shell
# First, make sure indexes and packages are up to date.
pkg update
pkg upgrade
# Then install dependencies for the cryptography library.
pkg install python build-essential openssl libffi rust
# Make sure pip is up to date, and install the wheel module.
pip install wheel pip --upgrade
# To allow the installer to build the cryptography module,
# we need to let it know what platform we are compiling for:
export CARGO_BUILD_TARGET="aarch64-linux-android"
# Start the install process for the cryptography module.
# Depending on your device, this can take several minutes,
# since the module must be compiled locally on your device.
pip install cryptography
# If the above installation succeeds, you can now install
# 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.
### 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.
```shell
# Install Python and development packages
sudo apt update
sudo apt install python3 python3-pip python3-dev
# Install Reticulum
python3 -m pip install rns
```
With these packages installed, `pip` will be able to build any missing dependencies
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.
```shell
# Install pipx
sudo apt install pipx
# Make installed programs available on the command line
pipx ensurepath
# 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:
```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:
```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.
### MacOS
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.
When Python and `pip` is available on your system, simply open a terminal window
and use one of the following commands:
```shell
# Install Reticulum and utilities with pip:
pip3 install rns
# On some versions, you may need to use the
# flag --break-system-packages to install:
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.
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:
```shell
export PATH=$PATH:~/Library/Python/3.9/bin
```
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.
#### 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.
To install Reticulum on OpenWRT, first log into a command line session, and
then use the following instructions:
```shell
# Install dependencies
opkg install python3 python3-pip python3-cryptography python3-pyserial
# Install Reticulum
pip install rns
# Start rnsd with debug logging enabled
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.
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.
### 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
dont 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.
```shell
# Install dependencies
sudo apt install python3 python3-pip python3-cryptography python3-pyserial
# Install Reticulum
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.
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.
```shell
# Install Python and development packages
sudo apt update
sudo apt install python3 python3-pip python3-dev
# Install Reticulum
python3 -m pip install rns
```
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.
```shell
# Install pipx
sudo apt install pipx
# Make installed programs available on the command line
pipx ensurepath
# 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:
```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:
```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.
### 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).
If you dont 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 dont 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:
```shell
pip install rns
```
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
do not support [PyCA/cryptography](https://github.com/pyca/cryptography), it is
important that you read and understand the [Cryptographic Primitives](understanding.md#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.
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.
+559
View File
@@ -0,0 +1,559 @@
# Git Over Reticulum
A set of utilities for distributed collaborative software development and publishing is included in RNS.
The system consists of two parts: The `rngit` node that hosts 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.
## The rngit Utility
The `rngit` utility provides full Git repository hosting and interaction over Reticulum. It allows you to host and manage Git repositories and releases on Reticulum nodes, and to interact with remote repositories using standard Git commands through the `rns://` URL scheme.
**Usage Examples**
Run `rngit` to start a repository node:
```text
$ rngit
[Notice] Starting Reticulum Git Node...
[Notice] Reticulum Git Node listening on <0d7334d411d00120cbad24edf355fdd2>
```
On the first run, `rngit` will create a default configuration file. You will then need to edit this, to point to your repository locations, configure access permissions, and perform any other necessary configuration.
View your identity and destination hashes:
```text
$ rngit --print-identity
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
```
If the page node is enabled, the output will also include the Nomad Network destination hash.
You can run `rngit` in service mode with logging to file:
```text
$ rngit -s
```
Clone a repository from a remote `rngit` node:
```text
$ git clone rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
```
Add a Reticulum remote to an existing repository:
```text
$ git remote add some_remote rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
```
Push changes to the Reticulum remote:
```text
$ git push some_remote master
```
Get changes from a remote repository:
```text
$ git pull rns_remote master
```
**All Command-Line Options (rngit)**
```text
usage: rngit.py [-h] [--config CONFIG] [--rnsconfig RNSCONFIG] [-s] [-i] [-v]
[-q] [--version]
Reticulum Git Repository Node
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-p, --print-identity print identity and destination info and exit
-s, --service rngit is running as a service and should log to file
-i, --interactive drop into interactive shell after initialisation
-v, --verbose increase verbosity
-q, --quiet decrease verbosity
--version show program's version number and exit
```
**All Command-Line Options (git-remote-rns)**
The `git-remote-rns` helper is automatically invoked by Git when interacting with `rns://` URLs. It is not typically run directly by users, but accepts the following environment variables for configuration:
- `RNGIT_CONFIG` - Path to alternative client configuration directory
- `RNS_CONFIG` - Path to alternative Reticulum configuration directory
The client configuration file is located at `~/.rngit/client_config` and allows adjusting parameters such as the reference batch size for transfers.
## Repository Structure
The `rngit` node organizes repositories into groups. Each group is a directory containing bare Git repositories. The repository path format is `group_name/repo_name`. For example, a repository at `/var/git/public/myrepo` would be accessible as `public/myrepo` via the URL `rns://DESTINATION_HASH/public/myrepo`.
**Configuration**
The `rngit` node configuration file is located at `~/.rngit/config` (or `/etc/rngit/config` for system-wide installations). The default configuration includes:
- Repository group paths defining where to find bare repositories
- Access permissions for groups and individual repositories
- Announce intervals for network visibility
- Optional statistics recording for repository activity
Access permissions can be configured at the group level in the config file, or per-repository using `.allowed` files. Permissions use the format `permission:target` where permission is `r` (read), `w` (write), `rw` (read/write), `c` (create) or `s` (stats) and target is `all`, `none`, or a specific identity hash.
The `s` (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set `record_stats = yes` in the `[rngit]` section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to `stats_ignore_identities`.
Repository-specific `.allowed` files can be static text files or executable scripts that output permission rules to stdout. A `group.allowed` file in a repository group directory applies to all repositories within that group.
## Serving Pages Over Nomad Network
In addition to providing Git repository access via the Git remote helper protocol, `rngit` can also run a [Nomad Network](https://github.com/markqvist/nomadnet) compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.
When enabled, the page node provides a complete interface to your repositories, with automatic Markdown to Micron conversion, syntax-highlighted code browsing, and detailed commit, diff and statistics views.
**Enabling the Git Page Node**
To enable the page node, add the following to your `~/.rngit/config` file:
```text
[pages]
serve_nomadnet = yes
```
When the page node is enabled, `rngit` will listen on a Nomad Network node destination in addition to the Git repository destination. You can view the destination hash by running:
```text
$ rngit --print-identity
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
Nomad Network Destination : <50824b711717f97c2fb1166ceddd5ea9>
```
**Accessing Repository Pages**
Once the page node is running, you can access it from any Nomad Network client by connecting to the Nomad Network destination. The page node provides the following views:
- **Front Page** - Lists all repository groups accessible to your identity
- **Group Page** - Shows all repositories within a group
- **Repository Page** - Displays repository overview, description and README
- **Releases** - List of releases for the repository, with information and downloads
- **File Browser** - Browse directory trees and view and download file contents
- **Commits View** - View commit history with pagination
- **Commit Details** - Detailed commit information with file changes and diffs
- **Refs View** - List branches and tags
- **Statistics** - Activity charts showing views, fetches and pushes over time
All pages respect the same permission system used for Git access. If an identity does not have read access to a repository, they will not be able to view its pages.
## Formatting & Syntax Highlighting
If the `pygments` Python module is installed on your system, the page node will automatically apply syntax highlighting to code files. The highlighting supports a wide range of programming languages and uses a color theme optimized for terminal display.
To enable syntax highlighting, install pygments:
```text
pip install pygments
```
**Markdown & Micron Support**
README files and other Markdown documents are automatically converted to Micron markup for display in Nomad Network clients. You can also write your README files directly in Micron, in which case they will display and render as such in any Nomad Network client. The file browser also supports viewing both rendered and raw Markdown and Micron documents.
Code blocks in Markdown can include language hints for syntax highlighting:
```text
```python
def hello_world():
print("Hello, Reticulum!")
```
```
## Customizing Templates
The page node uses a template system that allows complete customization of the generated pages. Templates are stored in the `~/.rngit/templates/` directory as Micron files.
The following template files are supported:
- `base.mu` - Base template wrapping all pages
- `front.mu` - Front page listing all groups
- `group.mu` - Group page listing repositories
- `repo.mu` - Repository overview page
- `releases.mu` - Release list page
- `release.mu` - Release details page
- `tree.mu` - File browser pages
- `blob.mu` - File content display
- `commits.mu` - Commit history listing
- `commit.mu` - Individual commit detail page
- `refs.mu` - Branches and tags listing
- `stats.mu` - Statistics page
Templates can include the following variables:
- `{PAGE_CONTENT}` - The main content of the page (required)
- `{NODE_NAME}` - The configured node name
- `{NAVIGATION}` - Breadcrumb navigation links
- `{VERSION}` - The rngit version number
- `{GEN_TIME}` - Page generation time
**Dynamic Templates**
Templates can be made executable to generate dynamic content. If a template file has the executable bit set, it will be executed and its stdout used as the template content.
**Icon Sets**
By default, the page node uses Nerd Font icons. If you prefer simpler icons or your terminal does not support Nerd Fonts, you can enable Unicode icons instead:
```text
[pages]
serve_nomadnet = yes
unicode_icons = yes
```
**Repository Statistics**
When statistics recording is enabled (see the `record_stats` configuration option), the page node can display activity charts for each repository. The statistics page shows:
- Total and peak views, fetches and pushes
- Daily activity charts over a 90-day period
- Combined activity visualization
To view statistics, a user must have the `s` (stats) permission for the repository. See the Access Configuration section for details on setting permissions.
**Repository Thanks**
The page node includes a “Thanks” feature that allows users to express appreciation for a repository. On each repository page, a “Thanks” link is displayed showing the current thanks count. Clicking this link registers a thank you for the repository.
**Configuration Example**
A complete page node configuration might look like this:
```text
[rngit]
node_name = My Git Node
announce_interval = 360
record_stats = yes
[repositories]
public = /var/git/public
internal = /var/git/internal
[access]
public = r:all
internal = rw:9710b86ba12c42d1d8f30f74fe509286
[pages]
serve_nomadnet = yes
unicode_icons = no
```
## Release Management
In addition to hosting Git repositories, `rngit` provides a complete release management system. This allows you to publish versioned releases with associated artifacts, release notes and metadata. Releases are managed through the `rngit release` subcommand, and are also viewable through the Nomad Network page interface.
**The Release Workflow**
Creating a release involves specifying a Git tag and a directory containing build artifacts or other files to distribute. The `rngit` client will open your configured `$EDITOR` to compose release notes, then upload all artifacts to the remote repository node.
To create a release, specify the tag name and path to artifacts:
```text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo create v1.2.0:./dist
```
This will:
1. Verify that the tag `v1.2.0` exists in the repository
2. Open your editor to write release notes
3. Upload all files from the `./dist` directory
4. Publish the release
If no `$EDITOR` environment variable is set, `rngit` will try to use `nano`, `vim` or `vi`. The editor will show a template with instructions. Lines starting with `#` will be ignored, and if the remaining content is empty after stripping comments, the release creation will be cancelled.
**Release Storage & Structure**
Releases are stored on the node in a directory named `repo_name.releases` next to the bare repository. Each release is a subdirectory containing:
- `META` - Release metadata in ConfigObj format
- `RELEASE.md` or `RELEASE.mu` - Release notes
- `artifacts/` - All uploaded files
- `THANKS` - Appreciation count from users
**Listing Releases**
To view all releases for a repository:
```text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list
Tag Status Created Objs Notes
------------------------------------------------------------------
v1.2.0 published 2025-01-15 14:32 3 Another release
v1.1.0 published 2024-12-03 09:15 2 Bug fix release
v1.0.0 published 2024-10-20 16:45 2 Initial release
```
**Viewing Release Details**
To see full information about a specific release:
```text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo view v1.2.0
Release : 0.9.2
Status : published
Created : 2026-05-04 23:53:09
Thanks : 5
Release Notes
=============
Version 1.2.0 release notes...
Artifacts (4)
=============
- myapp-1.2.0.tar.gz (1.5 MB)
- myapp-1.2.0.zip (1.6 MB)
- checksums.txt (256 B)
```
**Deleting Releases**
To remove a release:
```text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo delete v1.2.0
Are you sure you want to delete release 'v1.2.0'? [y/N]: y
Release v1.2.0 deleted
```
**Requirements & Validation**
- The specified tag must exist in the remote repository
- You must have `release` permission for the repository
- The target artifacts directory must exist and contain at least one file
- Release notes cannot be empty
**Permissions**
Release management requires the `release` permission, configured the same way as other repository permissions. In the config file or `.allowed` files, use `rel:target` to grant release management rights:
```text
# In .allowed file or config
rel:all # Allow everyone
rel:9710b86... # Allow specific identity
rel:none # Deny everyone
```
**Nomad Network Interface**
When the Nomad Network page node is enabled, releases are displayed on a dedicated releases page for each repository. Each release is listed with its tag, creation date, artifact count and a preview of the release notes. Clicking a release shows the full details including formatted release notes and a listing of all artifacts with their sizes.
Only releases with `published` status are visible through the Nomad Network interface. Draft releases (if supported in future implementations) would only be visible through the command-line interface.
**All Command-Line Options (rngit release)**
```text
usage: rngit release [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
[-i PATH] [-v] [-q] [--version]
[repository] [operation] [target]
Reticulum Git Release Manager
positional arguments:
repository URL of remote repository
operation list, view, create or delete
target tag and path to release artifacts directory
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-i, --identity PATH path to release identity
-v, --verbose
-q, --quiet
--version show program's version number and exit
```
## Work Documents
In addition to releases, `rngit` provides a work document management system for tracking tasks, investigations, issues and progress related to repositories. Work documents are stored as structured msgpack data and support threaded updates and comments.
**Listing Work Documents**
To view work documents for a repository:
```text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list
Active documents
=================
ID Title Author Created Comments
---------------------------------------------------------------------------
1 Implemented new feature 9710b86ba12c4f2e… 2025-01-15 14:32 3
2 Fixed bug in parser 8f3a21c9d84e927b… 2025-01-14 09:15 1
```
Use `--scope completed` to view completed work documents, or `--scope all` to see both active and completed.
**Viewing a Work Document**
To view a specific work document with all its comments:
```text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo view -d 1
Implement new feature (active #1)
=================================
Author : 9710b86ba12c42d1d8f30f74fe509286
Status : active
Created : 2026-05-05 15:11:11
Edited : 2026-05-05 18:22:11
Format : markdown
Updates : 0
This work document tracks the implementation of the new feature...
Updates
=======
#1 by 9710b86ba12c42d1d8f30f74fe509286 at 2026-05-05 15:38:37
-------------------------------------------------------------
Initial analysis complete
```
**Creating Work Documents**
To create a new work document:
```text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo create --title "Investigate performance issue"
```
This will open your configured `$EDITOR` to compose the document content. Save and exit to create the document, or save an empty document to cancel.
**Editing Work Documents**
To edit an existing work document:
```text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo edit -d 1
```
This fetches the current content, opens it in your editor, and sends any changes back to the node.
**Adding Comments**
To add an update to a work document:
```text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo update -d 1
```
This opens your editor to compose the update.
**Completing Work Documents**
To mark a work document as completed (moving it from `active` to `completed`):
```text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo complete -d 1
Work document #1 completed
```
**Activating Work Documents**
To mark a work document as active (moving it from `completed` to `active`):
```text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo activate -d 1
Work document #1 activated
```
**Deleting Work Documents**
To delete a work document and all its comments:
```text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo delete -id 1
Are you sure you want to delete active work document #1? [y/N]: y
Work document #1 deleted
```
**Permissions**
Users can view work documents and updates if the have `read` permission for the repository. If users have `read` and `interact`, they can also post updates/comments on existing work documents. Work document management requires having `write` and `interact` permission to the repository. These permissions are configured the same way as any other repository permissions. In the config file or `.allowed` files, use `i:target` to grant work document interaction rights:
```text
# In .allowed file or config
i:all # Allow everyone
i:9710b86... # Allow specific identity
i:none # Deny everyone
```
**Author Verification**
Users can only edit or delete work documents and updates they created. The author is cryptographically verified from the interacting links `remote_identity`.
**Storage Format**
Work documents are stored in a `repo_name.work` directory next to the repository, containing:
- `active/` - Active work documents
- `completed/` - Completed work documents
Each document is a numbered directory containing:
- `root` - The work document content and metadata (msgpack format)
- `N` - Numbered comment files (msgpack format)
**Nomad Network Interface**
When the Nomad Network page node is enabled, work documents are viewable through the web interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.
**All Command-Line Options (rngit work)**
```text
usage: rngit work [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
[-i PATH] [--scope SCOPE] [-t TITLE] [-d ID] [-v]
[-q] [--version]
[repository] [operation]
Reticulum Git Work Document Manager
positional arguments:
repository URL of remote repository
operation list, view, create, edit, delete, update or complete
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-i, --identity PATH path to identity
--scope SCOPE document scope: active, completed or all
-t, --title TITLE document title for create
-d, --id ID document ID
-v, --verbose
-q, --quiet
--version show program's version number and exit
```
+299
View File
@@ -0,0 +1,299 @@
# Communications Hardware
One of the truly valuable aspects of Reticulum is the ability to use it over
almost any conceivable kind of communications medium. The [interface types](interfaces.md#interfaces-main)
available for configuration in Reticulum are flexible enough to cover the use
of most wired and wireless communications hardware available, from decades-old
packet radio modems to modern millimeter-wave backhaul systems.
If you already have or operate some kind of communications hardware, there is a
very good chance that it will work with Reticulum out of the box. In case it does
not, it is possible to provide the necessary glue with very little effort using
for example the [PipeInterface](interfaces.md#interfaces-pipe) or the [TCPClientInterface](interfaces.md#interfaces-tcpc)
in combination with code like [TCP KISS Server](https://github.com/simplyequipped/tcpkissserver)
by [simplyequipped](https://github.com/simplyequipped).
It is also very easy to write and load [custom interface modules](interfaces.md#interfaces-custom)
into Reticulum, allowing you to communicate with more or less anything you can think of.
While this broad support and flexibility is very useful, an abundance of options
can sometimes make it difficult to know where to begin, especially when you are
starting from scratch.
This chapter will outline a few different sensible starting paths to get
real-world functional wireless communications up and running with minimal cost
and effort. Two fundamental devices categories will be covered, *RNodes* and
*WiFi-based radios*. Additionally, other common options will be briefly described.
Knowing how to employ just a few different types of hardware will make it possible
to build a wide range of useful networks with little effort.
## Combining Hardware Types
It is useful to combine different link and hardware types when designing and
building a network. One useful design pattern is to employ high-capacity point-to-point
links based on WiFi or millimeter-wave radios (with high-gain directional antennas)
for the network backbone, and using LoRa-based RNodes for covering large areas with
connectivity for client devices.
## RNode
Reliable and general-purpose long-range digital radio transceiver systems are
commonly either very expensive, difficult to set up and operate, hard to source,
power-hungry, or all of the above at the same time. In an attempt to alleviate
this situation, the transceiver system *RNode* was designed. It is important to
note that RNode is not one specific device, from one particular vendor, but
*an open plaform* that anyone can use to build interoperable digital transceivers
suited to their needs and particular situations.
An RNode is a general purpose, interoperable, low-power and long-range, reliable,
open and flexible radio communications device. Depending on its components, it can
operate on many different frequency bands, and use many different modulation
schemes, but most commonly, and for the purposes of this chapter, we will limit
the discussion to RNodes using *LoRa* modulation in common ISM bands.
**Avoid Confusion!** RNodes can use LoRa as a *physical-layer modulation*, but it
does not use, and has nothing to do with the *LoRaWAN* protocol and standard, commonly
used for centrally controlled IoT devices. RNodes use *raw LoRa modulation*, without
any additional protocol overhead. All high-level protocol functionality is handled
directly by Reticulum.
### Creating RNodes
RNode has been designed as a system that is easy to replicate across time and
space. You can put together a functioning transceiver using commonly available
components, and a few open source software tools. While you can design and build RNodes
completely from scratch, to your exact desired specifications, this chapter
will explain the easiest possible approach to creating RNodes: Using common
LoRa development boards. This approach can be boiled down to two simple steps:
1. Obtain one or more [supported development boards](#rnode-supported)
2. Install the RNode firmware with the [automated installer](#rnode-installation)
Once the firmware has been installed and provisioned by the install script, it
is ready to use with any software that supports RNodes, including Reticulum.
The device can be used with Reticulum by adding an [RNodeInterface](interfaces.md#interfaces-rnode)
to the configuration.
### Supported Boards and Devices
To create one or more RNodes, you will need to obtain supported development
boards or completed devices. The following boards and devices are supported
by the auto-installer.
---
![image](graphics/board_tbeam_supreme.png)
#### LilyGO T-Beam Supreme
- **Transceiver IC** Semtech SX1262 or SX1268
- **Device Platform** ESP32
- **Manufacturer** [LilyGO](https://lilygo.cn)
---
![image](graphics/board_tbeam.png)
#### LilyGO T-Beam
- **Transceiver IC** Semtech SX1262, SX1268, SX1276 or SX1278
- **Device Platform** ESP32
- **Manufacturer** [LilyGO](https://lilygo.cn)
---
![image](graphics/board_t3s3.png)
#### LilyGO T3S3
- **Transceiver IC** Semtech SX1262, SX1268, SX1276 or SX1278
- **Device Platform** ESP32
- **Manufacturer** [LilyGO](https://lilygo.cn)
---
![image](graphics/board_rak4631.png)
#### RAK4631-based Boards
- **Transceiver IC** Semtech SX1262 or SX1268
- **Device Platform** nRF52
- **Manufacturer** [RAK Wireless](https://www.rakwireless.com)
---
![image](graphics/board_opencomxl.png)
#### OpenCom XL
- **Transceiver ICs** Semtech SX1262 and SX1280 (dual transceiver)
- **Device Platform** nRF52
- **Manufacturer** [Liberated Embedded Systems](https://liberatedsystems.co.uk/)
---
![image](graphics/board_rnodev2.png)
#### Unsigned RNode v2.x
- **Transceiver IC** Semtech SX1276 or SX1278
- **Device Platform** ESP32
- **Manufacturer** [unsigned.io](https://unsigned.io)
---
![image](graphics/board_t3v21.png)
#### LilyGO LoRa32 v2.1
- **Transceiver IC** Semtech SX1276 or SX1278
- **Device Platform** ESP32
- **Manufacturer** [LilyGO](https://lilygo.cn)
---
![image](graphics/board_t3v20.png)
#### LilyGO LoRa32 v2.0
- **Transceiver IC** Semtech SX1276 or SX1278
- **Device Platform** ESP32
- **Manufacturer** [LilyGO](https://lilygo.cn)
---
![image](graphics/board_t3v10.png)
#### LilyGO LoRa32 v1.0
- **Transceiver IC** Semtech SX1276 or SX1278
- **Device Platform** ESP32
- **Manufacturer** [LilyGO](https://lilygo.cn)
---
![image](graphics/board_tdeck.png)
#### LilyGO T-Deck
- **Transceiver IC** Semtech SX1262 or SX1268
- **Device Platform** ESP32
- **Manufacturer** [LilyGO](https://lilygo.cn)
---
![image](graphics/board_techo.png)
#### LilyGO T-Echo
- **Transceiver IC** Semtech SX1262 or SX1268
- **Device Platform** nRF52
- **Manufacturer** [LilyGO](https://lilygo.cn)
---
![image](graphics/board_t114.png)
#### Heltec T114
- **Transceiver IC** Semtech SX1262 or SX1268
- **Device Platform** nRF52
- **Manufacturer** [Heltec Automation](https://heltec.org)
---
![image](graphics/board_heltec32v4.png)
#### Heltec LoRa32 v4.0
- **Transceiver IC** Semtech SX1262
- **Device Platform** ESP32
- **Manufacturer** [Heltec Automation](https://heltec.org)
---
![image](graphics/board_heltec32v30.png)
#### Heltec LoRa32 v3.0
- **Transceiver IC** Semtech SX1262 or SX1268
- **Device Platform** ESP32
- **Manufacturer** [Heltec Automation](https://heltec.org)
---
![image](graphics/board_heltec32v20.png)
#### Heltec LoRa32 v2.0
- **Transceiver IC** Semtech SX1276 or SX1278
- **Device Platform** ESP32
- **Manufacturer** [Heltec Automation](https://heltec.org)
---
### Installation
Once you have obtained compatible boards, you can install the [RNode Firmware](https://github.com/markqvist/RNode_Firmware)
using the [RNode Configuration Utility](https://github.com/markqvist/rnodeconfigutil).
If you have installed Reticulum on your system, the `rnodeconf` program will already be
available. If not, make sure that `Python3` and `pip` is installed on your system, and
then install Reticulum with with `pip`:
```default
pip install rns
```
Once installation has completed, it is time to start installing the firmware on your
devices. Run `rnodeconf` in auto-install mode like so:
```default
rnodeconf --autoinstall
```
The utility will guide you through the installation process by asking a series of
questions about your hardware. Simply follow the guide, and the utility will
auto-install and configure your devices.
### Usage with Reticulum
When the devices have been installed and provisioned, you can use them with Reticulum
by adding the [relevant interface section](interfaces.md#interfaces-rnode) to the configuration
file of Reticulum. In the configuraion you can specify all interface parameters,
such as serial port and on-air parameters.
## WiFi-based Hardware
It is possible to use all kinds of both short- and long-range WiFi-based hardware
with Reticulum. Any kind of hardware that fully supports bridged Ethernet over the
WiFi interface will work with the [AutoInterface](interfaces.md#interfaces-auto) in Reticulum.
Most devices will behave like this by default, or allow it via configuration options.
This means that you can simply configure the physical links of the WiFi based devices,
and start communicating over them using Reticulum. It is not necessary to enable any IP
infrastructure such as DHCP servers, DNS or similar, as long as at least Ethernet is
available, and packets are passed transparently over the physical WiFi-based devices.
Below is a list of example WiFi (and similar) radios that work well for high capacity
Reticulum links over long distances:
- [Ubiquiti airMAX radios](https://store.ui.com/collections/operator-airmax-devices)
- [Ubiquiti LTU radios](https://store.ui.com/collections/operator-ltu)
- [MikroTik radios](https://mikrotik.com/products/group/wireless-systems)
This list is by no means exhaustive, and only serves as a few examples of radio hardware
that is relatively cheap while providing long range and high capacity for Reticulum
networks. As in all other cases, it is also possible for Reticulum to co-exist with IP
networks running concurrently on such devices.
## Ethernet-based Hardware
Reticulum can run over any kind of hardware that can provide a switched Ethernet-based
medium. This means that anything from a plain Ethernet switch, to fiber-optic systems,
to data radios with Ethernet interfaces can be used by Reticulum.
The Ethernet medium does not need to have any IP infrastructure such as DHCP servers
or routing set up, but in case such infrastructure does exist, Reticulum will simply
co-exist with.
To use Reticulum over Ethernet-based mediums, it is generally enough to use the included
[AutoInterface](interfaces.md#interfaces-auto). This interface also works over any kind of
virtual networking adapter, such as `tun` and `tap` devices in Linux.
## Serial Lines & Devices
Using Reticulum over any kind of raw serial line is also possible with the
[SerialInterface](interfaces.md#interfaces-serial). This interface type is also useful for
using Reticulum over communications hardware that provides a serial port interface.
## Packet Radio Modems
Any packet radio modem that provides a standard KISS interface over USB, serial or TCP
can be used with Reticulum. This includes virtual software modems such as
[FreeDV TNC](https://github.com/xssfox/freedv-tnc) and [Dire Wolf](https://github.com/wb2osz/direwolf).
+231
View File
@@ -0,0 +1,231 @@
# Reticulum Network Stack Manual
This manual aims to provide you with all the information you need to
understand Reticulum, build networks or develop programs using it, or
to participate in the development of Reticulum itself.
* [What is Reticulum?](whatis.md)
* [Current Status](whatis.md#current-status)
* [Reference Implementation](whatis.md#reference-implementation)
* [What does Reticulum Offer?](whatis.md#what-does-reticulum-offer)
* [Where can Reticulum be Used?](whatis.md#where-can-reticulum-be-used)
* [Interface Types and Devices](whatis.md#interface-types-and-devices)
* [Getting Started Fast](gettingstartedfast.md)
* [Standalone Reticulum Installation](gettingstartedfast.md#standalone-reticulum-installation)
* [Resolving Dependency & Installation Issues](gettingstartedfast.md#resolving-dependency-installation-issues)
* [Try Using a Reticulum-based Program](gettingstartedfast.md#try-using-a-reticulum-based-program)
* [Using the Included Utilities](gettingstartedfast.md#using-the-included-utilities)
* [Creating a Network With Reticulum](gettingstartedfast.md#creating-a-network-with-reticulum)
* [Bootstrapping Connectivity](gettingstartedfast.md#bootstrapping-connectivity)
* [Finding Your Way](gettingstartedfast.md#finding-your-way)
* [Build Personal Infrastructure](gettingstartedfast.md#build-personal-infrastructure)
* [Mixing Strategies](gettingstartedfast.md#mixing-strategies)
* [Network Health & Responsibility](gettingstartedfast.md#network-health-responsibility)
* [Contributing to the Global Ret](gettingstartedfast.md#contributing-to-the-global-ret)
* [Connect to the Distributed Backbone](gettingstartedfast.md#connect-to-the-distributed-backbone)
* [Hosting Public Entrypoints](gettingstartedfast.md#hosting-public-entrypoints)
* [Connecting Reticulum Instances Over the Internet](gettingstartedfast.md#connecting-reticulum-instances-over-the-internet)
* [Adding Radio Interfaces](gettingstartedfast.md#adding-radio-interfaces)
* [Creating and Using Custom Interfaces](gettingstartedfast.md#creating-and-using-custom-interfaces)
* [Develop a Program with Reticulum](gettingstartedfast.md#develop-a-program-with-reticulum)
* [Platform-Specific Install Notes](gettingstartedfast.md#platform-specific-install-notes)
* [Android](gettingstartedfast.md#android)
* [ARM64](gettingstartedfast.md#arm64)
* [Debian Bookworm](gettingstartedfast.md#debian-bookworm)
* [MacOS](gettingstartedfast.md#macos)
* [OpenWRT](gettingstartedfast.md#openwrt)
* [Raspberry Pi](gettingstartedfast.md#raspberry-pi)
* [RISC-V](gettingstartedfast.md#risc-v)
* [Ubuntu Lunar](gettingstartedfast.md#ubuntu-lunar)
* [Windows](gettingstartedfast.md#windows)
* [Pure-Python Reticulum](gettingstartedfast.md#pure-python-reticulum)
* [Zen of Reticulum](zen.md)
* [The Illusion Of The Center](zen.md#the-illusion-of-the-center)
* [Fallacy Of The Cloud](zen.md#fallacy-of-the-cloud)
* [Decentralization Or Uncentralizability?](zen.md#decentralization-or-uncentralizability)
* [Death To The Address](zen.md#death-to-the-address)
* [Physics Of Trust](zen.md#physics-of-trust)
* [Hostile Environments](zen.md#hostile-environments)
* [Encryption Is Not A Feature](zen.md#encryption-is-not-a-feature)
* [Zero-Trust Architectures](zen.md#zero-trust-architectures)
* [Merits Of Scarcity](zen.md#merits-of-scarcity)
* [The Bandwidth Fallacy](zen.md#the-bandwidth-fallacy)
* [Cost Of A Byte](zen.md#cost-of-a-byte)
* [Flow & Time](zen.md#flow-time)
* [Liberation From Limits](zen.md#liberation-from-limits)
* [Sovereignty Through Infrastructure](zen.md#sovereignty-through-infrastructure)
* [A Carrier-Grade Fallacy](zen.md#a-carrier-grade-fallacy)
* [Personal Infrastructure](zen.md#personal-infrastructure)
* [The Ability To Disconnect](zen.md#the-ability-to-disconnect)
* [Identity and Nomadism](zen.md#identity-and-nomadism)
* [Portable Existence](zen.md#portable-existence)
* [Roaming Nodes](zen.md#roaming-nodes)
* [Announcing Presence](zen.md#announcing-presence)
* [Anchor In The Flow](zen.md#anchor-in-the-flow)
* [Ethics Of The Tool](zen.md#ethics-of-the-tool)
* [The Harm Principle](zen.md#the-harm-principle)
* [Public Domain Protocol](zen.md#public-domain-protocol)
* [Preserving Human Agency](zen.md#preserving-human-agency)
* [Design Patterns For Post-IP Systems](zen.md#design-patterns-for-post-ip-systems)
* [Store & Forward](zen.md#store-forward)
* [Naming Is Power](zen.md#naming-is-power)
* [The Interface Is The Medium](zen.md#the-interface-is-the-medium)
* [Emergent Patterns](zen.md#emergent-patterns)
* [Fabric Of The Independent](zen.md#fabric-of-the-independent)
* [The Work Is Finished](zen.md#the-work-is-finished)
* [Open Sky](zen.md#open-sky)
* [Programs Using Reticulum](software.md)
* [Programs & Utilities](software.md#programs-utilities)
* [Remote Shell](software.md#remote-shell)
* [Nomad Network](software.md#nomad-network)
* [RNS Page Node](software.md#rns-page-node)
* [Retipedia](software.md#retipedia)
* [Sideband](software.md#sideband)
* [MeshChatX](software.md#meshchatx)
* [Reticulum Relay Chat](software.md#reticulum-relay-chat)
* [RetiBBS](software.md#retibbs)
* [RBrowser](software.md#rbrowser)
* [Reticulum Network Telephone](software.md#reticulum-network-telephone)
* [LXST Phone](software.md#lxst-phone)
* [LXMFy](software.md#lxmfy)
* [LXMF Interactive Client](software.md#lxmf-interactive-client)
* [RNS FileSync](software.md#rns-filesync)
* [Micron Parser JS](software.md#micron-parser-js)
* [RNMon](software.md#rnmon)
* [Protocols](software.md#protocols)
* [LXMF](software.md#lxmf)
* [LXST](software.md#id16)
* [RRC](software.md#rrc)
* [Interface Modules & Connectivity Resources](software.md#interface-modules-connectivity-resources)
* [Using Reticulum on Your System](using.md)
* [Configuration & Data](using.md#configuration-data)
* [Included Utility Programs](using.md#included-utility-programs)
* [The rnsd Utility](using.md#the-rnsd-utility)
* [The rnstatus Utility](using.md#the-rnstatus-utility)
* [The rnid Utility](using.md#the-rnid-utility)
* [The rnpath Utility](using.md#the-rnpath-utility)
* [The rnprobe Utility](using.md#the-rnprobe-utility)
* [The rncp Utility](using.md#the-rncp-utility)
* [The rngit Utility](using.md#the-rngit-utility)
* [The rnx Utility](using.md#the-rnx-utility)
* [The rnsh Utility](using.md#the-rnsh-utility)
* [The rnodeconf Utility](using.md#the-rnodeconf-utility)
* [Discovering Interfaces](using.md#discovering-interfaces)
* [Remote Management](using.md#remote-management)
* [Blackhole Management](using.md#blackhole-management)
* [Local Blackhole Management](using.md#local-blackhole-management)
* [Automated List Sourcing](using.md#automated-list-sourcing)
* [Publishing Blackhole Lists](using.md#publishing-blackhole-lists)
* [Improving System Configuration](using.md#improving-system-configuration)
* [Fixed Serial Port Names](using.md#fixed-serial-port-names)
* [Reticulum as a System Service](using.md#reticulum-as-a-system-service)
* [Understanding Reticulum](understanding.md)
* [Motivation](understanding.md#motivation)
* [Goals](understanding.md#goals)
* [Introduction & Basic Functionality](understanding.md#introduction-basic-functionality)
* [Destinations](understanding.md#destinations)
* [Public Key Announcements](understanding.md#public-key-announcements)
* [Identities](understanding.md#understanding-identities)
* [Getting Further](understanding.md#getting-further)
* [Reticulum Transport](understanding.md#reticulum-transport)
* [Node Types](understanding.md#node-types)
* [The Announce Mechanism in Detail](understanding.md#the-announce-mechanism-in-detail)
* [Reaching the Destination](understanding.md#reaching-the-destination)
* [Resources](understanding.md#resources)
* [Network Identities](understanding.md#network-identities)
* [Conceptual Overview](understanding.md#conceptual-overview)
* [Current Usage](understanding.md#current-usage)
* [Future Implications](understanding.md#future-implications)
* [Creating and Using a Network Identity](understanding.md#creating-and-using-a-network-identity)
* [Reference Setup](understanding.md#reference-setup)
* [Protocol Specifics](understanding.md#protocol-specifics)
* [Packet Prioritisation](understanding.md#packet-prioritisation)
* [Interface Access Codes](understanding.md#interface-access-codes)
* [Wire Format](understanding.md#wire-format)
* [Announce Propagation Rules](understanding.md#announce-propagation-rules)
* [Cryptographic Primitives](understanding.md#cryptographic-primitives)
* [Communications Hardware](hardware.md)
* [Combining Hardware Types](hardware.md#combining-hardware-types)
* [RNode](hardware.md#rnode)
* [Creating RNodes](hardware.md#creating-rnodes)
* [Supported Boards and Devices](hardware.md#supported-boards-and-devices)
* [Installation](hardware.md#installation)
* [Usage with Reticulum](hardware.md#usage-with-reticulum)
* [WiFi-based Hardware](hardware.md#wifi-based-hardware)
* [Ethernet-based Hardware](hardware.md#ethernet-based-hardware)
* [Serial Lines & Devices](hardware.md#serial-lines-devices)
* [Packet Radio Modems](hardware.md#packet-radio-modems)
* [Configuring Interfaces](interfaces.md)
* [Custom Interfaces](interfaces.md#custom-interfaces)
* [Auto Interface](interfaces.md#auto-interface)
* [Backbone Interface](interfaces.md#backbone-interface)
* [Listeners](interfaces.md#listeners)
* [Connecting Remotes](interfaces.md#connecting-remotes)
* [TCP Server Interface](interfaces.md#tcp-server-interface)
* [TCP Client Interface](interfaces.md#tcp-client-interface)
* [UDP Interface](interfaces.md#udp-interface)
* [I2P Interface](interfaces.md#i2p-interface)
* [RNode LoRa Interface](interfaces.md#rnode-lora-interface)
* [RNode Multi Interface](interfaces.md#rnode-multi-interface)
* [Serial Interface](interfaces.md#serial-interface)
* [Pipe Interface](interfaces.md#pipe-interface)
* [KISS Interface](interfaces.md#kiss-interface)
* [AX.25 KISS Interface](interfaces.md#ax-25-kiss-interface)
* [Discoverable Interfaces](interfaces.md#discoverable-interfaces)
* [Enabling Discovery](interfaces.md#enabling-discovery)
* [Discovery Parameters](interfaces.md#discovery-parameters)
* [Interface Modes](interfaces.md#interface-modes)
* [Security Considerations](interfaces.md#security-considerations)
* [Example Configuration](interfaces.md#example-configuration)
* [Common Interface Options](interfaces.md#common-interface-options)
* [Interface Modes](interfaces.md#interfaces-modes)
* [Announce Rate Control](interfaces.md#announce-rate-control)
* [New Destination Rate Limiting](interfaces.md#new-destination-rate-limiting)
* [Path Request Burst Control](interfaces.md#path-request-burst-control)
* [Building Networks](networks.md)
* [Concepts & Overview](networks.md#concepts-overview)
* [Introductory Considerations](networks.md#introductory-considerations)
* [Destinations, Not Addresses](networks.md#destinations-not-addresses)
* [Transport Nodes and Instances](networks.md#transport-nodes-and-instances)
* [Trustless Networking](networks.md#trustless-networking)
* [Heterogeneous Connectivity](networks.md#heterogeneous-connectivity)
* [Git Over Reticulum](git.md)
* [The rngit Utility](git.md#the-rngit-utility)
* [Repository Structure](git.md#repository-structure)
* [Serving Pages Over Nomad Network](git.md#serving-pages-over-nomad-network)
* [Formatting & Syntax Highlighting](git.md#formatting-syntax-highlighting)
* [Customizing Templates](git.md#customizing-templates)
* [Release Management](git.md#release-management)
* [Work Documents](git.md#work-documents)
* [Support Reticulum](support.md)
* [Donations](support.md#donations)
* [Provide Feedback](support.md#provide-feedback)
* [Code Examples](examples.md)
* [Minimal](examples.md#minimal)
* [Announce](examples.md#announce)
* [Broadcast](examples.md#broadcast)
* [Echo](examples.md#echo)
* [Link](examples.md#link)
* [Identification](examples.md#example-identify)
* [Requests & Responses](examples.md#requests-responses)
* [Channel](examples.md#channel)
* [Buffer](examples.md#buffer)
* [Filetransfer](examples.md#filetransfer)
* [Custom Interfaces](examples.md#custom-interfaces)
* [Reticulum License](license.md)
* [API Reference](reference.md)
* [`Reticulum`](reference.md#RNS.Reticulum)
* [`Identity`](reference.md#RNS.Identity)
* [`Destination`](reference.md#RNS.Destination)
* [`Packet`](reference.md#RNS.Packet)
* [`PacketReceipt`](reference.md#RNS.PacketReceipt)
* [`Link`](reference.md#RNS.Link)
* [`RequestReceipt`](reference.md#RNS.RequestReceipt)
* [`Resource`](reference.md#RNS.Resource)
* [`Channel`](reference.md#RNS.Channel.Channel)
* [`MessageBase`](reference.md#RNS.MessageBase)
* [`Buffer`](reference.md#RNS.Buffer)
* [`RawChannelReader`](reference.md#RNS.RawChannelReader)
* [`RawChannelWriter`](reference.md#RNS.RawChannelWriter)
* [`Transport`](reference.md#RNS.Transport)
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
# Reticulum License
```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.
```
+320
View File
@@ -0,0 +1,320 @@
# Building Networks
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 youve 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 youre used to protocols like IP, lets at least start with some relief:
You dont 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. Theres plenty more neat aspects like that to Reticulum, but
were getting ahead of ourselves. Lets cover the basics first.
## Concepts & Overview
Before you start building your own networks, its 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 networks 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:
> * In a Reticulum network, any node can autonomously generate as many addresses
> (called *destinations* in Reticulum terminology) as it needs, which become
> globally reachable to the rest of the network. There is no central point of
> control over the address space.
> <br/>
> * Reticulum was designed to handle both very small, and very large networks.
> While the address space can support billions of endpoints, Reticulum is
> also very useful when just a few devices needs to communicate.
> <br/>
> * Low-bandwidth networks, like LoRa and packet radio, can interoperate and
> 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.
> <br/>
> * Reticulum provides sender/initiator anonymity by default. There is no way
> to filter traffic or discriminate it based on the source of the traffic.
> <br/>
> * All traffic is encrypted using ephemeral keys generated by an Elliptic Curve
> Diffie-Hellman key exchange on Curve25519. There is no way to inspect traffic
> contents, and no way to prioritise or throttle certain kinds of traffic.
> All transport and routing layers are thus completely agnostic to traffic type,
> and will pass all traffic equally.
> <br/>
> * Reticulum can function both with and without infrastructure. When *transport
> nodes* are available, they can route traffic over multiple hops for other
> nodes, and will function as a distributed cryptographic keystore. When there
> is no transport nodes available, all nodes that are within communication range
> can still communicate.
> <br/>
> * Every node can become a transport node, simply by enabling it in its
> configuration, but there is no need for every node on the network to be a
> transport node. Letting every node be a transport node will in most cases
> degrade the performance and reliability of the network.
> <br/>
> > *In general terms, if a node is stationary, well-connected and kept running
> > most of the time, it is a good candidate to be a transport node. For optimal
> > performance, a network should contain the amount of transport nodes that
> > provides connectivity to the intended area / topography, and not many more
> > than that.*
> * Reticulum is designed to work reliably in open, trustless environments. This
> means you can use it to create open-access networks, where participants can
> join and leave in a free and unorganised manner. This property allows an
> entirely new, and so far, mostly unexplored class of networked applications,
> where networks, and the information flow within them can form and dissolve
> organically.
> <br/>
> * You can just as easily create closed networks, since Reticulum allows you to
> add authentication to any interface. This means you can restrict access on
> any interface type, even when using legacy devices, such as modems. You can
> also mix authenticated and open interfaces on the same system. See the
> [Common Interface Options](interfaces.md#interfaces-options) section of the [Interfaces](interfaces.md#interfaces-main)
> chapter of this manual for information on how to set up interface authentication.
> <br/>
Reticulum allows you to mix very different kinds of networking mediums into a
unified mesh, or to keep everything within one medium. You could build a “virtual
network” running entirely over the Internet, where all nodes communicate over TCP
and UDP “channels”. You could also build such a network using other already-established
communications channels as the underlying carrier for Reticulum.
However, most real-world networks will probably involve either some form of
wireless or direct hardline communications. To allow Reticulum to communicate
over any type of medium, you must specify it in the configuration file, by default
located at `~/.reticulum/config`. See the [Supported Interfaces](interfaces.md#interfaces-main)
chapter of this manual for interface configuration examples.
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.
### Destinations, Not Addresses
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.
Reticulum replaces addresses with **destinations**. A destination is identified by a 16-byte
hash (128 bits) derived from a SHA-256 hash of the destinations 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>`.
The critical difference is that *any node can generate as many destinations as it
needs, without coordination*. A destinations uniqueness is guaranteed by the
collision resistance of SHA-256 and the inclusion of the nodes 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.
This has profound implications for network design:
* **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.
* **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.
* **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.
* **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.
### Transport Nodes and Instances
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.
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 **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 doesnt have a path for, Transport Nodes help
resolve the path through the network.
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.
The distinction is important. **Not** every node should be a Transport Node:
* **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.
* **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 networks 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 destinations 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 others 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 doesnt
natively interoperate with a packet radio network without additional infrastructure,
and you cant 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.
File diff suppressed because it is too large Load Diff
+161
View File
@@ -0,0 +1,161 @@
# 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.
### 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)](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).
### 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.
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.
### 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.
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.
### 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.
RetiBBS allows users to communicate through message boards in a secure manner.
### 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.
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.
### 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.
It supports interfacing directly with hardware peripherals such as GPIO keypads and LCD displays, providing a modular system for building secure hardware telephones.
### 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.
It supports various advanced features such as SAS verification, peer blocking, rate limiting, encrypted call history storage and contact management.
### 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.
## 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
+55
View File
@@ -0,0 +1,55 @@
# Support Reticulum
You can help support the continued development of open, free and private communications
systems by donating, providing feedback and contributing code and learning resources.
## Donations
Donations are gratefully accepted via the following channels:
```text
Monero:
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
Bitcoin:
bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6
Ethereum:
0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E
Liberapay:
https://liberapay.com/Reticulum/
Ko-Fi:
https://ko-fi.com/markqvist
```
Are certain features in the development roadmap are important to you or your
organisation? Make them a reality quickly by sponsoring their implementation.
## Provide Feedback
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. 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.
+900
View File
@@ -0,0 +1,900 @@
# 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 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 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.
## 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.
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 its 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.
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 [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*.
## Goals
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**
: Reticulum must be implemented with, and be able to run using only open source software. This is
critical to ensuring the availability, security and transparency of the system.
* **Hardware layer agnosticism**
: Reticulum must be fully hardware agnostic, and shall be useable over a wide range of
physical networking layers, such as data radios, serial lines, modems, handheld transceivers,
wired Ethernet, WiFi, or anything else that can carry a digital data stream. Hardware made for
dedicated Reticulum use shall be as cheap as possible and use off-the-shelf components, so
it can be easily modified and replicated by anyone interested in doing so.
* **Very low bandwidth requirements**
: Reticulum should be able to function reliably over links with a transmission capacity as low
as *5 bits per second*.
* **Encryption by default**
: Reticulum must use strong encryption by default for all communication.
* **Initiator Anonymity**
: It must be possible to communicate over a Reticulum network without revealing any identifying
information about oneself.
* **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.
* **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.
* **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.
* **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
own. The cost of setting up a functioning node should be less than $100 even if all parts
needs to be purchased.
## 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 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>`.
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.
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.
### 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:
* **Single**
: The *single* destination type is the most common type in Reticulum, and should be used for
most purposes. It is always identified by a unique public key. Any data sent to this
destination will be encrypted using ephemeral keys derived from an ECDH key exchange, and will
only be readable by the creator of the destination, who holds the corresponding private key.
* **Plain**
: A *plain* destination type is unencrypted, and suited for traffic that should be broadcast to a
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.
* **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
anyone in possession of the key, but as with the *plain* destination type, packets to this type
of destination are not currently transported over multiple hops, although a planned upgrade
to Reticulum will allow globally reachable *group* destinations.
* **Link**
: A *link* is a special destination type, that serves as an abstract channel to a *single*
destination, directly connected or over multiple hops. The *link* also offers reliability and
more efficient encryption, forward secrecy, initiator anonymity, and as such can be useful even
when a node is directly reachable. It also offers a more capable API and allows easily carrying
out requests and responses, large data transfers and more.
#### 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.
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:
```text
app name : environmentlogger
aspects : remotesensor, temperature
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.
**Take note!** There is a very important concept to understand here:
* Anyone can use the destination name `environmentlogger.remotesensor.temperature`
* 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.
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:
* **Single**
: When private communication between two endpoints is needed. Supports multiple hops.
* **Group**
: When private communication between two or more endpoints is needed. Supports multiple hops
indirectly, but must first be established through a *single* destination.
* **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.
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.
### 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.
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.
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
[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.
Seeing how *single* destinations are always tied to a private/public key pair leads us to the next topic.
### 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.
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.
### 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.
In the following sections, two concepts that allow this will be introduced, *paths* and *links*.
## 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.
To overcome such challenges, Reticulums *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 its 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*.
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 it is a *Transport Node*, it should be given the configuration directive `enable_transport = Yes`.
### 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:
* If this exact announce has already been received before, ignore it.
<br/>
* If not, record into a table which Transport Node the announce was received from, and how many times in
total it has been retransmitted to get here.
<br/>
* If the announce has been retransmitted *m+1* times, it will not be forwarded any more. By default, *m* is
set to 128.
<br/>
* After a randomised delay, the announce will be retransmitted on all interfaces that have bandwidth
available for processing announces. By default, the maximum bandwidth allocation for processing
announces is set at 2%, but can be configured on a per-interface basis.
<br/>
* If any given interface does not have enough bandwidth available for retransmitting the announce,
the announce will be assigned a priority inversely proportional to its hop count, and be inserted
into a queue managed by the interface.
<br/>
* When the interface has bandwidth available for processing an announce, it will prioritise announces
for destinations that are closest in terms of hops, thus prioritising reachability and connectivity
of local nodes, even on slow networks that connect to wider and faster networks.
<br/>
* After the announce has been re-transmitted, and if no other nodes are heard retransmitting the announce
with a greater hop count than when it left this node, transmitting it will be retried *r* times. By default,
*r* is set to 1.
<br/>
* If a newer announce from the same destination arrives, while an identical one is already waiting
to be transmitted, the newest announce is discarded. If the newest announce contains different
application specific data, it will replace the old announce.
<br/>
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.
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.
### 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.
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:
* A packet is always created with an associated destination and some payload data. When the packet is sent
to a *single* destination type, Reticulum will automatically create an ephemeral encryption key, perform
an ECDH key exchange with the destinations public key (or ratchet key, if available), and encrypt the information.
<br/>
* It is important to note that this key exchange does not require any network traffic. The sender already
knows the public key of the destination from an earlier received announce, and can thus perform the ECDH
key exchange locally, before sending the packet.
<br/>
* The public part of the newly generated ephemeral key-pair is included with the encrypted token, and sent
along with the encrypted payload data in the packet.
<br/>
* When the destination receives the packet, it can itself perform an ECDH key exchange and decrypt the
packet.
<br/>
* A new ephemeral key is used for every packet sent in this way.
<br/>
* Once the packet has been received and decrypted by the addressed destination, that destination can opt
to *prove* its receipt of the packet. It does this by calculating the SHA-256 hash of the received packet,
and signing this hash with its Ed25519 signing key. Transport nodes in the network can then direct this
*proof* back to the packets origin, where the signature can be verified against the destinations known
public signing key.
<br/>
* In case the packet is addressed to a *group* destination type, the packet will be encrypted with the
pre-shared AES-256 key associated with the destination. In case the packet is addressed to a *plain*
destination type, the payload data will not be encrypted. Neither of these two destination types can offer
forward secrecy. In general, it is recommended to always use the *single* destination type, unless it is
strictly necessary to use one of the others.
<br/>
For exchanges of larger amounts of data, or when longer sessions of bidirectional communication is desired, Reticulum offers the *Link* API. To establish a *link*, the following process is employed:
* First, the node that wishes to establish a link will send out a *link request* packet, that
traverses the network and locates the desired destination. Along the way, the Transport Nodes that
forward the packet will take note of this *link request*, and mark it as pending.
<br/>
* Second, if the destination accepts the *link request* , it will send back a packet that proves the
authenticity of its identity (and the receipt of the link request) to the initiating node. All
nodes that initially forwarded the packet will also be able to verify this proof, and thus
accept the validity of the *link* throughout the network. The link is now marked as *established*.
<br/>
* When the validity of the *link* has been accepted by forwarding nodes, these nodes will
remember the *link* , and it can subsequently be used by referring to a hash representing it.
<br/>
* As a part of the *link request*, an Elliptic Curve Diffie-Hellman key exchange takes place, that sets up an
efficiently encrypted tunnel between the two nodes. As such, this mode of communication is preferred,
even for situations when nodes can directly communicate, when the amount of data to be exchanged numbers
in the tens of packets, or whenever the use of the more advanced API functions is desired.
<br/>
* When a *link* has been set up, it automatically provides message receipt functionality, through
the same *proof* mechanism discussed before, so the sending node can obtain verified confirmation
that the information reached the intended recipient.
<br/>
* Once the *link* has been set up, the initiator can remain anonymous, or choose to authenticate towards
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.
<br/>
In a moment, we will discuss the details of how this methodology is
implemented, but lets 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
[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.
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
will randomly generate a new X25519 private/public key pair. It then creates a *link request*
packet, and broadcast it.
<br/>
<br/>
*It should be noted that the X25519 public/private keypair mentioned above is two separate keypairs:
An encryption key pair, used for derivation of a shared symmetric key, and a signing key pair, used
for signing and verifying messages on the link. They are sent together over the wire, and can be
considered as single public key for simplicity in this explanation.*
<br/>
* The *link request* is addressed to the destination hash of the desired destination, and
contains the following data: The newly generated X25519 public key *LKi*.
<br/>
* The broadcasted packet will be directed through the network according to the rules laid out
previously.
<br/>
* Any node that forwards the link request will store a *link id* in its *link table* , along with the
amount of hops the packet had taken when received. The link id is a hash of the entire link
request packet. If the link request packet is not *proven* by the addressed destination within some
set amount of time, the entry will be dropped from the *link table* again.
<br/>
* When the destination receives the link request packet, it will decide whether to accept the request.
If it is accepted, the destination will also generate a new X25519 private/public key pair, and
perform a Diffie Hellman Key Exchange, deriving a new symmetric key that will be used to encrypt the
channel, once it has been established.
<br/>
* A *link proof* packet is now constructed and transmitted over the network. This packet is
addressed to the *link id* of the *link*. It contains the following data: The newly generated X25519
public key *LKr* and an Ed25519 signature of the *link id* and *LKr* made by the *original signing key* of
the addressed destination.
<br/>
* By verifying this *link proof* packet, all nodes that originally transported the *link request*
packet to the destination from the originator can now verify that the intended destination received
the request and accepted it, and that the path they chose for forwarding the request was valid.
In successfully carrying out this verification, the transporting nodes marks the link as active.
An abstract bi-directional communication channel has now been established along a path in the network.
Packets can now be exchanged bi-directionally from either end of the link simply by adressing the
packets to the *link id* of the link.
<br/>
* When the source receives the *proof* , it will know unequivocally that a verified path has been
established to the destination. It can now also use the X25519 public key contained in the
*link proof* to perform its own Diffie Hellman Key Exchange and derive the symmetric key
that is used to encrypt the channel. Information can now be exchanged reliably and securely.
<br/>
#### NOTE
Its 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.
### Resources
For exchanging small amounts of data over a Reticulum network, the [Packet](reference.md#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 [Resource](reference.md#api-resource). A *Resource* can automatically
handle the reliable transfer of an arbitrary amount of data over an established [Link](reference.md#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.
[Resources](reference.md#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.
## 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 [Interface Discovery](using.md#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 Identitys 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.
```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 networks 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.
```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.
## 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.
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:
* **An Interface Device**
: Which provides access to the physical medium whereupon the communication
takes place, for example a radio with an integrated modem. A setup with a separate modem
connected to a radio would also be an interface device.
* **A Host Device**
: Some sort of computing device that can run the necessary software, communicate with the
interface device, and provide user interaction.
* **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:
* **Interface Device**
: A data radio consisting of a LoRa radio module, and a microcontroller with open source
firmware, that can connect to host devices via USB. It operates in either the 430, 868 or 900
MHz frequency bands. More details can be found on the [RNode Page](https://github.com/markqvist/rnode_firmware).
* **Host Device**
: Any computer device running Linux and Python. A Raspberry Pi with a Debian based OS is
a good place to start, but anything can be used.
* **Software Stack**
: The most recently released Python Implementation of Reticulum, running on a Linux-based
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).
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.
## 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.
### 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.
### 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 [Common Interface Options](interfaces.md#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.
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.
### Wire Format
```text
== Reticulum Wire Format ======
A Reticulum packet is composed of the following fields:
[HEADER 2 bytes] [ADDRESSES 16/32 bytes] [CONTEXT 1 byte] [DATA 0-465 bytes]
* The HEADER field is 2 bytes long.
* Byte 1: [IFAC Flag], [Header Type], [Context Flag], [Propagation Type],
[Destination Type] and [Packet Type]
* Byte 2: Number of hops
* Interface Access Code field if the IFAC flag was set.
* The length of the Interface Access Code can vary from
1 to 64 bytes according to physical interface
capabilities and configuration.
* The ADDRESSES field contains either 1 or 2 addresses.
* Each address is 16 bytes long.
* The Header Type flag in the HEADER field determines
whether the ADDRESSES field contains 1 or 2 addresses.
* Addresses are SHA-256 hashes truncated to 16 bytes.
* The CONTEXT field is 1 byte.
* It is used by Reticulum to determine packet context.
* The DATA field is between 0 and 465 bytes.
* It contains the packets data payload.
IFAC Flag
-----------------
open 0 Packet for publically accessible interface
authenticated 1 Interface authentication is included in packet
Header Types
-----------------
type 1 0 Two byte header, one 16 byte address field
type 2 1 Two byte header, two 16 byte address fields
Context Flag
-----------------
unset 0 The context flag is used for various types
set 1 of signalling, depending on packet context
Propagation Types
-----------------
broadcast 0
transport 1
Destination Types
-----------------
single 00
group 01
plain 10
link 11
Packet Types
-----------------
data 00
announce 01
link request 10
proof 11
+- Packet Example -+
HEADER FIELD DESTINATION FIELDS CONTEXT FIELD DATA FIELD
_______|_______ ________________|________________ ________|______ __|_
| | | | | | | |
01010000 00000100 [HASH1, 16 bytes] [HASH2, 16 bytes] [CONTEXT, 1 byte] [DATA]
|| | | | |
|| | | | +-- Hops = 4
|| | | +------- Packet Type = DATA
|| | +--------- Destination Type = SINGLE
|| +----------- Propagation Type = TRANSPORT
|+------------- Header Type = HEADER_2 (two byte header, two address fields)
+-------------- Access Codes = DISABLED
+- Packet Example -+
HEADER FIELD DESTINATION FIELD CONTEXT FIELD DATA FIELD
_______|_______ _______|_______ ________|______ __|_
| | | | | | | |
00000000 00000111 [HASH1, 16 bytes] [CONTEXT, 1 byte] [DATA]
|| | | | |
|| | | | +-- Hops = 7
|| | | +------- Packet Type = DATA
|| | +--------- Destination Type = SINGLE
|| +----------- Propagation Type = BROADCAST
|+------------- Header Type = HEADER_1 (two byte header, one address field)
+-------------- Access Codes = DISABLED
+- Packet Example -+
HEADER FIELD IFAC FIELD DESTINATION FIELD CONTEXT FIELD DATA FIELD
_______|_______ ______|______ _______|_______ ________|______ __|_
| | | | | | | | | |
10000000 00000111 [IFAC, N bytes] [HASH1, 16 bytes] [CONTEXT, 1 byte] [DATA]
|| | | | |
|| | | | +-- Hops = 7
|| | | +------- Packet Type = DATA
|| | +--------- Destination Type = SINGLE
|| +----------- Propagation Type = BROADCAST
|+------------- Header Type = HEADER_1 (two byte header, one address field)
+-------------- Access Codes = ENABLED
Size examples of different packet types
---------------------------------------
The following table lists example sizes of various
packet types. The size listed are the complete on-
wire size counting all fields including headers,
but excluding any interface access codes.
- Path Request : 51 bytes
- Announce : 167 bytes
- Link Request : 83 bytes
- Link Proof : 115 bytes
- Link RTT packet : 99 bytes
- Link keepalive : 20 bytes
```
### 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.
![image](graphics/if_mode_graph_b.png)
See the [Interface Modes](interfaces.md#interfaces-modes) section for a conceptual overview
of the different interface modes, and how they are configured.
<!-- (.. code-block:: text)
Full ────── ✓ ──┐ ┌── ✓ ── Full
AP ──────── ✓ ──┼───> Full >───┼── ✕ ── AP
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
Roaming ─── ✓ ──┘ └── ✓ ── Roaming
Full ────── ✕ ──┐ ┌── ✓ ── Full
AP ──────── ✕ ──┼────> AP >────┼── ✕ ── AP
Boundary ── ✕ ──┤ ├── ✓ ── Boundary
Roaming ─── ✕ ──┘ └── ✓ ── Roaming
Full ────── ✓ ──┐ ┌── ✓ ── Full
AP ──────── ✓ ──┼─> Roaming >──┼── ✕ ── AP
Boundary ── ✕ ──┤ ├── ✕ ── Boundary
Roaming ─── ✕ ──┘ └── ✕ ── Roaming
Full ────── ✓ ──┐ ┌── ✓ ── Full
AP ──────── ✓ ──┼─> Boundary >─┼── ✕ ── AP
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
Roaming ─── ✕ ──┘ └── ✕ ── Roaming -->
### 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 **is not** Reticulum, and possibly an
intentionally compromised or weakened clone. The utilised primitives are:
* Ed25519 for signatures
* X25519 for ECDH key exchanges
* HKDF for key derivation
* Encrypted tokens are based on the Fernet spec
* Ephemeral keys derived from an ECDH key exchange on Curve25519
* AES-256 in CBC mode with PKCS7 padding
* HMAC using SHA256 for message authentication
* IVs must be generated through `os.urandom()` or better
* No Fernet version and timestamp metadata fields
* SHA-256
* 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:
- `RNS/Cryptography/HKDF.py`
- `RNS/Cryptography/HMAC.py`
- `RNS/Cryptography/Token.py`
- `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.
Using the normal RNS installation procedures, it is not possible to install Reticulum on a
system without the required OpenSSL primitives being available, and if they are not, they will
be resolved and installed as a dependency. It is only possible to use the pure-python primitives
by manually specifying this, for example by using the `rnspure` package.
#### WARNING
If you want to use the internal pure-python primitives, it is **highly advisable** that you
have a good understanding of the risks that this pose, and make an informed decision on whether
those risks are acceptable to you.
File diff suppressed because it is too large Load Diff
+144
View File
@@ -0,0 +1,144 @@
# What is Reticulum?
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
[Zen of Reticulum](zen.md#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
acknowledgements and more.
From a users perspective, Reticulum allows the creation of applications that
respect and empower the autonomy and sovereignty of communities and individuals.
Reticulum enables secure digital communication that cannot be subjected to
outside control, manipulation or censorship.
Reticulum enables the construction of both small and potentially planetary-scale
networks, without any need for hierarchical or bureaucratic structures to control
or manage them, while ensuring individuals and communities full sovereignty
over their own network segments.
Reticulum is a **complete networking stack**, and does not need IP or higher
layers, although it is easy to utilise IP (with TCP or UDP) as the underlying
carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the
Internet or private IP networks. Reticulum is built directly on cryptographic
principles, allowing resilience and stable functionality in open and trustless
networks.
No kernel modules or drivers are required. Reticulum can run completely in
userland, and will run on practically any system that runs Python 3. Reticulum
runs well even on small single-board computers like the Pi Zero.
## Current Status
All core protocol features are implemented and functioning, but additions will probably occur as
real-world use is explored. The API and wire-format can be considered complete and stable, but
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 [Reticulum License](license.md#license).
The Reticulum Protocol was dedicated to the Public Domain in 2016.
## What does Reticulum Offer?
* 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 and simpler, 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
## 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,
ad-hoc WiFi, free-space optical links and similar systems are all examples
of the types of interfaces Reticulum was designed for.
An open-source LoRa-based interface called [RNode](https://unsigned.io/rnode)
has been designed as an example transceiver that is very suitable for
Reticulum. It is possible to build it yourself, to transform a common LoRa
development board into one, or it can be purchased as a complete transceiver
from various vendors.
Reticulum can also be encapsulated over existing IP networks, so theres
nothing stopping you from using it over wired Ethernet or your local WiFi
network, where itll 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.
As an example, its 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
added, 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.
## Interface Types and Devices
Reticulum implements a range of generalised interface types that covers the communications hardware that Reticulum can run over. If your hardware is not supported, its simple to [implement an interface class](examples.md#example-custominterface). Currently, Reticulum can use the following devices and communication mediums:
* Any Ethernet device
* WiFi devices
* Wired Ethernet devices
* Fibre-optic transceivers
* Data radios with Ethernet ports
* LoRa using [RNode](https://unsigned.io/rnode)
* Can be installed on [many popular LoRa boards](https://github.com/markqvist/rnodeconfigutil#supported-devices)
* Can be purchased as a [ready to use transceiver](https://unsigned.io/rnode)
* Packet Radio TNCs, such as [OpenModem](https://unsigned.io/openmodem)
* Any packet radio TNC in KISS mode
* Ideal for VHF and UHF radio
* Any device with a serial port
* The I2P network
* TCP over IP networks
* UDP over IP networks
* Anything you can connect via stdio
* Reticulum can use external programs and pipes as interfaces
* This can be used to easily hack in virtual interfaces
* Or to quickly create interfaces with custom hardware
* Anything else using [custom interface modules](interfaces.md#interfaces-custom) written in Python
For a full list and more details, see the [Supported Interfaces](interfaces.md#interfaces-main) chapter.
+414
View File
@@ -0,0 +1,414 @@
# 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 peoples computers. When you build for the cloud, you are building *for* a landlord. You are accepting that your applications 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*, its 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 cant 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 isnt 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; youre 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 dont know) vouches for it. We trust an app because it is in an app store (run by a corporation we dont 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. *“Its 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. Ill send a JSON object with metadata, timestamps, and user profile info (15KB).”*
- **The Zen Way:** *“I need to send a status update. Ill 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 doesnt 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 recipients 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 dont 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. Youre 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 doesnt 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 elses 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`, youre *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 dont 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:** *“Its 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 doesnt come in 250 milliseconds, we show a spinner. If it doesnt 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 stacks 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 doesnt 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 doesnt flicker when the signal drops. It doesnt 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*
+3 -3
View File
@@ -244,12 +244,12 @@ to your entry-point.
listen_on = 0.0.0.0
port = 4242
# On publicly available interfaces, it can be
# a good idea to configure sensible announce
# On publicly available interfaces, it is
# essential to configure sensible announce
# rate targets.
announce_rate_target = 3600
announce_rate_penalty = 3600
announce_rate_grace = 12
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.
+576
View File
@@ -0,0 +1,576 @@
.. _git-main:
******************
Git Over Reticulum
******************
A set of utilities for distributed collaborative software development and publishing is included in RNS.
The system consists of two parts: The ``rngit`` node that hosts 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.
The rngit Utility
=================
The ``rngit`` utility provides full Git repository hosting and interaction over Reticulum. It allows you to host and manage Git repositories and releases on Reticulum nodes, and to interact with remote repositories using standard Git commands through the ``rns://`` URL scheme.
**Usage Examples**
Run ``rngit`` to start a repository node:
.. code:: text
$ rngit
[Notice] Starting Reticulum Git Node...
[Notice] Reticulum Git Node listening on <0d7334d411d00120cbad24edf355fdd2>
On the first run, ``rngit`` will create a default configuration file. You will then need to edit this, to point to your repository locations, configure access permissions, and perform any other necessary configuration.
View your identity and destination hashes:
.. code:: text
$ rngit --print-identity
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
If the page node is enabled, the output will also include the Nomad Network destination hash.
You can run ``rngit`` in service mode with logging to file:
.. code:: text
$ rngit -s
Clone a repository from a remote ``rngit`` node:
.. code:: text
$ git clone rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
Add a Reticulum remote to an existing repository:
.. code:: text
$ git remote add some_remote rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
Push changes to the Reticulum remote:
.. code:: text
$ git push some_remote master
Get changes from a remote repository:
.. code:: text
$ git pull rns_remote master
**All Command-Line Options (rngit)**
.. code:: text
usage: rngit.py [-h] [--config CONFIG] [--rnsconfig RNSCONFIG] [-s] [-i] [-v]
[-q] [--version]
Reticulum Git Repository Node
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-p, --print-identity print identity and destination info and exit
-s, --service rngit is running as a service and should log to file
-i, --interactive drop into interactive shell after initialisation
-v, --verbose increase verbosity
-q, --quiet decrease verbosity
--version show program's version number and exit
**All Command-Line Options (git-remote-rns)**
The ``git-remote-rns`` helper is automatically invoked by Git when interacting with ``rns://`` URLs. It is not typically run directly by users, but accepts the following environment variables for configuration:
- ``RNGIT_CONFIG`` - Path to alternative client configuration directory
- ``RNS_CONFIG`` - Path to alternative Reticulum configuration directory
The client configuration file is located at ``~/.rngit/client_config`` and allows adjusting parameters such as the reference batch size for transfers.
Repository Structure
====================
The ``rngit`` node organizes repositories into groups. Each group is a directory containing bare Git repositories. The repository path format is ``group_name/repo_name``. For example, a repository at ``/var/git/public/myrepo`` would be accessible as ``public/myrepo`` via the URL ``rns://DESTINATION_HASH/public/myrepo``.
**Configuration**
The ``rngit`` node configuration file is located at ``~/.rngit/config`` (or ``/etc/rngit/config`` for system-wide installations). The default configuration includes:
- Repository group paths defining where to find bare repositories
- Access permissions for groups and individual repositories
- Announce intervals for network visibility
- Optional statistics recording for repository activity
Access permissions can be configured at the group level in the config file, or per-repository using ``.allowed`` files. Permissions use the format ``permission:target`` where permission is ``r`` (read), ``w`` (write), ``rw`` (read/write), ``c`` (create) or ``s`` (stats) and target is ``all``, ``none``, or a specific identity hash.
The ``s`` (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set ``record_stats = yes`` in the ``[rngit]`` section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to ``stats_ignore_identities``.
Repository-specific ``.allowed`` files can be static text files or executable scripts that output permission rules to stdout. A ``group.allowed`` file in a repository group directory applies to all repositories within that group.
Serving Pages Over Nomad Network
================================
In addition to providing Git repository access via the Git remote helper protocol, ``rngit`` can also run a `Nomad Network <https://github.com/markqvist/nomadnet>`_ compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.
When enabled, the page node provides a complete interface to your repositories, with automatic Markdown to Micron conversion, syntax-highlighted code browsing, and detailed commit, diff and statistics views.
**Enabling the Git Page Node**
To enable the page node, add the following to your ``~/.rngit/config`` file:
.. code:: text
[pages]
serve_nomadnet = yes
When the page node is enabled, ``rngit`` will listen on a Nomad Network node destination in addition to the Git repository destination. You can view the destination hash by running:
.. code:: text
$ rngit --print-identity
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
Nomad Network Destination : <50824b711717f97c2fb1166ceddd5ea9>
**Accessing Repository Pages**
Once the page node is running, you can access it from any Nomad Network client by connecting to the Nomad Network destination. The page node provides the following views:
- **Front Page** - Lists all repository groups accessible to your identity
- **Group Page** - Shows all repositories within a group
- **Repository Page** - Displays repository overview, description and README
- **Releases** - List of releases for the repository, with information and downloads
- **File Browser** - Browse directory trees and view and download file contents
- **Commits View** - View commit history with pagination
- **Commit Details** - Detailed commit information with file changes and diffs
- **Refs View** - List branches and tags
- **Statistics** - Activity charts showing views, fetches and pushes over time
All pages respect the same permission system used for Git access. If an identity does not have read access to a repository, they will not be able to view its pages.
Formatting & Syntax Highlighting
================================
If the ``pygments`` Python module is installed on your system, the page node will automatically apply syntax highlighting to code files. The highlighting supports a wide range of programming languages and uses a color theme optimized for terminal display.
To enable syntax highlighting, install pygments:
.. code:: text
pip install pygments
**Markdown & Micron Support**
README files and other Markdown documents are automatically converted to Micron markup for display in Nomad Network clients. You can also write your README files directly in Micron, in which case they will display and render as such in any Nomad Network client. The file browser also supports viewing both rendered and raw Markdown and Micron documents.
Code blocks in Markdown can include language hints for syntax highlighting:
.. code:: text
```python
def hello_world():
print("Hello, Reticulum!")
```
Customizing Templates
=====================
The page node uses a template system that allows complete customization of the generated pages. Templates are stored in the ``~/.rngit/templates/`` directory as Micron files.
The following template files are supported:
- ``base.mu`` - Base template wrapping all pages
- ``front.mu`` - Front page listing all groups
- ``group.mu`` - Group page listing repositories
- ``repo.mu`` - Repository overview page
- ``releases.mu`` - Release list page
- ``release.mu`` - Release details page
- ``tree.mu`` - File browser pages
- ``blob.mu`` - File content display
- ``commits.mu`` - Commit history listing
- ``commit.mu`` - Individual commit detail page
- ``refs.mu`` - Branches and tags listing
- ``stats.mu`` - Statistics page
Templates can include the following variables:
- ``{PAGE_CONTENT}`` - The main content of the page (required)
- ``{NODE_NAME}`` - The configured node name
- ``{NAVIGATION}`` - Breadcrumb navigation links
- ``{VERSION}`` - The rngit version number
- ``{GEN_TIME}`` - Page generation time
**Dynamic Templates**
Templates can be made executable to generate dynamic content. If a template file has the executable bit set, it will be executed and its stdout used as the template content.
**Icon Sets**
By default, the page node uses Nerd Font icons. If you prefer simpler icons or your terminal does not support Nerd Fonts, you can enable Unicode icons instead:
.. code:: text
[pages]
serve_nomadnet = yes
unicode_icons = yes
**Repository Statistics**
When statistics recording is enabled (see the ``record_stats`` configuration option), the page node can display activity charts for each repository. The statistics page shows:
- Total and peak views, fetches and pushes
- Daily activity charts over a 90-day period
- Combined activity visualization
To view statistics, a user must have the ``s`` (stats) permission for the repository. See the Access Configuration section for details on setting permissions.
**Repository Thanks**
The page node includes a "Thanks" feature that allows users to express appreciation for a repository. On each repository page, a "Thanks" link is displayed showing the current thanks count. Clicking this link registers a thank you for the repository.
**Configuration Example**
A complete page node configuration might look like this:
.. code:: text
[rngit]
node_name = My Git Node
announce_interval = 360
record_stats = yes
[repositories]
public = /var/git/public
internal = /var/git/internal
[access]
public = r:all
internal = rw:9710b86ba12c42d1d8f30f74fe509286
[pages]
serve_nomadnet = yes
unicode_icons = no
Release Management
==================
In addition to hosting Git repositories, ``rngit`` provides a complete release management system. This allows you to publish versioned releases with associated artifacts, release notes and metadata. Releases are managed through the ``rngit release`` subcommand, and are also viewable through the Nomad Network page interface.
**The Release Workflow**
Creating a release involves specifying a Git tag and a directory containing build artifacts or other files to distribute. The ``rngit`` client will open your configured ``$EDITOR`` to compose release notes, then upload all artifacts to the remote repository node.
To create a release, specify the tag name and path to artifacts:
.. code:: text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo create v1.2.0:./dist
This will:
1. Verify that the tag ``v1.2.0`` exists in the repository
2. Open your editor to write release notes
3. Upload all files from the ``./dist`` directory
4. Publish the release
If no ``$EDITOR`` environment variable is set, ``rngit`` will try to use ``nano``, ``vim`` or ``vi``. The editor will show a template with instructions. Lines starting with ``#`` will be ignored, and if the remaining content is empty after stripping comments, the release creation will be cancelled.
**Release Storage & Structure**
Releases are stored on the node in a directory named ``repo_name.releases`` next to the bare repository. Each release is a subdirectory containing:
- ``META`` - Release metadata in ConfigObj format
- ``RELEASE.md`` or ``RELEASE.mu`` - Release notes
- ``artifacts/`` - All uploaded files
- ``THANKS`` - Appreciation count from users
**Listing Releases**
To view all releases for a repository:
.. code:: text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list
Tag Status Created Objs Notes
------------------------------------------------------------------
v1.2.0 published 2025-01-15 14:32 3 Another release
v1.1.0 published 2024-12-03 09:15 2 Bug fix release
v1.0.0 published 2024-10-20 16:45 2 Initial release
**Viewing Release Details**
To see full information about a specific release:
.. code:: text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo view v1.2.0
Release : 0.9.2
Status : published
Created : 2026-05-04 23:53:09
Thanks : 5
Release Notes
=============
Version 1.2.0 release notes...
Artifacts (4)
=============
- myapp-1.2.0.tar.gz (1.5 MB)
- myapp-1.2.0.zip (1.6 MB)
- checksums.txt (256 B)
**Deleting Releases**
To remove a release:
.. code:: text
$ rngit release rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo delete v1.2.0
Are you sure you want to delete release 'v1.2.0'? [y/N]: y
Release v1.2.0 deleted
**Requirements & Validation**
- The specified tag must exist in the remote repository
- You must have ``release`` permission for the repository
- The target artifacts directory must exist and contain at least one file
- Release notes cannot be empty
**Permissions**
Release management requires the ``release`` permission, configured the same way as other repository permissions. In the config file or ``.allowed`` files, use ``rel:target`` to grant release management rights:
.. code:: text
# In .allowed file or config
rel:all # Allow everyone
rel:9710b86... # Allow specific identity
rel:none # Deny everyone
**Nomad Network Interface**
When the Nomad Network page node is enabled, releases are displayed on a dedicated releases page for each repository. Each release is listed with its tag, creation date, artifact count and a preview of the release notes. Clicking a release shows the full details including formatted release notes and a listing of all artifacts with their sizes.
Only releases with ``published`` status are visible through the Nomad Network interface. Draft releases (if supported in future implementations) would only be visible through the command-line interface.
**All Command-Line Options (rngit release)**
.. code:: text
usage: rngit release [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
[-i PATH] [-v] [-q] [--version]
[repository] [operation] [target]
Reticulum Git Release Manager
positional arguments:
repository URL of remote repository
operation list, view, create or delete
target tag and path to release artifacts directory
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-i, --identity PATH path to release identity
-v, --verbose
-q, --quiet
--version show program's version number and exit
.. raw:: latex
\newpage
Work Documents
==============
In addition to releases, ``rngit`` provides a work document management system for tracking tasks, investigations, issues and progress related to repositories. Work documents are stored as structured msgpack data and support threaded updates and comments.
**Listing Work Documents**
To view work documents for a repository:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo list
Active documents
=================
ID Title Author Created Comments
---------------------------------------------------------------------------
1 Implemented new feature 9710b86ba12c4f2e… 2025-01-15 14:32 3
2 Fixed bug in parser 8f3a21c9d84e927b… 2025-01-14 09:15 1
Use ``--scope completed`` to view completed work documents, or ``--scope all`` to see both active and completed.
**Viewing a Work Document**
To view a specific work document with all its comments:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo view -d 1
Implement new feature (active #1)
=================================
Author : 9710b86ba12c42d1d8f30f74fe509286
Status : active
Created : 2026-05-05 15:11:11
Edited : 2026-05-05 18:22:11
Format : markdown
Updates : 0
This work document tracks the implementation of the new feature...
Updates
=======
#1 by 9710b86ba12c42d1d8f30f74fe509286 at 2026-05-05 15:38:37
-------------------------------------------------------------
Initial analysis complete
**Creating Work Documents**
To create a new work document:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo create --title "Investigate performance issue"
This will open your configured ``$EDITOR`` to compose the document content. Save and exit to create the document, or save an empty document to cancel.
**Editing Work Documents**
To edit an existing work document:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo edit -d 1
This fetches the current content, opens it in your editor, and sends any changes back to the node.
**Adding Comments**
To add an update to a work document:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo update -d 1
This opens your editor to compose the update.
**Completing Work Documents**
To mark a work document as completed (moving it from ``active`` to ``completed``):
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo complete -d 1
Work document #1 completed
**Activating Work Documents**
To mark a work document as active (moving it from ``completed`` to ``active``):
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo activate -d 1
Work document #1 activated
**Deleting Work Documents**
To delete a work document and all its comments:
.. code:: text
$ rngit work rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo delete -id 1
Are you sure you want to delete active work document #1? [y/N]: y
Work document #1 deleted
**Permissions**
Users can view work documents and updates if the have ``read`` permission for the repository. If users have ``read`` and ``interact``, they can also post updates/comments on existing work documents. Work document management requires having ``write`` and ``interact`` permission to the repository. These permissions are configured the same way as any other repository permissions. In the config file or ``.allowed`` files, use ``i:target`` to grant work document interaction rights:
.. code:: text
# In .allowed file or config
i:all # Allow everyone
i:9710b86... # Allow specific identity
i:none # Deny everyone
**Author Verification**
Users can only edit or delete work documents and updates they created. The author is cryptographically verified from the interacting link's ``remote_identity``.
**Storage Format**
Work documents are stored in a ``repo_name.work`` directory next to the repository, containing:
- ``active/`` - Active work documents
- ``completed/`` - Completed work documents
Each document is a numbered directory containing:
- ``root`` - The work document content and metadata (msgpack format)
- ``N`` - Numbered comment files (msgpack format)
**Nomad Network Interface**
When the Nomad Network page node is enabled, work documents are viewable through the web interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.
**All Command-Line Options (rngit work)**
.. code:: text
usage: rngit work [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
[-i PATH] [--scope SCOPE] [-t TITLE] [-d ID] [-v]
[-q] [--version]
[repository] [operation]
Reticulum Git Work Document Manager
positional arguments:
repository URL of remote repository
operation list, view, create, edit, delete, update or complete
options:
-h, --help show this help message and exit
--config CONFIG path to alternative config directory
--rnsconfig RNSCONFIG
path to alternative Reticulum config directory
-i, --identity PATH path to identity
--scope SCOPE document scope: active, completed or all
-t, --title TITLE document title for create
-d, --id ID document ID
-v, --verbose
-q, --quiet
--version show program's version number and exit

Some files were not shown because too many files have changed in this diff Show More