Compare commits

...

1834 Commits

Author SHA1 Message Date
Mark Qvist b1f522277c Prepare release 2026-05-17 00:56:08 +02:00
Mark Qvist af6e0c9ecf Updated changelog 2026-05-17 00:47:15 +02:00
Mark Qvist 176567e3f1 Updated version 2026-05-17 00:39:15 +02:00
Mark Qvist 15cd4268ac Cleanup 2026-05-17 00:38:51 +02:00
Mark Qvist 9307db16c4 Allow disabling mirroring interval 2026-05-17 00:17:12 +02:00
Mark Qvist 0f29ab629a Updated rngit documentation 2026-05-17 00:07:16 +02:00
Mark Qvist b2a4ceb853 Updated default config 2026-05-16 23:37:45 +02:00
Mark Qvist 6c7f1d068b Implemented fork and mirror sync from upstreams 2026-05-16 23:02:45 +02:00
Mark Qvist b76beb602d Added scaffolding for periodic upstream mirror sync and manual fork/mirror sync 2026-05-16 22:06:16 +02:00
Mark Qvist 0c68f6491a Added fork and mirror indications to rngit page node 2026-05-16 21:14:02 +02:00
Mark Qvist 038981474a Added fork and mirroring support to rngit CLI and node 2026-05-16 20:21:01 +02:00
Mark Qvist df0b4a5165 Implemented rngit remote repo create 2026-05-16 17:35:00 +02:00
Mark Qvist db7359f56d Preparation for create, fork and mirror functionality. Refactored and expanded permissions system. Added group .allowed files. Prepared dynamic permissions resolution. Basic functional scaffolding for create/fork/mirror. 2026-05-16 16:16:10 +02:00
Mark Qvist 12e45b6483 Added work document proposals 2026-05-16 02:11:00 +02:00
Mark Qvist ba8fca6f87 Nicer stats page 2026-05-15 23:21:56 +02:00
Mark Qvist 9b99b72f61 Cleanup 2026-05-15 22:15:17 +02:00
Mark Qvist 03cfbc2eb6 Added half-block chart rendering 2026-05-15 22:09:27 +02:00
Mark Qvist c92872a81b Added download stats to rngit 2026-05-15 20:12:07 +02:00
Mark Qvist f3f4d9bca3 Cleanup 2026-05-15 17:32:10 +02:00
Mark Qvist e7a317f0a0 Use canonical Transport interface list add/removes. Improved announce cache cleaning. Adjusted logging. 2026-05-15 17:08:22 +02:00
Mark Qvist d5b64a4af3 Cleaned up log/print consistency for listener/initiator modes in rncp 2026-05-15 14:40:55 +02:00
Mark Qvist 5667a0bbac Better transfer completed feedback in rncp, thanks to neutral 2026-05-15 14:27:17 +02:00
Mark Qvist 7e46422c16 Auto-set latest release on creation 2026-05-15 00:58:17 +02:00
Mark Qvist 869a803149 Updated logging 2026-05-14 23:55:01 +02:00
Mark Qvist f744e4d9a3 Updated logging 2026-05-14 23:32:33 +02:00
Mark Qvist 1a7607cba3 Improved shared instance RPC error handling 2026-05-14 19:16:52 +02:00
Mark Qvist d881c111f6 Added latest release management to rngit 2026-05-14 14:13:42 +02:00
Mark Qvist bdac57ec0b Readme formatting 2026-05-14 12:02:54 +02:00
Mark Qvist c15f566cfa Updated readme 2026-05-14 11:58:05 +02:00
Mark Qvist bdc79b9097 Updated readme 2026-05-14 11:55:11 +02:00
Mark Qvist 102eccb77d Updated readme 2026-05-14 11:54:36 +02:00
Mark Qvist e8b236c7d8 Updated readme 2026-05-14 11:54:05 +02:00
Mark Qvist d69491eb80 Updated readme 2026-05-14 11:52:18 +02:00
Mark Qvist 256a4d0b92 Cleanup 2026-05-14 11:48:44 +02:00
Mark Qvist c5add012c1 Updated readme 2026-05-14 11:46:39 +02:00
Mark Qvist 6ecc8933b4 Updated readme 2026-05-14 11:44:07 +02:00
Mark Qvist 42b5661979 Updated readme 2026-05-14 11:35:30 +02:00
Mark Qvist 6333fb39bf Updated readme 2026-05-14 10:45:48 +02:00
Mark Qvist ea27a8b8a7 Updated readme 2026-05-14 10:43:57 +02:00
Mark Qvist 358f9c3b0c Updated readme 2026-05-14 10:42:33 +02:00
Mark Qvist cb3ef69072 Updated readme 2026-05-14 10:33:36 +02:00
Mark Qvist eee9354657 Updated readme 2026-05-14 10:26:21 +02:00
Mark Qvist ff86a1d7e6 Updated readme 2026-05-14 10:19:53 +02:00
Mark Qvist e49f31322c Redirect blob to tree page if target is a tree 2026-05-14 10:07:03 +02:00
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
Mark Qvist e1340e87eb Prepare release 2026-04-21 17:02:37 +02:00
Mark Qvist e9bfef2131 Cleanup 2026-04-21 16:55:59 +02:00
Mark Qvist b408699e65 Periodically clean known destinations data based on local relevance 2026-04-21 13:21:23 +02:00
Mark Qvist 3d1c508868 Improved BackboneInterface error handling 2026-04-21 00:24:00 +02:00
Mark Qvist 84e0746c9c Updated version 2026-04-20 23:49:24 +02:00
Mark Qvist b5658c4865 Keep track of which known destinations are actually in use, so irrelevant destination data can be cleaned 2026-04-20 23:48:57 +02:00
Mark Qvist d413a4bc53 Improved resource transfer timing calculations 2026-04-20 23:44:55 +02:00
Mark Qvist ce5ab902b6 Updated docs 2026-04-20 11:38:14 +02:00
Mark Qvist 294408b0bb Run non-background data persist synchronously 2026-04-19 01:32:12 +02:00
Mark Qvist 53372fbe4c Updated docs 2026-04-18 17:27:42 +02:00
Mark Qvist 7fdac2118b Prepare release 2026-04-18 16:07:38 +02:00
Mark Qvist 1dbf78ed71 Updated changelog 2026-04-18 16:06:14 +02:00
Mark Qvist c9101a0c21 Ensure loop-originating closures have variables captured at iteration-time. Thanks @taprootmx! 2026-04-18 15:36:33 +02:00
Mark Qvist 2e6264c04b Updated changelog 2026-04-18 15:24:29 +02:00
Mark Qvist e0aa46ba22 Improved gracious transport data persist handling 2026-04-18 14:50:45 +02:00
Mark Qvist 8093c3cd2c Added local destinations lookup map 2026-04-17 11:39:14 +02:00
Mark Qvist c6778e4e29 Improved transport tunnel handling. Improved memory consumption. Fixed disk I/O bound thread execution time starvation on cache management jobs. 2026-04-17 00:07:07 +02:00
Mark Qvist c77548d299 Updated docs 2026-04-15 18:54:54 +02:00
Mark Qvist 26d435ea64 Updated version 2026-04-15 18:48:59 +02:00
Mark Qvist c3f0d98e41 Refactoring work for free-threaded transport I/O. Added ingress control bypass on pending path requests. 2026-04-15 18:48:17 +02:00
Mark Qvist 3c50f4aee9 Updated logging 2026-04-15 12:06:15 +02:00
Mark Qvist 4a930ba82a Fixed invalid EPOLL modification error handler 2026-04-15 12:04:26 +02:00
Mark Qvist 866e63f0fe Apply patch from K8: Fix IFAC for autoconnected, discovered interfaces. 2026-04-15 10:37:41 +02:00
Mark Qvist d461cfa8ce Updated manual 2026-04-15 10:32:41 +02:00
Mark Qvist 18708636fb Updated manual 2026-04-13 20:38:55 +02:00
Mark Qvist 1901cca2f3 Prepare release 2026-04-13 11:28:22 +02:00
Mark Qvist 344019f108 Prepare release 2026-04-13 11:27:46 +02:00
Mark Qvist e22a8021d3 Copy on known destinations persist 2026-04-13 11:12:12 +02:00
Mark Qvist 111c9c0ed0 Fixed missing configuration entry generation for discovered I2P interfaces. Improved interface discovery validation. 2026-04-12 19:57:34 +02:00
Mark Qvist 2445d18149 Fixed invalid ingress control burst activation and subsequent path resolution failure due to incorrect announce frequency calculation 2026-04-12 18:39:06 +02:00
Mark Qvist 739523d559 Cancel pending resource segments recursively 2026-04-12 15:35:36 +02:00
Mark Qvist 23c0a493b1 Refactoring work for free-threaded transport I/O 2026-04-12 14:55:42 +02:00
Mark Qvist fa353fb0b3 Refactored transport jobs for free-threaded implementation 2026-04-12 13:33:15 +02:00
Mark Qvist 9f817bd918 Cleanup 2026-04-12 12:20:29 +02:00
Mark Qvist 2e5480a6bd Cleanup 2026-04-12 11:20:51 +02:00
Mark Qvist 1b50b7f446 Updated changelog 2026-03-12 00:56:18 +01:00
Mark Qvist ecc413ee01 Updated docs 2026-03-12 00:52:35 +01:00
Mark Qvist 0b1bf13b84 Updated version 2026-03-12 00:24:35 +01:00
Mark Qvist 1fc6e68f3f Fixed invalid application of IP/hostname validation for on non-relevant interfaces. Thanks @joakim! 2026-03-12 00:24:09 +01:00
Mark Qvist 1bee46ed81 Updated readme 2026-01-25 16:21:45 +01:00
Mark Qvist a7772ffcd9 Updated readme 2026-01-25 16:19:17 +01:00
Mark Qvist 1263444b2b Updated readme 2026-01-25 16:15:25 +01:00
Mark Qvist 286a78ef8c Prepare release 2026-01-17 21:25:15 +01:00
Mark Qvist 0accff3e18 Updated manual 2026-01-17 18:49:01 +01:00
Mark Qvist 5f62481e62 Improved autoconnect handling 2026-01-17 18:47:08 +01:00
Mark Qvist 82b8e1f79a Clean discovered interfaces with invalid target address 2026-01-17 17:17:39 +01:00
Mark Qvist 85e2ca96bc Updated docs 2026-01-17 17:09:13 +01:00
Mark Qvist fdbf287fee Improved reachable_on discovery announce field handling 2026-01-17 17:09:01 +01:00
Mark Qvist fa4b69181f Updated docs 2026-01-16 18:07:23 +01:00
Mark Qvist a32641d9f4 Updated readme 2026-01-12 17:26:45 +01:00
Mark Qvist 44d8db043e Updated readme 2026-01-12 17:24:22 +01:00
Mark Qvist be89b12c96 Updated readme 2026-01-12 17:23:18 +01:00
Mark Qvist fd954589b5 Added discovered_interfaces API method 2026-01-11 01:20:24 +01:00
Mark Qvist a2f44668b6 Updated docs 2026-01-10 23:08:11 +01:00
Mark Qvist ab2ab37844 Updated docs 2026-01-10 23:03:55 +01:00
Mark Qvist b280a734a2 Updated docs 2026-01-10 22:53:55 +01:00
Mark Qvist 5c1bd15639 Added Zen of Reticulum 2026-01-10 21:45:57 +01:00
Mark Qvist 24fc67f242 Updated readme 2026-01-10 21:44:19 +01:00
Mark Qvist 642e0fc87e Updated readme 2026-01-10 21:43:39 +01:00
Mark Qvist 1528c09049 Added logo 2026-01-10 21:43:27 +01:00
Mark Qvist 0f4617e9c4 Added Zen of Reticulum 2026-01-10 21:43:06 +01:00
Mark Qvist a496e22ad1 Handle potential race condition in request timeout 2026-01-09 00:51:13 +01:00
Mark Qvist a420565ded Updated version 2026-01-09 00:46:25 +01:00
Mark Qvist b3f0a479c2 Updated docs 2026-01-08 12:55:09 +01:00
Mark Qvist 9e18a6d1a8 Fixed regression in resource file transfers 2026-01-08 12:38:21 +01:00
Mark Qvist 34fd72dc97 Updated docs 2026-01-07 16:05:25 +01:00
Mark Qvist ed9df7b211 Updated docs 2026-01-07 16:05:01 +01:00
Mark Qvist 965dbca514 Updated docs 2026-01-07 15:53:07 +01:00
Mark Qvist f08272c853 Updated readme 2026-01-07 00:49:46 +01:00
Mark Qvist 843891cdd3 Updated docs 2026-01-06 21:31:42 +01:00
Mark Qvist a6d59b1fa7 Cleanup 2026-01-06 21:01:11 +01:00
Mark Qvist 51d1d9fbfd Consistency 2026-01-06 17:30:56 +01:00
Mark Qvist de1358be8b Utility shim for rnpkg 2026-01-06 17:30:02 +01:00
Mark Qvist 4eb5dbc633 Prepare release 2026-01-04 12:27:34 +01:00
Mark Qvist a1e6ce2357 Updated docs 2026-01-04 12:22:08 +01:00
Mark Qvist 16e833ddb7 Updated changelog 2026-01-04 01:49:52 +01:00
Mark Qvist 4af35bd7ea Updated docs 2026-01-04 01:12:48 +01:00
Mark Qvist 7d305527e9 Updated manual 2026-01-04 00:52:31 +01:00
Mark Qvist 1d84dc94a0 Implemented external IP resolution for interface discovery announcer 2026-01-04 00:50:35 +01:00
Mark Qvist f825ba38a0 Updated docs 2026-01-04 00:49:58 +01:00
Mark Qvist f076c2d143 Updated docs 2026-01-04 00:19:32 +01:00
Mark Qvist 58a20fffb5 Updated docs 2026-01-04 00:00:02 +01:00
Mark Qvist 15a123875f Implemented bootstrap interface handling 2026-01-03 22:29:26 +01:00
Mark Qvist 7cadb3af8b Added bootstrap_only interface option 2026-01-03 20:00:22 +01:00
Mark Qvist 01984a33eb Updated docs 2026-01-03 19:59:01 +01:00
Mark Qvist 7329817d95 Updated docs 2026-01-03 19:58:49 +01:00
Mark Qvist ad4af7dd50 Sanitize mode configuration for discovery-enabled interfaces 2026-01-03 02:58:47 +01:00
Mark Qvist f2a778ffa4 Implemented discovery announce encryption 2026-01-03 02:20:24 +01:00
Mark Qvist 1a77b5752c Added auto-connect of discovered interfaces on instance start 2026-01-03 00:52:39 +01:00
Mark Qvist 2b3d6a0989 Added auto-connect option for discovered interfaces 2026-01-03 00:36:50 +01:00
Mark Qvist 0b508a04b8 Added interface discovery source filtering by network identity 2026-01-02 21:03:18 +01:00
Mark Qvist 13aebeecf9 Implemented network identity handling 2026-01-02 17:16:24 +01:00
Mark Qvist 47d3c640d6 Cleanup 2026-01-02 13:34:05 +01:00
Mark Qvist 19f27598d9 Updated version 2026-01-01 23:32:28 +01:00
Mark Qvist f2ef22e1a0 Updated config descriptions 2026-01-01 23:32:02 +01:00
Mark Qvist 251e1b8a35 Implemented remote blackhole list updater 2026-01-01 23:12:40 +01:00
Mark Qvist 5de4e24a9f Added await_path method to transport API 2026-01-01 21:37:56 +01:00
Mark Qvist 5e4d32c4c0 Added ability to view published blackhole list 2026-01-01 20:13:00 +01:00
Mark Qvist e1327842b1 Added ability to specify duration and reason to blackhole entries 2026-01-01 18:07:19 +01:00
Mark Qvist c13412369a Implemented blackhole management 2026-01-01 17:35:41 +01:00
Mark Qvist 18e4e66db8 Cleanup 2026-01-01 15:13:53 +01:00
Mark Qvist 5392d635dd Improved announce processing 2026-01-01 14:51:33 +01:00
Mark Qvist e56e80aade Added discovery support for Weave interface 2026-01-01 14:49:40 +01:00
Mark Qvist 994c4fd699 Support interface discovery on Weave interface 2026-01-01 14:01:57 +01:00
Mark Qvist ef64fefa96 Cleanup 2026-01-01 14:01:05 +01:00
Mark Qvist 344ff21c1e Cleanup 2026-01-01 02:27:45 +01:00
Mark Qvist d34e06cb8c Cleanup 2025-12-31 19:27:12 +01:00
Mark Qvist 8f65a0320b Updated docs 2025-12-31 17:29:45 +01:00
Mark Qvist b42e1c93da Cleanup 2025-12-31 17:24:08 +01:00
Mark Qvist e0ca14eb21 Cleanup 2025-12-31 17:20:28 +01:00
Mark Qvist 48fe97291b Cleanup 2025-12-31 17:12:15 +01:00
Mark Qvist f400fd7b60 Added interface discovery output to rnstatus 2025-12-31 16:46:51 +01:00
Mark Qvist fd1d464f06 Added discovery configuration to configuration options 2025-12-31 15:23:43 +01:00
Mark Qvist 28afdb36fe Discovery listing and state information 2025-12-31 14:51:56 +01:00
Mark Qvist 6c7db096fc Fixed typo 2025-12-31 14:22:15 +01:00
Mark Qvist 5a7fcb0ec3 Fixed typo 2025-12-31 14:19:49 +01:00
Mark Qvist d647da7a4a Implemented discovery handler 2025-12-31 13:50:49 +01:00
Mark Qvist d7df390bb4 Added input sanitization to discovery data 2025-12-31 11:25:20 +01:00
Mark Qvist 9d36ff48dd Implemented on-network global interface discovery 2025-12-31 01:07:08 +01:00
Mark Qvist 8743388263 Cleanup 2025-12-30 21:34:36 +01:00
Mark Qvist 58486654d5 Added FUNDING.json 2025-12-29 16:45:16 +01:00
Mark Qvist 326d719a49 Force synchronous processing for entire announce logic flow 2025-12-28 23:46:39 +01:00
Mark Qvist c9b6dc007a Added MIRROR.md 2025-12-28 00:44:33 +01:00
Mark Qvist 1bcac5e234 Updated readme 2025-12-28 00:39:27 +01:00
Mark Qvist dad58e14e2 Added MIRROR.md 2025-12-28 00:37:30 +01:00
Mark Qvist db85939322 Restored rncp corrupt identity error indication 2025-12-27 11:00:38 +01:00
markqvist 4f4eb1fce5 Merge pull request #1023 from MikelCalvo/feature/rncp-custom-identity
Added custom identity support to rncp utility
2025-12-27 10:57:42 +01:00
Mark Qvist e55000ee1a Updated docs 2025-12-27 10:49:38 +01:00
Mark Qvist 9c2bf9fba8 Updated rnstatus monitor mode arguments 2025-12-27 10:49:22 +01:00
Mark Qvist 563784573b Merge branch 'master' of github.com:markqvist/Reticulum 2025-12-27 10:46:46 +01:00
markqvist e2903f18da Merge pull request #1024 from MikelCalvo/feature/rnstatus_monitor_mode
Added monitor mode to rnstatus
2025-12-27 10:46:32 +01:00
Mark Qvist 2f47456668 Enabled ingress limiting on per-peer Weave and Auto sub-interfaces 2025-12-27 10:43:28 +01:00
Mark Qvist 79b3101fe0 Updated docs 2025-12-22 23:56:58 +01:00
Mark Qvist 9788675934 Updated docs 2025-12-22 19:00:07 +01:00
Mark Qvist 10c63fcaa2 Updated rnodeconf version and urls 2025-12-22 18:57:36 +01:00
Mark Qvist 707c012318 Updated docs 2025-12-22 16:39:10 +01:00
Mark Qvist 3f30e17eb4 Updated docs 2025-12-22 14:23:36 +01:00
Mark Qvist 9eff138c3c Added fixed MTU configuration to TCPClientInterface 2025-12-22 14:23:27 +01:00
Mark Qvist b0fb5d1898 Merge branch 'master' of github.com:markqvist/Reticulum 2025-12-22 11:36:54 +01:00
Mark Qvist d542da38b2 Added descriptive error message on corrupt ratchet file 2025-12-22 11:36:21 +01:00
markqvist c8b446ecaf Update Contributing.md 2025-12-22 00:29:52 +01:00
markqvist 6ed6af5b98 Update Contributing.md 2025-12-21 14:23:01 +01:00
markqvist 12d39916b9 Update Contributing.md 2025-12-21 11:39:26 +01:00
markqvist 12d4de0619 Update Contributing.md 2025-12-21 11:38:23 +01:00
markqvist 7ab87f688a Update Contributing.md 2025-12-21 11:35:45 +01:00
markqvist 9024a277ac Update Contributing.md 2025-12-21 11:32:08 +01:00
Mark Qvist fc00d9a5aa Cleanup 2025-12-20 14:00:42 +01:00
markqvist 106a773f22 Update Contributing.md 2025-12-19 23:05:00 +01:00
Mark Qvist 93d9cb3b69 Added reverse unicast AutoInterface discovery packets for improving peer discovery on Android devices with broken WiFi multicast handling. 2025-12-19 14:16:03 +01:00
Mark Qvist 99504b7f7d Reverted AutoInterface ingress limit 2025-12-12 00:22:41 +01:00
Mark Qvist 72c1995551 Enabled ingress limiting on per-peer AutoInterface sub-interfaces 2025-12-02 21:24:43 +01:00
Mark Qvist 3d8c6c3839 Merge branch 'master' of github.com:markqvist/Reticulum 2025-12-02 21:18:32 +01:00
Mark Qvist 0a06ffd074 Updated manual 2025-12-02 21:18:11 +01:00
Mark Qvist 12abb544bf Cleanup 2025-12-02 21:18:02 +01:00
markqvist 78fe132cc2 Merge pull request #1026 from wincentbalin/master
Fixed typo
2025-12-01 23:50:57 +01:00
Wincent Balin b516d7f092 Fixed typo 2025-12-01 18:57:48 +01:00
Mikel Calvo 0961df316f Added monitor mode to rnstatus 2025-12-01 02:14:27 +01:00
Mikel Calvo 8ad2986877 Added custom identity support to rncp utility 2025-12-01 01:29:54 +01:00
Mark Qvist 6214487fb3 Updated manual 2025-11-27 22:18:46 +01:00
Mark Qvist 2219a5454c Updated makefile 2025-11-27 22:18:39 +01:00
Mark Qvist 712a5d1b06 Cleanup 2025-11-26 20:28:21 +01:00
Mark Qvist cbc3b800fb Updated manual 2025-11-24 02:06:56 +01:00
Mark Qvist e7348d0812 Merge branch 'master' of https://git.unsigned.io/markqvist/Reticulum 2025-11-23 14:57:04 +01:00
Mark Qvist 59e638402c Corrected logic regression. Fixes #1014. 2025-11-23 14:56:47 +01:00
Mark Qvist bcd6de015d Corrected logic regression. Fixes #1004. 2025-11-23 14:55:32 +01:00
Mark Qvist b798c84160 Updated changelog 2025-11-22 15:04:44 +01:00
Mark Qvist 708f666787 Updated changelog 2025-11-22 15:00:17 +01:00
Mark Qvist 4f03302ae2 Cleanup 2025-11-22 14:57:50 +01:00
Mark Qvist d8f6ab206b Updated docs 2025-11-22 12:06:58 +01:00
Mark Qvist 472e69fe9a Updated docs 2025-11-22 11:21:33 +01:00
Mark Qvist aeed5279f8 Fixed formatting 2025-11-22 11:21:05 +01:00
Mark Qvist f3b8965fa6 Fixed formatting 2025-11-22 11:16:29 +01:00
Mark Qvist 1bbaab1db9 Updated version 2025-11-21 17:10:13 +01:00
Mark Qvist bf2fcbba37 Added interference detection status and history to rnstatus output for RNode interfaces 2025-11-21 15:56:17 +01:00
Mark Qvist a63dd67a07 Updated changelog 2025-11-19 15:40:03 +01:00
Mark Qvist b27f9836ae Updated docs and manual 2025-11-19 15:39:30 +01:00
Mark Qvist 9504c5b863 Updated docs 2025-11-19 15:37:24 +01:00
Mark Qvist 9767b3453e Updated docs 2025-11-19 15:23:35 +01:00
Mark Qvist 643fbbbc84 Fixed broken links. Thanks @symbioquine in #999. 2025-11-19 15:21:55 +01:00
Mark Qvist 2a5bcd5f52 Updated docs 2025-11-19 15:16:46 +01:00
Mark Qvist 237c3160eb Updated RNode TCP read timeouts 2025-11-19 14:37:47 +01:00
Mark Qvist fcdcf1a2a8 RNode TCP connection on Android 2025-11-18 12:27:36 +01:00
Mark Qvist 7c99aca1d0 Improved reconnect/hotplug reliability and responsiveness for RNodes connected over WiFi 2025-11-18 03:12:42 +01:00
Mark Qvist 309f1999e7 Cleanup 2025-11-17 21:13:21 +01:00
Mark Qvist fa6de7ff79 Updated docs 2025-11-17 19:00:13 +01:00
Mark Qvist 47dfcab170 Added ability to configure RNode IP settings to rnodeconf 2025-11-17 18:46:36 +01:00
Mark Qvist 8abd19800f Updated version 2025-11-17 17:16:29 +01:00
Mark Qvist b2d6ed733d Added support for configuring RNode WiFi settings to rnodeconf 2025-11-17 17:16:06 +01:00
Mark Qvist 1179757893 Added support for connecting RNode devices over TCP connections 2025-11-17 00:31:37 +01:00
Mark Qvist d328ef5ce0 Cleanup 2025-11-15 14:20:43 +01:00
Mark Qvist f577d3018f Updated manual 2025-11-15 13:43:36 +01:00
Mark Qvist e6db629915 Updated manual 2025-11-15 13:42:14 +01:00
Mark Qvist acaab30b91 Updated manual 2025-11-15 13:35:00 +01:00
Mark Qvist 76cedeed07 Updated BLE connection read timeouts on Android 2025-11-15 12:11:45 +01:00
Mark Qvist 5beea74eb3 Handle serial port never being opened due to failure on interface detach for RNodeInterface 2025-11-11 10:29:06 +01:00
Mark Qvist 1f91a8f6f2 Updated manual 2025-11-10 19:05:36 +01:00
Mark Qvist 080216bd55 Updated version 2025-11-10 18:58:27 +01:00
Mark Qvist aa37172293 Added support for CPU temperature reporting from RNode devices 2025-11-10 18:44:57 +01:00
Mark Qvist 5836d7f8ba Added support for RNodes with PAs up to 37 dBm. Corrected Heltec V4 board parameters in rnodeconf. 2025-11-07 19:34:16 +01:00
Mark Qvist a699d7c110 Run github workflow tests on Python 3.11 2025-11-02 22:43:12 +01:00
Mark Qvist 8eedbb9d91 Run github workflow tests on Python 3.11 2025-11-02 22:41:25 +01:00
Mark Qvist 2afa85db60 Updated manual 2025-11-02 22:37:34 +01:00
Mark Qvist a5672e7afe Updated manual 2025-11-02 22:36:40 +01:00
Mark Qvist 77d40215c8 Updated documentation 2025-11-02 22:27:27 +01:00
Mark Qvist 0896df05b6 Workaround for https://github.com/python/cpython/issues/138720 2025-11-02 11:50:17 +01:00
Mark Qvist cf0b1c6237 AutoInterface peering timing 2025-11-01 17:29:53 +01:00
Mark Qvist 08b129c8e0 Cleanup 2025-11-01 17:23:40 +01:00
Mark Qvist e2ea397715 Added Heltec v4 to rnodeconf 2025-11-01 15:23:31 +01:00
Mark Qvist 25a73e6ef9 Added detection and logging of multicast echoes never arriving on AutoInterface system interfaces. Implements #905. 2025-11-01 14:51:50 +01:00
markqvist f1c4bba3f2 Merge pull request #599 from faragher/master
Ported rncp allow files to rnx
2025-11-01 14:18:03 +01:00
Mark Qvist 704019ded8 Fixed typo 2025-11-01 14:13:17 +01:00
markqvist 79f5b92bae Merge pull request #911 from SCWhite/patch-1
minor error in hardware.html
2025-11-01 14:12:51 +01:00
Mark Qvist 5aaa743ef8 Updated rnid output 2025-11-01 14:11:35 +01:00
markqvist 9721c0bf85 Merge pull request #901 from jacobeva/cmd_interfaces_fix
Change CMD_INTERFACES value to be 0x71, fix #900
2025-11-01 14:10:34 +01:00
Mark Qvist 56848cdb63 Ensure default destination app data can be generated and sent even on first system-internal discovery announce 2025-10-31 21:25:28 +01:00
Mark Qvist 41ad089ff7 Added path response status signalling to announce handlers 2025-10-30 15:00:39 +01:00
Mark Qvist 2df355d7b4 Updated version 2025-10-30 14:11:06 +01:00
Mark Qvist 39a63b0643 WeaveInterface compatibility on Android 2025-10-29 15:44:47 +01:00
Mark Qvist ddf14e5636 Updated WeaveInterface. Added support for Weave devices to rnstatus. 2025-10-29 12:43:20 +01:00
Mark Qvist 7138749307 Fixed RNodeInterface BLE reconnection hang on Android 2025-10-28 12:31:52 +01:00
Mark Qvist af7697f223 Fixed string formatting for Android 2025-10-28 12:31:30 +01:00
Mark Qvist 0bcb4b8573 Cleanup 2025-10-28 10:59:58 +01:00
Mark Qvist 6d47b59b1e Added WeaveInterface 2025-10-28 02:31:33 +01:00
Mark Qvist 3d8eaffe9a Added WeaveInterface 2025-10-28 02:20:22 +01:00
Mark Qvist d8039aca17 Changed CMD_INTERFACES to 0x71. Fixes #900. 2025-10-28 02:10:02 +01:00
Mark Qvist 4e4d379486 Fixed Android BLE MTU for RNodeInterface 2025-10-28 02:05:59 +01:00
SCWhite 87faffa785 minor error in hardware.html
manual/hareware.html: correct T114 device platform "ESP32" → "nRF52"
2025-09-04 02:59:53 +08:00
jacob.eva adef3f80f0 Change CMD_INTERFACES value to be 0x71, fix #900 2025-08-16 16:10:13 +01:00
Mark Qvist 319c798f78 Updated readme 2025-07-15 02:28:54 +02:00
Mark Qvist 8579a7f2a5 Updated changelog 2025-07-14 16:37:41 +02:00
Mark Qvist ffbbba7395 Updated manual 2025-07-14 16:37:06 +02:00
Mark Qvist e66745c9ef Updated documentation 2025-07-14 16:07:07 +02:00
Mark Qvist 45fc9338a7 Improved BLE MTU on Android 2025-07-13 23:45:50 +02:00
Mark Qvist f2969bd1b0 Improved BLE device discovery on Android 2025-07-13 22:07:47 +02:00
Mark Qvist e0f1f3f947 Merge branch 'master' of github.com:markqvist/Reticulum 2025-07-13 19:17:13 +02:00
Mark Qvist e3827f2e25 Updated manual 2025-07-13 19:17:00 +02:00
Mark Qvist fad1d4972c Fixed potential EPOLL hang on interface failure 2025-07-13 19:16:51 +02:00
markqvist 2c33ce6c98 Merge pull request #839 from wincentbalin/master
Fixed typo
2025-07-13 15:02:45 +02:00
Mark Qvist c0d7f42f17 Cleanup 2025-07-13 14:51:24 +02:00
Mark Qvist d5a8e4b056 Trace exception on public key load failure 2025-07-13 13:58:09 +02:00
Mark Qvist 76dd50a060 Fixed potential AutoInterface peer discovery add before final init complete 2025-07-13 13:14:18 +02:00
Mark Qvist 6f9a9a7ad9 Fixed link request handling with invalid link mode 2025-07-13 13:05:37 +02:00
Mark Qvist eaec9a493b Updated readme 2025-07-13 12:28:50 +02:00
Mark Qvist d3c8555b39 Updated documentation 2025-07-13 12:26:24 +02:00
Mark Qvist 446f5c0989 Merge branch 'master' of github.com:markqvist/Reticulum 2025-07-13 12:05:59 +02:00
markqvist f3b72a8a3c Merge pull request #844 from Aareon/patch-2
fix(rnstatus): Add validation for missing -i flag when using -R
2025-07-13 12:06:20 +02:00
markqvist d2c5a1f34b Merge pull request #856 from jacobeva/multi_transport_fix
Fix announce cap crash when transport mode enabled
2025-07-13 11:56:59 +02:00
markqvist 182b49cc04 Merge pull request #843 from Aareon/patch-1
docs: Document -R and -i flag dependency in rnstatus usage
2025-07-13 11:54:59 +02:00
Mark Qvist cc8bd34cd4 Merge branch 'master' of github.com:markqvist/Reticulum 2025-07-13 11:54:54 +02:00
Mark Qvist 957ece7394 Fixed typo 2025-07-13 11:53:25 +02:00
Mark Qvist 762343adf9 Updated version 2025-07-13 11:49:44 +02:00
Mark Qvist 8d32b378d9 Fixed log statements 2025-07-13 11:49:12 +02:00
jacob.eva 41e816d299 Fix announce cap crash when transport mode enabled 2025-07-01 18:16:24 +01:00
Aareon Sullivan 4226a62f23 fix(rnstatus): Add validation for missing -i flag when using -R
Add check to ensure `management_identity` is provided when using remote
query flag (`-R`). Prevents `TypeError` and provides clear error message
when user forgets to specify identity file with `-i` flag.

Before: `expected str, bytes or os.PathLike object, not NoneType`
After: `Remote management requires an identity file. Use -i to specify the path to a management identity.`

Fixes #792
2025-06-08 19:52:46 -05:00
Aareon Sullivan 5dda28559b docs: Document -R and -i flag dependency in rnstatus usage
Add usage example and clarify that -R requires -i flag for remote
transport instance status queries. Fixes confusion where users get
"expected str, bytes or os.PathLike object, not NoneType" error
when using -R without -i.

- Add remote status query example to Usage Examples section
- Update -R flag description to indicate -i requirement
- Add explanatory note about management identity authorization

Addresses user reported issue where documentation was unclear about
mandatory flag combination for remote management functionality.
2025-06-08 19:33:42 -05:00
Wincent Balin d055ca50d6 Fixed typo 2025-05-31 01:12:25 +02:00
Mark Qvist 799bcfc7aa Updated version 2025-05-26 19:08:03 +02:00
Mark Qvist 045cb662ef Removed legacy AES-128 handlers 2025-05-26 19:04:30 +02:00
Mark Qvist 51e3983bf8 Updated readme 2025-05-17 10:39:52 +02:00
Mark Qvist 95fdc41845 Updated readme 2025-05-17 10:39:30 +02:00
Mark Qvist d795fbeaf3 Updated documentation 2025-05-17 10:37:11 +02:00
Mark Qvist 13037d68ed Updated documentation 2025-05-17 10:35:49 +02:00
Mark Qvist 6da5df9f21 Merge branch 'master' of github.com:markqvist/Reticulum 2025-05-17 10:25:24 +02:00
Mark Qvist 8128f573ef Updated manual 2025-05-17 10:25:09 +02:00
markqvist accf104553 Update FUNDING.yml 2025-05-17 10:24:38 +02:00
Mark Qvist 5387264dcb Updated changelog 2025-05-15 22:24:33 +02:00
Mark Qvist 308a6906db Updated manual 2025-05-15 15:32:31 +02:00
Mark Qvist 96ce7e3f47 Updated changelog 2025-05-15 15:32:20 +02:00
Mark Qvist f186b6266b Implemented dynamic keepalive and link timeout calculation 2025-05-15 12:50:16 +02:00
Mark Qvist 756029e5af Added option to specify shared instance type 2025-05-15 01:14:55 +02:00
Mark Qvist c1673f39b6 Updated changelog 2025-05-13 19:46:18 +02:00
Mark Qvist 30a08c4192 Updated manual 2025-05-13 18:01:13 +02:00
Mark Qvist d680f4d411 Cleanup 2025-05-13 17:59:26 +02:00
Mark Qvist 29a52e19cf Cleanup 2025-05-13 17:25:00 +02:00
Mark Qvist 11511168dc Cleanup 2025-05-13 13:32:35 +02:00
Mark Qvist d4ea698236 Cleanup 2025-05-13 13:29:20 +02:00
Mark Qvist 11e06b477e Cleanup 2025-05-13 13:26:26 +02:00
Mark Qvist 4e4c68071f Removed legacy encryption modes. Default to AES-256 for links and packets. 2025-05-13 13:18:44 +02:00
Mark Qvist 5f502746a4 Updated tests 2025-05-13 13:16:37 +02:00
Mark Qvist 17bbb9c0b4 Updated docs 2025-05-12 20:20:29 +02:00
Mark Qvist 8b13d6e08b Fixed announce handlers not triggering after shared instance disappearance/reappearance 2025-05-12 11:41:06 +02:00
Mark Qvist efa512be32 Cleanup 2025-05-11 16:40:14 +02:00
Mark Qvist 594f5fba1e Added ability to return file resources for request responses. Added option to specify request response auto-compression limits. 2025-05-11 16:37:57 +02:00
Mark Qvist 2912fb2184 Added option to specify resource auto-compression limits 2025-05-11 16:37:19 +02:00
Mark Qvist 02496f39f7 Removed completed tasks from roadmap 2025-05-11 11:30:06 +02:00
Mark Qvist 4e31f113c6 Optimised hardware MTU autoconfig 2025-05-10 23:15:43 +02:00
Mark Qvist 9aded3e1da Updated readme 2025-05-10 23:09:21 +02:00
Mark Qvist 3337d18e9a Added allow overwrite option to rncp 2025-05-10 21:44:42 +02:00
Mark Qvist 2cb6d019f9 Improved rncp memory utilisation and performance 2025-05-10 21:19:57 +02:00
Mark Qvist 3dc260a300 Updated link tests 2025-05-10 20:59:23 +02:00
Mark Qvist 4d7f5b8ca6 Let shared instance handle packet hashlist 2025-05-10 20:58:54 +02:00
Mark Qvist 48be5f65d8 Faster link cleanup on close 2025-05-10 20:58:01 +02:00
Mark Qvist b5d854a55c Resource performance and memory optimisations 2025-05-10 20:57:32 +02:00
Mark Qvist 552663c625 Fixed offset 2025-05-10 17:00:27 +02:00
Mark Qvist e6f0b92464 Updated example 2025-05-10 17:00:02 +02:00
Mark Qvist 08a6820aa0 Updated descriptions 2025-05-10 15:42:22 +02:00
Mark Qvist cc1faa55be Updated readme 2025-05-10 15:39:51 +02:00
Mark Qvist 840966f3e6 Updated version 2025-05-10 15:38:28 +02:00
Mark Qvist 763078a1ae Added ability to include metadata on resource transfers 2025-05-10 15:38:06 +02:00
Mark Qvist 5fb6abd019 Added resource example 2025-05-10 15:32:06 +02:00
Mark Qvist 7065856229 Added resource with metadata tests 2025-05-10 15:31:35 +02:00
Mark Qvist 668ef9253a Cleanup 2025-05-09 12:07:00 +02:00
Mark Qvist 6f333b8234 Cleanup 2025-05-09 12:04:55 +02:00
Mark Qvist 32c839f497 Updated changelog 2025-05-09 11:31:54 +02:00
Mark Qvist cbdef1d538 Updated manual 2025-05-09 11:21:13 +02:00
Mark Qvist c398b34dd8 Fixed potential unhandled exception on fast-flapping connections 2025-05-08 10:57:34 +02:00
Mark Qvist 9a1884cfec Updated manual 2025-05-07 12:10:53 +02:00
Mark Qvist 378dc1e931 Added link mode get method to Link API 2025-05-06 19:09:40 +02:00
Mark Qvist be821b6927 Added instance_name option and description to default config file 2025-05-06 19:09:20 +02:00
Mark Qvist af46e98865 Improved ratchet persist reliability 2025-05-06 18:18:05 +02:00
Mark Qvist 65b1667ae7 Updated readme 2025-05-06 17:57:08 +02:00
Mark Qvist 5bc1fc2bde Updated readme 2025-05-06 17:55:45 +02:00
Mark Qvist 4ae0f28aa0 Cleanup 2025-05-06 17:48:38 +02:00
Mark Qvist 62ecc0549d Updated readme 2025-05-06 17:25:53 +02:00
Mark Qvist cbf4c71a73 Added pure-python AES-256 implementation 2025-05-06 17:20:55 +02:00
Mark Qvist 1d27fae370 Updated github test workflow 2025-05-06 16:45:16 +02:00
Mark Qvist 05b9a80a07 Path MTU clamping handling with link mode signalling 2025-05-06 16:37:04 +02:00
Mark Qvist 38241452d3 Dynamic link mode establishment 2025-05-06 16:31:36 +02:00
Mark Qvist 40e040807a Link mode signalling generators 2025-05-06 16:12:54 +02:00
Mark Qvist 437da99d63 Handle AES-256 compatibility in Identity 2025-05-06 16:12:15 +02:00
Mark Qvist 3cbcbec942 Updated tests for link modes 2025-05-06 14:23:42 +02:00
Mark Qvist bc7a8cd09f Updated documentation 2025-05-06 14:14:42 +02:00
Mark Qvist ab0ac46d5a Enabled AES_256_CBC link mode 2025-05-06 12:10:34 +02:00
Mark Qvist d7791c60e2 Implemented compatibility handling for AES-256 migration 2025-05-06 12:08:17 +02:00
Mark Qvist 5dc8cdc6dc Merge branch 'master' of github.com:markqvist/Reticulum 2025-04-29 11:42:23 +02:00
Mark Qvist cdc33a25c5 Updated readme 2025-04-29 11:42:07 +02:00
markqvist 2b6766f68a Merge pull request #801 from LinuxinaBit/master
Generative AI Policy
2025-04-28 15:29:32 +02:00
Linux in a Bit e871bbdc07 Add Generative AI Policy reminder to 🐛-bug-report.md 2025-04-20 15:04:55 -05:00
Linux in a Bit 6a98158ba6 [Contributing.md] Add LLM Policy; Emphasize CLA 2025-04-20 14:59:23 -05:00
Mark Qvist ef8d44c257 Updated changelog 2025-04-20 21:23:08 +02:00
Mark Qvist 6a48a4d1c0 Updated version 2025-04-18 12:25:47 +02:00
Mark Qvist 4d2ba28934 Docs build 2025-04-17 23:04:00 +02:00
Mark Qvist 98d4f1c69e Fixed instance name from config being overwritten if option was not last in section 2025-04-17 15:02:08 +02:00
Mark Qvist a0f0d73204 Improved ratchet persist 2025-04-17 14:25:24 +02:00
Mark Qvist 1dbb1a6a35 Merge branch 'linkmodes' 2025-04-16 14:11:14 +02:00
Mark Qvist cc50ca82b8 Added todo note 2025-04-16 14:09:43 +02:00
Mark Qvist 373790c890 Added AES-256 support to Link 2025-04-16 14:05:34 +02:00
Mark Qvist ef30d21b58 Added AES_256_CBC support to Token 2025-04-16 14:03:40 +02:00
Mark Qvist c4cafed6aa Added AES_128_CBC and AES_256_CBC mode proxies 2025-04-16 14:01:26 +02:00
Mark Qvist 828eec5e0d Cleanup 2025-04-16 01:30:11 +02:00
Mark Qvist a8c50fe7d4 Link mode signalling fields 2025-04-16 00:44:30 +02:00
Mark Qvist ab9fc7b370 Updated docs 2025-04-16 00:43:35 +02:00
Mark Qvist 0dc972f7c9 Updated docs 2025-04-16 00:41:50 +02:00
Mark Qvist 796cffe29d Updated docs 2025-04-16 00:40:29 +02:00
Mark Qvist a0f6c99fb5 Updated docs 2025-04-16 00:37:11 +02:00
Mark Qvist eff0c91ed0 Updated docs 2025-04-16 00:35:56 +02:00
Mark Qvist dba6cd8393 Updated license 2025-04-15 20:48:12 +02:00
Mark Qvist e7daceec82 Updated license 2025-04-15 20:19:33 +02:00
Mark Qvist a65473f6ab Updated docs 2025-04-15 18:57:12 +02:00
Mark Qvist 1851fda9e0 Fixed interface string representation 2025-04-15 18:51:52 +02:00
Mark Qvist 80eec131f8 Updated docs 2025-04-15 18:25:12 +02:00
Mark Qvist bfe5b876de Fixed occasional io thread hang on shutdown 2025-04-15 18:04:32 +02:00
Mark Qvist da8a0ee5e9 Updated docs 2025-04-15 17:45:01 +02:00
Mark Qvist 3269384439 Cleanup 2025-04-15 17:44:52 +02:00
Mark Qvist 9a766eac8c Add init to interface utils 2025-04-15 14:04:02 +02:00
Mark Qvist 9d2456500a Added rnodeconf autoinstaller support for XIAO ESP32S3 boards 2025-04-13 03:42:47 +02:00
Mark Qvist df85beac3e Merge branch 'master' of github.com:markqvist/Reticulum 2025-04-12 21:30:41 +02:00
Mark Qvist 3dd020cb86 Fix string representation 2025-04-12 21:30:36 +02:00
markqvist 67da6be040 Merge pull request #769 from easytarget/xiao-esp32s3-wio
add rnodeconf support for SeeedStudio XIAO esp32s3 wio
2025-04-12 21:29:47 +02:00
Mark Qvist d2efd6c3e4 Allow AP mode on Backbone and TCP interfaces 2025-04-12 11:01:57 +02:00
Mark Qvist ea4a525db6 Cleanup 2025-04-11 12:40:22 +02:00
Mark Qvist c83043b087 Cleanup 2025-04-11 12:38:46 +02:00
Mark Qvist c07e968218 Cleanup 2025-04-11 12:30:22 +02:00
Mark Qvist a6eeac14d2 Added internal netinfo implementation 2025-04-11 12:28:36 +02:00
Mark Qvist a65bc3bc7b Added internal netinfo implementation 2025-04-11 12:25:52 +02:00
Mark Qvist 8e4b0b3b16 Use internal netinfo implementation 2025-04-11 12:22:58 +02:00
Mark Qvist d34cefe31d Updated readme 2025-04-11 12:22:24 +02:00
Mark Qvist 3a68a3fc02 Removed ifaddr library 2025-04-11 12:15:27 +02:00
Mark Qvist a4b6a64611 Fixed typo 2025-04-10 13:26:44 +02:00
Mark Qvist 4f189f5319 Updated manual 2025-04-10 00:23:53 +02:00
Mark Qvist cb69085280 Updated hardware section of docs 2025-04-10 00:16:01 +02:00
Mark Qvist f4d13986af Disable AP mode on BackboneInterface 2025-04-09 23:47:49 +02:00
Mark Qvist 6125c835f7 Updated interface documentation 2025-04-09 23:45:14 +02:00
Mark Qvist 3049049d5b Use abstract domain sockets for RPC 2025-04-09 17:15:38 +02:00
Mark Qvist 628c4984a3 Added IPv6 support to BackboneInterface 2025-04-09 14:23:39 +02:00
Mark Qvist b58cb3c0ed Cache clean interval 2025-04-09 00:09:17 +02:00
Mark Qvist b267687c7f Announce cache handling 2025-04-09 00:01:08 +02:00
Mark Qvist 581b16f87c Improved link and reverse table culling 2025-04-08 16:25:18 +02:00
Mark Qvist f9d42082a2 Clean up importlib imports 2025-04-08 15:23:44 +02:00
Mark Qvist f8925eaed1 Exclude built documentation from sdist 2025-04-08 14:36:59 +02:00
Mark Qvist f4c1ece10a Updated manual 2025-04-08 14:36:30 +02:00
Mark Qvist d13b034cab Cleanup 2025-04-08 14:06:07 +02:00
Mark Qvist 008afd88d1 Cleanup 2025-04-08 14:04:21 +02:00
Mark Qvist 68ca903db4 Updated Identity API docstring 2025-04-08 14:02:10 +02:00
Mark Qvist 8f4b4fa82d Add ability to search for identity by identity hash 2025-04-08 13:54:22 +02:00
Mark Qvist 768f562437 Fixed compact log format output 2025-04-08 13:48:48 +02:00
Mark Qvist 9f0a4bfe69 Don't reference interface instances in tunnel path lists 2025-04-08 13:20:02 +02:00
Mark Qvist 13b4291840 Epoll backend switch 2025-04-08 02:33:32 +02:00
Mark Qvist 6dc33126a5 Remove null byte from abstract socket name 2025-04-08 02:09:44 +02:00
Mark Qvist fa31dced22 Tunnel table indices 2025-04-08 01:35:59 +02:00
Mark Qvist 194f6aef1d Clean BackboneInterface file descriptor refs immediately 2025-04-07 20:22:20 +02:00
Mark Qvist a12b630a4e Only collect when necessary 2025-04-07 19:03:19 +02:00
Mark Qvist c3ff73591a Fix addr_info property 2025-04-07 18:48:12 +02:00
Mark Qvist 1967811d68 Error logging 2025-04-07 17:55:34 +02:00
Mark Qvist 0e24a0d8bb Cleanup 2025-04-07 17:17:30 +02:00
Mark Qvist 5913f61e7d Cleanup 2025-04-07 15:31:27 +02:00
Mark Qvist 9a7e517c73 Updated version 2025-04-07 15:04:19 +02:00
Mark Qvist 99af71de75 Store only announce packet hashes in path table instead of full announce 2025-04-07 15:03:37 +02:00
Mark Qvist 06848b6731 Added missing none check on interface socket 2025-04-07 15:02:32 +02:00
Mark Qvist 4ece3a6140 Cleanup 2025-04-07 14:30:34 +02:00
Mark Qvist ae92432878 Added transport table index specifiers 2025-04-07 13:54:14 +02:00
Mark Qvist a4468da9b1 Refactored destination_table to path_table 2025-04-07 12:47:41 +02:00
Mark Qvist 187931a0ea Added interactive shell option to rnsd 2025-04-07 12:41:17 +02:00
Mark Qvist d3533e17e8 Cleanup 2025-04-07 01:42:49 +02:00
Mark Qvist b0944429db Merge branch 'master' of github.com:markqvist/Reticulum 2025-04-07 01:09:23 +02:00
Mark Qvist 7170573da7 Cleanup 2025-04-07 01:04:37 +02:00
Mark Qvist 4cd94c776a Added ability to run local shared instance over abstract domain sockets 2025-04-07 00:46:40 +02:00
Mark Qvist 3483de1fc2 Use epoll backend for LocalInterface 2025-04-06 22:50:43 +02:00
Mark Qvist df3c2cffb3 Work on BackboneInterface 2025-04-06 21:42:54 +02:00
markqvist f0e3bc0c14 Merge pull request #783 from LinuxinaBit/patch-1
Wording fixes in Contributing.md
2025-04-06 19:29:34 +02:00
Mark Qvist b4d1d54ccb Cleanup 2025-04-06 18:45:36 +02:00
Mark Qvist de3438248f Run all BackboneInterface I/O on single epoll instance 2025-04-06 18:17:37 +02:00
Mark Qvist 456eea9c13 Log instead of raise on outbound on closed link 2025-04-05 14:40:00 +02:00
Mark Qvist 3cdebb6e8a Work on BackboneInterface 2025-04-05 14:06:05 +02:00
Mark Qvist e0a9dad114 Docs 2025-04-05 14:05:43 +02:00
Mark Qvist b1aa355d5b Cleanup 2025-04-05 00:02:54 +02:00
Mark Qvist 129591392f Updated configobj and removed six dependency 2025-04-04 23:28:04 +02:00
Linux in a Bit e51f0f14d9 Wording fixes in Contributing.md 2025-04-03 16:14:28 -05:00
Mark Qvist 2c520bb936 Cleanup 2025-04-03 17:50:21 +02:00
Mark Qvist d3bccb2b4e Detach on BackboneInterface 2025-04-03 17:48:26 +02:00
Mark Qvist e28f44cfe5 Interface compat notice 2025-04-03 17:43:24 +02:00
Mark Qvist 45e5c85868 Added BackboneInterface skeleton 2025-04-03 17:39:32 +02:00
Mark Qvist c5bc92e4ea Added loader for BackboneInterface 2025-04-03 17:38:00 +02:00
Mark Qvist ebb8a35129 Improved rncp stats. Added no-compress option to listener. 2025-04-03 17:37:36 +02:00
Mark Qvist f2046b2453 Slots on packet 2025-04-03 17:36:37 +02:00
Mark Qvist f7351a3eb5 Fixed missing none check on TCPInterface 2025-04-03 17:36:09 +02:00
Mark Qvist 28d55279d8 Merge branch 'master' of github.com:markqvist/Reticulum 2025-03-31 16:37:22 +02:00
markqvist 8104db4fcc Merge pull request #713 from jacobeva/multi-spec
Update multi interface interaction spec
2025-03-31 16:37:04 +02:00
Mark Qvist b8658cd47c Fixed IF mode warnings 2025-03-31 16:36:53 +02:00
markqvist ecaa8d53e0 Merge pull request #770 from qbit/doc-fixes
A few fixes for the documentation
2025-03-24 14:36:25 +01:00
Aaron Bieber ca1ec1acef docs: remove stray "both" from networks document 2025-03-24 07:30:35 -06:00
Aaron Bieber 13283cb8e2 docs: fix spelling in examples page 2025-03-24 07:28:59 -06:00
Owen 5a42adb05b add XIAO esp32s3 wio 868Mhz board 2025-03-24 02:55:02 +01:00
Mark Qvist 98afe98870 Cleanup 2025-03-13 20:11:44 +01:00
Mark Qvist f5420d3be3 Updated changelog 2025-03-13 19:36:39 +01:00
Mark Qvist 50b5ab80c4 Updated manual and changelog 2025-03-13 19:36:17 +01:00
Mark Qvist e6371d74b5 Updated manual 2025-03-13 19:31:38 +01:00
Mark Qvist 0ab38faeac Merge branch 'master' of github.com:markqvist/Reticulum 2025-03-13 19:25:29 +01:00
Mark Qvist b0444104cc Wait for announce timebase tick when announcing via rnid. Fixes #752. 2025-03-13 19:25:10 +01:00
Mark Qvist 4757d6ee87 Remove corrupt ratchet files when cleaning ratchets 2025-03-13 18:59:03 +01:00
markqvist 1780965ef8 Update README.md 2025-03-12 11:29:08 +01:00
Mark Qvist aaa88e9b7d Fixed AutoInterface deferred init 2025-03-09 19:01:54 +01:00
Mark Qvist 17ce91a4a2 Fixed AutoInterface deferred init 2025-03-09 18:39:23 +01:00
Mark Qvist 08751a762a Add timing deviation stats to encrypt/decrypt tests 2025-02-25 12:31:29 +01:00
Mark Qvist 77c0beecf2 Fix X25519 base-point according to RFC7748 2025-02-25 12:27:15 +01:00
Mark Qvist 28bcf6a8ac Cleanup 2025-02-24 12:19:28 +01:00
Mark Qvist 61004b4dfb Updated docs 2025-02-24 12:05:23 +01:00
Mark Qvist e5c22b8a3f Run tests against CRNS 2025-02-24 12:04:10 +01:00
Mark Qvist 001d0f30aa Enabled link MTU discovery 2025-02-24 12:03:54 +01:00
Mark Qvist fbe4bb03d1 Run rnprobe via CRNS shim 2025-02-24 11:48:09 +01:00
Mark Qvist 3469b6beb8 Run rnid via CRNS shim 2025-02-24 11:46:35 +01:00
Mark Qvist c696efe0bc Run rnstatus via CRNS shim 2025-02-24 11:45:19 +01:00
Mark Qvist d0ca61f373 Implemented child interface spawning on AutoInterface 2025-02-24 01:57:39 +01:00
Mark Qvist 350687eda9 Link MTU upgrade on AutoInterface 2025-02-23 22:40:36 +01:00
Mark Qvist d898641e6a Added link API functions 2025-02-23 20:01:40 +01:00
Mark Qvist db576d73bb Cleanup 2025-02-23 15:18:12 +01:00
Mark Qvist 5fcdd17665 Added on-demand object code compilation and loader 2025-02-23 14:45:24 +01:00
Mark Qvist e8f2bd9b0c Updated manual 2025-02-22 21:21:51 +01:00
Mark Qvist 0ff51fed44 Updated docs 2025-02-17 22:11:43 +01:00
Mark Qvist 6e25f96024 Updated changelog 2025-02-17 22:11:38 +01:00
Mark Qvist ad228fb3b3 Copy list structures on persist 2025-02-17 19:29:23 +01:00
Mark Qvist a61b20a066 Updated version 2025-02-17 18:21:58 +01:00
Mark Qvist a49b04af21 Added missing check for path announce emission timestamp in lower hop-count announce processing. Closes #717. 2025-02-17 18:21:38 +01:00
jacob.eva 3002023a70 Update multi interface interaction spec 2025-02-10 18:31:46 +00:00
Mark Qvist f030cf6f22 Fixed potential daemon thread IO buffer deadlock on externally mediated shutdown signal 2025-02-09 17:52:49 +01:00
Mark Qvist 9e7641d2d3 Trace application-originated exception frames on LocalInterface 2025-01-27 12:29:28 +01:00
Mark Qvist c909871fb7 Updated issue template 2025-01-27 10:25:46 +01:00
Mark Qvist 47f60b0320 Fixed missing rx/tx stat assignment 2025-01-26 01:13:58 +01:00
Mark Qvist 6797909d90 Updated logging 2025-01-24 21:04:48 +01:00
Mark Qvist fd6d8ffff8 Updated logging 2025-01-21 23:55:49 +01:00
Mark Qvist 06de7f4a3d Updated logging 2025-01-21 23:22:39 +01:00
Mark Qvist 7221becd35 Updated manual 2025-01-19 21:09:50 +01:00
Mark Qvist a51f5f2eaf Updated changelog 2025-01-19 21:09:40 +01:00
Mark Qvist 9e8d71ddaf Updated examples 2025-01-19 20:38:41 +01:00
Mark Qvist 9bc55a9047 Updated manual 2025-01-19 12:56:32 +01:00
Mark Qvist 3e7ab5136e Better thread configuration 2025-01-19 00:57:36 +01:00
Mark Qvist d2cf3c2a7e Added error on configured radio parameter mismatch on Android 2025-01-19 00:10:35 +01:00
Mark Qvist 77519f1a0c Updated version 2025-01-18 21:38:13 +01:00
Mark Qvist e869b3cac9 Added resource reject signalling 2025-01-18 21:37:17 +01:00
Mark Qvist a2878f1722 Cleanup 2025-01-17 12:48:10 +01:00
Mark Qvist 748a7290a9 Updated docs 2025-01-17 12:47:02 +01:00
Mark Qvist 6e80a553c8 Updated changelog 2025-01-17 12:46:54 +01:00
Mark Qvist ec7aa44a17 Updated version 2025-01-17 12:41:48 +01:00
Mark Qvist 4fa335639c Added T3S3 support to rnodeconf 2025-01-16 19:10:49 +01:00
Mark Qvist 67195c0b14 Improved logging 2025-01-16 17:49:50 +01:00
Mark Qvist ad1e6a41ee Improved daemon restart time on systems with many interfaces 2025-01-16 17:48:16 +01:00
Mark Qvist a56d93fc1e Fixed potential logging deadlock 2025-01-16 17:37:47 +01:00
Mark Qvist b8aa6a3e44 Improved LocalInterface detach 2025-01-16 15:57:43 +01:00
Mark Qvist 1709cd929a Improved interface detach on shared instance shutdown 2025-01-16 14:12:30 +01:00
Mark Qvist 4f4961257c Improved interface detach on shared instance shutdown 2025-01-16 14:09:18 +01:00
Mark Qvist 1b48f43a0d Corrected T3S3 SX1280 model codes in rnodeconf 2025-01-16 12:04:54 +01:00
Mark Qvist e5d446a54e Retry ratchet reload on potential I/O conflicts 2025-01-16 12:04:29 +01:00
Mark Qvist 0af768e742 Cleanup 2025-01-14 19:36:49 +01:00
Mark Qvist 1a7d20a8d6 Cleanup 2025-01-14 19:02:15 +01:00
Mark Qvist ec4f4d5a83 Cleanup 2025-01-14 18:57:02 +01:00
Mark Qvist 8cefa4b2a9 Improved resource transfer timing 2025-01-14 18:24:56 +01:00
Mark Qvist 2331f1ea3e Fixed link MTU clamping 2025-01-14 18:21:31 +01:00
Mark Qvist be7dafa30c Added MTU autoconfiguration on interfaces 2025-01-14 18:19:51 +01:00
Mark Qvist 3e20cb1b67 Added resource EIFR continuity to split resource handling 2025-01-14 18:19:07 +01:00
Mark Qvist 097e136662 Fixed rnstatus display bug 2025-01-14 18:18:27 +01:00
Mark Qvist e3a716224d Implemented MTU autoconfiguration on interfaces 2025-01-14 18:17:53 +01:00
Mark Qvist 80dc567a53 Handle negative time in time formatters 2025-01-14 12:45:17 +01:00
Mark Qvist c6576d6504 Added link MTU discovery configuration option 2025-01-14 00:13:56 +01:00
Mark Qvist 89d5d9517d Added print device config option to rnodeconf 2025-01-13 21:48:35 +01:00
Mark Qvist dc315653c0 Added interference status to RNodeInterface 2025-01-13 21:06:24 +01:00
Mark Qvist 746b403890 Noise floor output formatting 2025-01-13 16:37:18 +01:00
Mark Qvist fc619460f0 Updated manual 2025-01-13 15:42:46 +01:00
Mark Qvist cd0f82d9ad Updated tests 2025-01-13 15:42:32 +01:00
Mark Qvist 330c2aacac Fixed incorrect resource SDU calculation when link MTU is set 2025-01-13 14:42:03 +01:00
Mark Qvist 63da084bbe Updated docs 2025-01-13 14:41:38 +01:00
Mark Qvist cbbd8221ee Fixed typo 2025-01-13 14:41:21 +01:00
Mark Qvist 1d18d53052 Updated speedtest example 2025-01-12 23:48:56 +01:00
Mark Qvist ceccf3153b Correct link MDU calculation 2025-01-12 23:48:21 +01:00
Mark Qvist bde33e7d84 Added support for dynamic link MTU to Channel and Buffer 2025-01-12 23:26:18 +01:00
Mark Qvist 93330d96a0 Updated manual 2025-01-12 20:56:13 +01:00
Mark Qvist d93ce62878 Updated HW MTUs 2025-01-12 20:56:06 +01:00
Mark Qvist eafa4aefbb Added log format 2025-01-12 18:51:27 +01:00
Mark Qvist 53df2fa5e0 Improved profiler 2025-01-12 17:51:02 +01:00
Mark Qvist abc657806d Added cumulative utilisation to profiler 2025-01-12 17:32:11 +01:00
Mark Qvist a0f219f7f4 Last-hop LR MTU clamping 2025-01-12 17:31:17 +01:00
Mark Qvist 47eba03a4b Single HW_MTU field 2025-01-12 17:29:06 +01:00
Mark Qvist 3289cd1299 Cleanup 2025-01-12 17:28:32 +01:00
Mark Qvist ab5fcd7a5b Added live traffic stats counting and output to rnstatus 2025-01-11 19:30:00 +01:00
Mark Qvist 45494f21aa Allow IFAC bitmask generation for large packet sizes 2025-01-11 17:26:51 +01:00
Mark Qvist 5d677d2fb7 Set correct hardware MTU 2025-01-11 17:25:03 +01:00
Mark Qvist 808082e300 Link proof MTU 2025-01-11 17:24:39 +01:00
Mark Qvist 97cfdfd023 Unify link ID across versions regardless of MTU discovery support 2025-01-11 16:58:09 +01:00
Mark Qvist 9b15cf2295 Check link MTU discovery is enabled 2025-01-11 15:52:40 +01:00
Mark Qvist eaa68c2d04 Updated docs 2025-01-11 14:56:45 +01:00
Mark Qvist ac5ca78c77 Improved split resource transfer performance 2025-01-11 14:25:27 +01:00
Mark Qvist 5b17dbdfd6 Improved packet filter performance 2025-01-11 14:24:40 +01:00
Mark Qvist d4ed20c7d5 Improved rncp status output 2025-01-11 14:23:53 +01:00
Mark Qvist a5093ea8f0 Updated version 2025-01-11 13:22:49 +01:00
Mark Qvist f5cf438abd Improve resource transfer throughput on high-MTU links 2025-01-11 13:22:18 +01:00
Mark Qvist bf6e73e163 Path MTU discovery for links 2025-01-11 11:43:47 +01:00
Mark Qvist 503f475ca5 Read link MTU from link request packet 2025-01-11 03:12:31 +01:00
Mark Qvist 8506118aee Cleanup 2025-01-11 01:45:09 +01:00
Mark Qvist dfa295a90a Cleanup 2025-01-11 01:31:57 +01:00
Mark Qvist 3ace1583da Packets go brrrr 2025-01-11 01:26:46 +01:00
Mark Qvist c62b66195d Optimised profiler timing overhead 2025-01-10 21:37:45 +01:00
Mark Qvist b724836d2b Changed profiler to context manager 2025-01-10 20:07:17 +01:00
Mark Qvist 1e1b9dc79e Fixed missing check for dict entry existence 2025-01-10 12:40:11 +01:00
Mark Qvist c668a51e39 Cleanup 2025-01-10 12:39:25 +01:00
Mark Qvist 09b34d34c6 Only persist ratchets when new 2025-01-10 12:39:04 +01:00
Mark Qvist 54e18e41c5 Updated changelog 2025-01-10 11:42:30 +01:00
Mark Qvist 5550bca040 Updated manual 2025-01-10 11:42:06 +01:00
Mark Qvist f7a02351d4 Added interference avoidance configuration to rnodeconf 2025-01-09 17:46:12 +01:00
Mark Qvist 3125b99043 Cleanup 2025-01-09 15:21:59 +01:00
Mark Qvist 158765abb7 Added noise floor stat output to rnodeconf 2025-01-09 15:18:29 +01:00
Mark Qvist 81aa9ac5b6 Added Heltec T114 to rnodeconf 2025-01-09 15:17:41 +01:00
Mark Qvist 55f5842587 Added new channel stat and CSMA parameters to RNodeInterface 2025-01-09 15:15:54 +01:00
Mark Qvist 38dd63a99a Updated issue template 2025-01-06 11:38:37 +01:00
Mark Qvist 558cd6c4a7 Updated version 2025-01-06 11:38:29 +01:00
Mark Qvist 15e6a1bfde Add support for SX1280 with PA 2025-01-03 22:35:01 +01:00
Mark Qvist c1087e62fd Added ability to initiate display reconditioning to rnodeconf 2024-12-31 14:14:14 +01:00
Mark Qvist 9d924dcd6d Added ability to set display rotation to rnodeconf 2024-12-31 13:22:57 +01:00
Mark Qvist 163d2ed157 Fixed missing console image install on Heltec V3 2024-12-12 13:06:52 +01:00
Mark Qvist 68f07ddd38 Updated manual 2024-12-11 22:26:48 +01:00
Mark Qvist d956b93c13 Updated changelog 2024-12-11 22:26:41 +01:00
Mark Qvist 3036305662 Cleanup 2024-12-11 22:17:58 +01:00
Mark Qvist ee603ce68e Updated manual 2024-12-11 19:56:37 +01:00
Mark Qvist 989513cb46 Updated version 2024-12-11 19:41:35 +01:00
Mark Qvist 7e52c37580 Allow announce handler to receive announce packet hash 2024-12-11 19:18:02 +01:00
Mark Qvist 0984f92fa2 Fixed typo 2024-12-11 19:17:14 +01:00
Mark Qvist 2ab2d8e9df Updated changelog 2024-12-09 22:22:22 +01:00
Mark Qvist b828e0e858 Updated manual 2024-12-09 22:10:46 +01:00
Mark Qvist d4dd706bba Merge branch 'master' of github.com:markqvist/Reticulum 2024-12-08 14:27:37 +01:00
Mark Qvist ed30fa3e0a Added ability to reflect RNS logs to app-internal log handler callback 2024-12-08 14:27:17 +01:00
Mark Qvist 5e2b3df623 Added ability to run rnstatus as application-local imported module 2024-12-08 14:26:51 +01:00
Mark Qvist ae7dffdfc0 Added display read command to RNodeInterface 2024-12-08 14:25:58 +01:00
Mark Qvist 32b5c7a3af Updated documentation 2024-12-08 14:24:51 +01:00
markqvist 8b08658b7f Merge pull request #629 from jacobeva/refactor-fix
Fix RNodeMultiInterface to work with refactored interfaces
2024-12-07 22:32:27 +01:00
jacob.eva ee79c3a732 Fix RNodeMultiInterface to work with refactored interfaces 2024-12-07 21:28:14 +00:00
Mark Qvist 0e5f4aa08a Fixed missing artifact 2024-12-05 16:43:58 +01:00
Mark Qvist ec0407e5c8 Updated version 2024-12-05 16:40:53 +01:00
Mark Qvist db1380c413 Disable building manual 2024-12-05 16:36:44 +01:00
markqvist 7e3979dac0 Merge pull request #626 from gretel/add-revised-workflow
ci/cd: add release automation
2024-12-05 16:29:24 +01:00
Mark Qvist c1b6bde4a7 Updated documentation 2024-12-02 14:24:42 +01:00
Mark Qvist 8df89cc2d0 Allow dynamic sub-module import from compiled python bytecode 2024-12-02 14:20:34 +01:00
Mark Qvist 19adadf4cf Fixed imports for OpenWRT build 2024-12-01 09:09:39 +01:00
gretel c30feb3fc2 ci/cd: add release automation
Publishes a release when tagged with a `semver` version:
- X.Y.Z for "production quality" (1.0.0)
- X.Y.Z-suffix for development (1.0.0-alpha.1)

Release will be marked as 'prerelease' accordingly.

For now, any release will be marked 'draft'.
2024-11-30 21:43:54 +01:00
Mark Qvist 4c81589d5b Updated manual 2024-11-30 01:08:58 +01:00
Mark Qvist c014357e24 Updated documentation 2024-11-29 15:11:51 +01:00
Mark Qvist ec41dc1a03 Updated documentation 2024-11-29 15:11:47 +01:00
Mark Qvist 463dfa6fb4 Updated documentation 2024-11-29 15:10:35 +01:00
Mark Qvist 0354b5969d Updated documentation 2024-11-29 10:12:44 +01:00
Mark Qvist fc225bd55d Updated getting started and install instructions sections 2024-11-29 10:12:34 +01:00
Mark Qvist 67562126fc Refactored interface imports 2024-11-27 17:45:05 +01:00
Mark Qvist 9319d613f5 Updated documentation and manual 2024-11-24 14:34:43 +01:00
Mark Qvist 014994a788 Updated changelog 2024-11-24 14:34:38 +01:00
Mark Qvist 0f8efe3de1 Updated documentation and manual 2024-11-24 14:03:50 +01:00
Mark Qvist 274a8ca76a Fixed typo 2024-11-23 10:41:17 +01:00
Mark Qvist ea3ad6b287 Only attempt to get RNS status if a shared instance already exists 2024-11-22 23:11:57 +01:00
Mark Qvist f095b9cb8e Added init option for requiring existing shared instance 2024-11-22 23:11:34 +01:00
Mark Qvist 6f8d3e882a Updated docs and readme 2024-11-22 15:40:41 +01:00
Mark Qvist aabb763cea Refactored fernet to token 2024-11-22 15:19:12 +01:00
Mark Qvist 04d2626809 Updated docs and manual 2024-11-22 14:39:58 +01:00
Mark Qvist 823bfd537c Refactored processIncoming to process_incoming 2024-11-22 14:39:27 +01:00
Mark Qvist 434ebd2954 Fixed interface example bitrate init 2024-11-22 14:31:06 +01:00
Mark Qvist 44782c3429 Updated docs and manual 2024-11-22 14:25:18 +01:00
Mark Qvist 890846fa8d Added custom interfaces to documentation and readme 2024-11-22 14:16:53 +01:00
Mark Qvist 36c761e8dd Refactored processOutgoing to process_outgoing 2024-11-22 14:12:55 +01:00
Mark Qvist 4a4b625075 Implemented custom interface loading 2024-11-22 14:07:48 +01:00
Mark Qvist 4223203134 Added example custom interface 2024-11-22 14:07:17 +01:00
Mark Qvist e6966fe19a Cleanup 2024-11-22 12:16:29 +01:00
Mark Qvist e81c22cf53 Fixed spawned interface count sometimes being inaccurate on TCP and I2P interfaces 2024-11-22 12:02:18 +01:00
Mark Qvist c02e59e3ab Prepare interface modularity 2024-11-22 11:33:40 +01:00
Mark Qvist 5d5abf352b Prepare interface modularity 2024-11-22 11:27:46 +01:00
Mark Qvist ec9bb33d16 Apply KISS beacon frame length fix to Android-specific KISS interface 2024-11-22 11:20:28 +01:00
markqvist f3e836cec8 Merge pull request #618 from gretel/fix-kiss-callsign-beacon
Fix KISS beacon frame formatting and add sync pattern
2024-11-22 11:17:59 +01:00
Mark Qvist 8a50528111 Prepare interface modularity 2024-11-21 19:03:56 +01:00
gretel 9523595282 Fix KISS beacon frame length
Fix frame length handling to meet minimum length requirements (15 bytes) for
TNCs like Direwolf. Previously, raw beacon data was being sent directly,
causing frame length errors.

Changed code to pad beacon data with zeros to ensure minimum frame length.
2024-11-21 18:57:26 +01:00
Mark Qvist a762af035a Prepare interface modularity 2024-11-21 14:41:22 +01:00
Mark Qvist 760ab981d0 Prepare interface modularity for Android-specific interfaces 2024-11-21 13:51:34 +01:00
Mark Qvist 7b43ff0cef Cleanup 2024-11-21 13:13:41 +01:00
Mark Qvist 996161e2f4 Internal interface config handling for RNodeMultiInterface 2024-11-21 13:11:17 +01:00
Mark Qvist bf633bba5d Internal interface config handling for RNodeInterface 2024-11-21 13:03:03 +01:00
Mark Qvist 8337a5945d Internal interface config handling for AX25KISSInterface 2024-11-21 12:30:07 +01:00
Mark Qvist a736b3adfc Internal interface config handling for KISSInterface 2024-11-21 12:25:59 +01:00
Mark Qvist 25127cd3c9 Internal interface config handling for PipeInterface 2024-11-21 12:22:09 +01:00
Mark Qvist ebf084cff0 Internal interface config handling for SerialInterface 2024-11-21 12:16:44 +01:00
Mark Qvist cd8fe95d91 Internal interface config handling for I2PInterface 2024-11-21 12:10:21 +01:00
Mark Qvist e2efc61208 Added Yggdrasil example to interface documentation 2024-11-20 20:50:08 +01:00
Mark Qvist 5de63d5bf2 Internal interface config handling for TCPClientInterface 2024-11-20 20:39:44 +01:00
Mark Qvist c9d744f88a Internal interface config handling for TCPServerInterface 2024-11-20 20:27:01 +01:00
Mark Qvist 18e0dbddfa Internal interface config handling for UDPInterface 2024-11-20 20:20:40 +01:00
Mark Qvist 52c816cb27 Cleanup 2024-11-20 20:18:17 +01:00
Mark Qvist 582d2b91f5 Internal interface config handling for AutoInterface 2024-11-20 20:14:02 +01:00
Mark Qvist 28a0dbb0e0 Updated version 2024-11-20 19:56:02 +01:00
Mark Qvist 2895806541 Added IPv6 info to TCP interface documentation 2024-11-20 19:55:18 +01:00
Mark Qvist 5b8de73143 Correctly display IPv6 addresses in interface names 2024-11-20 19:24:06 +01:00
Mark Qvist 212af2f43b Automatically select IPv6 address for IPv6-only interfaces 2024-11-20 19:16:15 +01:00
Mark Qvist 1282061701 Add interface scope for link-local IPv6 addresses 2024-11-20 18:02:50 +01:00
Mark Qvist 49dba483a9 Use address structure according to target address family 2024-11-20 17:10:08 +01:00
Mark Qvist ebec63487f Added prefer_ipv6 option to TCPServerInterface 2024-11-20 16:53:14 +01:00
Mark Qvist 9373819234 Add ability to bind to AF_INET6 sockets based on both device name and IP addresses 2024-11-20 16:44:39 +01:00
markqvist 04925d8004 Merge pull request #601 from deavmi/patch-2
Allow binding to IPv6 (if present)
2024-11-20 14:28:46 +01:00
markqvist 4284084fef Merge pull request #600 from deavmi/patch-1
Determine AF FAMILY from getaddrinfo BEFORE socket ctor
2024-11-20 14:28:34 +01:00
Tristan B. Velloza Kildaire 63ad2afe3f Reapply "Allow binding to IPv6 (if present)"
This reverts commit 61712d322a.
2024-11-04 13:25:55 +02:00
Tristan B. Velloza Kildaire 61712d322a Revert "Allow binding to IPv6 (if present)"
This reverts commit f55004a574.
2024-11-04 13:25:46 +02:00
Tristan B. Velloza Kildaire 3599066356 Revert "Test"
This reverts commit 18c2a38b97.
2024-11-04 13:05:27 +02:00
Tristan B. Velloza Kildaire 18c2a38b97 Test 2024-11-04 13:02:45 +02:00
Tristan B. Velloza Kildaire f55004a574 Allow binding to IPv6 (if present)
If an interface has an IPv6 address record associated with it then, and only then, prefer that.

Otherwise AF_INET is used (Ipv4 address)
2024-11-03 17:54:59 +02:00
Tristan B. Velloza Kildaire 1768ddc459 Determine AF FAMILY from getaddrinfo BEFORE socket ctor
Before we call the `socket.socket(...)` constructor function, let us first provide `self.target_ip` and `self.target_port` to `socket.getaddrinfo(...)` (static function) and then get the AF family from it. Then we pass this into the ctor
2024-11-03 14:37:28 +02:00
faragher 95cea24527 Ported rncp allow files to rnx 2024-11-02 23:06:43 -05:00
Mark Qvist d002a75f34 Updated changelog 2024-10-20 14:09:12 +02:00
Mark Qvist 0b6d239551 Updated changelog 2024-10-20 14:07:54 +02:00
Mark Qvist 926b811a84 Updated docs 2024-10-20 14:04:48 +02:00
Mark Qvist 2bc8e11ad5 Updated version 2024-10-20 13:45:52 +02:00
Mark Qvist f5412f5c0b Fixed invalid link RSSI, SNR and Q data returned from API functions. Improved link physical layer stats updates. 2024-10-20 13:34:02 +02:00
Mark Qvist 5470f752b4 Cleanup 2024-10-20 12:26:54 +02:00
markqvist 48c006a94c Merge pull request #589 from faragher/master
Fixed file access bug, added fail-safe access
2024-10-20 12:18:23 +02:00
faragher 8445417661 Fixed file access bug, added fail-safe access 2024-10-19 12:39:48 -05:00
Mark Qvist 30248854ed Updated changelog 2024-10-11 17:13:03 +02:00
Mark Qvist f34bc75588 Updated docs 2024-10-11 16:47:53 +02:00
Mark Qvist 3b23e2f37d Improved RNode BLE reconnection reliability 2024-10-11 13:38:16 +02:00
Mark Qvist 7417cf5947 Add rnode battery state to rnstatus output 2024-10-11 10:14:10 +02:00
Mark Qvist 60d8da843c Disable tty module dependency for rnx, since it is currently unused 2024-10-11 09:54:09 +02:00
Mark Qvist f9667fd684 Fixed missing import on Android 2024-10-10 23:49:20 +02:00
Mark Qvist d9269c6047 Updated version 2024-10-10 23:32:09 +02:00
Mark Qvist 6521f839cd Fixed resource transfers hanging for a long time over slow links if proof packet is lost 2024-10-10 17:06:43 +02:00
Mark Qvist d63bbcdc0a Updated changelog 2024-10-10 00:45:09 +02:00
Mark Qvist c36c7186de Updated docs 2024-10-10 00:44:33 +02:00
Mark Qvist 6fec76205c Added save directory option to rncp 2024-10-10 00:41:57 +02:00
Mark Qvist 715f4d9fcb Updated version 2024-10-09 20:03:05 +02:00
Mark Qvist 8d7857c4e2 Fixed rncp fstrings for Android build 2024-10-09 19:53:07 +02:00
Mark Qvist c9a2b45368 Added physical layer transfer rate output option to rncp 2024-10-09 19:39:39 +02:00
Mark Qvist c57d927660 Cleanup 2024-10-09 19:38:46 +02:00
Mark Qvist 8d98c8751a Fixed resource progress calculation bug. Actually fixes #522. 2024-10-09 19:38:25 +02:00
Mark Qvist 527f6cc906 Fuxed typo 2024-10-07 22:10:17 +02:00
Mark Qvist a0d61f6441 Added error descriptions for modem communication timeout 2024-10-07 20:55:34 +02:00
Mark Qvist c5687f190b Updated manual 2024-10-06 10:49:56 +02:00
Mark Qvist 44d1f6d0e5 Updated changelog 2024-10-06 10:49:48 +02:00
Mark Qvist ac09bc3567 Updated manual 2024-10-06 10:28:26 +02:00
Mark Qvist a41bce012b Fix docs images for PDF generation 2024-10-06 10:27:27 +02:00
Mark Qvist 83a2999d29 Revert AF_INET6 addition to TCPInterface, since it breaks normal IPv4 connectivity for interface 2024-10-06 10:01:55 +02:00
markqvist 4465fa9882 Merge pull request #545 from deavmi/master
Support IPv6 for outbound TCP interface (TCPClientInterface)
2024-10-05 23:46:28 +02:00
Mark Qvist ce974db084 Merge branch 'master' of github.com:markqvist/Reticulum 2024-10-05 23:45:48 +02:00
markqvist e6c1dc075b Merge pull request #556 from jacobeva/rnode-multi-fix
Fix interface values not being set on RNodeSubInterface instances
2024-10-05 23:45:21 +02:00
Mark Qvist 9602f67b06 Merge branch 'master' of github.com:markqvist/Reticulum 2024-10-05 23:44:17 +02:00
markqvist ef798e0d54 Merge pull request #543 from jacobeva/display-fix
Allow for use of display by master on NRF52
2024-10-05 23:43:56 +02:00
Mark Qvist 5cd8d229fb Updated manual 2024-10-05 23:43:28 +02:00
Mark Qvist d4808b7ff1 Added supported boards to manual 2024-10-05 23:43:02 +02:00
markqvist 3dc8729e70 Merge pull request #565 from jacobeva/framing-fix
Fix RNodeMultiInterface interface framing
2024-10-05 23:03:36 +02:00
markqvist f500a063dc Merge pull request #564 from prusnak/docs-hardware
docs: add Heltec LoRa32 v3.0 and LilyGO LoRa32 v1.0 to hardware
2024-10-05 23:00:43 +02:00
Mark Qvist eca1e53b55 Added support for T-Beam Supreme, T-Deck and T3S3 devices with SX127X chips to rnodeconf 2024-10-05 22:29:31 +02:00
Mark Qvist 53226d7035 Cap resource max window for resource transfer over very slow links 2024-10-05 20:54:42 +02:00
Mark Qvist 7363c9c821 Increase PATH_REQUEST_RG to 1.5 seconds 2024-10-05 19:20:48 +02:00
Mark Qvist bb8b8b4f81 Added handling for receiving a link proof after the link had timed out and been closed, but before it having been purged from active links table 2024-10-05 18:43:56 +02:00
Mark Qvist 0f0f459321 Updated version 2024-10-05 17:05:41 +02:00
Mark Qvist df887f6d63 Added product and model code defines for new boards to rnodeconf 2024-10-05 17:05:34 +02:00
Mark Qvist b526e3554c Added low memory error decsription to RNodeInterface 2024-10-05 17:05:02 +02:00
Mark Qvist 903ab53fc9 Fixed init fail due to missing library on Android/Termux 2024-10-05 17:04:39 +02:00
Mark Qvist f461a7827b Added T-Deck defines to rnodeconf 2024-10-03 00:52:38 +02:00
Mark Qvist 62091b28b0 Fixed version comparison 2024-10-02 02:54:18 +02:00
Mark Qvist 48045856bf Updated changelog 2024-10-02 02:09:41 +02:00
Mark Qvist 6ba5efcb42 Updated documentation 2024-10-02 02:08:41 +02:00
Mark Qvist a505441b98 Added BLE connection config to docs 2024-10-02 02:05:00 +02:00
Mark Qvist 976e5543e1 Updated changelog 2024-10-02 01:58:35 +02:00
Mark Qvist fcc7b50ac6 Updated docs 2024-10-01 23:53:53 +02:00
Mark Qvist 72971d1aef Handle RNode BLE MTU request errors 2024-10-01 23:52:04 +02:00
Mark Qvist 9a8d46ab21 Updated version 2024-10-01 17:28:40 +02:00
Mark Qvist 8adab7ee7d Added BLE support to Android RNodeInterface 2024-10-01 17:27:45 +02:00
Mark Qvist b5bde99322 Added RNode battery info to rnstatus output 2024-10-01 17:25:44 +02:00
Mark Qvist 560c8e164c Added BLE support to RNodeInterface 2024-10-01 17:25:16 +02:00
jacob.eva e059363f1d Version bump for CE firmware version which will contain framing change 2024-10-01 16:02:07 +01:00
jacob.eva 4930477b99 Fix interface framing assignment conflict 2024-10-01 15:58:27 +01:00
Mark Qvist 312489e4dc Added BLE config support to RNodeInterface 2024-09-30 19:09:35 +02:00
Pavol Rusnak 43d8fdb423 docs: add Heltec LoRa32 v3.0 and LilyGO LoRa32 v1.0 to hardware 2024-09-29 11:51:43 +02:00
Mark Qvist 1c56385473 Added display blanking timeout configuration to rnodeconf 2024-09-29 02:35:44 +02:00
Mark Qvist 787af92ade Added option to configure NeoPixel intensity to rnodeconf 2024-09-27 20:07:04 +02:00
Mark Qvist 131dbd2813 Updated changelog 2024-09-25 13:26:23 +02:00
Mark Qvist 9df81ce365 Updated manual 2024-09-25 13:25:43 +02:00
Mark Qvist 490a56450a Updated changelog 2024-09-25 13:23:15 +02:00
Mark Qvist 52a5156304 Cleanup 2024-09-25 13:20:41 +02:00
Mark Qvist 538e7320fd Updated docs 2024-09-25 13:17:03 +02:00
Mark Qvist 2d351a59e9 Updated version 2024-09-25 13:11:17 +02:00
Mark Qvist 2269d6cef9 Updated readme 2024-09-25 13:06:31 +02:00
Mark Qvist 813edc8b17 Updated readme 2024-09-25 13:04:23 +02:00
Mark Qvist 099e344996 Updated roadmap 2024-09-25 12:43:40 +02:00
Mark Qvist 42319a092d Added additional information to interface stats 2024-09-24 20:26:15 +02:00
Mark Qvist cdee3b6191 Updated changelog 2024-09-24 10:12:51 +02:00
Mark Qvist e41d8ff296 Updated docs 2024-09-24 10:09:37 +02:00
Mark Qvist 946bea8825 Update version 2024-09-22 11:43:35 +02:00
Mark Qvist ba856ea1c4 Handle link transport edge case 2024-09-21 19:04:28 +02:00
jacob.eva 9a97195b8c Fix interface values not being set on RNodeSubInterface instances 2024-09-20 17:50:34 +01:00
Mark Qvist 3e4172b697 Updated changelog 2024-09-18 23:40:38 +02:00
Mark Qvist 66163776c2 Updated changelog 2024-09-18 23:32:45 +02:00
Mark Qvist 3dbde726c1 Updated manual 2024-09-18 23:31:27 +02:00
Mark Qvist 97ae4d74b3 Updated docs 2024-09-17 14:56:22 +02:00
Mark Qvist c71ece6b8e Updated version 2024-09-16 20:11:12 +02:00
Mark Qvist 1e45a002e1 Merge branch 'master' of github.com:markqvist/Reticulum 2024-09-16 20:10:55 +02:00
markqvist 68e64523b5 Merge pull request #552 from liamcottle/fix/ax25-kiss-interface
fix KISSInterface is not defined error for AX25KISSInterface
2024-09-16 19:56:13 +02:00
Mark Qvist d9e6145034 Raise exception when SINGLE destination is created without identity 2024-09-16 18:20:53 +02:00
Mark Qvist a91e67129e Update profiler output 2024-09-16 18:20:31 +02:00
liamcottle 76362bad4a fix KISSInterface is not defined error for AX25KISSInterface 2024-09-16 14:27:08 +12:00
Mark Qvist 421b5ef32e Recursive profiler results output 2024-09-15 16:46:52 +02:00
Mark Qvist 8d61ee8a81 Added performance profiler 2024-09-15 15:12:53 +02:00
Mark Qvist 2329181c88 Prioritize interfaces according to bitrate 2024-09-15 14:14:00 +02:00
markqvist 8ea0dc65c4 Merge pull request #551 from jacobeva/opencom_xl
Add support for openCom XL
2024-09-14 23:44:48 +02:00
jacob.eva bba67836f0 Add support for openCom XL 2024-09-13 11:30:54 +01:00
Mark Qvist a666bb6e73 Added minimum link traffic timeout 2024-09-12 17:52:40 +02:00
Mark Qvist 7b7ebbec90 Updated roadmap 2024-09-09 15:13:21 +02:00
Mark Qvist 8b3523dee0 Updated changelog 2024-09-09 15:09:42 +02:00
Mark Qvist 2901ed2bae Updated changelog 2024-09-09 15:09:07 +02:00
Mark Qvist 34010c94d1 Updated manual 2024-09-09 15:08:46 +02:00
Mark Qvist a4b5248a4c Cleanup 2024-09-09 14:48:58 +02:00
Mark Qvist 75272d77a5 Cleanup 2024-09-09 14:47:28 +02:00
Mark Qvist d4ad4589dd Cleanup 2024-09-09 14:46:58 +02:00
Mark Qvist 8d45ad36eb Cleanup 2024-09-09 14:46:32 +02:00
Mark Qvist 2a0d411869 Cleanup 2024-09-09 14:45:08 +02:00
Mark Qvist b9421347ef Cleanup 2024-09-09 14:43:50 +02:00
markqvist ffec78d49a Merge pull request #544 from deavmi/deavmi-patch-1
Add a pinch of CI/CD (no CD yet)
2024-09-09 14:42:30 +02:00
Mark Qvist 356ae378f9 Cleanup 2024-09-09 14:32:07 +02:00
Mark Qvist 28e3919dbd T-Echo product and model codes 2024-09-09 14:30:06 +02:00
markqvist 58a19610c4 Merge pull request #541 from jeremybox/t-echo
Add support for TECHO device
2024-09-09 14:18:15 +02:00
Mark Qvist 50b1eae380 File move fix for windows 2024-09-09 02:11:46 +02:00
Mark Qvist c119ef4273 Standardised ratchet id getter 2024-09-08 20:33:35 +02:00
Mark Qvist b506ca94d0 Updated documentation and manual 2024-09-08 17:56:02 +02:00
Mark Qvist a072a5b074 Added automatic ratchet reload if required ratchet is unavailable 2024-09-08 17:48:25 +02:00
Mark Qvist 3a580e74de Make ratchet IDs available to applications 2024-09-08 14:55:07 +02:00
jeremy 9a20a3929a correct t-echo model 2024-09-07 19:17:06 -04:00
Mark Qvist fe054fd03c Added destination ratchet ID getter to API 2024-09-07 22:32:03 +02:00
Mark Qvist 4524a17e67 Updated documentation 2024-09-06 19:52:11 +02:00
Mark Qvist 8a82d6bfeb Allow announce handlers to also receive path responses 2024-09-06 19:52:05 +02:00
Mark Qvist 971f5ffadd Check ratchet dir exists before cleaning 2024-09-05 15:58:54 +02:00
Mark Qvist 6a392fdb0f Updated readme 2024-09-05 15:21:45 +02:00
Mark Qvist b42e075be0 Updated manual and documentation 2024-09-05 15:17:58 +02:00
Mark Qvist 4bc8a0b69b Updated manual and documentation 2024-09-05 15:16:09 +02:00
Mark Qvist 9ef10a7b3e Expanded and documented ratchet API 2024-09-05 15:02:22 +02:00
Mark Qvist 320704f812 Updated documentation 2024-09-05 14:58:06 +02:00
Mark Qvist c5e5986b89 Updated documentation 2024-09-05 12:58:35 +02:00
Tristan Brice Velloza Kildaire 5c6ee07d66 TCPInterface
- When connect(s, Bool)` is called construct a socket that supports both address families
2024-09-05 00:07:35 +02:00
Tristan Brice Velloza Kildaire 3eb8d92028 Rename 2024-09-04 23:59:03 +02:00
Tristan Brice Velloza Kildaire ef3baf2cd9 Add bade
(Will work once active on mark's repo)
2024-09-04 23:58:16 +02:00
Tristan Brice Velloza Kildaire f2f936d846 Clean up testing 2024-09-04 23:56:55 +02:00
Tristan Brice Velloza Kildaire 6599e210de Fixed up test 2024-09-04 23:56:01 +02:00
Mark Qvist d21dda2830 Set context flags on path response 2024-09-04 19:39:59 +02:00
Mark Qvist 6ac393bbcd Updated ratchet count 2024-09-04 19:33:04 +02:00
Mark Qvist 0c04663942 Merge branch 'master' of github.com:markqvist/Reticulum 2024-09-04 19:08:26 +02:00
Mark Qvist bfa216de54 Cleanup 2024-09-04 19:08:18 +02:00
markqvist a4b1606921 Merge pull request #542 from jacobeva/master
Remove match and therefore dependency on Python 3.10
2024-09-04 19:01:08 +02:00
Mark Qvist ad0db9c95c Updated version 2024-09-04 17:47:26 +02:00
Mark Qvist 2fdcbec860 Updated documentation 2024-09-04 17:40:02 +02:00
Mark Qvist dd889d16d4 Added ratchets example 2024-09-04 17:37:34 +02:00
Mark Qvist a11f14e75f Implemented ratchets 2024-09-04 17:37:18 +02:00
Mark Qvist c32086c6f1 Updated documentation 2024-09-04 17:00:11 +02:00
jacob.eva 9d744e2317 Allow for display use by master on NRF52 on Android 2024-09-04 11:54:32 +01:00
jacob.eva d64064691a Allow for use of display by master on NRF52 2024-09-04 11:52:41 +01:00
Mark Qvist 54eaff203f Implemented ratchet generation, rotation and persistence 2024-09-04 12:02:55 +02:00
Mark Qvist 2bf75f60bc Cleanup 2024-09-04 11:23:08 +02:00
Mark Qvist 3f64141455 Fixed missing establishment_rate property init on Link 2024-09-04 10:32:54 +02:00
jeremy b4ac3df2d0 remove t-echo menu items 2024-09-03 17:24:11 -04:00
jeremy 8193f3621c remove symlink 2024-09-03 17:17:17 -04:00
jeremybox 5166596375 Update RNodeInterface.py
reverts extra debugging message detail
2024-09-03 17:14:07 -04:00
jacob.eva 063ea2bb7a Remove match and therefore dependency on Python 3.10 2024-09-03 22:12:25 +01:00
jeremy 625db2622d Pushing changes to branch 2024-09-03 17:09:59 -04:00
Tristan B. Velloza Kildaire a8bc468e21 Update python-app.yml 2024-09-03 18:53:11 +02:00
Tristan B. Velloza Kildaire 95c4269869 Create python-app.yml 2024-09-03 18:52:10 +02:00
jeremy 65a40aefb6 trying to get techo working 2024-09-03 01:57:07 -04:00
jeremy a840bd4aaf changes needed to support the t-echo device 2024-08-31 23:39:36 -04:00
Mark Qvist 7f2154110c Cleanup 2024-08-30 13:33:51 +02:00
Mark Qvist 9bc957e442 Updated documentation 2024-08-29 23:46:10 +02:00
Mark Qvist 6d5ef3a511 Cleanup 2024-08-29 23:45:16 +02:00
Mark Qvist dec9145d65 Updated manual and documentation 2024-08-29 17:02:22 +02:00
Mark Qvist b3536f16e8 Added remote management config options to example config 2024-08-29 16:50:05 +02:00
Mark Qvist 4e21b6f3b9 Updated changelog 2024-08-29 16:29:58 +02:00
Mark Qvist 31e0939657 Updated manual 2024-08-29 15:41:16 +02:00
Mark Qvist bd9aa2954b Improved resource transfer performance for segmented files 2024-08-29 15:26:53 +02:00
Mark Qvist 3a5ee15dd8 Cleanup 2024-08-29 15:25:37 +02:00
Mark Qvist 166b00b6bf Updated documentation 2024-08-29 15:25:12 +02:00
Mark Qvist 2413add00d Cleanup 2024-08-29 14:54:40 +02:00
Mark Qvist 169d1921be Added JSON output to rnpath utility 2024-08-29 14:51:38 +02:00
Mark Qvist 7be6a0e000 Fixed exit code 2024-08-29 13:20:00 +02:00
Mark Qvist d3b8c1c829 Added path and rate tables to remote management 2024-08-29 13:19:39 +02:00
Mark Qvist 8ee11ac32c Added request concluded status to Link API 2024-08-29 13:14:55 +02:00
Mark Qvist cf87b1352a Added max hops filter to rnpath table output 2024-08-29 11:17:07 +02:00
Mark Qvist 219d717afb Added timeout argument to rnstatus remote queries 2024-08-29 09:35:33 +02:00
Mark Qvist e8d1897edd Added remote transport instance status to rnstatus utility 2024-08-29 01:54:34 +02:00
Mark Qvist bce37fe8c0 Fixed rnstatus JSON output bug when IFAC was enabled on an interface 2024-08-28 23:25:18 +02:00
Mark Qvist 0c95d720db Improved rncp progress status display 2024-08-28 21:34:16 +02:00
Mark Qvist 96527380c3 Improved rncp progress status display 2024-08-28 21:21:38 +02:00
Mark Qvist 035a44e34d Fixed invalid resource progress reported in some cases 2024-08-28 21:21:09 +02:00
Mark Qvist 59bb09426c Updated documentation 2024-08-28 20:37:19 +02:00
Mark Qvist 6ac07989b0 Added link age to link API 2024-08-28 20:36:51 +02:00
Mark Qvist f1d6cda337 Added RNodeMultiInterface to documentation 2024-08-28 18:47:33 +02:00
Mark Qvist 4aa60243a7 Merge branch 'master' of github.com:markqvist/Reticulum 2024-08-28 18:27:01 +02:00
markqvist eb4fc3362a Merge pull request #530 from jacobeva/master
Add RNodeMultiInterface support
2024-08-28 18:26:32 +02:00
Mark Qvist 849bd1bdad Fixed typo 2024-08-28 16:54:31 +02:00
markqvist cdce0c4223 Merge pull request #534 from faragher/master
Migrating BtB Server
2024-08-28 16:36:36 +02:00
faragher 4e16e6ac0e Server Migration 2024-08-27 14:07:25 -05:00
faragher 9e054ae71d Server Migration 2024-08-27 14:06:34 -05:00
faragher 2fad5464da Server Migration 2024-08-27 14:04:54 -05:00
jacob.eva 3c4783b25e Merge branch 'master' of https://github.com/markqvist/Reticulum 2024-08-19 08:29:16 +01:00
jacob.eva 5feb833573 Add RNodeMultiInterface 2024-08-19 08:19:42 +01:00
jacob.eva 60e6b712d2 Update minimum SF 2024-08-19 08:19:32 +01:00
Mark Qvist a1be97bd69 Merge branch 'master' of github.com:markqvist/Reticulum 2024-08-17 16:07:41 +02:00
Mark Qvist 07ff9fc663 Updated readme 2024-08-17 16:07:20 +02:00
markqvist 2ef87a5e70 Merge pull request #512 from attermann/master
Fix for broken `--rom` manual device provisioning
2024-08-17 14:42:06 +02:00
Mark Qvist e3948526fe Cleanup 2024-08-17 14:38:07 +02:00
markqvist 2943d59042 Merge pull request #516 from jschulthess/master
Link example - Allow server to gracefully exit
2024-08-17 14:35:18 +02:00
markqvist 1335ffd528 Merge pull request #521 from nathmo/nathmo-patch-egraceful_xit
fixed small typo : egraceful_xit()
2024-08-17 14:33:51 +02:00
Nathann Morand 4e783ced31 fixed small typo egraceful_xit()
typo in Reticulum/RNS/Utilities/rnodeconf.py (egraceful_xit())
that cause a crash if we run rnodeconf -i on an upprovisionned node
2024-07-20 13:54:43 +02:00
Jürg Schulthess 228667578e Allow server to gracefully exit 2024-06-21 17:01:56 +02:00
Mark Qvist 6ded42edd7 Updated readme 2024-06-05 00:36:34 +02:00
Mark Qvist d1a150329a Updated documentation 2024-06-02 13:32:59 +02:00
Mark Qvist 893dc2877c Updated readme 2024-06-02 09:52:21 +02:00
Mark Qvist 86224ef387 Updated documentation 2024-06-02 09:42:43 +02:00
Mark Qvist 794cac98fe Updated readme 2024-06-02 08:43:30 +02:00
Mark Qvist cfdba51640 Merge branch 'master' of github.com:markqvist/Reticulum 2024-06-02 08:42:31 +02:00
Mark Qvist c4ecbf29cb Updated docs 2024-06-02 08:39:38 +02:00
Mark Qvist c80289987c Updated readme 2024-06-02 08:39:21 +02:00
Mark Qvist 9371f857a8 Updated documentation 2024-06-01 16:32:58 +02:00
markqvist 4fdb9dda40 Merge pull request #509 from liamcottle/master
Fix for macos failing to set firmware hash on NRF52
2024-05-31 13:47:01 +02:00
liamcottle c4705fd594 check platform is macos before delaying nrf52 reset 2024-05-31 13:12:39 +12:00
Mark Qvist 30228d12f7 Updated readme 2024-05-29 19:09:43 +02:00
Chad Attermann 1cee0a2619 Fix for broken --rom manual device provisioning
Initializes `selected_model` with the value of model specified on the
command line.
2024-05-29 09:04:14 -06:00
liamcottle df92fb1bcf fix for macOS failing to set firmware hash on NRF52 when resetting too quickly 2024-05-29 11:39:13 +12:00
Mark Qvist 3a163c6f09 Added fetch request jail option to rncp 2024-05-28 20:58:20 +02:00
Mark Qvist 1f6560619e Added link table stats to rnstatus 2024-05-26 01:28:40 +02:00
Mark Qvist b994db3745 Updated command line option description 2024-05-25 22:39:50 +02:00
Mark Qvist 173a534572 Updated version 2024-05-25 22:38:25 +02:00
Mark Qvist fc7268a8ff Added switch for allowing file fetch to rncp utility 2024-05-25 22:37:50 +02:00
Mark Qvist 0049c98684 Added comment about path resolution 2024-05-22 12:41:38 +02:00
Mark Qvist 3ef6c06b51 Fixed incorrect TX power limit on Android 2024-05-22 12:40:21 +02:00
Mark Qvist 0bb1108771 Mark path unresponsive when link establishment fails due to potential interface-local destination roaming 2024-05-19 12:35:38 +02:00
Mark Qvist ba2feaa211 Updated changelog 2024-05-18 18:51:17 +02:00
Mark Qvist 097d2b0dd9 Updated changelog 2024-05-18 18:48:32 +02:00
Mark Qvist bb0ce4faca Added T3S3 flashing, fixed Heltec V3 autoinstaller menu 2024-05-18 18:40:21 +02:00
Mark Qvist 5915228f5b Updated documentation 2024-05-18 18:38:06 +02:00
Mark Qvist 0b66649158 Avoid nRF52 hard reset after EEPROM wipe 2024-05-18 00:18:54 +02:00
markqvist e28dd6e14a Merge pull request #502 from jacobeva/master
Extend RAK4631 support
2024-05-18 00:15:48 +02:00
markqvist 0a15b4c6c1 Merge branch 'master' into master 2024-05-18 00:15:13 +02:00
markqvist 62db09571d Merge pull request #504 from jacobeva/hash-feature
Add ability to get target and calculated firmware hash from device
2024-05-18 00:04:24 +02:00
Mark Qvist 444ae0206b Added better handling on Windows of interfaces that are non-adoptable for AutoInterface 2024-05-17 23:54:48 +02:00
Mark Qvist 4b07e30b9d Updated version 2024-05-17 23:54:04 +02:00
markqvist 583e65419e Merge pull request #506 from liamcottle/feature/windows-auto-interface
Implement AutoInterface support on Windows
2024-05-17 16:32:33 +02:00
liamcottle 1564930a51 auto interface working on windows 2024-05-17 04:09:11 +12:00
markqvist b81b1de4eb Merge pull request #500 from faragher/master
Windows DTR timing fix
2024-05-15 20:06:36 +02:00
jacob.eva 746a38f818 Add ability to get target and calculated firmware hash from device 2024-05-13 22:55:49 +01:00
jacob.eva c230eceaa6 Extend RAK4631 support 2024-05-13 21:49:57 +01:00
Mark Qvist 09d9285104 Allow recursive path resolution for clients on roaming-mode interfaces 2024-05-12 12:31:51 +02:00
faragher 3551662187 Changing log levels 2024-05-08 02:19:59 -05:00
faragher f7f34e0ea3 Windows DTR timing adjustments 2024-05-08 02:14:29 -05:00
Mark Qvist 43fc2a6c92 Updated changelog 2024-05-05 20:05:30 +02:00
Mark Qvist b17175dfef Updated changelog 2024-05-05 19:57:48 +02:00
Mark Qvist 1103784997 Updated documentation 2024-05-05 19:56:33 +02:00
Mark Qvist d2feb8b136 Improved path response logic 2024-05-04 21:57:03 +02:00
Mark Qvist f595648a9b Updated tests 2024-05-04 20:27:27 +02:00
Mark Qvist b06f5285c5 Fix LR proof delivery on unknown hop count paths 2024-05-04 20:27:04 +02:00
Mark Qvist 8330f70a27 Fixed link packet routing in topologies where transport packets leak to non-intended instances in the link chain 2024-05-04 19:52:02 +02:00
Mark Qvist 15e10b9435 Added expected hops property to link class 2024-05-04 19:15:57 +02:00
Mark Qvist b91c852330 Updated path request timing 2024-05-04 16:19:04 +02:00
Mark Qvist 75acdf5902 Updated version 2024-05-03 23:49:39 +02:00
Mark Qvist dae40f2684 Removed T3S3 build from autoinstaller 2024-05-03 18:20:17 +02:00
Mark Qvist 4edacf82f3 Merge branch 'master' of github.com:markqvist/Reticulum 2024-05-03 16:22:37 +02:00
markqvist 4b0a0668a5 Update Contributing.md 2024-05-01 17:50:15 +02:00
markqvist a52af17123 Merge pull request #495 from jschulthess/master
optionally load identity file from file in Echo and Link examples
2024-05-01 17:28:10 +02:00
Mark Qvist 0b0a3313c5 Multicast address type modifications 2024-05-01 15:49:48 +02:00
markqvist 34af2e7af7 Merge pull request #476 from thiaguetz/feat/multicast-address-type
feat: implement multicast address type definition on AutoInterface configuration
2024-05-01 15:44:03 +02:00
Jürg Schulthess 12bf7977d2 fix comment 2024-04-29 08:25:40 +02:00
Jürg Schulthess b69b939d6f realign with upstream 2024-04-29 08:10:48 +02:00
Jürg Schulthess b5556f664b realign with upstream 2024-04-29 08:07:22 +02:00
Jürg Schulthess f804ba0263 explicit exit not needed 2024-04-29 08:04:04 +02:00
Jürg Schulthess 84a1ab0ca3 add option to load identity from file 2024-04-29 07:59:55 +02:00
markqvist 465695b9ae Merge pull request #490 from nothingbutlucas/master
docs: Fix a typo. startig / starting
2024-04-22 01:33:10 +02:00
Mark Qvist a999a4a250 Added support for T3S3 boards to rnodeconf autoinstaller 2024-04-22 01:26:35 +02:00
nothingbutlucas cbb5d99280 docs: Fix a typo. startig / starting
Signed-off-by: nothingbutlucas <69118979+nothingbutlucas@users.noreply.github.com>
2024-04-21 16:11:03 -03:00
Mark Qvist 64f5192c79 Changed rnodeconf autoinstaller menu order 2024-04-20 22:25:57 +02:00
Mark Qvist d223ebc8c0 Added rnodeconf autoinstaller support for Heltec LoRa32 V3 boards 2024-04-20 22:03:14 +02:00
markqvist c28f413fe6 Merge pull request #486 from cobraPA/upstream_add_heltec_v3
Add product and model, plus support for Heltec V3 serial only setup to rnodeconf.
2024-04-20 18:54:09 +02:00
Kevin Brosius 92e5f65887 Add product and model, plus support for Heltec V3 serial only setup
to rnodeconf.
2024-04-11 01:41:50 -04:00
Mark Qvist b977f33df6 Display error on unknown model capabilities instead of fail 2024-03-28 12:05:30 +01:00
Mark Qvist 589fcb8201 Added custom EEPROM bootstrap to rnodeconf 2024-03-28 00:04:48 +01:00
Mark Qvist e5427d70ac Added custom EEPROM bootstrap to rnodeconf 2024-03-27 21:48:32 +01:00
Mark Qvist 2f5381b307 Added TCXO model code comment 2024-03-24 11:51:44 +01:00
Thiaguetz 11baace08d feat: implement multicast address type definition on AutoInterface configuration 2024-03-23 00:54:56 -03:00
Mark Qvist a4d5b5cb17 Merge branch 'master' of github.com:markqvist/Reticulum 2024-03-19 11:52:58 +01:00
Mark Qvist 9cb181690e Added link getter to resource advertisement class 2024-03-19 11:52:32 +01:00
markqvist ff6604290e Update LICENSE 2024-03-10 22:14:29 +01:00
markqvist 2dbd3cbc0f Update Contributing.md 2024-03-10 22:14:03 +01:00
markqvist 2a11097cac Update Contributing.md 2024-03-10 22:13:33 +01:00
markqvist c0e3181ae3 Update Contributing.md 2024-03-10 22:11:44 +01:00
markqvist 5a0316ae7f Update Contributing.md 2024-03-10 20:39:49 +01:00
Mark Qvist 177bb62610 Updated changelog 2024-03-09 21:09:06 +01:00
Mark Qvist 7cd3cde398 Updated changelog 2024-03-09 21:08:17 +01:00
Mark Qvist 29bdcea616 Updated manual 2024-03-09 21:05:59 +01:00
Mark Qvist d9460c43ad Updated version 2024-03-09 21:01:12 +01:00
markqvist fb02e980db Merge pull request #461 from attermann/firmware_repos
Support for alternate download URL for custom firmware images
2024-03-08 01:10:34 +01:00
Mark Qvist 4947463440 Updated roadmap 2024-03-06 12:14:36 +01:00
Chad Attermann 5565349255 Fixed installation of alternate firmware version
Required version info was not being downloaded when alternate (not latest)
version is selected rsulting in the error "Could not read locally cached
release information."
2024-03-05 19:02:47 -07:00
Chad Attermann 1b7b131adc Added support for alternate firmware download URL
New command line option `--fw-url` accepts an alternate URL to use for
downloading firmware images.
Note this feature is moderately opinionated when it comes to directory
structure. The intent is to be compatible with GitHub releases, so the
latest version info is expected to be found at
"{fw-url}latest/download/release.json" and firmware images at
"{fw-url}download/{version}/{firmware_file.zip}".
2024-03-05 17:14:52 -07:00
Mark Qvist ace0d997d4 Updated changelog 2024-03-02 00:40:44 +01:00
Mark Qvist 798c252284 Updated manual 2024-03-02 00:40:35 +01:00
Mark Qvist 7da22c8580 Updated documentation build 2024-03-01 00:47:12 +01:00
Mark Qvist eefbb89cde Updated version 2024-03-01 00:05:40 +01:00
Mark Qvist 18f50ff1ae Limit amount of random blobs kept in memory and persisted to disk. Add check for non-existent announce in processing table. 2024-03-01 00:03:56 +01:00
Mark Qvist 05e97ac0db Fixed saving known destination when on-disk storage file has become corrupted 2024-02-29 23:23:41 +01:00
Mark Qvist c2c3a144d2 Added payload data inactivity metric to Link API 2024-02-29 23:05:16 +01:00
markqvist ea369015ee Update issue templates 2024-02-29 17:07:53 +01:00
markqvist 9745842862 Update issue templates 2024-02-29 17:05:46 +01:00
markqvist 246289c52d Create config.yml 2024-02-29 17:04:17 +01:00
markqvist ff71cb2f98 Update issue templates 2024-02-29 16:58:57 +01:00
Mark Qvist 5ca1ef1777 Revert EEPROM check logic 2024-02-29 16:18:39 +01:00
Mark Qvist 2b764b4af8 Allow EEPROM checksum mismatch on autoinstall. Fixes #432. 2024-02-29 15:50:45 +01:00
Mark Qvist a62843cd75 Updated readme 2024-02-16 17:54:31 +01:00
Mark Qvist 633435390d Added ability to flash T3 boards with TCXO 2024-02-16 17:32:01 +01:00
Mark Qvist 1e207ef972 Updated readme 2024-02-16 17:31:42 +01:00
Mark Qvist 35e9a0b38a Updated changelog 2024-02-14 16:58:51 +01:00
Mark Qvist 3d7f3825fb Updated manual 2024-02-14 16:54:29 +01:00
Mark Qvist 04b67a545d Updated version 2024-02-13 19:01:07 +01:00
Mark Qvist 61c2fbd0da Merge branch 'master' of github.com:markqvist/Reticulum 2024-02-13 19:00:00 +01:00
Mark Qvist 1aba4ec43a Added support for SX126x-based RNodes 2024-02-13 18:59:23 +01:00
markqvist 841a3daa26 Merge pull request #439 from jacobeva/master
Update min and max values to support SX1280
2024-02-09 22:30:32 +01:00
jacob.eva d98f03f245 Update min and max values to support SX1280 2024-02-09 21:17:58 +00:00
Mark Qvist 878e67f69d Fixed invalid RSSI offset reference. Fixes #433. 2024-01-18 23:01:54 +01:00
Mark Qvist e582a6d6d1 Updated changelog 2024-01-17 22:59:02 +01:00
Mark Qvist a948afb816 Updated manual 2024-01-17 22:56:24 +01:00
Mark Qvist 86a294388f Merge branch 'master' of github.com:markqvist/Reticulum 2024-01-17 22:52:48 +01:00
Mark Qvist 429a0b1bd3 Updated changelog 2024-01-17 22:52:01 +01:00
Mark Qvist ee8bb42633 Updated manual 2024-01-17 22:51:16 +01:00
Mark Qvist c659388a2c Updated manual 2024-01-17 22:36:17 +01:00
markqvist eaa8199988 Merge pull request #428 from jacobeva/master
Add NRF52 support
2024-01-17 01:33:07 +01:00
jacob.eva 4f890e7e8a Added NRF52 support 2024-01-16 21:30:31 +00:00
Mark Qvist a37e039424 Check input_file attribut 2024-01-14 18:57:23 +01:00
Mark Qvist 8e1e2a9c54 Added debug function 2024-01-14 18:56:20 +01:00
Mark Qvist e4f94c9d0b Updated docs 2024-01-14 18:55:44 +01:00
Mark Qvist b007530123 Adjusted resource timeout calculation 2024-01-14 01:06:43 +01:00
Mark Qvist 4066bba303 Merge branch 'master' of github.com:markqvist/Reticulum 2024-01-14 00:48:14 +01:00
Mark Qvist 8951517d01 Updated version 2024-01-14 00:47:45 +01:00
Mark Qvist ae1d962b9b Fixed large resource transfers failing under some conditions 2024-01-14 00:46:55 +01:00
Mark Qvist a2caa47334 Improved link tests 2024-01-14 00:12:30 +01:00
Mark Qvist 9f43da9105 Fixed rnprobe formatting issue 2024-01-13 16:37:48 +01:00
Mark Qvist 038c696db9 Fixed missing check on malformed advertisement packets 2024-01-13 16:36:11 +01:00
Mark Qvist 8fa6ec144c Updated readme 2024-01-03 12:05:30 +01:00
Mark Qvist a8ccff7c55 Updated contribution guidelines 2024-01-03 12:00:10 +01:00
markqvist a5783da407 Merge pull request #416 from jooray/patch-2
Fix typo
2023-12-31 12:24:48 +01:00
Juraj Bednar bec3cee425 Fix typo 2023-12-30 23:47:51 +01:00
Mark Qvist b15bd19de5 Added funding info 2023-12-30 22:00:46 +01:00
Mark Qvist 38390fd021 Updated license 2023-12-30 21:57:40 +01:00
Mark Qvist 40e0eee64f Updated license 2023-12-30 21:55:20 +01:00
Mark Qvist af4cbb1baf Added funding info 2023-12-30 21:53:50 +01:00
Mark Qvist d3f4192fe3 Added funding info 2023-12-30 21:52:41 +01:00
Mark Qvist 47ef62ac11 Updated contribution guidelines 2023-12-30 21:43:35 +01:00
Mark Qvist d15ddc7a49 Updated contribution guidelines 2023-12-30 17:34:51 +01:00
Mark Qvist d67c8eb1cd Fixed potential division by zero 2023-12-25 11:39:24 +01:00
Mark Qvist f4de5d5199 Updated changelog 2023-12-07 15:52:20 +01:00
Mark Qvist 34e42988ea Updated docs 2023-12-07 15:51:22 +01:00
Mark Qvist 81d5d41149 Updated changelog 2023-12-07 15:51:15 +01:00
Mark Qvist 6b3f3a37f0 Updated version 2023-12-06 00:07:06 +01:00
Mark Qvist 60a604f635 Carrier change flag on listener replace 2023-12-06 00:06:45 +01:00
Mark Qvist 55a2daf379 Updated docs 2023-12-02 02:14:49 +01:00
Mark Qvist 2dbde13321 Added identity import and export in hex, base32 and base64 formats 2023-12-02 02:10:22 +01:00
Mark Qvist 6620dcde6b Updated docs 2023-11-14 10:06:28 +01:00
Mark Qvist 60966d5bb1 Updated changelog 2023-11-14 10:06:19 +01:00
Mark Qvist ea22a53bf2 Updated docs 2023-11-13 23:38:46 +01:00
Mark Qvist 7b9526b4ed Updated version 2023-11-13 23:23:40 +01:00
Mark Qvist 676074187a Added timeout and wait options to rnprobe and improved output formatting 2023-11-13 23:22:58 +01:00
Mark Qvist 5dd2c31caf Generate receipts prior to raw transmit 2023-11-13 23:12:59 +01:00
Mark Qvist 2db400a1a0 Updated changelog 2023-11-13 23:11:29 +01:00
Mark Qvist b68dbaf15e Updated log levels 2023-11-08 15:23:29 +01:00
Mark Qvist 84febcdf95 Updated changelog 2023-11-06 11:28:22 +01:00
Mark Qvist c972ef90c8 Updated manual 2023-11-06 11:21:32 +01:00
Mark Qvist 19a74e3130 Updated changelog 2023-11-06 11:21:23 +01:00
Mark Qvist 5ba789f782 Updated single-packet timing 2023-11-06 11:10:38 +01:00
Mark Qvist 58b5501e17 Cleanup 2023-11-06 11:08:31 +01:00
Mark Qvist b584832b8f Fixed logging error messages when a local client connects while instance is starting up 2023-11-06 11:06:14 +01:00
Mark Qvist fc0cf17c4d Updated docs 2023-11-05 23:37:45 +01:00
Mark Qvist 001dd369ec Updated version 2023-11-05 23:37:38 +01:00
Mark Qvist 9ce2ea4a5c Updated link test 2023-11-05 23:36:19 +01:00
Mark Qvist eec8814c22 Updated version 2023-11-05 23:29:06 +01:00
Mark Qvist 7a6ed68482 Set socket options 2023-11-05 22:57:03 +01:00
Mark Qvist cd9e23f2de Updated manual 2023-11-04 23:19:08 +01:00
Mark Qvist ffa84de0bc Updated changelog 2023-11-04 23:18:59 +01:00
Mark Qvist 89d3cdba17 Updated docs 2023-11-04 18:13:26 +01:00
Mark Qvist 2ba5843f22 Updated version 2023-11-04 18:05:42 +01:00
Mark Qvist c4d0f08767 Improved resource transfers over unreliable links 2023-11-04 18:05:20 +01:00
Mark Qvist db1cdec2a2 Fixed premature request timeout 2023-11-04 17:59:27 +01:00
Mark Qvist 1eea1a6a22 Updated example 2023-11-04 17:56:20 +01:00
Mark Qvist 4a69ce5a98 Updated changelog 2023-11-02 21:44:48 +01:00
Mark Qvist 8d653cba9b Updated docs 2023-11-02 21:39:57 +01:00
Mark Qvist a6126a6bc5 Updated version 2023-11-02 21:37:16 +01:00
Mark Qvist 957c2b3bc1 Fixed invalid reference 2023-11-02 21:33:21 +01:00
Mark Qvist 494bde4e79 Updated docs 2023-11-02 18:53:22 +01:00
Mark Qvist 5e39136dff Fixed missing path state resetting on stale path rediscovery 2023-11-02 16:15:42 +01:00
Mark Qvist 4b26a86a73 Added probe count option to rnprobe 2023-11-02 16:14:38 +01:00
Mark Qvist 43a6e280c0 Fixed bluetooth read timeouts on Android in environments with hight 2.4G noise 2023-11-02 16:08:49 +01:00
Mark Qvist 237a45b2ca Don't send rediscovery requests on local originator 2023-11-02 13:33:12 +01:00
Mark Qvist b161650ced Adjusted link timings 2023-11-02 13:04:09 +01:00
Mark Qvist 24975eac31 Updated version 2023-11-02 13:03:53 +01:00
Mark Qvist 5d1ff36565 Updated docs 2023-11-02 13:03:39 +01:00
Mark Qvist 628777900e Fixed attribute 2023-11-02 12:44:57 +01:00
Mark Qvist 12e87425dc Adjusted timings 2023-11-02 12:24:42 +01:00
Mark Qvist 873f049e20 Fixed redundant rediscovery path request 2023-11-02 04:35:57 +01:00
Mark Qvist 2ea963ed03 Fixed missing timeout calculation 2023-11-02 04:35:10 +01:00
Mark Qvist 1d1276d6dd Updated changelog 2023-10-31 12:24:59 +01:00
Mark Qvist 83741724b0 Updated documentation 2023-10-31 12:24:18 +01:00
Mark Qvist a4143cfe6d Improved link error handling. Fixes #387. 2023-10-31 11:44:12 +01:00
Mark Qvist 3d645ae2f4 Updated documentation 2023-10-31 11:09:54 +01:00
Mark Qvist 5ba125c801 Updated documentation 2023-10-31 10:53:43 +01:00
Mark Qvist badb392898 Updated manual 2023-10-28 00:40:07 +02:00
Mark Qvist c0e1ce8d86 Updated documentation and manual 2023-10-28 00:28:41 +02:00
markqvist 0bc248c5e4 Merge pull request #385 from jschulthess/master
Add user systemd service to manual
2023-10-28 00:23:10 +02:00
Mark Qvist 798dfb1727 Added ability to query physical layer stats on links 2023-10-28 00:05:35 +02:00
Mark Qvist a451b987aa Updated documentation 2023-10-28 00:03:53 +02:00
Mark Qvist f01074e5b8 Implemented link establishment on ultra low bandwidth links 2023-10-27 18:16:52 +02:00
Mark Qvist 0e12442a28 Local interface bitrate simulation 2023-10-27 18:12:53 +02:00
Jürg Schulthess a4e8489a34 fix code text syntax 2023-10-25 14:09:24 +02:00
Jürg Schulthess 276b6fbd22 fix indentation 2023-10-25 14:07:34 +02:00
Jürg Schulthess 52ab08c289 add user systemd service 2023-10-25 13:31:37 +02:00
Mark Qvist 38236366cf Improved pretty print output 2023-10-24 13:24:40 +02:00
Mark Qvist af3cc3c5dd Updated version 2023-10-24 01:45:07 +02:00
Mark Qvist 35ed1f950c Updated version 2023-10-24 01:43:50 +02:00
Mark Qvist c050ef945e Updated pretty-print functions 2023-10-24 01:41:49 +02:00
Mark Qvist bed71fa3f8 Added physical layer link stats to link and packet classes 2023-10-24 01:41:12 +02:00
Mark Qvist cf125daf5c Added link quality calculation to RNode interface 2023-10-24 01:40:17 +02:00
Mark Qvist 9f425c2e8d Updated exceptions 2023-10-24 01:39:25 +02:00
Mark Qvist 0dc78241ac Updated version 2023-10-19 01:39:47 +02:00
Mark Qvist 01e963e891 Updated manual 2023-10-19 01:39:39 +02:00
Mark Qvist b3731524ac Improved path re-discovery in changing topographies 2023-10-19 00:38:41 +02:00
Mark Qvist 67c7395ea7 Improved shared interface reconnection on service restart 2023-10-18 23:18:59 +02:00
Mark Qvist fddf36a920 Updated manual 2023-10-16 19:33:13 +02:00
Mark Qvist 4f561a8c0c Added exception handling to interface detach 2023-10-16 18:54:36 +02:00
Mark Qvist 778d6105c1 Updated readme 2023-10-10 00:32:15 +02:00
Mark Qvist 60c94dc9b6 Updated readme 2023-10-10 00:29:40 +02:00
Mark Qvist f71395e449 Updated readme 2023-10-10 00:26:28 +02:00
Mark Qvist 1abacca9bf Fixed missing command definition 2023-10-08 18:02:38 +02:00
Mark Qvist 40281d5403 Updated changelog 2023-10-07 16:42:10 +02:00
Mark Qvist e0da489156 Updated manual 2023-10-07 16:33:54 +02:00
Mark Qvist 2dcf1350e7 Updated changelog 2023-10-07 16:33:45 +02:00
Mark Qvist 1e280611ce Updated documentation and manuals 2023-10-07 13:02:42 +02:00
Mark Qvist f1d107846f Updated version 2023-10-07 13:00:16 +02:00
Mark Qvist cc951dcb53 Added RPC key configuration option to manual 2023-10-07 12:40:30 +02:00
Mark Qvist b5856a3706 Added configuration option to specify shared instance RPC key 2023-10-07 12:34:10 +02:00
Mark Qvist ed3479da9a Reordered airtime stats 2023-10-04 23:46:35 +02:00
Mark Qvist 5e15f421b7 Updated manual 2023-10-02 18:01:28 +02:00
Mark Qvist 0a9366ba6e Updated Android log level on bluetooth failure 2023-10-02 17:39:19 +02:00
Mark Qvist cf31435f39 Updated docs 2023-10-02 17:36:52 +02:00
Mark Qvist 9f58860842 Added missing super init on Android interfaces 2023-10-02 17:36:33 +02:00
Mark Qvist 875348383d Updated roadmap 2023-10-01 23:46:01 +02:00
Mark Qvist f79f190525 Changed ir utility name to rnir. Closes #377. 2023-10-01 23:39:43 +02:00
Mark Qvist 5e27a81412 Updated changelog 2023-10-01 12:41:45 +02:00
Mark Qvist 0dcb009579 Updated docs and manual 2023-10-01 12:34:50 +02:00
Mark Qvist 943f76804b Updated utility documentation 2023-10-01 12:34:29 +02:00
Mark Qvist 8bbe6ae3ae Updated docs and manual 2023-10-01 12:09:49 +02:00
Mark Qvist f0d85dd078 Merge branch 'master' of github.com:markqvist/Reticulum 2023-10-01 11:46:57 +02:00
Mark Qvist f85dda1829 Fixed typos in examples 2023-10-01 11:46:30 +02:00
markqvist 91e064cdf1 Merge pull request #375 from connervieira/patch-1
Fixed some typos
2023-10-01 11:46:25 +02:00
Mark Qvist fb4e53f6e3 Configured announce ingress limit defaults 2023-10-01 11:39:24 +02:00
Mark Qvist 03340ed091 Added ability to drop all paths via a specific transport instance to rnpath 2023-10-01 11:39:07 +02:00
Mark Qvist ed424fa0a2 Updated documentation 2023-10-01 09:51:27 +02:00
Mark Qvist 406ab216d1 Updated documentation 2023-10-01 09:24:25 +02:00
Mark Qvist 00d8a2064d Fixed typos 2023-10-01 09:24:17 +02:00
Mark Qvist 38b920e393 Updated docs and manual 2023-10-01 01:59:22 +02:00
Mark Qvist 1ed000c4d9 Updated manual 2023-10-01 01:35:17 +02:00
Mark Qvist d360958d10 Updated documentation 2023-10-01 01:35:00 +02:00
Mark Qvist fcdb455d73 Added sort mode to rnstatus 2023-10-01 01:08:19 +02:00
Mark Qvist 575639b721 Updated documentation 2023-10-01 01:08:08 +02:00
Mark Qvist 492573f9fe Added ingress control interface configuraion options 2023-10-01 00:43:26 +02:00
Mark Qvist c5d30f8ee6 Cleanup 2023-10-01 00:24:03 +02:00
Mark Qvist 3c4791a622 Implemented announce ingress control 2023-10-01 00:16:32 +02:00
Mark Qvist 803a5736c9 Added held announce stats to rnstatus 2023-10-01 00:12:49 +02:00
Mark Qvist 267ffbdf5f Updated version 2023-09-30 22:37:43 +02:00
Mark Qvist 52028aa44c Added ingress control config option 2023-09-30 21:07:22 +02:00
Mark Qvist c5248d53d6 Fixed frequency pretty print function 2023-09-30 19:22:39 +02:00
Mark Qvist 2d2f0947ac Fixed frequency pretty print function 2023-09-30 19:18:30 +02:00
Mark Qvist 4fa616a326 Added interface sorting and announce rate display to rnstatus 2023-09-30 19:14:39 +02:00
Mark Qvist 136713eec1 Added announce frequency stats 2023-09-30 19:13:58 +02:00
Mark Qvist 0fd75cb819 Added announce frequency sampling to interfaces 2023-09-30 19:11:10 +02:00
Mark Qvist ea52153969 Added convenience function for printing frequencies 2023-09-30 19:09:26 +02:00
Mark Qvist 3854781028 Updated manual 2023-09-30 19:08:57 +02:00
Conner Vieira ec2805f357 Fixed some typos 2023-09-29 20:54:48 -04:00
Mark Qvist b5cb3a65dd Fixed announce queue not clearing all announces with exceeded retry limit at the same time 2023-09-30 00:25:47 +02:00
Mark Qvist c79cb3aa20 Resolver skeleton 2023-09-29 23:18:30 +02:00
Mark Qvist 8bff119691 Added Identity Resolver skeleton 2023-09-29 12:44:03 +02:00
Mark Qvist 5e0b2c5b42 Allow rnid aspect lengths of 1 2023-09-29 12:29:37 +02:00
Mark Qvist 8908022b88 Updated license headers 2023-09-29 10:31:20 +02:00
Mark Qvist b0dda0ed86 Added Resolver class 2023-09-29 10:31:00 +02:00
Mark Qvist 6ae72d4225 Updated exit codes 2023-09-29 10:30:19 +02:00
Mark Qvist 0a188a2d39 Fixed output formatting in rncp 2023-09-25 15:29:41 +02:00
Mark Qvist 036abb28fe Added timeout option to rnprobe 2023-09-25 15:27:24 +02:00
Mark Qvist a732767a28 Fixed local RSSI and SNR cache pop order 2023-09-25 14:17:58 +02:00
Mark Qvist 32a1261d98 Updated manual 2023-09-22 12:01:17 +02:00
Mark Qvist 27c5af3bbc Updated manual 2023-09-22 10:07:10 +02:00
Mark Qvist 5872108da3 Added timeout to rnprobe 2023-09-22 10:04:37 +02:00
Mark Qvist 8f6c6b76de Updated changelog 2023-09-21 21:24:26 +02:00
Mark Qvist 99db625c62 Updated manual 2023-09-21 21:23:28 +02:00
Mark Qvist fdf6a31cbd Updated changelog 2023-09-21 21:23:19 +02:00
Mark Qvist 75f353d7e2 Updated documentation 2023-09-21 19:12:34 +02:00
Mark Qvist 82f204fb44 Added ability to enable a built-in probe responder destination for Transport Instances 2023-09-21 18:48:08 +02:00
Mark Qvist 8d4492ecfd Updated documentation 2023-09-21 18:47:40 +02:00
Mark Qvist f8a53458d6 Added respond_to_probes option to example config 2023-09-21 18:33:14 +02:00
Mark Qvist 4229837170 Updated documentation 2023-09-21 18:32:46 +02:00
Mark Qvist 4be2ae6c70 Fixed verbose output bug in rnprobe 2023-09-21 18:32:36 +02:00
Mark Qvist dbdeba2fe0 Updated rnprobe utility 2023-09-21 17:49:14 +02:00
Mark Qvist 7e34b61f37 Added link status check on identify 2023-09-21 14:12:32 +02:00
Mark Qvist bf726ed2c7 Fixed missing timeout check in rncp 2023-09-21 14:12:14 +02:00
Mark Qvist fa54a2affe Updated documentation 2023-09-21 13:51:03 +02:00
Mark Qvist 62e1d0e554 Updated version 2023-09-21 13:46:51 +02:00
Mark Qvist 9c823a038b Impproved path re-discovery on Transport Instances when local nodes roam to other network segments 2023-09-21 13:46:28 +02:00
Mark Qvist 1e6cd50f46 Updated rnstatus output 2023-09-21 12:07:11 +02:00
Mark Qvist 06716e4873 Disabled caching until redesign 2023-09-21 12:05:37 +02:00
Mark Qvist 8e4a1e3ffa Increased AutoInterface peering timeout on Android 2023-09-20 00:53:51 +02:00
Mark Qvist 0abb3bd4c3 Update changelog 2023-09-19 18:46:28 +02:00
Mark Qvist 336574daed Updated manual 2023-09-19 18:46:23 +02:00
Mark Qvist 07938ba111 Added ability to set custom RNode display address to rnodeconf 2023-09-19 18:33:37 +02:00
Mark Qvist e699eb6d25 Updated changelog 2023-09-19 11:27:06 +02:00
Mark Qvist 3864549752 Updated changelog 2023-09-19 11:22:58 +02:00
Mark Qvist 0b934cd0f6 Updated manual 2023-09-19 11:13:30 +02:00
Mark Qvist 5bac38a752 Updated rncp output 2023-09-19 10:14:02 +02:00
Mark Qvist 72c8d4d3dd Updated docs 2023-09-19 10:13:45 +02:00
Mark Qvist b8c6ea015e Fixed missing attribute check 2023-09-19 10:13:27 +02:00
Mark Qvist ffe1beb7ae Updated log statement 2023-09-19 10:13:04 +02:00
Mark Qvist 21c6dbfce0 Added check for destination direction on annonuce 2023-09-19 10:11:45 +02:00
Mark Qvist 70cbb8dc79 Updated utilities section of docs 2023-09-18 23:16:57 +02:00
Mark Qvist 334f2a364d Added fetch mode to rncp 2023-09-18 22:40:29 +02:00
Mark Qvist b477354235 Added fetch mode to rncp 2023-09-18 22:22:44 +02:00
Mark Qvist 254c966159 Fixed potential None reference 2023-09-18 20:52:36 +02:00
Mark Qvist 7ee9b07d9c Added silent mode to rncp 2023-09-18 16:36:58 +02:00
Mark Qvist 839b72469c Added allowed_identities file support to rncp 2023-09-18 16:12:45 +02:00
Mark Qvist 874d76b343 Added Transport Instance uptime to rnstatut output 2023-09-18 15:45:55 +02:00
Mark Qvist 7497e7aa0c Updated readme 2023-09-18 13:04:38 +02:00
Mark Qvist efa084fb0f Updated readme 2023-09-18 13:04:24 +02:00
Mark Qvist 48e4a27054 Updated manual 2023-09-18 13:02:41 +02:00
Mark Qvist 96cf6a790e Updated documentation 2023-09-18 13:02:18 +02:00
Mark Qvist d7b54ff397 Updated readme 2023-09-18 13:02:08 +02:00
Mark Qvist 90ab065073 Updated manual 2023-09-18 12:36:08 +02:00
Mark Qvist b6f0784311 Added rnid utility to manual. Updated communications hardware section. 2023-09-18 12:35:54 +02:00
Mark Qvist e37ec654ee Fixed rnid output bug 2023-09-18 12:07:30 +02:00
Mark Qvist b237d51276 Cleanup 2023-09-18 11:00:36 +02:00
Mark Qvist 155ea24008 Added channel CSMA parameter stats to RNode Interface 2023-09-18 00:45:38 +02:00
Mark Qvist 8c8affc800 Improved Channel sequencing, retries and transfer efficiency 2023-09-18 00:42:54 +02:00
Mark Qvist 481062fca1 Added adaptive compression to Buffer class 2023-09-18 00:39:27 +02:00
Mark Qvist ffcc5560dc Updated version 2023-09-18 00:34:15 +02:00
Mark Qvist 09e146ef0b Updated channel tests 2023-09-18 00:34:02 +02:00
Mark Qvist 4c6b04ff69 Fixed invalid path for firmware hash generation while using extracted firmware to autoinstall 2023-09-15 13:49:15 +02:00
Mark Qvist 9889b479d1 Fixed inadverdent AutoInterface multi-IF deque hit for resource transfer retries 2023-09-14 22:14:31 +02:00
Mark Qvist 95dec00c76 Updated roadmap 2023-09-14 00:34:45 +02:00
Mark Qvist cff268926d Updated changelog 2023-09-14 00:22:02 +02:00
Mark Qvist 6fa88f4e4a Updated manual 2023-09-14 00:21:23 +02:00
Mark Qvist ab8e6791fe Updated changelog 2023-09-14 00:21:08 +02:00
Mark Qvist 13c45cc59a Added channel stat reporting and airtime controls to RNode interface 2023-09-13 21:15:32 +02:00
Mark Qvist 67c468884f Added channel load and airtime stats to rnstatus output 2023-09-13 20:07:53 +02:00
Mark Qvist f028d44609 Added airtime config info to docs 2023-09-13 20:07:31 +02:00
Mark Qvist 18b952e612 Added airtime config options, improved periodic data persist 2023-09-13 20:07:07 +02:00
Mark Qvist 25178d8f50 Updated docs 2023-09-13 13:37:37 +02:00
Mark Qvist 1c0b7c00fd Updated version 2023-09-13 13:24:50 +02:00
Mark Qvist 2439761529 Prevent answering path requests on roaming-mode interfaces for next-hop instances on same roaming-mode interface 2023-09-13 13:03:22 +02:00
Mark Qvist 8803dd5b65 Catch error when undefined next-hop path data is returned 2023-09-13 13:02:05 +02:00
Mark Qvist d15d04eae5 Updated debug logging 2023-09-13 13:01:14 +02:00
Mark Qvist bf40f74a4a Updated documentation build 2023-09-05 12:08:59 +02:00
Mark Qvist c0339c0f46 Updated testnet info 2023-08-30 02:15:34 +02:00
Mark Qvist b64bb166c0 Updated testnet info 2023-08-30 01:50:12 +02:00
Mark Qvist 31d30030dc Updated readme 2023-08-29 18:50:05 +02:00
Mark Qvist 556e111a98 Updated manual 2023-08-15 17:09:20 +02:00
Mark Qvist 70b0dd621b Updated install section 2023-08-15 11:27:22 +02:00
Mark Qvist f7d3212651 Updated install section 2023-08-15 11:00:59 +02:00
Mark Qvist 0a29f0cfa1 Updated changelog 2023-08-15 10:38:29 +02:00
Mark Qvist 97153ad59d Updated explanation text 2023-08-15 10:30:49 +02:00
Mark Qvist bc8378fb60 Merge branch 'master' of github.com:markqvist/Reticulum 2023-08-15 10:27:15 +02:00
markqvist 3320cf8da8 Merge pull request #363 from blackjack75/master
Added suggestion to use lower baudrate if flashing fails on ESP32
2023-08-15 10:26:57 +02:00
markqvist bb53bd3f27 Merge pull request #362 from Erethon/eeprom-dump-dir
rnodeconf: Dump eeprom under specific directory
2023-08-15 10:25:17 +02:00
Mark Qvist 73eed59fab Updated docs 2023-08-15 10:23:51 +02:00
Santiago Lema 91ede52634 Added suggestion to use lower baudrate if flashing fails on ESP32 2023-08-14 20:47:40 +02:00
Dionysis Grigoropoulos 93f13a98b2 rnodeconf: Dump eeprom under specific directory 2023-08-14 20:08:40 +03:00
Mark Qvist c87c5c9709 Updated docs 2023-08-14 16:46:00 +02:00
markqvist b0c6c53430 Merge pull request #360 from Erethon/set-baud-rate-when-flashing
rnodeconf: Add option to set baud when flashing
2023-08-14 16:42:26 +02:00
Mark Qvist 94a5222390 Updated version 2023-08-13 20:38:41 +02:00
Dionysis Grigoropoulos 98bb304060 rnodeconf: Add option to set baud when flashing 2023-08-12 02:37:05 +03:00
Mark Qvist 08bfd923ea Fixed possible invalid comparison in link watchdog job 2023-08-05 15:10:00 +02:00
Mark Qvist ae28f04ce4 Added bytes input to destination hash convenience functions 2023-07-10 00:54:02 +02:00
Mark Qvist 024a742f2a Updated changelog 2023-07-09 16:51:54 +02:00
Mark Qvist df184f3e54 Updated docs 2023-07-09 16:48:45 +02:00
Mark Qvist 5542410afa Updated version 2023-07-09 16:45:52 +02:00
Mark Qvist 99205cdc0f Fixed typo in rnid 2023-07-09 16:29:40 +02:00
Mark Qvist 8c936af963 Merge branch 'master' of github.com:markqvist/Reticulum 2023-06-29 22:12:30 +02:00
Mark Qvist 7fe751e74f Updated documentation 2023-06-29 16:52:06 +02:00
markqvist 6d551578c3 Merge pull request #325 from npetrangelo/patch-3
Update __init__.py
2023-06-22 20:05:37 +02:00
markqvist 40c85fb607 Merge pull request #330 from Erethon/rnodeconf-device-selection
Fix bug in device selection of rnodeconf
2023-06-22 20:00:42 +02:00
Dionysis Grigoropoulos 743736b376 Fix bug in device selection of rnodeconf 2023-06-21 00:02:11 +03:00
Mark Qvist 7fdb431d70 Updated changelog 2023-06-13 19:27:53 +02:00
Mark Qvist ebcc3d8912 Updated manual 2023-06-13 19:27:07 +02:00
Mark Qvist 32e29a54c3 Updated manual 2023-06-13 19:21:03 +02:00
Mark Qvist 049733c4b6 Fixed race condition for link initiators on timed out link establishment 2023-06-13 19:20:54 +02:00
Mark Qvist 420d58527d Merge branch 'master' of github.com:markqvist/Reticulum 2023-06-13 16:11:28 +02:00
Mark Qvist bab779a34c Fixed race condition for link initiators on timed out link establishment 2023-06-13 16:10:47 +02:00
markqvist 45aa71b2b7 Merge pull request #326 from SebastianObi/master
RNodeInterface - Fixed missing init of 'r_stat_snr'.
2023-06-07 18:40:50 +02:00
SebastianObi 6dcfe2cad6 Fixed missing init of 'r_stat_snr'.
This this will otherwise lead to the error:
AttributeError: 'RNodeInterface' object has no attribute 'r_stat_snr'
2023-06-07 17:43:14 +02:00
SebastianObi f206047908 Fixed missing init of 'r_stat_snr'.
This this will otherwise lead to the error:
AttributeError: 'RNodeInterface' object has no attribute 'r_stat_snr'
2023-06-07 17:42:44 +02:00
Nathan Petrangelo 6ce979a7de Update __init__.py
Auto convert log messages to strings on the way in
2023-06-05 17:31:52 -04:00
Mark Qvist 97f97eb063 Updated changelog 2023-06-03 16:04:18 +02:00
Mark Qvist f3db762e9f Updated documentation 2023-06-03 16:03:13 +02:00
Mark Qvist f9f623dfa5 Updated version and changelog 2023-06-03 15:52:44 +02:00
Mark Qvist ffa6bec3b4 Updated parser 2023-06-02 21:24:57 +02:00
Mark Qvist 4f78973751 Fixed race condition when timed-out link receives a late establishment proof a few milliseconds after it has timed out 2023-06-02 21:24:49 +02:00
Mark Qvist a8a7af4b74 Handle missing identity file in rncp. Fixes #317. 2023-05-31 15:39:55 +02:00
Mark Qvist 45295c779c Updated changelog 2023-05-19 11:38:46 +02:00
Mark Qvist a82376d1f5 Updated manuals 2023-05-19 11:35:45 +02:00
Mark Qvist 75c6248264 Updated documentation 2023-05-19 11:31:43 +02:00
Mark Qvist 9294ab4f97 Updated version 2023-05-19 11:31:36 +02:00
Mark Qvist f01193e854 Updated documentation 2023-05-19 03:06:24 +02:00
Mark Qvist d7375bc4c3 Fixed callback invocation on channel receive 2023-05-19 01:58:28 +02:00
Mark Qvist 1a860c6ffd Add EOF signal on buffer close 2023-05-19 01:57:20 +02:00
Mark Qvist 800ed3af7a Fixed ready callback invocation 2023-05-18 23:35:28 +02:00
Mark Qvist 9c8e79546c Fixed missing check in receipt culling 2023-05-18 23:33:26 +02:00
Mark Qvist 4c272aa536 Updated buffer tests for windowed channel 2023-05-18 23:32:29 +02:00
Mark Qvist e184861822 Enabled channel tests 2023-05-18 23:31:29 +02:00
Mark Qvist d40e19f08d Updated gitignore 2023-05-18 23:29:31 +02:00
Mark Qvist 817ee0721a Updated manual 2023-05-12 12:38:12 +02:00
Mark Qvist 22ec4afdab Updated changelog 2023-05-12 12:38:02 +02:00
Mark Qvist 61626897e7 Add channel window mode for slow links 2023-05-11 21:28:13 +02:00
Mark Qvist 6fd3edbb8f Updated docs 2023-05-11 20:55:28 +02:00
Mark Qvist fc5b02ed5d Added medium window to channel 2023-05-11 20:23:36 +02:00
Mark Qvist a06e752b76 Added multi-interface duplicate deque to AutoInterface 2023-05-11 19:54:26 +02:00
Mark Qvist 3a947bf81b Updated documentation 2023-05-11 19:53:40 +02:00
Mark Qvist 31121ca885 Updated documentation 2023-05-11 18:49:01 +02:00
Mark Qvist 387b8c46ff Cleanup 2023-05-11 18:35:01 +02:00
Mark Qvist 66fda34b20 Cleanup 2023-05-11 17:48:07 +02:00
Mark Qvist 1542c5f4fe Fixed received link packet proofs not resetting watchdog stale timer 2023-05-11 16:22:44 +02:00
Mark Qvist 523fc7b8f9 Adjusted loglevel 2023-05-11 16:09:25 +02:00
Mark Qvist 73faf04ea1 Tuned channel windowing 2023-05-10 20:01:33 +02:00
Mark Qvist e10ddf9d2d Cleanup 2023-05-10 19:28:28 +02:00
Mark Qvist 641a7ea75d Implemented basic channel windowing 2023-05-10 19:15:45 +02:00
Mark Qvist e543d5c27f Implemented basic channel windowing 2023-05-10 19:15:20 +02:00
Mark Qvist 01c59ab0c6 Cleanup 2023-05-10 18:44:05 +02:00
Mark Qvist a4c64abed4 Initial framework for channel windowing 2023-05-10 18:43:17 +02:00
Mark Qvist 7df11a6f67 Fixed missing isolation of packet delivery callback 2023-05-10 18:40:46 +02:00
Mark Qvist 1bd6020163 Cleanup 2023-05-10 18:40:18 +02:00
Mark Qvist b3ac3131b5 Updated version 2023-05-09 23:07:47 +02:00
Mark Qvist f522cb1db1 Added per-packet compression to buffer 2023-05-09 22:13:57 +02:00
Mark Qvist d96a4853fe Fixed version display 2023-05-09 22:13:23 +02:00
Mark Qvist 52a0447fea Fixed resent packets not getting repacked 2023-05-09 22:12:49 +02:00
Mark Qvist e82e6d56f1 Added ability to trust external signing keys to rnodeconf 2023-05-09 15:31:02 +02:00
Mark Qvist 3967ef453d Updated documentation 2023-05-05 13:47:29 +02:00
Mark Qvist 76f7751d5f Updated documentation 2023-05-05 13:46:23 +02:00
Mark Qvist 8716ffc873 Updated documentation 2023-05-05 13:38:06 +02:00
Mark Qvist b476e4cfb0 Updated changelog 2023-05-05 11:48:00 +02:00
Mark Qvist 7ec77a10d3 Updated changelog 2023-05-05 11:46:06 +02:00
Mark Qvist 55a9c5ef71 Updated documentation 2023-05-05 11:27:52 +02:00
Mark Qvist 6d3ba31993 Updated readme 2023-05-05 11:14:50 +02:00
Mark Qvist d3f4a674aa Updated readme 2023-05-05 11:13:18 +02:00
Mark Qvist 599ab20ed0 Updated readme 2023-05-05 11:09:12 +02:00
Mark Qvist dcf33e125b Cleanup 2023-05-05 10:43:27 +02:00
Mark Qvist 01600b96a4 Fix import paths 2023-05-05 10:37:22 +02:00
Mark Qvist 64bdc4c18c Fix import paths 2023-05-05 10:25:15 +02:00
Mark Qvist 0889b8a7c5 Updated manual 2023-05-05 10:09:17 +02:00
Mark Qvist 1b2fee3ab8 Fixed EPUB output 2023-05-05 09:43:21 +02:00
Mark Qvist da7a4433c0 Updated documentation 2023-05-04 23:30:27 +02:00
Mark Qvist 5e5d89cc92 Removed dependency on netifaces. 2023-05-04 23:19:43 +02:00
Mark Qvist a3bee4baa9 Removed netifaces dependency from AutoInterface 2023-05-04 17:55:58 +02:00
Mark Qvist fab83ec399 Restructured library 2023-05-04 17:55:38 +02:00
Mark Qvist b740e36985 Added ifaddr module 2023-05-04 17:46:56 +02:00
Mark Qvist 29693c6fe2 Updated documentation 2023-05-04 12:42:12 +02:00
Mark Qvist 72638f40a6 Updated documentation 2023-05-04 12:23:25 +02:00
Mark Qvist 8d29e83d90 Updated dependencies 2023-05-04 12:23:16 +02:00
Mark Qvist 53b325d34d Added support for T3 v1.0 to rnodeconf 2023-05-03 15:56:19 +02:00
Mark Qvist d31cf6e297 Added ability to configure RNode display intensity 2023-05-03 14:26:47 +02:00
Mark Qvist e386a5d08b Use native Python unzip for updates 2023-05-03 12:57:38 +02:00
Mark Qvist d467ed9ece Merge branch 'master' of github.com:markqvist/Reticulum 2023-05-03 12:27:10 +02:00
Mark Qvist 892a467d74 Update version 2023-05-03 12:26:48 +02:00
markqvist 4366e71f34 Merge pull request #272 from VioletEternity/windows
Improve Windows compatibility for rnodeconf
2023-05-03 12:26:36 +02:00
Mark Qvist 7e9998b4fd Use included platform detection method 2023-05-03 12:21:57 +02:00
markqvist 79abe93139 Merge pull request #278 from VioletEternity/windows-so_reuseaddr
Use SO_EXCLUSIVEADDRUSE instead of SO_REUSEADDR on Windows
2023-05-03 12:18:49 +02:00
Mark Qvist d69d4b3920 Fixed firmware extraction for unverifiable devices. Fixes #266. 2023-05-02 18:10:04 +02:00
Mark Qvist 3300541181 Fixed invalid error code in conditional. Fixes #284. 2023-05-02 17:45:30 +02:00
Mark Qvist 3848059f19 Only use ifname for link-local discovery scopes. Fixes #283. 2023-05-02 17:39:06 +02:00
Mark Qvist 30021d89cb Fixed header bits in get_packed_flags(). Fixes #275. 2023-05-02 17:33:38 +02:00
Mark Qvist 29019724bd Added verbosity argument to Reticulum instantiation. Fixes #238. 2023-05-02 16:42:04 +02:00
Maya ba7838c04e Use SO_EXCLUSIVEADDRUSE instead of SO_REUSEADDR on Windows.
On Linux, SO_REUSEADDR is used so that a socket in TIME-WAIT state can
be rebound after a listening process is restarted. It does not allow two
processes to listen on the exact same (addr, port) combination. However,
on Windows, it does, and SO_EXCLUSIVEADDRUSE is required to reproduce
the Linux behavior.

Reticulum relies on an error being returned by bind() that reuses
the same (addr, port) combination as another process to detect whether
there is a shared instance already running. Setting SO_EXCLUSIVEADDRUSE
makes this detection process work on Windows as well.
2023-04-19 03:03:15 +01:00
Maya af16c68e47 Make esptool.py invocation compatible with Windows. 2023-04-13 18:17:14 +01:00
Maya bda5717051 Use standard Python zipfile module to decompress firmware 2023-04-13 18:10:21 +01:00
Mark Qvist fac4973329 Fixed potential race condition in announce queue handling for AutoInterface 2023-03-09 18:32:14 +01:00
Mark Qvist 90cfaa4e82 Updated manual 2023-03-08 14:54:04 +01:00
Mark Qvist 443aa575df Updated changelog 2023-03-08 14:53:52 +01:00
Mark Qvist 619771c3a3 Updated changelog 2023-03-08 14:43:35 +01:00
Mark Qvist 18a56cfd52 Updated manual 2023-03-08 14:27:51 +01:00
Mark Qvist 55c39ff27c Updated roadmap 2023-03-08 14:10:56 +01:00
Mark Qvist 159c7a9a52 Fixed rnstatus JSON output error 2023-03-08 14:10:33 +01:00
Mark Qvist af8edc335b Updated roadmap 2023-03-08 12:35:41 +01:00
Mark Qvist 4d3ea37bc3 Updated roadmap and docs 2023-03-08 12:34:09 +01:00
Mark Qvist 226004da94 Ignore lo0 in all cases. Fixes #237. 2023-03-07 16:43:10 +01:00
Mark Qvist 47b358351f Exclude tests from wheel. Fixes #241. 2023-03-07 16:31:31 +01:00
markqvist f5d77a1dfb Merge pull request #252 from acehoss/bugfix/buffer-missing-segments
Bugfix: buffer missing segments
2023-03-05 17:59:03 +01:00
Aaron Heise 9c9f0a20f9 Handle sequence overflow when checking incoming message 2023-03-04 23:54:07 -06:00
Aaron Heise 6d9d410a70 Address multiple issues with Buffer and Channel
- StreamDataMessage now packed by struct rather than umsgpack for a more predictable size
- Added protected variable on LocalInterface to allow tests to simulate a low bandwidth connection
- Retry timer now has exponential backoff and a more sane starting value
- Link proves packet _before_ sending contents to Channel; this should help prevent spurious retries especially on half-duplex links
- Prevent Transport packet filter from filtering out duplicate packets for Channel; handle duplicates in Channel to ensure the packet is reproven (in case the original proof packet was lost)
- Fix up other tests broken by these changes
2023-03-04 23:37:58 -06:00
Mark Qvist d8f3ad8d3f Temporarily disabled extra-level log statement 2023-03-04 19:30:47 +01:00
Mark Qvist a1b75b9746 Increased per-hop timeout 2023-03-04 19:30:23 +01:00
Mark Qvist 80f3bfaece Adjusted StreamDataMessage overhead calculation 2023-03-04 19:06:47 +01:00
Mark Qvist 37b2d8a6ec Fixed Link MDU output in phyparams() 2023-03-04 18:37:28 +01:00
Mark Qvist 777fea9cea Differentiate exception between link establishment callback, and internal RTT packet handling 2023-03-04 18:32:36 +01:00
Mark Qvist bbfdd37935 Added check for link state before sending 2023-03-04 18:31:07 +01:00
Mark Qvist 07484725a0 Updated documentation 2023-03-04 17:57:18 +01:00
Mark Qvist 709b126a67 Updated strings in Buffer example 2023-03-04 17:56:50 +01:00
Mark Qvist 28e6302b3d Updated versions 2023-03-04 17:56:30 +01:00
Mark Qvist 27861e96f8 Updated documentation 2023-03-03 22:16:13 +01:00
markqvist e36312a3cb Merge pull request #250 from acehoss/feature/buffer
Buffer: send and receive binary data over Channel
2023-03-03 17:21:25 +01:00
Aaron Heise 5b5dbdaa91 Add example to documentation 2023-03-02 17:21:32 -06:00
Aaron Heise 99dc97365f Merge remote-tracking branch 'origin/feature/buffer' into feature/buffer 2023-03-02 17:17:40 -06:00
Aaron Heise aac2b9f987 Buffer: send and receive binary data over Channel
(also some minor fixes in channel)
2023-03-02 17:17:18 -06:00
Aaron Heise 067c275c46 Buffer: send and receive binary data over Channel
(also some minor fixes in channel)
2023-03-02 17:13:55 -06:00
Mark Qvist 58004d7c05 Updated documentation 2023-03-02 12:47:55 +01:00
Mark Qvist aa0d9c5c13 Merge branch 'master' of github.com:markqvist/Reticulum 2023-03-02 12:05:06 +01:00
Mark Qvist 9e46950e28 Added output to echo example 2023-03-02 12:04:50 +01:00
markqvist a6551fc019 Merge pull request #246 from gdt/fix-transmit-hash
AutoInterface: Drop embedded scope identifier on fe80::
2023-03-02 11:34:00 +01:00
markqvist a06ae40797 Merge pull request #236 from faragher/master
Additional error messages for offline flashing.
2023-03-02 11:31:31 +01:00
markqvist 1db08438df Merge pull request #248 from Erethon/hkdf-remove-dead-code
hkdf: Remove duplicate check if the salt is None
2023-03-02 11:29:18 +01:00
markqvist 89aa51ab61 Merge pull request #245 from acehoss/feature/channel
Channel: reliable delivery over Link
2023-03-02 11:27:15 +01:00
Dionysis Grigoropoulos ddb7a92c15 hkdf: Remove duplicate check if the salt is None
The second if isn't needed since we initialize the salt with zeroes
earlier. If instead we meant to pass an empty bytes class to the HMAC
implementation, the end result would be the same, since it's gonna get
padded with zeroes in the HMAC code.
2023-03-01 16:22:51 +02:00
Greg Troxel e273900e87 AutoInterface: Drop embedded scope identifier on fe80::
The code previously dropped scope identifiers expressed as a trailing
"%ifname", which happens on macOS.  On NetBSD and OpenBSD (and likely
FreeBSD, not tested), the scope identifier is embedded.  Drop that
form of identifier as well, because we keep address and ifname
separate, and because the scope identifier must not be part of
computing the hash of the address.

Resolves #240, failure to peer on NetBSD and OpenBSD.
2023-02-28 10:19:46 -05:00
Aaron Heise d2d121d49f Fix broken Channel test 2023-02-28 08:38:36 -06:00
Aaron Heise 9963cf37b8 Fix exceptions on Channel shutdown 2023-02-28 08:38:23 -06:00
Aaron Heise 72300cc821 Revert "Only send proof if link is still active" 2023-02-28 08:24:13 -06:00
Aaron Heise 8168d9bb92 Only send proof if link is still active 2023-02-28 08:13:07 -06:00
Aaron Heise 8f0151fed6 Tidy up PR 2023-02-27 21:33:50 -06:00
Aaron Heise d3c4928eda Tidy up PR 2023-02-27 21:31:41 -06:00
Aaron Heise 68f95cd80b Tidy up PR 2023-02-27 21:30:13 -06:00
Aaron Heise 42935c8238 Make the PR have zero deletions 2023-02-27 21:15:25 -06:00
Aaron Heise 118acf77b8 Fix up documentation even more 2023-02-27 21:10:28 -06:00
Aaron Heise 661964277f Fix up documentation for building 2023-02-27 19:05:25 -06:00
Aaron Heise 464dc23ff0 Add some internal documenation 2023-02-27 17:36:04 -06:00
Aaron Heise 44dc2d06c6 Add channel tests to all test suite
Also print name in each test
2023-02-26 11:47:46 -06:00
Aaron Heise c00b592ed9 System-reserved channel message types
- a message handler can return logical True to prevent subsequent message handlers from running
- Message types >= 0xff00 are reserved for system/framework messages
2023-02-26 11:39:49 -06:00
Aaron Heise e005826151 Allow channel message handlers to short circuit
- a message handler can return logical True to prevent subsequent message handlers from running
2023-02-26 11:23:38 -06:00
Aaron Heise a61b15cf6a Added channel example 2023-02-26 07:26:12 -06:00
Aaron Heise fe3a3e22f7 Expose Channel on Link
Separates channel interface from link

Also added: allow multiple message handlers
2023-02-26 07:25:49 -06:00
Aaron Heise 68cb4a6740 Initial work on Channel 2023-02-25 18:23:25 -06:00
Mark Qvist 9f06bed34c Updated readme and roadmap 2023-02-23 17:27:05 +01:00
Mark Qvist 3b1936ef48 Added EPUB output to documentation build 2023-02-23 17:25:38 +01:00
Michael Faragher 5b3d26a90a Additional error messages for offline flashing. 2023-02-22 12:49:24 -06:00
markqvist b381a61be8 Update Changelog.md 2023-02-18 23:35:41 +01:00
Mark Qvist 1e2fa2068c Updated manual 2023-02-18 16:53:18 +01:00
Mark Qvist c604214bb9 Improved RNode reconnection when serial device disappears 2023-02-18 13:31:22 +01:00
Mark Qvist e738c9561a Updated manual 2023-02-17 21:53:07 +01:00
Mark Qvist 994d1c8ee5 Updated roadmap 2023-02-17 21:41:41 +01:00
Mark Qvist ce21800537 Merge branch 'master' of https://git.unsigned.io/markqvist/Reticulum 2023-02-17 21:33:04 +01:00
Mark Qvist d02cdd5471 Added JSON output to rnstatus 2023-02-17 21:29:35 +01:00
Mark Qvist 7018e412d5 Updated roadmap 2023-02-17 21:28:13 +01:00
Mark Qvist 94f7505076 Updated docs 2023-02-17 21:25:14 +01:00
Mark Qvist b82ecf047a Added Link establishment rate calculation 2023-02-17 09:54:18 +01:00
Mark Qvist f21b93403a Updated documentation 2023-02-17 09:53:27 +01:00
Mark Qvist 59c88bc43b Merge branch 'master' of github.com:markqvist/Reticulum 2023-02-15 12:53:37 +01:00
Mark Qvist 8e98c1b038 Updated roadmap 2023-02-15 12:51:41 +01:00
Mark Qvist 4d3570fe4c Updated version 2023-02-15 12:28:06 +01:00
markqvist 3706769c33 Updated link. Fixes #216. 2023-02-09 22:27:11 +01:00
Mark Qvist ce91c34b21 Merge branch 'master' of https://git.unsigned.io/markqvist/Reticulum 2023-02-09 16:22:39 +01:00
Mark Qvist e37aa5e51a Added contribution guidelines 2023-02-09 16:18:59 +01:00
Mark Qvist 80af0f4539 Updated roadmap 2023-02-09 14:07:30 +01:00
Mark Qvist fc818f00f1 Merge branch 'master' of github.com:markqvist/Reticulum 2023-02-09 11:54:06 +01:00
Mark Qvist a55d39b7d4 Added Link ID to response_generator callback signature 2023-02-09 11:52:54 +01:00
Mark Qvist 8e264359db Fixed link 2023-02-09 11:25:51 +01:00
markqvist cbaeaa9f81 Merge pull request #203 from Erethon/rnodeconf-typo
rnodeconf: Typo fix on board versions
2023-02-04 19:21:21 +01:00
Dionysis Grigoropoulos 323c2285ce rnodeconf: Typo fix on board versions 2023-02-04 17:16:57 +02:00
Mark Qvist 5b6d0ec337 Updated manual 2023-02-04 16:00:07 +01:00
Mark Qvist 2bbb0f5ec2 Fixed missing entrypoint 2023-02-04 15:59:58 +01:00
Mark Qvist e385c79abd Updated manual 2023-02-04 15:38:44 +01:00
Mark Qvist 86faf6c28d Updated roadmap 2023-02-04 15:36:11 +01:00
Mark Qvist 6d8a3f09e5 Updated readme 2023-02-04 15:35:55 +01:00
Mark Qvist 1e88a390f4 Updated manual 2023-02-04 14:28:28 +01:00
Mark Qvist e9ae255f84 Added fallback version URL to rnodeconf updater 2023-02-04 14:18:11 +01:00
Mark Qvist 42dfee8557 Added Bluetooth pairing PIN output 2023-02-04 13:45:12 +01:00
Mark Qvist 177e724457 Updated roadmap 2023-02-04 12:17:05 +01:00
Mark Qvist 1b55ac7f24 Added destination hash generation and announce functionality to rnid utility 2023-02-03 20:27:39 +01:00
Mark Qvist 5447ed85c1 Updated documentation 2023-02-03 11:32:54 +01:00
Mark Qvist d7aacba797 Cleanup 2023-02-03 10:13:36 +01:00
Mark Qvist b92ddeccff Cleanup 2023-02-03 08:29:32 +01:00
Mark Qvist 6fac96ec18 Mask entire header 2023-02-03 00:11:11 +01:00
Mark Qvist 53ceafcebd Improved IFAC mask derivation 2023-02-02 23:59:02 +01:00
Mark Qvist 4df67304d6 Added payload masking to interfaces with IFAC enabled 2023-02-02 20:48:52 +01:00
Mark Qvist ac07ba1368 Added Identity generation to rnid utility 2023-02-02 19:26:27 +01:00
Mark Qvist ece064d46e Updated version 2023-02-02 19:05:15 +01:00
Mark Qvist 86ae42a049 Updated docs 2023-02-02 19:04:52 +01:00
Mark Qvist 08e480387b Added signing and validation to rnid 2023-02-02 19:02:05 +01:00
Mark Qvist f4241ae9c2 Added basic rnid utility 2023-02-02 17:45:59 +01:00
Mark Qvist b6928b7d83 Merge branch 'master' of github.com:markqvist/Reticulum 2023-02-02 10:40:58 +01:00
markqvist 3b2fbe02c6 Merge pull request #189 from Erethon/master
Fix bug where announce_identity could be undefined
2023-02-02 10:41:42 +01:00
markqvist a38bde7801 Merge pull request #191 from Erethon/packet-header-fix
packet: Fix header_type matching according to IFAC
2023-02-02 10:22:44 +01:00
markqvist df132d1d59 Merge pull request #199 from Erethon/doc-fixes
docs: Fix typos, remove old info about rnsconfig
2023-02-02 10:16:13 +01:00
Mark Qvist 143f7fa683 Merge branch 'master' of github.com:markqvist/Reticulum 2023-02-02 10:15:41 +01:00
Dionysis Grigoropoulos feb614d186 docs: Fix typos, remove old info about rnsconfig 2023-02-01 22:30:56 +02:00
Mark Qvist 159be78f23 Updated docs 2023-02-01 15:44:23 +01:00
Mark Qvist 4a6c6568e2 Merge branch 'master' of github.com:markqvist/Reticulum 2023-02-01 13:45:05 +01:00
Mark Qvist e64fa08c74 Updated documentation. Fixes #197. 2023-02-01 13:44:00 +01:00
markqvist 6651976423 Merge pull request #193 from jooray/patch-1
Fix a typo
2023-01-28 23:10:14 +01:00
Juraj Bednar 5decf22b8b Fix a typo
Fix documentation: rncp called instead of rnx in rnx example
2023-01-28 21:32:37 +01:00
Mark Qvist a731a8b047 Merge branch 'master' of https://git.unsigned.io/markqvist/Reticulum 2023-01-27 18:51:37 +01:00
Mark Qvist 9bb9571fc9 Updated documentation 2023-01-27 18:51:25 +01:00
Dionysis Grigoropoulos 6ecae615de packet: Fix header_type matching according to IFAC
Ever since IFAC/Interface Access Codes were introduced, the header type
is one bit long and not two.
2023-01-27 15:29:06 +02:00
Dionysis Grigoropoulos 72ca6316f6 Fix bug where announce_identity could be undefined 2023-01-26 22:05:38 +02:00
Mark Qvist 0f023cc533 Updated roadmap 2023-01-19 15:14:15 +01:00
Mark Qvist 9f9a4a14d3 Updated changelog 2023-01-14 21:02:01 +01:00
Mark Qvist 0609251270 Updated manual 2023-01-14 20:51:17 +01:00
Mark Qvist e4f0b2dc39 Allow rnodeconf to provision RNodes from extracted firmwares on systems without prior tools installed 2023-01-14 20:47:34 +01:00
Mark Qvist 2ef06f2bd3 Updated documentation 2023-01-14 20:46:32 +01:00
Mark Qvist c5a586175d Updated version 2023-01-14 15:06:30 +01:00
Mark Qvist 2a1ec6592c Added autoinstall and updating from extracted RNode Firmwares to rnodeconf 2023-01-14 14:51:44 +01:00
Mark Qvist eed7698ed3 Added firmware extraction from existing devices to rnodeconf 2023-01-14 13:20:19 +01:00
Mark Qvist 205c612a0f Updated roadmap 2023-01-14 10:22:21 +01:00
Mark Qvist 8d96673bec Updated flasher paths 2023-01-14 00:55:34 +01:00
Mark Qvist 62a13eb0e8 Added RNode Bootstrap Console info to rnodeconf autoinstaller 2023-01-14 00:28:34 +01:00
Mark Qvist 10d03753b5 Updated documentation 2023-01-13 12:00:12 +01:00
Mark Qvist f19b87759f Merge branch 'master' of https://git.unsigned.io/markqvist/Reticulum 2023-01-13 11:59:42 +01:00
Mark Qvist 04f009f57c Updated manual 2023-01-13 12:00:07 +01:00
Mark Qvist 78253093c7 Updated rnodeconf 2023-01-13 11:59:38 +01:00
Mark Qvist 63d54dbecb Added console image flashing to rnodeconf 2023-01-11 13:56:41 +01:00
Mark Qvist 32922868b9 Updated rnodeconf install guide 2023-01-11 11:45:10 +01:00
Mark Qvist e18f6d2969 Updated screenshots 2023-01-08 01:04:49 +01:00
Mark Qvist 08f4462ef8 Updated roadmap 2023-01-04 17:43:29 +01:00
Mark Qvist 7ed0726feb Updated documentation Getting Started section 2023-01-01 18:49:13 +01:00
Mark Qvist 2839d39350 Updated documentation images 2023-01-01 18:48:15 +01:00
Mark Qvist c992573257 Updated roadmap 2023-01-01 17:04:20 +01:00
Mark Qvist d64e547436 Updated roadmap 2022-12-29 15:18:10 +01:00
Mark Qvist 7eb0e03cb9 Updated roadmap 2022-12-29 15:17:00 +01:00
Mark Qvist f1deef696b Updated roadmap 2022-12-29 14:48:38 +01:00
Mark Qvist 48e14902d0 Updated roadmap 2022-12-29 14:42:45 +01:00
Mark Qvist 8acf63a195 Updated changelog 2022-12-29 14:40:48 +01:00
Mark Qvist 392bd65322 Added changelog 2022-12-29 14:35:55 +01:00
Mark Qvist 4ab3074d30 Updated roadmap 2022-12-29 14:33:08 +01:00
Mark Qvist 4de612e2fb Added release history to change log 2022-12-29 14:15:05 +01:00
Mark Qvist 3b192bfb47 Updated roadmap 2022-12-29 14:10:50 +01:00
Mark Qvist 0d562c89a7 Updated roadmap 2022-12-29 14:10:21 +01:00
Mark Qvist 972922fff1 Updated roadmap 2022-12-29 14:09:47 +01:00
Mark Qvist 296a2d91e8 Updated roadmap 2022-12-29 14:06:28 +01:00
Mark Qvist 446fb79786 Updated roadmap 2022-12-29 14:04:11 +01:00
Mark Qvist 700601d63e Updated documentation and manual 2022-12-23 23:32:38 +01:00
Mark Qvist 274c7199b0 Updated version 2022-12-23 23:27:37 +01:00
Mark Qvist 7960226883 Fixed missing path invalidation on failed link establishments made from a shared instance client 2022-12-23 23:26:50 +01:00
Mark Qvist bb74878e94 Reordered property assignment 2022-12-23 23:24:26 +01:00
Mark Qvist 549d22be68 Updated documentation and manual 2022-12-22 21:13:44 +01:00
Mark Qvist 5c2c935b6f Updated version 2022-12-22 21:08:02 +01:00
Mark Qvist 8402541c73 Faster roaming path recovery for multiple interface non-transport instances 2022-12-22 20:17:09 +01:00
Mark Qvist c34c268a6a Added carrier change detection flag to AutoInterface 2022-12-22 18:20:34 +01:00
Mark Qvist 8fcdc4613c Adjusted loglevels 2022-12-22 18:20:13 +01:00
Mark Qvist f645fa569b Fixed AutoInterface multicast echoes failing on interfaces with rolling MAC addresses on every re-connect 2022-12-22 17:46:46 +01:00
Mark Qvist 469947dab9 Updated manual 2022-12-22 15:49:47 +01:00
Mark Qvist 2386fc3635 Updated documentation and manual 2022-12-22 15:11:53 +01:00
Mark Qvist e9e98a00c2 Updated version 2022-12-22 15:07:36 +01:00
Mark Qvist b305eb8e0a Improved path response handling. Prepared destination path response handling for multi-path Transport. 2022-12-22 11:28:56 +01:00
Mark Qvist dd7931d421 Added signal quality stats to announce log output 2022-12-22 11:26:59 +01:00
Mark Qvist 191dce1301 Updated manual 2022-12-20 21:13:23 +01:00
Mark Qvist 3b5a27ba60 Updated readme 2022-12-20 21:08:08 +01:00
Mark Qvist 3c91f7f18b Updated documentation 2022-12-20 20:57:49 +01:00
Mark Qvist 171457713b Improved RNode hotplug over Bluetooth on Android 2022-12-20 15:17:46 +01:00
Mark Qvist 67ee8d6aab Added originator check to path rediscovery on failed links 2022-12-19 01:31:00 +01:00
Mark Qvist 13fa7d49d9 Added automatic path rediscovery on failed link establishments 2022-12-19 01:15:49 +01:00
Mark Qvist 66d921e669 Improved resource advertisement retry handling 2022-12-19 01:10:34 +01:00
Mark Qvist 85f60ea04e Added check for already transferring resource to Link class 2022-12-19 01:04:49 +01:00
Mark Qvist 4870e741f6 Added link request proof signature validation for every transport hop 2022-12-18 21:27:14 +01:00
Mark Qvist f71c1986af Added Heltec USB issue notice to autoinstaller 2022-12-16 23:34:31 +01:00
Mark Qvist 30d8e351dd Updated version 2022-12-16 23:21:22 +01:00
Mark Qvist 5e62e3bc22 Merge branch 'master' of https://git.unsigned.io/markqvist/Reticulum 2022-12-15 21:17:16 +01:00
Mark Qvist 1a67e276ad Updated broken link. Fixes #174. Thanks @mkinney! 2022-12-15 21:16:20 +01:00
Mark Qvist df37a4a884 Updated broken link 2022-12-15 21:15:47 +01:00
Mark Qvist d26bbbd59f Merge branch 'master' of https://git.unsigned.io/markqvist/Reticulum 2022-12-15 17:14:15 +01:00
Mark Qvist 2a264fa7d6 Fixed invalid driver proxy for Qinheng CH34x chips on Android 2022-12-15 17:14:09 +01:00
Mark Qvist d5e0a461cf Fixed invalid check for None 2022-11-25 00:42:22 +01:00
Mark Qvist e28dbd4afa Updated manual 2022-11-24 17:48:04 +01:00
Mark Qvist 8626dcd69f Updated roadmap 2022-11-24 17:30:01 +01:00
Mark Qvist e34f21f4dc Updated roadmap 2022-11-24 17:29:25 +01:00
Mark Qvist f692e81b8e Fixed AutoInterface roaming on Android devices that rotate Ethernet/WiFi MAC addresses on reconnect 2022-11-24 17:19:01 +01:00
Mark Qvist 28e43b52f9 Updated manual 2022-11-24 17:16:43 +01:00
Mark Qvist 680d17fb98 Improved startup time for instances and programs connected to a shared instance 2022-11-24 13:28:22 +01:00
Mark Qvist 1e477c976c Updated documentation 2022-11-24 12:32:43 +01:00
Mark Qvist ab301cdb79 Updated version 2022-11-24 10:45:45 +01:00
Mark Qvist cecb4b3acb Fixed buffered input stream reader not working on Android API levels < 30 2022-11-23 20:39:49 +01:00
Mark Qvist de53a105a4 Improved time pretty-print function 2022-11-23 17:15:46 +01:00
Mark Qvist 9e4ae3c6fe Updated roadmap 2022-11-22 20:20:23 +01:00
Mark Qvist 3482d84bc0 Updated manual 2022-11-17 18:19:42 +01:00
Mark Qvist 51c5c85fcd Updated readme 2022-11-17 16:51:59 +01:00
Mark Qvist 57aeab43a2 Updated readme and roadmap 2022-11-17 12:39:09 +01:00
Mark Qvist 92cccddaab Updated readme and roadmap 2022-11-17 12:36:41 +01:00
Mark Qvist 3de182192a Updated readme and roadmap 2022-11-17 12:35:21 +01:00
Mark Qvist aca6b0c110 Added roadmap 2022-11-17 12:25:48 +01:00
Mark Qvist 3d6e7a9597 Updated docs 2022-11-14 11:25:47 +01:00
markqvist 21da55dd39 Merge pull request #154 from thatv/master
Fixed Hop-number in docs
2022-11-14 11:23:10 +01:00
thatv 9e664af1c6 Update understanding.html 2022-11-12 21:37:27 +01:00
Mark Qvist 7736ed589e Updated manual 2022-11-03 23:08:37 +01:00
Mark Qvist f22504d080 Improved I2P recovery time on unresponsive tunnels 2022-11-03 22:47:08 +01:00
Mark Qvist f22e5cc200 Fixed socket references. Closes #146. 2022-11-03 19:51:04 +01:00
Mark Qvist 87b73b6c67 Updated docs 2022-11-03 19:48:39 +01:00
Mark Qvist 36906f6567 Updated version 2022-11-03 18:05:13 +01:00
Mark Qvist 52edb54d21 Updated readme 2022-11-03 18:05:04 +01:00
Mark Qvist 88b88b9b64 Fixed missing check for socket state 2022-11-03 18:03:00 +01:00
Mark Qvist 76fcad0b53 Added better I2P state visibility to rnstatus util 2022-11-03 17:49:25 +01:00
Mark Qvist 01e520b082 Adjusted I2P interface timings 2022-11-03 16:30:07 +01:00
Mark Qvist 1d2a0fe4c8 Improved I2P tunnel state detection. Fixed missing IFAC init on spawned I2P interfaces. 2022-11-03 15:22:34 +01:00
Mark Qvist 0f19ced9d3 Fixed missing IFAC identity init on spawned TCP clients. Closes #137. 2022-11-03 14:16:00 +01:00
281 changed files with 72772 additions and 23176 deletions
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: ✨ Feature Request or Idea
url: https://github.com/markqvist/Reticulum/discussions/new?category=ideas
about: Propose and discuss features and ideas
- name: 💬 Questions, Help & Discussion
about: Ask anything, or get help
url: https://github.com/markqvist/Reticulum/discussions/new/choose
- name: 📖 Read the Reticulum Manual
url: https://markqvist.github.io/Reticulum/manual/
about: The complete documentation for Reticulum
+40
View File
@@ -0,0 +1,40 @@
---
name: "\U0001F41B Bug Report"
about: Report a reproducible bug
title: ''
labels: ''
assignees: ''
---
**Read the Contribution Guidelines**
Before creating a bug report on this issue tracker, you **must** read the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md). Issues that do not follow the contribution guidelines **will be deleted without comment**.
- The issue tracker is used by developers of this project. **Do not use it to ask general questions, or for support requests**.
- Ideas and feature requests can be made on the [Discussions](https://github.com/markqvist/Reticulum/discussions). **Only** feature requests accepted by maintainers and developers are tracked and included on the issue tracker. **Do not post feature requests here**.
- Do not submit code written using large language models (LLMs) or other generative 'AI' programs (see the [Generative AI Policy](/Contributing.md#generative-ai-policy) for details).
- After reading the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md), **delete this section only** (*"Read the Contribution Guidelines"*) from your bug report, **and fill in all the other sections**.
**Describe the Bug**
First of all: Is this really a bug? Is it reproducible?
If this is a request for help because something is not working as you expected, stop right here, and go to the [discussions](https://github.com/markqvist/Reticulum/discussions) instead, where you can post your questions and get help from other users.
If this really is a bug or issue with the software, remove this section of the template, and provide **a clear and concise description of what the bug is**.
**To Reproduce**
Describe in detail how to reproduce the bug.
**Expected Behavior**
A clear and concise description of what you expected to happen.
**Logs & Screenshots**
Please include any relevant log output. If applicable, also add screenshots to help explain your problem. In most cases, without any relevant log output, we will not be able to determine the cause of the bug, or reproduce it.
**System Information**
- OS and version
- Python version
- Program version
**Additional context**
Add any other context about the problem here.
+98
View File
@@ -0,0 +1,98 @@
name: Build Reticulum
on:
push:
branches:
- '*'
tags:
- "[0-9]+.[0-9]+.[0-9]+*"
pull_request:
branches:
- master
paths-ignore:
- .gitignore
- LICENSE
permissions:
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
- run: |
python -m pip install -q cryptography
make test
package:
needs: test
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
environment: ${{ contains(github.ref, '-') && 'development' || 'production' }}
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
- run: |
python -m pip install -q build wheel setuptools
make remove_symlinks
make build_wheel
make build_pure_wheel
make create_symlinks
- uses: actions/upload-artifact@v4
with:
name: package
path: dist/*.whl
# documentation:
# needs: test
# if: startsWith(github.ref, 'refs/tags/')
# runs-on: ubuntu-latest
# environment: ${{ contains(github.ref, '-') && 'development' || 'production' }}
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-python@v5
# with:
# python-version: 3.x
# - run: |
# sudo apt-get -qq update && sudo apt-get -qq install latexmk texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended
# python -m pip -q install sphinx sphinx-copybutton
# cd docs && make latexpdf && make epub
# - uses: actions/upload-artifact@v4
# with:
# name: documentation
# path: |
# docs/build/latex/*.pdf
# docs/build/epub/*.epub
release:
needs: [package]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
environment: ${{ contains(github.ref, '-') && 'development' || 'production' }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: .artifacts
- uses: softprops/action-gh-release@v2
with:
files: |
# .artifacts/package/**.whl
# .artifacts/documentation/latex/reticulumnetworkstack.pdf
# .artifacts/documentation/epub/ReticulumNetworkStack.epub
draft: true
generate_release_notes: false
prerelease: ${{ contains(github.ref, '-') }}
fail_on_unmatched_files: true
+2
View File
@@ -10,5 +10,7 @@ docs/build
rns*.egg-info
profile.data
tests/rnsconfig/storage
tests/rnsconfig/logfile*
*.data
*.result
.buildinfo.bak
+26
View File
@@ -0,0 +1,26 @@
compiling = False
noticed = False
notice_delay = 0.3
import time
import sys
import threading
from importlib.util import find_spec
if find_spec("pyximport") and find_spec("cython"):
import pyximport; pyxloader = pyximport.install(pyimport=True, language_level=3)[1]
def notice_job():
global noticed
started = time.time()
while compiling:
if time.time() > started+notice_delay and compiling:
noticed = True
print("Compiling RNS object code... ", end="")
sys.stdout.flush()
break
time.sleep(0.1)
compiling = True
threading.Thread(target=notice_job, daemon=True).start()
import RNS; compiling = False
if noticed: print("Done."); sys.stdout.flush()
+2131
View File
File diff suppressed because it is too large Load Diff
+58
View File
@@ -0,0 +1,58 @@
# Contributing to Reticulum
Welcome, and thank you for your interest in contributing to Reticulum!
Apart from writing code, there are many ways in which you can contribute. Before interacting with this community, read these short and simple guidelines.
## Expected Conduct
First and foremost, there is one simple requirement for taking part in this community: While we primarily interact virtually, your actions matter and have real consequences. Therefore: **Act like a responsible, civilized person** - especially in the face of disputes and heated disagreements. Speak your mind here; discussions are welcome. Just do so in the spirit of being face-to-face with everyone else. Thank you.
In order to keep the discussion forums and issue trackers navigable and useful, the following types of posts will be deleted without notice:
- Spam.
- Questions that have already been adequately answered elsewhere. Use the search function.
- Low-effort posts or comments that contain no actual information or useful content. This is not a tea-house.
- Post or comments solely containing personal opinions or beliefs without adding anything to the discussion. Facebook and X exists.
- Content that simply waste the developer's / maintainer's time with completely obvious "ideas", "insights" or "recommendations". Yes, we have *at least* 8 neurons ourselves.
- Posts that fail to understand that developing a highly complex software project with a very small amount of resources and people takes time. Imagining perfection on our behalf is useless.
If you're new to the community and start out your engagement with any of the above transgressions, you will simply be banned without notice or explanation, and your post will be deleted.
If you find this "harsh", "unfair" or "unwelcoming", go somewhere else. This is not social club, but a work environment for the people contributing to the project.
## Asking Questions
If you want to ask a question, **do not open an issue**. The issue tracker is used by people *working on Reticulum* to track bugs, issues and improvements. Instead, ask away on the [discussions](https://github.com/markqvist/Reticulum/discussions).
Do not post feature requests or general ideas on the issue tracker, or in direct messages to the primary developers. You are much more likely to get a response and start a constructive discussion by posting your ideas in the public channels created for these purposes.
## Reporting Issues
If you have found a bug or issue in this project, please report it using the [issue tracker](https://github.com/markqvist/Reticulum/issues). Be sure to include details on how to reproduce the bug.
Anything submitted to the issue tracker that does not follow these guidelines will be closed and removed without comments or explanation.
## Writing Code
If you are interested in contributing code, fixing open issues or adding features, please coordinate the effort with the maintainer or one of the main developers **before** submitting a pull request. Before deciding to contribute, it is also a good idea to ensure your efforts are in alignment with the [Roadmap](./Roadmap.md) and current development focus.
Pull requests have a high chance of being accepted if they are:
- In alignment with the [Roadmap](./Roadmap.md) or solve an open issue or feature request
- Sufficiently tested to work with all API functions, and pass the standard test suite
- Functionally and conceptually complete and well-designed
- Not simply formatting or code style changes
- Well-documented
Even new ideas and proposals that have not been approved by a maintainer, or fall outside the established roadmap, are *occasionally* accepted - if they possess the remaining of the above qualities. If not, they will be closed and removed without comments or explanation.
## Generative AI Policy
Contributions written using large language models (LLMs) or other generative 'AI' programs are prohibited. LLMs produce errors so frequently and in a way that is so unlike human error that such issues are incredibly time-consuming to spot and fix. This is not a worthwhile tradeoff for Reticulum.
This applies to all Reticulum-related projects and documentation, as well as all submitted issues and discussion in official channels, except in cases where language translation and/or speech recogntion technologies are required for communication.
## Contributor License Agreement
By contributing code to this project, you agree that copyright for the code is transferred to the Reticulum maintainers and that the code is irrevocably placed under the [Reticulum License](./LICENSE).
+4 -3
View File
@@ -6,6 +6,7 @@
import argparse
import random
import sys
import RNS
# Let's define an app name. We'll use this for all
@@ -32,7 +33,7 @@ def program_setup(configpath):
# Destinations are endpoints in Reticulum, that can be addressed
# and communicated with. Destinations can also announce their
# existence, which will let the network know they are reachable
# and autoomatically create paths to them, from anywhere else
# and automatically create paths to them, from anywhere else
# in the network.
destination_1 = RNS.Destination(
identity,
@@ -53,7 +54,7 @@ def program_setup(configpath):
)
# We configure the destinations to automatically prove all
# packets adressed to it. By doing this, RNS will automatically
# packets addressed to it. By doing this, RNS will automatically
# generate a proof for each incoming packet and transmit it
# back to the sender of that packet. This will let anyone that
# tries to communicate with the destination know whether their
@@ -168,4 +169,4 @@ if __name__ == "__main__":
except KeyboardInterrupt:
print("")
exit()
sys.exit(0)
+1 -1
View File
@@ -118,4 +118,4 @@ if __name__ == "__main__":
except KeyboardInterrupt:
print("")
exit()
sys.exit(0)
+322
View File
@@ -0,0 +1,322 @@
##########################################################
# This RNS example demonstrates how to set up a link to #
# a destination, and pass binary data over it using a #
# channel buffer. #
##########################################################
from __future__ import annotations
import os
import sys
import time
import argparse
from datetime import datetime
import RNS
from RNS.vendor import umsgpack
# Let's define an app name. We'll use this for all
# destinations we create. Since this echo example
# is part of a range of example utilities, we'll put
# them all within the app namespace "example_utilities"
APP_NAME = "example_utilities"
##########################################################
#### Server Part #########################################
##########################################################
# A reference to the latest client link that connected
latest_client_link = None
# A reference to the latest buffer object
latest_buffer = None
# This initialisation is executed when the users chooses
# to run as a server
def server(configpath):
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our example
server_identity = RNS.Identity()
# We create a destination that clients can connect to. We
# want clients to create links to this destination, so we
# need to create a "single" destination type.
server_destination = RNS.Destination(
server_identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
APP_NAME,
"bufferexample"
)
# We configure a function that will get called every time
# a new client creates a link to this destination.
server_destination.set_link_established_callback(client_connected)
# Everything's ready!
# Let's Wait for client requests or user input
server_loop(server_destination)
def server_loop(destination):
# Let the user know that everything is ready
RNS.log(
"Link buffer example "+
RNS.prettyhexrep(destination.hash)+
" running, waiting for a connection."
)
RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
# We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server
# destination on the network, which will let clients
# know how to create messages directed towards it.
while True:
entered = input()
destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
# When a client establishes a link to our server
# destination, this function will be called with
# a reference to the link.
def client_connected(link):
global latest_client_link, latest_buffer
latest_client_link = link
RNS.log("Client connected")
link.set_link_closed_callback(client_disconnected)
# If a new connection is received, the old reader
# needs to be disconnected.
if latest_buffer:
latest_buffer.close()
# Create buffer objects.
# The stream_id parameter to these functions is
# a bit like a file descriptor, except that it
# is unique to the *receiver*.
#
# In this example, both the reader and the writer
# use stream_id = 0, but there are actually two
# separate unidirectional streams flowing in
# opposite directions.
#
channel = link.get_channel()
latest_buffer = RNS.Buffer.create_bidirectional_buffer(0, 0, channel, server_buffer_ready)
def client_disconnected(link):
RNS.log("Client disconnected")
def server_buffer_ready(ready_bytes: int):
"""
Callback from buffer when buffer has data available
:param ready_bytes: The number of bytes ready to read
"""
global latest_buffer
data = latest_buffer.read(ready_bytes)
data = data.decode("utf-8")
RNS.log("Received data over the buffer: " + data)
reply_message = "I received \""+data+"\" over the buffer"
reply_message = reply_message.encode("utf-8")
latest_buffer.write(reply_message)
latest_buffer.flush()
##########################################################
#### Client Part #########################################
##########################################################
# A reference to the server link
server_link = None
# A reference to the buffer object, needed to share the
# object from the link connected callback to the client
# loop.
buffer = None
# This initialisation is executed when the users chooses
# to run as a client
def client(destination_hexhash, configpath):
# We need a binary representation of the destination
# hash that was entered on the command line
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError(
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
)
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
sys.exit(0)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Check if we know a path to the destination
if not RNS.Transport.has_path(destination_hash):
RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
RNS.Transport.request_path(destination_hash)
while not RNS.Transport.has_path(destination_hash):
time.sleep(0.1)
# Recall the server identity
server_identity = RNS.Identity.recall(destination_hash)
# Inform the user that we'll begin connecting
RNS.log("Establishing link with server...")
# When the server identity is known, we set
# up a destination
server_destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
APP_NAME,
"bufferexample"
)
# And create a link
link = RNS.Link(server_destination)
# We'll also set up functions to inform the
# user when the link is established or closed
link.set_link_established_callback(link_established)
link.set_link_closed_callback(link_closed)
# Everything is set up, so let's enter a loop
# for the user to interact with the example
client_loop()
def client_loop():
global server_link
# Wait for the link to become active
while not server_link:
time.sleep(0.1)
should_quit = False
while not should_quit:
try:
print("> ", end=" ")
text = input()
# Check if we should quit the example
if text == "quit" or text == "q" or text == "exit":
should_quit = True
server_link.teardown()
else:
# Otherwise, encode the text and write it to the buffer.
text = text.encode("utf-8")
buffer.write(text)
# Flush the buffer to force the data to be sent.
buffer.flush()
except Exception as e:
RNS.log("Error while sending data over the link buffer: "+str(e))
should_quit = True
server_link.teardown()
# This function is called when a link
# has been established with the server
def link_established(link):
# We store a reference to the link
# instance for later use
global server_link, buffer
server_link = link
# Create buffer, see server_client_connected() for
# more detail about setting up the buffer.
channel = link.get_channel()
buffer = RNS.Buffer.create_bidirectional_buffer(0, 0, channel, client_buffer_ready)
# Inform the user that the server is
# connected
RNS.log("Link established with server, enter some text to send, or \"quit\" to quit")
# When a link is closed, we'll inform the
# user, and exit the program
def link_closed(link):
if link.teardown_reason == RNS.Link.TIMEOUT:
RNS.log("The link timed out, exiting now")
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
RNS.log("The link was closed by the server, exiting now")
else:
RNS.log("Link closed, exiting now")
time.sleep(1.5)
sys.exit(0)
# When the buffer has new data, read it and write it to the terminal.
def client_buffer_ready(ready_bytes: int):
global buffer
data = buffer.read(ready_bytes)
RNS.log("Received data over the link buffer: " + data.decode("utf-8"))
print("> ", end=" ")
sys.stdout.flush()
##########################################################
#### Program Startup #####################################
##########################################################
# This part of the program runs at startup,
# and parses input of from the user, and then
# starts up the desired program mode.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="Simple buffer example")
parser.add_argument(
"-s",
"--server",
action="store_true",
help="wait for incoming link requests from clients"
)
parser.add_argument(
"--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
parser.add_argument(
"destination",
nargs="?",
default=None,
help="hexadecimal hash of the server destination",
type=str
)
args = parser.parse_args()
if args.config:
configarg = args.config
else:
configarg = None
if args.server:
server(configarg)
else:
if (args.destination == None):
print("")
parser.print_help()
print("")
else:
client(args.destination, configarg)
except KeyboardInterrupt:
print("")
sys.exit(0)
+389
View File
@@ -0,0 +1,389 @@
##########################################################
# This RNS example demonstrates how to set up a link to #
# a destination, and pass structured messages over it #
# using a channel. #
##########################################################
import os
import sys
import time
import argparse
from datetime import datetime
import RNS
from RNS.vendor import umsgpack
# Let's define an app name. We'll use this for all
# destinations we create. Since this echo example
# is part of a range of example utilities, we'll put
# them all within the app namespace "example_utilities"
APP_NAME = "example_utilities"
##########################################################
#### Shared Objects ######################################
##########################################################
# Channel data must be structured in a subclass of
# MessageBase. This ensures that the channel will be able
# to serialize and deserialize the object and multiplex it
# with other objects. Both ends of a link will need the
# same object definitions to be able to communicate over
# a channel.
#
# Note: The objects we wish to use over the channel must
# be registered with the channel, and each link has a
# different channel instance. See the client_connected
# and link_established functions in this example to see
# how message types are registered.
# Let's make a simple message class called StringMessage
# that will convey a string with a timestamp.
class StringMessage(RNS.MessageBase):
# The MSGTYPE class variable needs to be assigned a
# 2 byte integer value. This identifier allows the
# channel to look up your message's constructor when a
# message arrives over the channel.
#
# MSGTYPE must be unique across all message types we
# register with the channel. MSGTYPEs >= 0xf000 are
# reserved for the system.
MSGTYPE = 0x0101
# The constructor of our object must be callable with
# no arguments. We can have parameters, but they must
# have a default assignment.
#
# This is needed so the channel can create an empty
# version of our message into which the incoming
# message can be unpacked.
def __init__(self, data=None):
self.data = data
self.timestamp = datetime.now()
# Finally, our message needs to implement functions
# the channel can call to pack and unpack our message
# to/from the raw packet payload. We'll use the
# umsgpack package bundled with RNS. We could also use
# the struct package bundled with Python if we wanted
# more control over the structure of the packed bytes.
#
# Also note that packed message objects must fit
# entirely in one packet. The number of bytes
# available for message payloads can be queried from
# the channel using the Channel.MDU property. The
# channel MDU is slightly less than the link MDU due
# to encoding the message header.
# The pack function encodes the message contents into
# a byte stream.
def pack(self) -> bytes:
return umsgpack.packb((self.data, self.timestamp))
# And the unpack function decodes a byte stream into
# the message contents.
def unpack(self, raw):
self.data, self.timestamp = umsgpack.unpackb(raw)
##########################################################
#### Server Part #########################################
##########################################################
# A reference to the latest client link that connected
latest_client_link = None
# This initialisation is executed when the users chooses
# to run as a server
def server(configpath):
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our link example
server_identity = RNS.Identity()
# We create a destination that clients can connect to. We
# want clients to create links to this destination, so we
# need to create a "single" destination type.
server_destination = RNS.Destination(
server_identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
APP_NAME,
"channelexample"
)
# We configure a function that will get called every time
# a new client creates a link to this destination.
server_destination.set_link_established_callback(client_connected)
# Everything's ready!
# Let's Wait for client requests or user input
server_loop(server_destination)
def server_loop(destination):
# Let the user know that everything is ready
RNS.log(
"Channel example "+
RNS.prettyhexrep(destination.hash)+
" running, waiting for a connection."
)
RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
# We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server
# destination on the network, which will let clients
# know how to create messages directed towards it.
while True:
entered = input()
destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
# When a client establishes a link to our server
# destination, this function will be called with
# a reference to the link.
def client_connected(link):
global latest_client_link
latest_client_link = link
RNS.log("Client connected")
link.set_link_closed_callback(client_disconnected)
# Register message types and add callback to channel
channel = link.get_channel()
channel.register_message_type(StringMessage)
channel.add_message_handler(server_message_received)
def client_disconnected(link):
RNS.log("Client disconnected")
def server_message_received(message):
"""
A message handler
@param message: An instance of a subclass of MessageBase
@return: True if message was handled
"""
global latest_client_link
# When a message is received over any active link,
# the replies will all be directed to the last client
# that connected.
# In a message handler, any deserializable message
# that arrives over the link's channel will be passed
# to all message handlers, unless a preceding handler indicates it
# has handled the message.
#
#
if isinstance(message, StringMessage):
RNS.log("Received data on the link: " + message.data + " (message created at " + str(message.timestamp) + ")")
reply_message = StringMessage("I received \""+message.data+"\" over the link")
latest_client_link.get_channel().send(reply_message)
# Incoming messages are sent to each message
# handler added to the channel, in the order they
# were added.
# If any message handler returns True, the message
# is considered handled and any subsequent
# handlers are skipped.
return True
##########################################################
#### Client Part #########################################
##########################################################
# A reference to the server link
server_link = None
# This initialisation is executed when the users chooses
# to run as a client
def client(destination_hexhash, configpath):
# We need a binary representation of the destination
# hash that was entered on the command line
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError(
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
)
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
sys.exit(0)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Check if we know a path to the destination
if not RNS.Transport.has_path(destination_hash):
RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
RNS.Transport.request_path(destination_hash)
while not RNS.Transport.has_path(destination_hash):
time.sleep(0.1)
# Recall the server identity
server_identity = RNS.Identity.recall(destination_hash)
# Inform the user that we'll begin connecting
RNS.log("Establishing link with server...")
# When the server identity is known, we set
# up a destination
server_destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
APP_NAME,
"channelexample"
)
# And create a link
link = RNS.Link(server_destination)
# We'll also set up functions to inform the
# user when the link is established or closed
link.set_link_established_callback(link_established)
link.set_link_closed_callback(link_closed)
# Everything is set up, so let's enter a loop
# for the user to interact with the example
client_loop()
def client_loop():
global server_link
# Wait for the link to become active
while not server_link:
time.sleep(0.1)
should_quit = False
while not should_quit:
try:
print("> ", end=" ")
text = input()
# Check if we should quit the example
if text == "quit" or text == "q" or text == "exit":
should_quit = True
server_link.teardown()
# If not, send the entered text over the link
if text != "":
message = StringMessage(text)
packed_size = len(message.pack())
channel = server_link.get_channel()
if channel.is_ready_to_send():
if packed_size <= channel.mdu:
channel.send(message)
else:
RNS.log(
"Cannot send this packet, the data size of "+
str(packed_size)+" bytes exceeds the link packet MDU of "+
str(channel.MDU)+" bytes",
RNS.LOG_ERROR
)
else:
RNS.log("Channel is not ready to send, please wait for " +
"pending messages to complete.", RNS.LOG_ERROR)
except Exception as e:
RNS.log("Error while sending data over the link: "+str(e))
should_quit = True
server_link.teardown()
# This function is called when a link
# has been established with the server
def link_established(link):
# We store a reference to the link
# instance for later use
global server_link
server_link = link
# Register messages and add handler to channel
channel = link.get_channel()
channel.register_message_type(StringMessage)
channel.add_message_handler(client_message_received)
# Inform the user that the server is
# connected
RNS.log("Link established with server, enter some text to send, or \"quit\" to quit")
# When a link is closed, we'll inform the
# user, and exit the program
def link_closed(link):
if link.teardown_reason == RNS.Link.TIMEOUT:
RNS.log("The link timed out, exiting now")
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
RNS.log("The link was closed by the server, exiting now")
else:
RNS.log("Link closed, exiting now")
time.sleep(1.5)
sys.exit(0)
# When a packet is received over the channel, we
# simply print out the data.
def client_message_received(message):
if isinstance(message, StringMessage):
RNS.log("Received data on the link: " + message.data + " (message created at " + str(message.timestamp) + ")")
print("> ", end=" ")
sys.stdout.flush()
##########################################################
#### Program Startup #####################################
##########################################################
# This part of the program runs at startup,
# and parses input of from the user, and then
# starts up the desired program mode.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="Simple channel example")
parser.add_argument(
"-s",
"--server",
action="store_true",
help="wait for incoming link requests from clients"
)
parser.add_argument(
"--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
parser.add_argument(
"destination",
nargs="?",
default=None,
help="hexadecimal hash of the server destination",
type=str
)
args = parser.parse_args()
if args.config:
configarg = args.config
else:
configarg = None
if args.server:
server(configarg)
else:
if (args.destination == None):
print("")
parser.print_help()
print("")
else:
client(args.destination, configarg)
except KeyboardInterrupt:
print("")
sys.exit(0)
+5 -3
View File
@@ -6,6 +6,7 @@
##########################################################
import argparse
import sys
import RNS
# Let's define an app name. We'll use this for all
@@ -46,7 +47,7 @@ def server(configpath):
)
# We configure the destination to automatically prove all
# packets adressed to it. By doing this, RNS will automatically
# packets addressed to it. By doing this, RNS will automatically
# generate a proof for each incoming packet and transmit it
# back to the sender of that packet.
echo_destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
@@ -130,7 +131,7 @@ def client(destination_hexhash, configpath, timeout=None):
except Exception as e:
RNS.log("Invalid destination entered. Check your input!")
RNS.log(str(e)+"\n")
exit()
sys.exit(0)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
@@ -210,6 +211,7 @@ def client(destination_hexhash, configpath, timeout=None):
# If we do not know this destination, tell the
# user to wait for an announce to arrive.
RNS.log("Destination is not yet known. Requesting path...")
RNS.log("Hit enter to manually retry once an announce is received.")
RNS.Transport.request_path(destination_hash)
# This function is called when our reply destination
@@ -327,4 +329,4 @@ if __name__ == "__main__":
client(args.destination, configarg, timeout=timeoutarg)
except KeyboardInterrupt:
print("")
exit()
sys.exit(0)
+297
View File
@@ -0,0 +1,297 @@
# This example illustrates creating a custom interface
# definition, that can be loaded and used by Reticulum at
# runtime. Any number of custom interfaces can be created
# and loaded. To use the interface place it in the folder
# ~/.reticulum/interfaces, and add an interface entry to
# your Reticulum configuration file similar to this:
# [[Example Custom Interface]]
# type = ExampleInterface
# enabled = no
# mode = gateway
# port = /dev/ttyUSB0
# speed = 115200
# databits = 8
# parity = none
# stopbits = 1
from time import sleep
import sys
import threading
import time
# This HDLC helper class is used by the interface
# to delimit and packetize data over the physical
# medium - in this case a serial connection.
class HDLC():
# This example interface packetizes data using
# simplified HDLC framing, similar to PPP
FLAG = 0x7E
ESC = 0x7D
ESC_MASK = 0x20
@staticmethod
def escape(data):
data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
return data
# Let's define our custom interface class. It must
# be a sub-class of the RNS "Interface" class.
class ExampleInterface(Interface):
# All interface classes must define a default
# IFAC size, used in IFAC setup when the user
# has not specified a custom IFAC size. This
# option is specified in bytes.
DEFAULT_IFAC_SIZE = 8
# The following properties are local to this
# particular interface implementation.
owner = None
port = None
speed = None
databits = None
parity = None
stopbits = None
serial = None
# All Reticulum interfaces must have an __init__
# method that takes 2 positional arguments:
# The owner RNS Transport instance, and a dict
# of configuration values.
def __init__(self, owner, configuration):
# The following lines demonstrate handling
# potential dependencies required for the
# interface to function correctly.
import importlib
if importlib.util.find_spec('serial') != None:
import serial
else:
RNS.log("Using this interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
RNS.panic()
# We start out by initialising the super-class
super().__init__()
# To make sure the configuration data is in the
# correct format, we parse it through the following
# method on the generic Interface class. This step
# is required to ensure compatibility on all the
# platforms that Reticulum supports.
ifconf = Interface.get_config_obj(configuration)
# Read the interface name from the configuration
# and set it on our interface instance.
name = ifconf["name"]
self.name = name
# We read configuration parameters from the supplied
# configuration data, and provide default values in
# case any are missing.
port = ifconf["port"] if "port" in ifconf else None
speed = int(ifconf["speed"]) if "speed" in ifconf else 9600
databits = int(ifconf["databits"]) if "databits" in ifconf else 8
parity = ifconf["parity"] if "parity" in ifconf else "N"
stopbits = int(ifconf["stopbits"]) if "stopbits" in ifconf else 1
# In case no port is specified, we abort setup by
# raising an exception.
if port == None:
raise ValueError(f"No port specified for {self}")
# All interfaces must supply a hardware MTU value
# to the RNS Transport instance. This value should
# be the maximum data packet payload size that the
# underlying medium is capable of handling in all
# cases without any segmentation.
self.HW_MTU = 564
# We initially set the "online" property to false,
# since the interface has not actually been fully
# initialised and connected yet.
self.online = False
# In this case, we can also set the indicated bit-
# rate of the interface to the serial port speed.
self.bitrate = speed
# Configure internal properties on the interface
# according to the supplied configuration.
self.pyserial = serial
self.serial = None
self.owner = owner
self.port = port
self.speed = speed
self.databits = databits
self.parity = serial.PARITY_NONE
self.stopbits = stopbits
self.timeout = 100
if parity.lower() == "e" or parity.lower() == "even":
self.parity = serial.PARITY_EVEN
if parity.lower() == "o" or parity.lower() == "odd":
self.parity = serial.PARITY_ODD
# Since all required parameters are now configured,
# we will try opening the serial port.
try:
self.open_port()
except Exception as e:
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
raise e
# If opening the port succeeded, run any post-open
# configuration required.
if self.serial.is_open:
self.configure_device()
else:
raise IOError("Could not open serial port")
# Open the serial port with supplied configuration
# parameters and store a reference to the open port.
def open_port(self):
RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE)
self.serial = self.pyserial.Serial(
port = self.port,
baudrate = self.speed,
bytesize = self.databits,
parity = self.parity,
stopbits = self.stopbits,
xonxoff = False,
rtscts = False,
timeout = 0,
inter_byte_timeout = None,
write_timeout = None,
dsrdtr = False,
)
# The only thing required after opening the port
# is to wait a small amount of time for the
# hardware to initialise and then start a thread
# that reads any incoming data from the device.
def configure_device(self):
sleep(0.5)
thread = threading.Thread(target=self.read_loop)
thread.daemon = True
thread.start()
self.online = True
RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
# This method will be called from our read-loop
# whenever a full packet has been received over
# the underlying medium.
def process_incoming(self, data):
# Update our received bytes counter
self.rxb += len(data)
# And send the data packet to the Transport
# instance for processing.
self.owner.inbound(data, self)
# The running Reticulum Transport instance will
# call this method on the interface whenever the
# interface must transmit a packet.
def process_outgoing(self,data):
if self.online:
# First, escape and packetize the data
# according to HDLC framing.
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
# Then write the framed data to the port
written = self.serial.write(data)
# Update the transmitted bytes counter
# and ensure that all data was written
self.txb += len(data)
if written != len(data):
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
# This read loop runs in a thread and continously
# receives bytes from the underlying serial port.
# When a full packet has been received, it will
# be sent to the process_incoming methed, which
# will in turn pass it to the Transport instance.
def read_loop(self):
try:
in_frame = False
escape = False
data_buffer = b""
last_read_ms = int(time.time()*1000)
while self.serial.is_open:
if self.serial.in_waiting:
byte = ord(self.serial.read(1))
last_read_ms = int(time.time()*1000)
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.process_incoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
elif (in_frame and len(data_buffer) < self.HW_MTU):
if (byte == HDLC.ESC):
escape = True
else:
if (escape):
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
byte = HDLC.FLAG
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
byte = HDLC.ESC
escape = False
data_buffer = data_buffer+bytes([byte])
else:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
data_buffer = b""
in_frame = False
escape = False
sleep(0.08)
except Exception as e:
self.online = False
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error:
RNS.panic()
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
self.online = False
self.serial.close()
self.reconnect_port()
# This method handles serial port disconnects.
def reconnect_port(self):
while not self.online:
try:
time.sleep(5)
RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
self.open_port()
if self.serial.is_open:
self.configure_device()
except Exception as e:
RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("Reconnected serial port for "+str(self))
# Signal to Reticulum that this interface should
# not perform any ingress limiting.
def should_ingress_limit(self):
return False
# We must provide a string representation of this
# interface, that is used whenever the interface
# is printed in logs or external programs.
def __str__(self):
return "ExampleInterface["+self.name+"]"
# Finally, register the defined interface class as the
# target class for Reticulum to use as an interface
interface_class = ExampleInterface
+5 -7
View File
@@ -224,7 +224,7 @@ def client(destination_hexhash, configpath):
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
exit()
sys.exit(0)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
@@ -449,8 +449,7 @@ def link_established(link):
# And set up a small job to check for
# a potential timeout in receiving the
# file list
thread = threading.Thread(target=filelist_timeout_job)
thread.setDaemon(True)
thread = threading.Thread(target=filelist_timeout_job, daemon=True)
thread.start()
# This job just sleeps for the specified
@@ -463,7 +462,7 @@ def filelist_timeout_job():
global server_files
if len(server_files) == 0:
RNS.log("Timed out waiting for filelist, exiting")
os._exit(0)
sys.exit(0)
# When a link is closed, we'll inform the
@@ -476,9 +475,8 @@ def link_closed(link):
else:
RNS.log("Link closed, exiting now")
RNS.Reticulum.exit_handler()
time.sleep(1.5)
os._exit(0)
sys.exit(0)
# When RNS detects that the download has
# started, we'll update our menu state
@@ -602,4 +600,4 @@ if __name__ == "__main__":
except KeyboardInterrupt:
print("")
exit()
sys.exit(0)
+3 -4
View File
@@ -133,7 +133,7 @@ def client(destination_hexhash, configpath):
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
exit()
sys.exit(0)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
@@ -245,9 +245,8 @@ def link_closed(link):
else:
RNS.log("Link closed, exiting now")
RNS.Reticulum.exit_handler()
time.sleep(1.5)
os._exit(0)
sys.exit(0)
# When a packet is received over the link, we
# simply print out the data.
@@ -311,4 +310,4 @@ if __name__ == "__main__":
except KeyboardInterrupt:
print("")
exit()
sys.exit(0)
+3 -4
View File
@@ -119,7 +119,7 @@ def client(destination_hexhash, configpath):
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
exit()
sys.exit(0)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
@@ -222,9 +222,8 @@ def link_closed(link):
else:
RNS.log("Link closed, exiting now")
RNS.Reticulum.exit_handler()
time.sleep(1.5)
os._exit(0)
sys.exit(0)
# When a packet is received over the link, we
# simply print out the data.
@@ -288,4 +287,4 @@ if __name__ == "__main__":
except KeyboardInterrupt:
print("")
exit()
sys.exit(0)
+4 -3
View File
@@ -5,6 +5,7 @@
##########################################################
import argparse
import sys
import RNS
# Let's define an app name. We'll use this for all
@@ -25,7 +26,7 @@ def program_setup(configpath):
# Destinations are endpoints in Reticulum, that can be addressed
# and communicated with. Destinations can also announce their
# existence, which will let the network know they are reachable
# and autoomatically create paths to them, from anywhere else
# and automatically create paths to them, from anywhere else
# in the network.
destination = RNS.Destination(
identity,
@@ -36,7 +37,7 @@ def program_setup(configpath):
)
# We configure the destination to automatically prove all
# packets adressed to it. By doing this, RNS will automatically
# packets addressed to it. By doing this, RNS will automatically
# generate a proof for each incoming packet and transmit it
# back to the sender of that packet. This will let anyone that
# tries to communicate with the destination know whether their
@@ -98,4 +99,4 @@ if __name__ == "__main__":
except KeyboardInterrupt:
print("")
exit()
sys.exit(0)
+341
View File
@@ -0,0 +1,341 @@
##########################################################
# This RNS example demonstrates a simple client/server #
# echo utility that uses ratchets to rotate encryption #
# keys everytime an announce is sent. #
##########################################################
import argparse
import sys
import RNS
# Let's define an app name. We'll use this for all
# destinations we create. Since this echo example
# is part of a range of example utilities, we'll put
# them all within the app namespace "example_utilities"
APP_NAME = "example_utilities"
##########################################################
#### Server Part #########################################
##########################################################
# This initialisation is executed when the users chooses
# to run as a server
def server(configpath):
global reticulum
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our echo server
server_identity = RNS.Identity()
# We create a destination that clients can query. We want
# to be able to verify echo replies to our clients, so we
# create a "single" destination that can receive encrypted
# messages. This way the client can send a request and be
# certain that no-one else than this destination was able
# to read it.
echo_destination = RNS.Destination(
server_identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
APP_NAME,
"ratchet",
"echo",
"request"
)
# Enable ratchets on the destination by providing a file
# path to store ratchets. In this example, we will just
# use a temporary file, but in real-world applications,
# it's extremely important to keep this file secure, since
# it contains encryption keys for the destination.
destination_hexhash = RNS.hexrep(echo_destination.hash, delimit=False)
echo_destination.enable_ratchets(f"/tmp/{destination_hexhash}.ratchets")
# We configure the destination to automatically prove all
# packets addressed to it. By doing this, RNS will automatically
# generate a proof for each incoming packet and transmit it
# back to the sender of that packet.
echo_destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
# Tell the destination which function in our program to
# run when a packet is received. We do this so we can
# print a log message when the server receives a request
echo_destination.set_packet_callback(server_callback)
# Everything's ready!
# Let's Wait for client requests or user input
announceLoop(echo_destination)
def announceLoop(destination):
# Let the user know that everything is ready
RNS.log(
"Ratcheted echo server "+
RNS.prettyhexrep(destination.hash)+
" running, hit enter to manually send an announce (Ctrl-C to quit)"
)
# We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server
# destination on the network, which will let clients
# know how to create messages directed towards it.
while True:
entered = input()
destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
def server_callback(message, packet):
global reticulum
# Tell the user that we received an echo request, and
# that we are going to send a reply to the requester.
# Sending the proof is handled automatically, since we
# set up the destination to prove all incoming packets.
reception_stats = ""
if reticulum.is_connected_to_shared_instance:
reception_rssi = reticulum.get_packet_rssi(packet.packet_hash)
reception_snr = reticulum.get_packet_snr(packet.packet_hash)
if reception_rssi != None:
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
if reception_snr != None:
reception_stats += " [SNR "+str(reception_snr)+" dBm]"
else:
if packet.rssi != None:
reception_stats += " [RSSI "+str(packet.rssi)+" dBm]"
if packet.snr != None:
reception_stats += " [SNR "+str(packet.snr)+" dB]"
RNS.log("Received packet from echo client, proof sent"+reception_stats)
##########################################################
#### Client Part #########################################
##########################################################
# This initialisation is executed when the users chooses
# to run as a client
def client(destination_hexhash, configpath, timeout=None):
global reticulum
# We need a binary representation of the destination
# hash that was entered on the command line
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError(
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
)
destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e:
RNS.log("Invalid destination entered. Check your input!")
RNS.log(str(e)+"\n")
sys.exit(0)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# We override the loglevel to provide feedback when
# an announce is received
if RNS.loglevel < RNS.LOG_INFO:
RNS.loglevel = RNS.LOG_INFO
# Tell the user that the client is ready!
RNS.log(
"Echo client ready, hit enter to send echo request to "+
destination_hexhash+
" (Ctrl-C to quit)"
)
# We enter a loop that runs until the user exits.
# If the user hits enter, we will try to send an
# echo request to the destination specified on the
# command line.
while True:
input()
# Let's first check if RNS knows a path to the destination.
# If it does, we'll load the server identity and create a packet
if RNS.Transport.has_path(destination_hash):
# To address the server, we need to know it's public
# key, so we check if Reticulum knows this destination.
# This is done by calling the "recall" method of the
# Identity module. If the destination is known, it will
# return an Identity instance that can be used in
# outgoing destinations.
server_identity = RNS.Identity.recall(destination_hash)
# We got the correct identity instance from the
# recall method, so let's create an outgoing
# destination. We use the naming convention:
# example_utilities.ratchet.echo.request
# This matches the naming we specified in the
# server part of the code.
request_destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
APP_NAME,
"ratchet",
"echo",
"request"
)
# The destination is ready, so let's create a packet.
# We set the destination to the request_destination
# that was just created, and the only data we add
# is a random hash.
echo_request = RNS.Packet(request_destination, RNS.Identity.get_random_hash())
# Send the packet! If the packet is successfully
# sent, it will return a PacketReceipt instance.
packet_receipt = echo_request.send()
# If the user specified a timeout, we set this
# timeout on the packet receipt, and configure
# a callback function, that will get called if
# the packet times out.
if timeout != None:
packet_receipt.set_timeout(timeout)
packet_receipt.set_timeout_callback(packet_timed_out)
# We can then set a delivery callback on the receipt.
# This will get automatically called when a proof for
# this specific packet is received from the destination.
packet_receipt.set_delivery_callback(packet_delivered)
# Tell the user that the echo request was sent
RNS.log("Sent echo request to "+RNS.prettyhexrep(request_destination.hash))
else:
# If we do not know this destination, tell the
# user to wait for an announce to arrive.
RNS.log("Destination is not yet known. Requesting path...")
RNS.log("Hit enter to manually retry once an announce is received.")
RNS.Transport.request_path(destination_hash)
# This function is called when our reply destination
# receives a proof packet.
def packet_delivered(receipt):
global reticulum
if receipt.status == RNS.PacketReceipt.DELIVERED:
rtt = receipt.get_rtt()
if (rtt >= 1):
rtt = round(rtt, 3)
rttstring = str(rtt)+" seconds"
else:
rtt = round(rtt*1000, 3)
rttstring = str(rtt)+" milliseconds"
reception_stats = ""
if reticulum.is_connected_to_shared_instance:
reception_rssi = reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
reception_snr = reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
if reception_rssi != None:
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
if reception_snr != None:
reception_stats += " [SNR "+str(reception_snr)+" dB]"
else:
if receipt.proof_packet != None:
if receipt.proof_packet.rssi != None:
reception_stats += " [RSSI "+str(receipt.proof_packet.rssi)+" dBm]"
if receipt.proof_packet.snr != None:
reception_stats += " [SNR "+str(receipt.proof_packet.snr)+" dB]"
RNS.log(
"Valid reply received from "+
RNS.prettyhexrep(receipt.destination.hash)+
", round-trip time is "+rttstring+
reception_stats
)
# This function is called if a packet times out.
def packet_timed_out(receipt):
if receipt.status == RNS.PacketReceipt.FAILED:
RNS.log("Packet "+RNS.prettyhexrep(receipt.hash)+" timed out")
##########################################################
#### Program Startup #####################################
##########################################################
# This part of the program gets run at startup,
# and parses input from the user, and then starts
# the desired program mode.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="Simple ratcheted echo server and client utility")
parser.add_argument(
"-s",
"--server",
action="store_true",
help="wait for incoming packets from clients"
)
parser.add_argument(
"-t",
"--timeout",
action="store",
metavar="s",
default=None,
help="set a reply timeout in seconds",
type=float
)
parser.add_argument("--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
parser.add_argument(
"destination",
nargs="?",
default=None,
help="hexadecimal hash of the server destination",
type=str
)
args = parser.parse_args()
if args.server:
configarg=None
if args.config:
configarg = args.config
server(configarg)
else:
if args.config:
configarg = args.config
else:
configarg = None
if args.timeout:
timeoutarg = float(args.timeout)
else:
timeoutarg = None
if (args.destination == None):
print("")
parser.print_help()
print("")
else:
client(args.destination, configarg, timeout=timeoutarg)
except KeyboardInterrupt:
print("")
sys.exit(0)
+7 -8
View File
@@ -1,6 +1,6 @@
##########################################################
# This RNS example demonstrates how to set perform #
# requests and receive responses over a link. #
# This RNS example demonstrates how to perform requests #
# and receive responses over a link. #
##########################################################
import os
@@ -23,8 +23,8 @@ APP_NAME = "example_utilities"
# A reference to the latest client link that connected
latest_client_link = None
def random_text_generator(path, data, request_id, remote_identity, requested_at):
RNS.log("Generating response to request "+RNS.prettyhexrep(request_id))
def random_text_generator(path, data, request_id, link_id, remote_identity, requested_at):
RNS.log("Generating response to request "+RNS.prettyhexrep(request_id)+" on link "+RNS.prettyhexrep(link_id))
texts = ["They looked up", "On each full moon", "Becky was upset", "Ill stay away from it", "The pet shop stocks everything"]
return texts[random.randint(0, len(texts)-1)]
@@ -119,7 +119,7 @@ def client(destination_hexhash, configpath):
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
exit()
sys.exit(0)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
@@ -226,9 +226,8 @@ def link_closed(link):
else:
RNS.log("Link closed, exiting now")
RNS.Reticulum.exit_handler()
time.sleep(1.5)
os._exit(0)
sys.exit(0)
##########################################################
@@ -284,4 +283,4 @@ if __name__ == "__main__":
except KeyboardInterrupt:
print("")
exit()
sys.exit(0)
+294
View File
@@ -0,0 +1,294 @@
##########################################################
# This RNS example demonstrates how to transfer a #
# resource over an established link #
##########################################################
import os
import sys
import time
import random
import argparse
import RNS
# Let's define an app name. We'll use this for all
# destinations we create. Since this echo example
# is part of a range of example utilities, we'll put
# them all within the app namespace "example_utilities"
APP_NAME = "example_utilities"
##########################################################
#### Server Part #########################################
##########################################################
# A reference to the latest client link that connected
latest_client_link = None
# This initialisation is executed when the users chooses
# to run as a server
def server(configpath):
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our link example
server_identity = RNS.Identity()
# We create a destination that clients can connect to. We
# want clients to create links to this destination, so we
# need to create a "single" destination type.
server_destination = RNS.Destination(
server_identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
APP_NAME,
"resourceexample"
)
# We configure a function that will get called every time
# a new client creates a link to this destination.
server_destination.set_link_established_callback(client_connected)
# Everything's ready!
# Let's Wait for client resources or user input
server_loop(server_destination)
def server_loop(destination):
# Let the user know that everything is ready
RNS.log(
"Resource example "+
RNS.prettyhexrep(destination.hash)+
" running, waiting for a connection."
)
RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
# We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server
# destination on the network, which will let clients
# know how to create messages directed towards it.
while True:
entered = input()
destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
# When a client establishes a link to our server
# destination, this function will be called with
# a reference to the link.
def client_connected(link):
global latest_client_link
RNS.log("Client connected")
# We configure the link to accept all resources
# and set a callback for completed resources
link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
link.set_resource_concluded_callback(resource_concluded)
link.set_link_closed_callback(client_disconnected)
latest_client_link = link
def client_disconnected(link):
RNS.log("Client disconnected")
def resource_concluded(resource):
if resource.status == RNS.Resource.COMPLETE:
RNS.log(f"Resource {resource} received")
RNS.log(f"Metadata: {resource.metadata}")
RNS.log(f"Data length: {os.stat(resource.data.name).st_size}")
RNS.log(f"Data can be read directly from: {resource.data}")
RNS.log(f"Data can be moved or copied from: {resource.data.name}")
RNS.log(f"First 32 bytes of data: {RNS.hexrep(resource.data.read(32))}")
else:
RNS.log(f"Receiving resource {resource} failed")
##########################################################
#### Client Part #########################################
##########################################################
# A reference to the server link
server_link = None
def random_text_generator():
texts = ["They looked up", "On each full moon", "Becky was upset", "Ill stay away from it", "The pet shop stocks everything"]
return texts[random.randint(0, len(texts)-1)]
# This initialisation is executed when the users chooses
# to run as a client
def client(destination_hexhash, configpath):
# We need a binary representation of the destination
# hash that was entered on the command line
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError(
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
)
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
sys.exit(0)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Check if we know a path to the destination
if not RNS.Transport.has_path(destination_hash):
RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
RNS.Transport.request_path(destination_hash)
while not RNS.Transport.has_path(destination_hash):
time.sleep(0.1)
# Recall the server identity
server_identity = RNS.Identity.recall(destination_hash)
# Inform the user that we'll begin connecting
RNS.log("Establishing link with server...")
# When the server identity is known, we set
# up a destination
server_destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
APP_NAME,
"resourceexample"
)
# And create a link
link = RNS.Link(server_destination)
# We'll set up functions to inform the
# user when the link is established or closed
link.set_link_established_callback(link_established)
link.set_link_closed_callback(link_closed)
# Everything is set up, so let's enter a loop
# for the user to interact with the example
client_loop()
def client_loop():
global server_link
# Wait for the link to become active
while not server_link:
time.sleep(0.1)
should_quit = False
while not should_quit:
try:
print("> ", end=" ")
text = input()
# Check if we should quit the example
if text == "quit" or text == "q" or text == "exit":
should_quit = True
server_link.teardown()
else:
# Generate 32 megabytes of random data
data = os.urandom(32*1024*1024)
RNS.log(f"Data length: {len(data)}")
RNS.log(f"First 32 bytes of data: {RNS.hexrep(data[:32])}")
# Generate some metadata
metadata = {"text": random_text_generator(), "numbers": [1,2,3,4], "blob": os.urandom(16)}
# Send the resource
resource = RNS.Resource(data, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False)
# Alternatively, you can stream data
# directly from an open file descriptor
# with open("/path/to/file", "rb") as data_file:
# resource = RNS.Resource(data_file, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False)
except Exception as e:
RNS.log("Error while sending resource over the link: "+str(e))
should_quit = True
server_link.teardown()
def resource_concluded_sending(resource):
if resource.status == RNS.Resource.COMPLETE: RNS.log(f"The resource {resource} was sent successfully")
else: RNS.log(f"Sending the resource {resource} failed")
# This function is called when a link
# has been established with the server
def link_established(link):
# We store a reference to the link
# instance for later use
global server_link
server_link = link
# Inform the user that the server is
# connected
RNS.log("Link established with server, hit enter to send a resource, or type in \"quit\" to quit")
# When a link is closed, we'll inform the
# user, and exit the program
def link_closed(link):
if link.teardown_reason == RNS.Link.TIMEOUT:
RNS.log("The link timed out, exiting now")
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
RNS.log("The link was closed by the server, exiting now")
else:
RNS.log("Link closed, exiting now")
time.sleep(1.5)
sys.exit(0)
##########################################################
#### Program Startup #####################################
##########################################################
# This part of the program runs at startup,
# and parses input of from the user, and then
# starts up the desired program mode.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="Simple resource example")
parser.add_argument(
"-s",
"--server",
action="store_true",
help="wait for incoming resources from clients"
)
parser.add_argument(
"--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
parser.add_argument(
"destination",
nargs="?",
default=None,
help="hexadecimal hash of the server destination",
type=str
)
args = parser.parse_args()
if args.config:
configarg = args.config
else:
configarg = None
if args.server:
server(configarg)
else:
if (args.destination == None):
print("")
parser.print_help()
print("")
else:
client(args.destination, configarg)
except KeyboardInterrupt:
print("")
sys.exit(0)
+10 -20
View File
@@ -149,8 +149,6 @@ def server_packet_received(message, packet):
time.sleep(0.2)
rc = 0
received_data = 0
# latest_client_link.teardown()
# os._exit(0)
##########################################################
@@ -159,6 +157,7 @@ def server_packet_received(message, packet):
# A reference to the server link
server_link = None
should_quit = False
# This initialisation is executed when the users chooses
# to run as a client
@@ -175,7 +174,7 @@ def client(destination_hexhash, configpath):
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
exit()
sys.exit(0)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
@@ -216,7 +215,7 @@ def client(destination_hexhash, configpath):
client_loop()
def client_loop():
global server_link
global server_link, should_quit
# Wait for the link to become active
while not server_link:
@@ -224,16 +223,7 @@ def client_loop():
should_quit = False
while not should_quit:
try:
text = input()
# Check if we should quit the example
if text == "quit" or text == "q" or text == "exit":
should_quit = True
server_link.teardown()
except Exception as e:
raise e
time.sleep(0.2)
# This function is called when a link
# has been established with the server
@@ -246,8 +236,8 @@ def link_established(link):
# Inform the user that the server is
# connected
RNS.log("Link established with server,sending...")
rd = os.urandom(RNS.Link.MDU)
RNS.log("Link established with server, sending...")
rd = os.urandom(link.mdu)
started = time.time()
while link.status == RNS.Link.ACTIVE and data_sent < data_cap*1.25:
RNS.Packet(server_link, rd, create_receipt=False).send()
@@ -276,17 +266,17 @@ def link_established(link):
# When a link is closed, we'll inform the
# user, and exit the program
def link_closed(link):
global should_quit
if link.teardown_reason == RNS.Link.TIMEOUT:
RNS.log("The link timed out, exiting now")
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
RNS.log("The link was closed by the server, exiting now")
else:
RNS.log("Link closed, exiting now")
RNS.Reticulum.exit_handler()
should_quit = True
time.sleep(1.5)
os._exit(0)
sys.exit(0)
def client_packet_received(message, packet):
pass
@@ -344,4 +334,4 @@ if __name__ == "__main__":
except KeyboardInterrupt:
print("")
exit()
sys.exit(0)
+7
View File
@@ -0,0 +1,7 @@
{
"drips": {
"ethereum": {
"ownedBy": "0xae89F3B94fC4AD6563F0864a55F9a697a90261ff"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
liberapay: Reticulum
ko_fi: markqvist
custom: "https://unsigned.io/donate"
+12 -4
View File
@@ -1,6 +1,6 @@
MIT License, unless otherwise noted
Reticulum License
Copyright (c) 2016-2022 Mark Qvist / unsigned.io
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
@@ -9,8 +9,16 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
- The Software shall not be used in any kind of system which includes amongst
its functions the ability to purposefully do harm to human beings.
- The Software shall not be used, directly or indirectly, in the creation of
an artificial intelligence, machine learning or language model training
dataset, including but not limited to any use that contributes to the
training or development of such a model or algorithm.
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+33
View File
@@ -0,0 +1,33 @@
This repository is a public mirror. All potential future development is happening elsewhere.
I am stepping back from all public-facing interaction with this project. Reticulum has always been primarily my work, and continuing in the current public, internet-facing model is no longer sustainable.
The software remains available for use as-is. Occasional updates may appear at unpredictable intervals, but there will be no support, no responses to issues, no discussions, and no community management in this or any other public venue. If it doesn't work for you, it doesn't work. That is the entire extent of available troubleshooting assistance I can offer you.
If you've followed this project for a while, you already know what this means. You know who designed, wrote and tested this, and you know how many years of my life it took. You'll also know about both my particular challenges and strengths, and how I believe anything worth building needs to be built and maintained with our own hands.
Seven months ago, I said I needed to step back, that I was exhausted, and that I needed to recover. I believed a public resolve would be enough to effectuate that, but while striving to get just a few more useful features and protocols out, the unproductive requests and demands also ramped up, and I got pulled back into the same patterns and draining interactions that I'd explicitly said I couldn't sustain anymore.
So here's what you might have already guessed: I'm done playing the game by rules I can't win at.
Everything you need is right here, and by any sensible measure, it's done. Anyone who wants to invest the time, skill and persistence can build on it, or completely re-imagine it with different priorities. That was always the point.
The people who actually contributed - you know who you are, and you know I mean it when I say: Thank you. All of you who've used this to build something real - that was the goal, and you did it without needing me to hold your hand.
The rest of you: You have what you need. Use it or don't. I am not going to be the person who explains it to you anymore.
This is not a temporary break. It's not "see you after some rest", but a recognition that the current model is fundamentally incompatible with my life, my health, and my reality.
If you want to support continued work, you can do so at the donation links listed in this repository. But please understand, that this is not purchasing support or guaranteeing updates. It is support for work that happens on my timeline, according to my capacity, which at the moment is not what it was.
If you want Reticulum to continue evolving, you have the power to make that happen. The protocol is public domain. The code is open source. Everything you need is right here. I've provided the tools, but building what comes next is not my responsibility anymore. It's yours.
To the small group of people who has actually been here, and understood what this work was and what it cost - you already know where to find me if it actually matters.
To everyone else: This is where we part ways. No hard feelings. It's just time.
---
असतो मा सद्गमय
तमसो मा ज्योतिर्गमय
मृत्योर्मा अमृतं गमय
+28 -10
View File
@@ -2,7 +2,7 @@ all: release
test:
@echo Running tests...
python -m tests.all
python3 -m tests.all
clean:
@echo Cleaning...
@@ -24,6 +24,13 @@ clean:
@make -C docs clean
@echo Done
purge_docs:
@echo Purging documentation build...
@-rm -rf ./docs/manual
@-rm -rf ./docs/markdown
@-rm -rf ./docs/*.pdf
@-rm -rf ./docs/*.epub
remove_symlinks:
@echo Removing symlinks for build...
-rm Examples/RNS
@@ -34,28 +41,39 @@ create_symlinks:
-ln -s ../RNS ./Examples/
-ln -s ../../RNS ./RNS/Utilities/
build_sdist_only:
build_sdist: purge_docs
python3 setup.py sdist
build_wheel:
python3 setup.py sdist bdist_wheel
python3 setup.py bdist_wheel
build_pure_wheel:
python3 setup.py sdist bdist_wheel --pure
python3 setup.py bdist_wheel --pure
documentation:
make -C docs html
make -C docs html markdown
manual:
make -C docs latexpdf
make -C docs latexpdf epub
release: test remove_symlinks build_wheel build_pure_wheel documentation manual create_symlinks
build_spkg: remove_symlinks build_sdist create_symlinks
release: test remove_symlinks build_sdist build_wheel build_pure_wheel documentation manual create_symlinks
debug: remove_symlinks build_wheel build_pure_wheel create_symlinks
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
+145 -134
View File
@@ -1,8 +1,12 @@
Reticulum Network Stack β <img align="right" src="https://static.pepy.tech/personalized-badge/rns?period=total&units=international_system&left_color=grey&right_color=blue&left_text=Installs"/>
Reticulum Network Stack <img align="right" src="https://static.pepy.tech/personalized-badge/rns?period=month&units=international_system&left_color=grey&right_color=blue&left_text=Installs/month" style="padding-left:10px"/><a href="https://github.com/markqvist/Reticulum/actions/workflows/build.yml"><img align="right" src="https://github.com/markqvist/Reticulum/actions/workflows/build.yml/badge.svg"/></a>
==========
<p align="center"><img width="200" src="https://raw.githubusercontent.com/markqvist/Reticulum/master/docs/source/graphics/rns_logo_512.png"></p>
*This repository is [a public mirror](./MIRROR.md). All development is happening elsewhere.*
To understand the foundational philosophy and goals of this system, read the [Zen of Reticulum](Zen%20of%20Reticulum.md).
Reticulum is the cryptography-based networking stack for building local and wide-area
networks with readily available hardware. It can operate even with very high latency
and extremely low bandwidth. Reticulum allows you to build wide-area networks
@@ -35,44 +39,75 @@ userland, and can run on practically any system that runs Python 3.
## Read The Manual
The full documentation for Reticulum is available at [markqvist.github.io/Reticulum/manual/](https://markqvist.github.io/Reticulum/manual/).
You can also [download the Reticulum manual as a PDF](https://github.com/markqvist/Reticulum/raw/master/docs/Reticulum%20Manual.pdf)
You can also download the [Reticulum manual as a PDF](https://github.com/markqvist/Reticulum/raw/master/docs/Reticulum%20Manual.pdf) or [as an e-book in EPUB format](https://github.com/markqvist/Reticulum/raw/master/docs/Reticulum%20Manual.epub).
For more info, see [unsigned.io/projects/reticulum](https://unsigned.io/projects/reticulum/)
For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ section of the wiki](https://github.com/markqvist/Reticulum/wiki/Frequently-Asked-Questions).
## Notable Features
- Coordination-less globally unique addressing and identification
- Fully self-configuring multi-hop routing
- 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
- Forward Secrecy with ephemeral Elliptic Curve Diffie-Hellman keys on Curve25519
- Reticulum uses the [Fernet](https://github.com/fernet/spec/blob/master/Spec.md) specification for on-the-wire / over-the-air encryption
- Keys are ephemeral and derived from an ECDH key exchange on Curve25519
- AES-128 in CBC mode with PKCS7 padding
- 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
- A variety of supported interface types
- 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, transfer coordination and checksumming are automatic
- 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 bandwidth cost of setting up an encrypted link is 3 packets totaling 297 bytes
- 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 `<bc7291552be7a58f361522990465165c>`.
Compatibility with the Reticulum Protocol is defined as having full interoperability,
and sufficient functional parity with this reference implementation. Any specific protocol
implementation that achieves this is Reticulum. Any that does not is not Reticulum.
The reference implementation is licensed under the Reticulum License.
The Reticulum Protocol was dedicated to the Public Domain in 2016.
## Examples of Reticulum Applications
If you want to quickly get an idea of what Reticulum can do, take a look at the
following resources.
[Programs Using Reticulum](https://reticulum.network/manual/software.html)
section of the manual, or the following resources:
- For an off-grid, encrypted and resilient mesh communications platform, see [Nomad Network](https://github.com/markqvist/NomadNet)
- The Android, Linux and macOS app [Sideband](https://github.com/markqvist/Sideband) has a graphical interface and focuses on ease of use.
- [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.
- [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 500 bits per second throughput, and an MTU of 500 bytes. Data radios,
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.
@@ -102,14 +137,28 @@ 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 pip:
To simply install Reticulum and related utilities on your system, the easiest way is via `pip`.
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).
```bash
pip install rns
```
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).
If you are using an operating system that blocks normal user package installation via `pip`,
you can return `pip` to normal behaviour by editing the `~/.config/pip/pip.conf` file,
and adding the following directive in the `[global]` section:
```text
[global]
break-system-packages = true
```
Alternatively, you can use the `pipx` tool to install Reticulum in an isolated environment:
```bash
pipx install rns
```
When first started, Reticulum will create a default configuration file,
providing basic connectivity to other Reticulum peers that might be locally
@@ -133,25 +182,31 @@ section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/)
- An interface status utility called `rnstatus`, that displays information about interfaces
- The path lookup and management tool `rnpath` letting you view and modify path tables
- A diagnostics tool called `rnprobe` for checking connectivity to destinations
- A simple file transfer program called `rncp` making it easy to copy files to remote systems
- The remote command execution program `rnx` let's you run commands and
programs and retrieve output from remote systems
- 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 `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.
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 relatively simple to implement an interface class. I will
gratefully accept pull requests for custom interfaces if they are generally
useful.
not supported, it's [simple to implement a custom interface module](https://markqvist.github.io/Reticulum/manual/interfaces.html#custom-interfaces).
Currently, the following interfaces are supported:
Pull requests for custom interfaces are gratefully accepted, provided they are
generally useful and well-tested in real-world usage.
Currently, the following built-in interfaces are supported:
- Any Ethernet device
- LoRa using [RNode](https://unsigned.io/projects/rnode/)
- 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
@@ -166,56 +221,23 @@ 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 500 bits per second
to 20 megabits per second, with physical mediums faster than that not being
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
Reticulum should currently be considered beta software. All core protocol
features are implemented and functioning, but additions will probably occur as
real-world use is explored. There will be bugs. The API and wire-format can be
considered relatively stable at the moment, but could change if warranted.
## Development Roadmap
- Improving [the manual](https://markqvist.github.io/Reticulum/manual/) with sections specifically for beginners
- Performance and memory optimisations
- Utilities for managing identities, signing and encryption
- User friendly interface configuration tool
- More interface types for even broader compatibility
- Plain ESP32 devices (ESP-Now, WiFi, Bluetooth, etc.)
- More LoRa transceivers
- IR Transceivers
- OpenWRT support
- Metric-based path selection
- Distributed Destination Naming System
- Network-wide path balancing
- Globally routable multicast
- Bindings for other programming languages
- Multiple paths in path table for quick recovery on link failures
- A portable Reticulum implementation in C, see [#21](https://github.com/markqvist/Reticulum/discussions/21)
- Easy way to share interface configurations, see [#19](https://github.com/markqvist/Reticulum/discussions/19)
- More interface types
- AT-compatible modems
- Optical mediums
- AWDL / OWL
- HF Modems
- CAN-bus
- ZeroMQ
- MQTT
- XBee
- SPI
- i²c
- Tor
All core protocol features are implemented and functioning, but additions will
probably occur as real-world use is explored and understood. The API and wire-format
can be considered stable.
## Dependencies
The installation of the default `rns` package requires the dependencies listed
The installation of the default `rns` package requires only two external dependencies, listed
below. Almost all systems and distributions have readily available packages for
these dependencies, and when the `rns` package is installed with `pip`, they
will be downloaded and installed as well.
- [PyCA/cryptography](https://github.com/pyca/cryptography)
- [netifaces](https://github.com/al45tair/netifaces)
- [pyserial](https://github.com/pyserial/pyserial)
On more unusual systems, and in some rare cases, it might not be possible to
@@ -238,107 +260,95 @@ that do not support [PyCA/cryptography](https://github.com/pyca/cryptography),
it is important that you read and understand the [Cryptographic
Primitives](#cryptographic-primitives) section of this document.
## Bootstrapping Connectivity
Reticulum is not a service you subscribe to, nor is it a single global network you "join".
Reticulum provides functionality for discovering available public interfaces
over the network itself, and the broader community has provided various directories
of publicly available entrypoints to bootstrap connectivity.
To learn how to establish initial connectivity over Reticulum, read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual.
If you already have a general idea of how this works, you can use community-run
sites such as [directory.rns.recipes](https://directory.rns.recipes/) and [rmap.world](https://rmap.world)
to find interface definitions for initial connectivity to the global distributed Reticulum backbone.
## Public Testnet
If you just want to get started experimenting without building any physical
networks, you are welcome to join the Unsigned.io RNS Testnet. The testnet is
just that, an informal network for testing and experimenting. It will be up
most of the time, and anyone can join, but it also means that there's no
guarantees for service availability.
The testnet runs the very latest version of Reticulum (often even a short while
before it is publicly released). Sometimes experimental versions of Reticulum
might be deployed to nodes on the testnet, which means strange behaviour might
occur. If none of that scares you, you can join the testnet via either TCP or
I2P. Just add one of the following interfaces to your Reticulum configuration
file:
```
# TCP/IP interface to the Dublin Hub
[[RNS Testnet Dublin]]
type = TCPClientInterface
enabled = yes
target_host = dublin.connect.reticulum.network
target_port = 4965
# TCP/IP interface to the Frankfurt Hub
[[RNS Testnet Frankfurt]]
type = TCPClientInterface
enabled = yes
target_host = frankfurt.connect.reticulum.network
target_port = 5377
# Interface to I2P Hub A
[[RNS Testnet I2P Hub A]]
type = I2PInterface
enabled = yes
peers = mrwqlsioq4hoo2lmeeud7dkfscnm7yxak7dmiyvsrnpfag3z5tsq.b32.i2p
# Interface to I2P Hub B
[[RNS Testnet I2P Hub B]]
type = I2PInterface
enabled = yes
peers = iwoqtz22dsr73aemwpw7guocplsjjoamyl7sogj33qtcd6ds4mza.b32.i2p
```
The testnet also contains a number of [Nomad Network](https://github.com/markqvist/nomadnet) nodes, and LXMF propagation nodes.
***Important!** Historically, a developer-targeted testnet was made available by the Reticulum project itself. As the amount of global Reticulum nodes and entrypoints have grown to a substantial quantity, this public testnet, including the Amsterdam Testnet entrypoint, has now been decommissioned. If your still have instances that relied on this entrypoint for connectivity, transition to using the distributed backbone instead. Reticulum now includes a full on-network interface discovery and connectivity bootstrapping system. Read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual for pointers.*
## Support Reticulum
You can help support the continued development of open, free and private communications systems by donating via one of the following channels:
For this to be possible, I need your help. Please support the continued development of open, free and private communications systems by donating via one of the following channels:
- Monero:
```
```
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
```
- Ethereum
```
0x81F7B979fEa6134bA9FD5c701b3501A2e61E897a
```
- Bitcoin
```
3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq
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.
## Cryptographic Primitives
Reticulum uses a simple suite of efficient, strong and modern cryptographic
Reticulum uses a simple suite of efficient, strong and well-tested cryptographic
primitives, with widely available implementations that can be used both on
general-purpose CPUs and on microcontrollers. The necessary primitives are:
general-purpose CPUs and on microcontrollers.
- Ed25519 for signatures
- X22519 for ECDH key exchanges
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
- Modified Fernet for encrypted tokens
- AES-128 in CBC mode
- HMAC for message authentication
- No Fernet version and timestamp fields
- 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 `os.urandom()` or better
- AES-256 in CBC mode with PKCS7 padding
- No Fernet version and timestamp metadata fields
- SHA-256
- SHA-512
In the default installation configuration, the `X25519`, `Ed25519` and
`AES-128-CBC` primitives are provided by [OpenSSL](https://www.openssl.org/)
In the default installation configuration, the `X25519`, `Ed25519`,
and `AES-256-CBC` primitives are provided by [OpenSSL](https://www.openssl.org/)
(via the [PyCA/cryptography](https://github.com/pyca/cryptography) package).
The hashing functions `SHA-256` and `SHA-512` are provided by the standard
Python [hashlib](https://docs.python.org/3/library/hashlib.html). The `HKDF`,
`HMAC`, `Fernet` primitives, and the `PKCS7` padding function are always
`HMAC`, `Token` primitives, and the `PKCS7` padding function are always
provided by the following internal implementations:
- [HKDF.py](RNS/Cryptography/HKDF.py)
- [HMAC.py](RNS/Cryptography/HMAC.py)
- [Fernet.py](RNS/Cryptography/Fernet.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 & PyCA are not available on the system when
in pure Python. If OpenSSL and PyCA are not available on the system when
Reticulum is started, Reticulum will instead use the internal pure-python
primitives. A trivial consequence of this is performance, with the OpenSSL
backend being *much* faster. The most important consequence however, is the
potential loss of security by using primitives that has not seen the same
amount of scrutiny, testing and review as those from OpenSSL.
Please note that by default, installing Reticulum will **require** OpenSSL and
PyCA to also be automatically installed if not already available. It is only
possible to use the pure-python primitives if this requirement is specifically
overridden by the user, for example by installing the `rnspure` package instead
of the normal `rns` package, or by running directly from local source-code.
If you want to use the internal pure-python primitives, it is **highly
advisable** that you have a good understanding of the risks that this pose, and
make an informed decision on whether those risks are acceptable to you.
@@ -362,12 +372,13 @@ projects:
- [PyCA/cryptography](https://github.com/pyca/cryptography), *BSD License*
- [Pure-25519](https://github.com/warner/python-pure25519) by [Brian Warner](https://github.com/warner), *MIT License*
- [Pysha2](https://github.com/thomdixon/pysha2) by [Thom Dixon](https://github.com/thomdixon), *MIT License*
- [Python-AES](https://github.com/orgurar/python-aes) by [Or Gur Arie](https://github.com/orgurar), *MIT License*
- [Python AES-128](https://github.com/orgurar/python-aes) by [Or Gur Arie](https://github.com/orgurar), *MIT License*
- [Python AES-256](https://github.com/boppreh/aes) by [BoppreH](https://github.com/boppreh), *MIT License*
- [Curve25519.py](https://gist.github.com/nickovs/cc3c22d15f239a2640c185035c06f8a3#file-curve25519-py) by [Nicko van Someren](https://gist.github.com/nickovs), *Public Domain*
- [I2Plib](https://github.com/l-n-s/i2plib) by [Viktor Villainov](https://github.com/l-n-s)
- [PySerial](https://github.com/pyserial/pyserial) by Chris Liechti, *BSD License*
- [Netifaces](https://github.com/al45tair/netifaces) by [Alastair Houghton](https://github.com/al45tair), *MIT License*
- [Configobj](https://github.com/DiffSK/configobj) by Michael Foord, Nicola Larosa, Rob Dennis & Eli Courtwright, *BSD License*
- [Six](https://github.com/benjaminp/six) by [Benjamin Peterson](https://github.com/benjaminp), *MIT License*
- [ifaddr](https://github.com/pydron/ifaddr) by 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)
+273
View File
@@ -0,0 +1,273 @@
>> Reticulum Network Stack
To understand the foundational philosophy and goals of this system, read the `_`!`[Zen of Reticulum`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=Zen+of+Reticulum.md]`!`_.
Reticulum is the cryptography-based networking stack for building local and wide-area networks with readily available hardware. It can operate even with very high latency and extremely low bandwidth. Reticulum allows you to build wide-area networks with off-the-shelf tools, and offers end-to-end encryption and connectivity, initiator anonymity, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, unforgeable delivery acknowledgements and more.
The vision of Reticulum is to allow anyone to be their own network operator, and to make it cheap and easy to cover vast areas with a myriad of independent, inter-connectable and autonomous networks. Reticulum `!is not`! `*one`* network. It is `!a tool`! for building `*thousands of networks`*. Networks without kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate with each other, and require no central oversight. Networks for human beings. `*Networks for the people`*.
Reticulum is a complete networking stack, and does not rely on IP or higher layers, but it is possible to use IP as the underlying carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the Internet or private IP networks.
Having no dependencies on traditional networking stacks frees up overhead that has been used to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality, even in open and trustless networks.
No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python.
>> Read The Manual
The full documentation for Reticulum is available on `_`!`[this node`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
You can also download the `_`!`[Reticulum manual as a PDF`:/file/download`g=reticulum|r=reticulum|ref=HEAD|path=docs%2FReticulum+Manual.pdf]`!`_ or `_`!`[as an e-book in EPUB format`:/file/download`g=reticulum|r=reticulum|ref=HEAD|path=docs%2FReticulum+Manual.pdf]`!`_.
>> Notable Features
• Coordination-less globally unique addressing and identification
• Fully self-configuring multi-hop routing over heterogeneous carriers
• Flexible scalability over heterogeneous topologies
• Reticulum can carry data over any mixture of physical mediums and topologies
• Low-bandwidth networks can co-exist and interoperate with large, high-bandwidth networks
• Initiator anonymity, communicate without revealing your identity
• Reticulum does not include source addresses on any packets
• Asymmetric X25519 encryption and Ed25519 signatures as a basis for all communication
• The foundational Reticulum Identity Keys are 512-bit Elliptic Curve keysets
• Forward Secrecy is available for all communication types, both for single packets and over links
• Reticulum uses the following format for encrypted tokens:
• Ephemeral per-packet and link keys and derived from an ECDH key exchange on Curve25519
• AES-256 in CBC mode with PKCS7 padding
• HMAC using SHA256 for authentication
• IVs are generated through os.urandom()
• Unforgeable packet delivery confirmations
• Flexible and extensible interface system
• Reticulum includes a large variety of built-in interface types
• Ability to load and utilise custom user- or community-supplied interface types
• Easily create your own custom interfaces for communicating over anything
• Authentication and virtual network segmentation on all supported interface types
• An intuitive and easy-to-use API
• Simpler and easier to use than sockets APIs, but more powerful
• Makes building distributed and decentralised applications much simpler
• Reliable and efficient transfer of arbitrary amounts of data
• Reticulum can handle a few bytes of data or files of many gigabytes
• Sequencing, compression, transfer coordination and checksumming are automatic
• The API is very easy to use, and provides transfer progress
• Lightweight, flexible and expandable Request/Response mechanism
• Efficient link establishment
• Total cost of setting up an encrypted and verified link is only 3 packets, totalling 297 bytes
• Low cost of keeping links open at only 0.44 bits per second
• Reliable sequential delivery with Channel and Buffer mechanisms
>> Reference Implementation
The Python code in this repository is the Reference Implementation of Reticulum. The Reticulum Protocol is defined entirely and authoritatively by this reference implementation, and its associated manual. It is maintained by Mark Qvist, identified by the Reticulum Identity `B333<bc7291552be7a58f361522990465165c>`b.
Compatibility with the Reticulum Protocol is defined as having full interoperability, and sufficient functional parity with this reference implementation. Any specific protocol implementation that achieves this is Reticulum. Any that does not is not Reticulum.
The reference implementation is licensed under the Reticulum License.
The Reticulum Protocol was dedicated to the Public Domain in 2016.
>> Examples of Reticulum Applications
If you want to quickly get an idea of what Reticulum can do, take a look at the `_`!`[Programs Using Reticulum`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/software.md]`!`_ section of the manual, or the following resources:
• `_`!`[LXMF`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=lxmf]`!`_ is a distributed, delay and disruption tolerant message transfer protocol built on Reticulum
• The `_`!`[LXST`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=lxst]`!`_ protocol and framework provides real-time audio and signals transport over Reticulum. It
includes primitives and utilities for building voice-based applications and hardware devices,
such as the `B333rnphone`b program, that can be used to build hardware telephones.
• For an off-grid, encrypted and resilient mesh communications platform, see `_`!`[Nomad Network`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=nomadnet]`!`_.
• The Android, Linux, macOS and Windows app `_`!`[Sideband`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=sideband]`!`_ has a graphical interface and many advanced
features, such as file transfers, image and voice messages, real-time voice calls, a distributed
telemetry system, mapping capabilities and full plugin extensibility.
• `_`!`[MeshChatX`c10d80b1a42fa958c37a6cc30dc04f53]`!`_ (`_`!`[source`5399f5a0212477618821e91e88ce053b:/page/repo.mu`g=quad4|r=MeshChatX]`_`!) is a full-featured LXMF client with many built-in tools and functionalities,
that also supports image and voice messages, file transfers and voice calls. It also includes a
built-in page browser for browsing Nomad Network nodes.
• You can use the included `_`!`[rnsh`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/using.md|anchor=the-rnsh-utility]`!`_ program to establish remote shell sessions over Reticulum.
>> Where can Reticulum be used?
Over practically any medium that can support at least a half-duplex channel with greater throughput than 5 bits per second, and an MTU of 500 bytes. Data radios, modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, WiFi and Ethernet devices, free-space optical links, and similar systems are all examples of the types of physical devices Reticulum can use.
An open-source LoRa-based interface called `_`!`[RNode`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/hardware.md|anchor=rnode]`!`_ has been designed specifically for use with Reticulum. It is possible to build yourself, or it can be purchased as a complete transceiver that just needs a USB connection to the host.
Reticulum can also be encapsulated over existing IP networks, so there's nothing stopping you from using it over wired Ethernet, your local WiFi network or the Internet, where it'll work just as well. In fact, one of the strengths of Reticulum is how easily it allows you to connect different mediums into a self-configuring, resilient and encrypted mesh, using any available mixture of available infrastructure.
As an example, it's possible to set up a Raspberry Pi connected to both a LoRa radio, a packet radio TNC and a WiFi network. Once the interfaces are configured, Reticulum will take care of the rest, and any device on the WiFi network can communicate with nodes on the LoRa and packet radio sides of the network, and vice versa.
>> How do I get started?
The best way to get started with the Reticulum Network Stack depends on what you want to do. For full details and examples, have a look at the `_`!`[Getting Started Fast`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/gettingstartedfast.md]`!`_ section of the `_`!`[Reticulum Manual`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
To simply install Reticulum and related utilities on your system, the easiest way is via `B333pip`b. You can then start any program that uses Reticulum, or start Reticulum as a system service with `_`!`[the rnsd utility`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/using.md|anchor=the-rnsd-utility]`!`_.
`B333
`=
pip install rns
`=
`b
If you are using an operating system that blocks normal user package installation via `B333pip`b, you can return `B333pip`b to normal behaviour by editing the `B333~/.config/pip/pip.conf`b file, and adding the following directive in the `B333[global]`b section:
`B333
`=
[global]
break-system-packages = true
`=
`b
Alternatively, you can use the `B333pipx`b tool to install Reticulum in an isolated environment:
`B333
`=
pipx install rns
`=
`b
When first started, Reticulum will create a default configuration file, providing basic connectivity to other Reticulum peers that might be locally reachable. The default config file contains a few examples, and references for creating a more complex configuration.
If you have an old version of `B333pip`b on your system, you may need to upgrade it first with `B333pip install pip --upgrade`b. If you no not already have `B333pip`b installed, you can install it using the package manager of your system with `B333sudo apt install python3-pip`b or similar.
For more detailed examples on how to expand communication over many mediums such as packet radio or LoRa, serial ports, or over fast IP links and the Internet using the UDP and TCP interfaces, take a look at the `_`!`[Supported Interfaces`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/interfaces.md]`!`_ section of the `_`!`[Reticulum Manual`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
>> Included Utilities
Reticulum includes a range of useful utilities for managing your networks, viewing status and information, and other tasks. You can read more about these programs in the `_`!`[Included Utility Programs`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/using.md|anchor=included-utility-programs]`!`_ section of the `_`!`[Reticulum Manual`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
• The system daemon `B333rnsd`b for running Reticulum as an always-available service
• An interface status utility called `B333rnstatus`b, that displays information about interfaces
• The path lookup and management tool `B333rnpath`b letting you view and modify path tables
• A diagnostics tool called `B333rnprobe`b for checking connectivity to destinations
• A simple file transfer program called `B333rncp`b making it easy to transfer files between systems
• The identity management and encryption utility `B333rnid`b let's you manage Identities and encrypt/decrypt files
• The `B333rnsh`b program allows you to establish fully interactive shell session with remote systems
• The remote command execution program `B333rnx`b let's you run simple commands and programs and retrieve output from remote systems
• The `B333rngit`b program provides a full multi-repository Git node for serving repositories over Reticulum
• The included `B333git-remote-rns`b helper allows you to interact with Git repositories over Reticulum
>> Supported interface types and devices
Reticulum implements a range of generalised interface types that covers most of the communications hardware that Reticulum can run over. If your hardware is not supported, it's `_`!`[simple to implement a custom interface module`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/interfaces.md|anchor=custom-interfaces]`!`_.
Currently, the following built-in interfaces are supported:
• Any Ethernet device
• LoRa using `_`!`[RNode`a8d24177d946de4f1f0a0fe1af9a1338:/page/repo.mu`g=reticulum|r=rnode_firmware]`!`_
• Packet Radio TNCs (with or without AX.25)
• KISS-compatible hardware and software modems
• Any device with a serial port
• TCP over IP networks
• UDP over IP networks
• External programs via stdio or pipes
• Custom hardware via stdio or pipes
>> Performance
Reticulum targets a `*very`* wide usable performance envelope, but prioritises functionality and performance on low-bandwidth mediums. The goal is to provide a dynamic performance envelope from 250 bits per second, to 1 gigabit per second on normal hardware.
Currently, the usable performance envelope is approximately 150 bits per second to 500 megabits per second, with physical mediums faster than that not being saturated. Performance beyond the current level is intended for future upgrades, but not highly prioritised at this point in time.
>> Current Status
All core protocol features are implemented and functioning, but additions will probably occur as real-world use is explored and understood. The API and wire-format can be considered stable.
>> Dependencies
The installation of the default `B333rns`b package requires only two external dependencies, listed below. Almost all systems and distributions have readily available packages for these dependencies, and when the `B333rns`b package is installed with `B333pip`b, they will be downloaded and installed as well.
• PyCA/cryptography
• pyserial
On more unusual systems, and in some rare cases, it might not be possible to install or even compile one or more of the above modules. In such situations, you can use the `B333rnspure`b package instead, which require no external dependencies for installation. Please note that the contents of the `B333rns`b and `B333rnspure`b packages are `*identical`*. The only difference is that the `B333rnspure`b package lists no dependencies required for installation.
No matter how Reticulum is installed and started, it will load external dependencies only if they are `*needed`* and `*available`*. If for example you want to use Reticulum on a system that cannot support `B333pyserial`b, it is perfectly possible to do so using the `B333rnspure`b package, but Reticulum will not be able to use serial-based interfaces. All other available modules will still be loaded when needed.
`!Please Note!`! If you use the `B333rnspure`b package to run Reticulum on systems that do not support PyCA/cryptography, it is important that you read and understand the `!Cryptographic Primitives`! section of this document.
>> Bootstrapping Connectivity
Reticulum is not a service you subscribe to, nor is it a single global network you "join". Reticulum provides functionality for discovering available public interfaces over the network itself, and the broader community has provided various directories of publicly available entrypoints to bootstrap connectivity.
To learn how to establish initial connectivity over Reticulum, read the `_`!`[Bootstrapping Connectivity`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/gettingstartedfast.md|anchor=bootstrapping-connectivity]`!`_ section of the manual.
If you already have a general idea of how this works, you can use community-run sites such as `_`!`[rns.recipes`9ce92808be498e9e05590ff27cbfdfe4]`!`_ and `_`!`[rmap.world`a4a5e861626ce97c9aa544d9ecdf6d22]`!`_ to find interface definitions for initial connectivity to the global distributed Reticulum backbone.
>> Public Testnet
`!`*Important!`! Historically, a developer-targeted testnet was made available by the Reticulum project itself. As the amount of global Reticulum nodes and entrypoints have grown to a substantial quantity, this public testnet, including the Amsterdam Testnet entrypoint, has now been decommissioned. If you still have instances that relied on this entrypoint for connectivity, transition to using the distributed backbone instead. Reticulum now includes a full on-network interface discovery and connectivity bootstrapping system. Read the `_`[Bootstrapping Connectivity`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/gettingstartedfast.md|anchor=bootstrapping-connectivity]`_ section of the manual for pointers.`*
>> Support Reticulum
For this to be possible, I need your help. Please support the continued development of open, free and private communications systems by donating via one of the following channels:
• `!Monero`!
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
• `!Bitcoin`!
bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6
• `!Ethereum`!
0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E
• `!Liberapay`!
`[https://liberapay.com/Reticulum/]
• `!Ko-Fi`!
`[https://ko-fi.com/markqvist]
>> Cryptographic Primitives
Reticulum uses a simple suite of efficient, strong and well-tested cryptographic primitives, with widely available implementations that can be used both on general-purpose CPUs and on microcontrollers.
One of the primary considerations for choosing this particular set of primitives is that they can be implemented `*safely`* with relatively few pitfalls, on practically all current computing platforms.
The primitives listed here `!are authoritative`!. Anything `*claiming`* to be Reticulum, but not using these exact primitives `FTA35050`!is not`!`f Reticulum, and possibly an intentionally compromised or weakened clone. The utilised primitives are:
• Reticulum Identity Keys are 512-bit Curve25519 keysets
• A 256-bit Ed25519 key for signatures
• A 256-bit X22519 key for ECDH key exchanges
• HKDF for key derivation
• Encrypted tokens are based on the `_`!`[Fernet spec`https://github.com/fernet/spec/]`!`_
• Ephemeral keys derived from an ECDH key exchange on Curve25519
• HMAC using SHA256 for message authentication
• IVs must be generated through `B333os.urandom()`b or better
• AES-256 in CBC mode with PKCS7 padding
• No Fernet version and timestamp metadata fields
• SHA-256
• SHA-512
In the default installation configuration, the `B333X25519`b, `B333Ed25519`b, and `B333AES-256-CBC`b primitives are provided by `_`!`[OpenSSL`https://www.openssl.org/]`!`_ (via the `_`!`[PyCA/cryptography`https://github.com/pyca/cryptography]`!`_ package). The hashing functions `B333SHA-256`b and `B333SHA-512`b are provided by the standard Python `_`!`[hashlib`https://docs.python.org/3/library/hashlib.html]`!`_. The `B333HKDF`b, `B333HMAC`b, `B333Token`b primitives, and the `B333PKCS7`b padding function are always provided by the following internal implementations:
• `_`!`[HKDF.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/HKDF.py]`!`_
• `_`!`[HMAC.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/HMAC.py]`!`_
• `_`!`[Token.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/Token.py]`!`_
• `_`!`[PKCS7.py`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=RNS/Cryptography/PKCS7.py]`!`_
Reticulum also includes a complete implementation of all necessary primitives in pure Python. If OpenSSL and PyCA are not available on the system when Reticulum is started, Reticulum will instead use the internal pure-python primitives. A trivial consequence of this is performance, with the OpenSSL backend being `*much`* faster. The most important consequence however, is the potential loss of security by using primitives that has not seen the same amount of scrutiny, testing and review as those from OpenSSL.
Please note that by default, installing Reticulum will `!require`! OpenSSL and PyCA to also be automatically installed if not already available. It is only possible to use the pure-python primitives if this requirement is specifically overridden by the user, for example by installing the `B333rnspure`b package instead of the normal `B333rns`b package, or by running directly from local source-code.
If you want to use the internal pure-python primitives, it is `!highly advisable`! that you have a good understanding of the risks that this pose, and make an informed decision on whether those risks are acceptable to you.
Reticulum is relatively young software, and should be considered as such. While it has been built with cryptography best-practices very foremost in mind, it _has not_ been externally security audited, and there could very well be privacy or security breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch.
>> Acknowledgements & Credits
Reticulum can only exist because of the mountain of Open Source work it was built on top of, the contributions of everyone involved, and everyone that has supported the project through the years. To everyone who has helped, thank you so much.
A number of other modules and projects are either part of, or used by Reticulum. Sincere thanks to the authors and contributors of the following projects:
• `_`!`[PyCA/cryptography`https://github.com/pyca/cryptography]`!`_, `*BSD License`*
• `_`!`[Pure-25519`https://github.com/warner/python-pure25519]`!`_, by `_`!`[Brian Warner`https://github.com/warner]`!`_, `*MIT License`*
• `_`!`[Pysha2`https://github.com/thomdixon/pysha2]`!`_ by `_`!`[Thom Dixon`https://github.com/thomdixon]`!`_, `*MIT License`*
• `_`!`[Python AES-128`https://github.com/orgurar/python-aes]`!`_ by `_`!`[Or Gur Arie`https://github.com/orgurar]`!`_, `*MIT License`*
• `_`!`[Python AES-256`https://github.com/boppreh/aes]`!`_ by `_`!`[BoppreH`https://github.com/boppreh]`!`_, `*MIT License`*
• `_`!`[Curve25519.py`https://gist.github.com/nickovs/cc3c22d15f239a2640c185035c06f8a3]`!`_ by `_`!`[Nicko van Someren`https://gist.github.com/nickovs]`!`_, `*Public Domain`*
• `_`!`[I2Plib`https://github.com/l-n-s/i2plib]`!`_ by `_`!`[Viktor Villainov`https://github.com/l-n-s]`!`_
• `_`!`[PySerial`https://github.com/pyserial/pyserial]`!`_ by Chris Liechti, `*BSD License`*
• `_`!`[Configobj`https://github.com/DiffSK/configobj]`!`_ by Michael Foord, Nicola Larosa, Rob Dennis & Eli Courtwright, `*BSD License`*
• `_`!`[ifaddr`https://github.com/pydron/ifaddr]`!`_ by Stefan C. Mueller, `*MIT License`*
• `_`!`[Umsgpack.py`https://github.com/vsergeev/u-msgpack-python]`!`_ by `_`!`[Ivan A. Sergeev`https://github.com/vsergeev]`!`_
• `_`!`[rnsh`https://github.com/acehoss/rnsh]`!`_ by `_`!`[Aaron Heise`https://github.com/acehoss]`!`_
• `_`!`[Python`https://www.python.org]`!`_
+371
View File
@@ -0,0 +1,371 @@
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations
import bz2
import sys
import time
import threading
from threading import RLock
import struct
from RNS.Channel import Channel, MessageBase, SystemMessageTypes
import RNS
from io import RawIOBase, BufferedRWPair, BufferedReader, BufferedWriter
from typing import Callable
from contextlib import AbstractContextManager
class StreamDataMessage(MessageBase):
MSGTYPE = SystemMessageTypes.SMT_STREAM_DATA
"""
Message type for ``Channel``. ``StreamDataMessage``
uses a system-reserved message type.
"""
STREAM_ID_MAX = 0x3fff # 16383
"""
The stream id is limited to 2 bytes - 2 bit
"""
OVERHEAD = 2 + 6 # 2 for stream data message header, 6 for channel envelope
MAX_DATA_LEN = RNS.Link.MDU - OVERHEAD
"""
When the Buffer package is imported, this value is
calculcated based on the value of OVERHEAD
"""
def __init__(self, stream_id: int = None, data: bytes = None, eof: bool = False, compressed: bool = False):
"""
This class is used to encapsulate binary stream
data to be sent over a ``Channel``.
:param stream_id: id of stream relative to receiver
:param data: binary data
:param eof: set to True if signalling End of File
"""
super().__init__()
if stream_id is not None and stream_id > self.STREAM_ID_MAX:
raise ValueError("stream_id must be 0-16383")
self.stream_id = stream_id
self.compressed = compressed
self.data = data or bytes()
self.eof = eof
def pack(self) -> bytes:
if self.stream_id is None:
raise ValueError("stream_id")
header_val = (0x3fff & self.stream_id) | (0x8000 if self.eof else 0x0000) | (0x4000 if self.compressed > 0 else 0x0000)
return bytes(struct.pack(">H", header_val) + (self.data if self.data else bytes()))
def unpack(self, raw):
self.stream_id = struct.unpack(">H", raw[:2])[0]
self.eof = (0x8000 & self.stream_id) > 0
self.compressed = (0x4000 & self.stream_id) > 0
self.stream_id = self.stream_id & 0x3fff
self.data = raw[2:]
if self.compressed:
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):
"""
An implementation of RawIOBase that receives
binary stream data sent over a ``Channel``.
This class generally need not be instantiated directly.
Use :func:`RNS.Buffer.create_reader`,
:func:`RNS.Buffer.create_writer`, and
:func:`RNS.Buffer.create_bidirectional_buffer` functions
to create buffered streams with optional callbacks.
For additional information on the API of this
object, see the Python documentation for
``RawIOBase``.
"""
def __init__(self, stream_id: int, channel: Channel):
"""
Create a raw channel reader.
:param stream_id: local stream id to receive at
:param channel: ``Channel`` object to receive from
"""
self._stream_id = stream_id
self._channel = channel
self._lock = RLock()
self._buffer = bytearray()
self._eof = False
self._channel._register_message_type(StreamDataMessage, is_system_type=True)
self._channel.add_message_handler(self._handle_message)
self._listeners: [Callable[[int], None]] = []
def add_ready_callback(self, cb: Callable[[int], None]):
"""
Add a function to be called when new data is available.
The function should have the signature ``(ready_bytes: int) -> None``
:param cb: function to call
"""
with self._lock:
self._listeners.append(cb)
def remove_ready_callback(self, cb: Callable[[int], None]):
"""
Remove a function added with :func:`RNS.RawChannelReader.add_ready_callback()`
:param cb: function to remove
"""
with self._lock:
self._listeners.remove(cb)
def _handle_message(self, message: MessageBase):
if isinstance(message, StreamDataMessage):
if message.stream_id == self._stream_id:
with self._lock:
if message.data is not None:
self._buffer.extend(message.data)
if message.eof:
self._eof = True
for listener in self._listeners:
try:
threading.Thread(target=listener, name="Message Callback", args=[len(self._buffer)], daemon=True).start()
except Exception as ex:
RNS.log("Error calling RawChannelReader(" + str(self._stream_id) + ") callback: " + str(ex), RNS.LOG_ERROR)
return True
return False
def _read(self, __size: int) -> bytes | None:
with self._lock:
result = self._buffer[:__size]
self._buffer = self._buffer[__size:]
return result if len(result) > 0 or self._eof else None
def readinto(self, __buffer: bytearray) -> int | None:
ready = self._read(len(__buffer))
if ready is not None:
__buffer[:len(ready)] = ready
return len(ready) if ready is not None else None
def writable(self) -> bool:
return False
def seekable(self) -> bool:
return False
def readable(self) -> bool:
return True
def close(self):
with self._lock:
self._channel.remove_message_handler(self._handle_message)
self._listeners.clear()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
return False
class RawChannelWriter(RawIOBase, AbstractContextManager):
"""
An implementation of RawIOBase that receives
binary stream data sent over a channel.
This class generally need not be instantiated directly.
Use :func:`RNS.Buffer.create_reader`,
:func:`RNS.Buffer.create_writer`, and
:func:`RNS.Buffer.create_bidirectional_buffer` functions
to create buffered streams with optional callbacks.
For additional information on the API of this
object, see the Python documentation for
``RawIOBase``.
"""
MAX_CHUNK_LEN = 1024*16
COMPRESSION_TRIES = 4
def __init__(self, stream_id: int, channel: Channel):
"""
Create a raw channel writer.
:param stream_id: remote stream id to sent do
:param channel: ``Channel`` object to send on
"""
self._stream_id = stream_id
self._channel = channel
self._eof = False
self._mdu = channel.mdu - StreamDataMessage.OVERHEAD
def write(self, __b: bytes) -> int | None:
try:
comp_tries = RawChannelWriter.COMPRESSION_TRIES
comp_try = 1
comp_success = False
chunk_len = len(__b)
if chunk_len > RawChannelWriter.MAX_CHUNK_LEN:
chunk_len = RawChannelWriter.MAX_CHUNK_LEN
__b = __b[:RawChannelWriter.MAX_CHUNK_LEN]
chunk_segment = None
while chunk_len > 32 and comp_try < comp_tries:
chunk_segment_length = int(chunk_len/comp_try)
compressed_chunk = bz2.compress(__b[:chunk_segment_length])
compressed_length = len(compressed_chunk)
if compressed_length < StreamDataMessage.MAX_DATA_LEN and compressed_length < chunk_segment_length:
comp_success = True
break
else:
comp_try += 1
if comp_success:
chunk = compressed_chunk
processed_length = chunk_segment_length
else:
chunk = bytes(__b[:StreamDataMessage.MAX_DATA_LEN])
processed_length = len(chunk)
message = StreamDataMessage(self._stream_id, chunk, self._eof, comp_success)
self._channel.send(message)
return processed_length
except RNS.Channel.ChannelException as cex:
if cex.type != RNS.Channel.CEType.ME_LINK_NOT_READY:
raise
return 0
def close(self):
try:
link_rtt = self._channel._outlet.link.rtt
timeout = time.time() + (link_rtt * len(self._channel._tx_ring) * 1)
except Exception as e:
timeout = time.time() + 15
while time.time() < timeout and not self._channel.is_ready_to_send():
time.sleep(0.05)
self._eof = True
self.write(bytes())
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
return False
def seekable(self) -> bool:
return False
def readable(self) -> bool:
return False
def writable(self) -> bool:
return True
class Buffer:
"""
Static functions for creating buffered streams that send
and receive over a ``Channel``.
These functions use ``BufferedReader``, ``BufferedWriter``,
and ``BufferedRWPair`` to add buffering to
``RawChannelReader`` and ``RawChannelWriter``.
"""
@staticmethod
def create_reader(stream_id: int, channel: Channel,
ready_callback: Callable[[int], None] | None = None) -> BufferedReader:
"""
Create a buffered reader that reads binary data sent
over a ``Channel``, with an optional callback when
new data is available.
Callback signature: ``(ready_bytes: int) -> None``
For more information on the reader-specific functions
of this object, see the Python documentation for
``BufferedReader``
:param stream_id: the local stream id to receive from
:param channel: the channel to receive on
:param ready_callback: function to call when new data is available
:return: a BufferedReader object
"""
reader = RawChannelReader(stream_id, channel)
if ready_callback:
reader.add_ready_callback(ready_callback)
return BufferedReader(reader)
@staticmethod
def create_writer(stream_id: int, channel: Channel) -> BufferedWriter:
"""
Create a buffered writer that writes binary data over
a ``Channel``.
For more information on the writer-specific functions
of this object, see the Python documentation for
``BufferedWriter``
:param stream_id: the remote stream id to send to
:param channel: the channel to send on
:return: a BufferedWriter object
"""
writer = RawChannelWriter(stream_id, channel)
return BufferedWriter(writer)
@staticmethod
def create_bidirectional_buffer(receive_stream_id: int, send_stream_id: int, channel: Channel,
ready_callback: Callable[[int], None] | None = None) -> BufferedRWPair:
"""
Create a buffered reader/writer pair that reads and
writes binary data over a ``Channel``, with an
optional callback when new data is available.
Callback signature: ``(ready_bytes: int) -> None``
For more information on the reader-specific functions
of this object, see the Python documentation for
``BufferedRWPair``
:param receive_stream_id: the local stream id to receive at
:param send_stream_id: the remote stream id to send to
:param channel: the channel to send and receive on
:param ready_callback: function to call when new data is available
:return: a BufferedRWPair object
"""
reader = RawChannelReader(receive_stream_id, channel)
if ready_callback:
reader.add_ready_callback(ready_callback)
writer = RawChannelWriter(send_stream_id, channel)
return BufferedRWPair(reader, writer)
+705
View File
@@ -0,0 +1,705 @@
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations
import collections
import enum
import threading
import time
from types import TracebackType
from typing import Type, Callable, TypeVar, Generic, NewType
import abc
import contextlib
import struct
import RNS
from abc import ABC, abstractmethod
TPacket = TypeVar("TPacket")
class SystemMessageTypes(enum.IntEnum):
SMT_STREAM_DATA = 0xff00
class ChannelOutletBase(ABC, Generic[TPacket]):
"""
An abstract transport layer interface used by Channel.
DEPRECATED: This was created for testing; eventually
Channel will use Link or a LinkBase interface
directly.
"""
@abstractmethod
def send(self, raw: bytes) -> TPacket:
raise NotImplemented()
@abstractmethod
def resend(self, packet: TPacket) -> TPacket:
raise NotImplemented()
@property
@abstractmethod
def mdu(self):
raise NotImplemented()
@property
@abstractmethod
def rtt(self):
raise NotImplemented()
@property
@abstractmethod
def is_usable(self):
raise NotImplemented()
@abstractmethod
def get_packet_state(self, packet: TPacket) -> MessageState:
raise NotImplemented()
@abstractmethod
def timed_out(self):
raise NotImplemented()
@abstractmethod
def __str__(self):
raise NotImplemented()
@abstractmethod
def set_packet_timeout_callback(self, packet: TPacket, callback: Callable[[TPacket], None] | None,
timeout: float | None = None):
raise NotImplemented()
@abstractmethod
def set_packet_delivered_callback(self, packet: TPacket, callback: Callable[[TPacket], None] | None):
raise NotImplemented()
@abstractmethod
def get_packet_id(self, packet: TPacket) -> any:
raise NotImplemented()
class CEType(enum.IntEnum):
"""
ChannelException type codes
"""
ME_NO_MSG_TYPE = 0
ME_INVALID_MSG_TYPE = 1
ME_NOT_REGISTERED = 2
ME_LINK_NOT_READY = 3
ME_ALREADY_SENT = 4
ME_TOO_BIG = 5
class ChannelException(Exception):
"""
An exception thrown by Channel, with a type code.
"""
def __init__(self, ce_type: CEType, *args):
super().__init__(args)
self.type = ce_type
class MessageState(enum.IntEnum):
"""
Set of possible states for a Message
"""
MSGSTATE_NEW = 0
MSGSTATE_SENT = 1
MSGSTATE_DELIVERED = 2
MSGSTATE_FAILED = 3
class MessageBase(abc.ABC):
"""
Base type for any messages sent or received on a Channel.
Subclasses must define the two abstract methods as well as
the ``MSGTYPE`` class variable.
"""
# MSGTYPE must be unique within all classes sent over a
# channel. Additionally, MSGTYPE > 0xf000 are reserved.
MSGTYPE = None
"""
Defines a unique identifier for a message class.
* Must be unique within all classes registered with a ``Channel``
* Must be less than ``0xf000``. Values greater than or equal to ``0xf000`` are reserved.
"""
@abstractmethod
def pack(self) -> bytes:
"""
Create and return the binary representation of the message
:return: binary representation of message
"""
raise NotImplemented()
@abstractmethod
def unpack(self, raw: bytes):
"""
Populate message from binary representation
:param raw: binary representation
"""
raise NotImplemented()
MessageCallbackType = NewType("MessageCallbackType", Callable[[MessageBase], bool])
class Envelope:
"""
Internal wrapper used to transport messages over a channel and
track its state within the channel framework.
"""
def unpack(self, message_factories: dict[int, Type]) -> MessageBase:
msgtype, self.sequence, length = struct.unpack(">HHH", self.raw[:6])
raw = self.raw[6:]
ctor = message_factories.get(msgtype, None)
if ctor is None:
raise ChannelException(CEType.ME_NOT_REGISTERED, f"Unable to find constructor for Channel MSGTYPE {hex(msgtype)}")
message = ctor()
message.unpack(raw)
self.unpacked = True
self.message = message
return message
def pack(self) -> bytes:
if self.message.__class__.MSGTYPE is None:
raise ChannelException(CEType.ME_NO_MSG_TYPE, f"{self.message.__class__} lacks MSGTYPE")
data = self.message.pack()
self.raw = struct.pack(">HHH", self.message.MSGTYPE, self.sequence, len(data)) + data
self.packed = True
return self.raw
def __init__(self, outlet: ChannelOutletBase, message: MessageBase = None, raw: bytes = None, sequence: int = None):
self.ts = time.time()
self.id = id(self)
self.message = message
self.raw = raw
self.packet: TPacket = None
self.sequence = sequence
self.outlet = outlet
self.tries = 0
self.unpacked = False
self.packed = False
self.tracked = False
class Channel(contextlib.AbstractContextManager):
"""
Provides reliable delivery of messages over
a link.
``Channel`` differs from ``Request`` and
``Resource`` in some important ways:
**Continuous**
Messages can be sent or received as long as
the ``Link`` is open.
**Bi-directional**
Messages can be sent in either direction on
the ``Link``; neither end is the client or
server.
**Size-constrained**
Messages must be encoded into a single packet.
``Channel`` is similar to ``Packet``, except that it
provides reliable delivery (automatic retries) as well
as a structure for exchanging several types of
messages over the ``Link``.
``Channel`` is not instantiated directly, but rather
obtained from a ``Link`` with ``get_channel()``.
"""
# The initial window size at channel setup
WINDOW = 2
# Absolute minimum window size
WINDOW_MIN = 2
WINDOW_MIN_LIMIT_SLOW = 2
WINDOW_MIN_LIMIT_MEDIUM = 5
WINDOW_MIN_LIMIT_FAST = 16
# The maximum window size for transfers on slow links
WINDOW_MAX_SLOW = 5
# The maximum window size for transfers on mid-speed links
WINDOW_MAX_MEDIUM = 12
# The maximum window size for transfers on fast links
WINDOW_MAX_FAST = 48
# For calculating maps and guard segments, this
# must be set to the global maximum window.
WINDOW_MAX = WINDOW_MAX_FAST
# If the fast rate is sustained for this many request
# rounds, the fast link window size will be allowed.
FAST_RATE_THRESHOLD = 10
# If the RTT rate is higher than this value,
# the max window size for fast links will be used.
RTT_FAST = 0.18
RTT_MEDIUM = 0.75
RTT_SLOW = 1.45
# The minimum allowed flexibility of the window size.
# The difference between window_max and window_min
# will never be smaller than this value.
WINDOW_FLEXIBILITY = 4
SEQ_MAX = 0xFFFF
SEQ_MODULUS = SEQ_MAX+1
def __init__(self, outlet: ChannelOutletBase):
"""
@param outlet:
"""
self._outlet = outlet
self._lock = threading.RLock()
self._tx_ring: collections.deque[Envelope] = collections.deque()
self._rx_ring: collections.deque[Envelope] = collections.deque()
self._message_callbacks: [MessageCallbackType] = []
self._next_sequence = 0
self._next_rx_sequence = 0
self._message_factories: dict[int, Type[MessageBase]] = {}
self._max_tries = 5
self.fast_rate_rounds = 0
self.medium_rate_rounds = 0
if self._outlet.rtt > Channel.RTT_SLOW:
self.window = 1
self.window_max = 1
self.window_min = 1
self.window_flexibility = 1
else:
self.window = Channel.WINDOW
self.window_max = Channel.WINDOW_MAX_SLOW
self.window_min = Channel.WINDOW_MIN
self.window_flexibility = Channel.WINDOW_FLEXIBILITY
def __enter__(self) -> Channel:
return self
def __exit__(self, __exc_type: Type[BaseException] | None, __exc_value: BaseException | None,
__traceback: TracebackType | None) -> bool | None:
self._shutdown()
return False
def register_message_type(self, message_class: Type[MessageBase]):
"""
Register a message class for reception over a ``Channel``.
Message classes must extend ``MessageBase``.
:param message_class: Class to register
"""
self._register_message_type(message_class, is_system_type=False)
def _register_message_type(self, message_class: Type[MessageBase], *, is_system_type: bool = False):
with self._lock:
if not issubclass(message_class, MessageBase):
raise ChannelException(CEType.ME_INVALID_MSG_TYPE,
f"{message_class} is not a subclass of {MessageBase}.")
if message_class.MSGTYPE is None:
raise ChannelException(CEType.ME_INVALID_MSG_TYPE,
f"{message_class} has invalid MSGTYPE class attribute.")
if message_class.MSGTYPE >= 0xf000 and not is_system_type:
raise ChannelException(CEType.ME_INVALID_MSG_TYPE,
f"{message_class} has system-reserved message type.")
try:
message_class()
except Exception as ex:
raise ChannelException(CEType.ME_INVALID_MSG_TYPE,
f"{message_class} raised an exception when constructed with no arguments: {ex}")
self._message_factories[message_class.MSGTYPE] = message_class
def add_message_handler(self, callback: MessageCallbackType):
"""
Add a handler for incoming messages. A handler
has the following signature:
``(message: MessageBase) -> bool``
Handlers are processed in the order they are
added. If any handler returns True, processing
of the message stops; handlers after the
returning handler will not be called.
:param callback: Function to call
"""
with self._lock:
if callback not in self._message_callbacks:
self._message_callbacks.append(callback)
def remove_message_handler(self, callback: MessageCallbackType):
"""
Remove a handler added with ``add_message_handler``.
:param callback: handler to remove
"""
with self._lock:
if callback in self._message_callbacks:
self._message_callbacks.remove(callback)
def _shutdown(self):
with self._lock:
self._message_callbacks.clear()
self._clear_rings()
def _clear_rings(self):
with self._lock:
for envelope in self._tx_ring:
if envelope.packet is not None:
self._outlet.set_packet_timeout_callback(envelope.packet, None)
self._outlet.set_packet_delivered_callback(envelope.packet, None)
self._tx_ring.clear()
self._rx_ring.clear()
def _emplace_envelope(self, envelope: Envelope, ring: collections.deque[Envelope]) -> bool:
with self._lock:
i = 0
for existing in ring:
if envelope.sequence == existing.sequence:
RNS.log(f"Envelope: Emplacement of duplicate envelope with sequence "+str(envelope.sequence), RNS.LOG_EXTREME)
return False
if envelope.sequence < existing.sequence and not (self._next_rx_sequence - envelope.sequence) > (Channel.SEQ_MAX//2):
ring.insert(i, envelope)
envelope.tracked = True
return True
i += 1
envelope.tracked = True
ring.append(envelope)
return True
def _run_callbacks(self, message: MessageBase):
cbs = self._message_callbacks.copy()
for cb in cbs:
try:
if cb(message):
return
except Exception as e:
RNS.log("Channel "+str(self)+" experienced an error while running a message callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
def _receive(self, raw: bytes):
try:
envelope = Envelope(outlet=self._outlet, raw=raw)
with self._lock:
message = envelope.unpack(self._message_factories)
if envelope.sequence < self._next_rx_sequence:
window_overflow = (self._next_rx_sequence+Channel.WINDOW_MAX) % Channel.SEQ_MODULUS
if window_overflow < self._next_rx_sequence:
if envelope.sequence > window_overflow:
RNS.log("Invalid packet sequence ("+str(envelope.sequence)+") received on channel "+str(self), RNS.LOG_EXTREME)
return
else:
RNS.log("Invalid packet sequence ("+str(envelope.sequence)+") received on channel "+str(self), RNS.LOG_EXTREME)
return
is_new = self._emplace_envelope(envelope, self._rx_ring)
if not is_new:
RNS.log("Duplicate message received on channel "+str(self), RNS.LOG_EXTREME)
return
else:
with self._lock:
contigous = []
for e in self._rx_ring:
if e.sequence == self._next_rx_sequence:
contigous.append(e)
self._next_rx_sequence = (self._next_rx_sequence + 1) % Channel.SEQ_MODULUS
if self._next_rx_sequence == 0:
for e in self._rx_ring:
if e.sequence == self._next_rx_sequence:
contigous.append(e)
self._next_rx_sequence = (self._next_rx_sequence + 1) % Channel.SEQ_MODULUS
for e in contigous:
if not e.unpacked:
m = e.unpack(self._message_factories)
else:
m = e.message
self._rx_ring.remove(e)
self._run_callbacks(m)
except Exception as e:
RNS.log("An error ocurred while receiving data on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
def is_ready_to_send(self) -> bool:
"""
Check if ``Channel`` is ready to send.
:return: True if ready
"""
if not self._outlet.is_usable:
return False
with self._lock:
outstanding = 0
for envelope in self._tx_ring:
if envelope.outlet == self._outlet:
if not envelope.packet or not self._outlet.get_packet_state(envelope.packet) == MessageState.MSGSTATE_DELIVERED:
outstanding += 1
if outstanding >= self.window:
return False
return True
def _packet_tx_op(self, packet: TPacket, op: Callable[[TPacket], bool]):
with self._lock:
envelope = next(filter(lambda e: self._outlet.get_packet_id(e.packet) == self._outlet.get_packet_id(packet),
self._tx_ring), None)
if envelope and op(envelope):
envelope.tracked = False
if envelope in self._tx_ring:
self._tx_ring.remove(envelope)
if self.window < self.window_max:
self.window += 1
# TODO: Remove at some point
# RNS.log("Increased "+str(self)+" window to "+str(self.window), RNS.LOG_DEBUG)
if self._outlet.rtt != 0:
if self._outlet.rtt > Channel.RTT_FAST:
self.fast_rate_rounds = 0
if self._outlet.rtt > Channel.RTT_MEDIUM:
self.medium_rate_rounds = 0
else:
self.medium_rate_rounds += 1
if self.window_max < Channel.WINDOW_MAX_MEDIUM and self.medium_rate_rounds == Channel.FAST_RATE_THRESHOLD:
self.window_max = Channel.WINDOW_MAX_MEDIUM
self.window_min = Channel.WINDOW_MIN_LIMIT_MEDIUM
# TODO: Remove at some point
# RNS.log("Increased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
# RNS.log("Increased "+str(self)+" min window to "+str(self.window_min), RNS.LOG_DEBUG)
else:
self.fast_rate_rounds += 1
if self.window_max < Channel.WINDOW_MAX_FAST and self.fast_rate_rounds == Channel.FAST_RATE_THRESHOLD:
self.window_max = Channel.WINDOW_MAX_FAST
self.window_min = Channel.WINDOW_MIN_LIMIT_FAST
# TODO: Remove at some point
# RNS.log("Increased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
# RNS.log("Increased "+str(self)+" min window to "+str(self.window_min), RNS.LOG_DEBUG)
else:
RNS.log("Envelope not found in TX ring for "+str(self), RNS.LOG_EXTREME)
if not envelope:
RNS.log("Spurious message received on "+str(self), RNS.LOG_EXTREME)
def _packet_delivered(self, packet: TPacket):
self._packet_tx_op(packet, lambda env: True)
def _update_packet_timeouts(self):
for envelope in self._tx_ring:
updated_timeout = self._get_packet_timeout_time(envelope.tries)
if envelope.packet and hasattr(envelope.packet, "receipt") and envelope.packet.receipt and envelope.packet.receipt.timeout:
if updated_timeout > envelope.packet.receipt.timeout:
envelope.packet.receipt.set_timeout(updated_timeout)
def _get_packet_timeout_time(self, tries: int) -> float:
to = pow(1.5, tries - 1) * max(self._outlet.rtt*2.5, 0.025) * (len(self._tx_ring)+1.5)
return to
def _packet_timeout(self, packet: TPacket):
def retry_envelope(envelope: Envelope) -> bool:
if envelope.tries >= self._max_tries:
RNS.log("Retry count exceeded on "+str(self)+", tearing down Link.", RNS.LOG_ERROR)
self._shutdown() # start on separate thread?
self._outlet.timed_out()
return True
envelope.tries += 1
self._outlet.resend(envelope.packet)
self._outlet.set_packet_delivered_callback(envelope.packet, self._packet_delivered)
self._outlet.set_packet_timeout_callback(envelope.packet, self._packet_timeout, self._get_packet_timeout_time(envelope.tries))
self._update_packet_timeouts()
if self.window > self.window_min:
self.window -= 1
# TODO: Remove at some point
# RNS.log("Decreased "+str(self)+" window to "+str(self.window), RNS.LOG_DEBUG)
if self.window_max > (self.window_min+self.window_flexibility):
self.window_max -= 1
# TODO: Remove at some point
# RNS.log("Decreased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
# TODO: Remove at some point
# RNS.log("Decreased "+str(self)+" window to "+str(self.window), RNS.LOG_EXTREME)
return False
if self._outlet.get_packet_state(packet) != MessageState.MSGSTATE_DELIVERED:
self._packet_tx_op(packet, retry_envelope)
def send(self, message: MessageBase) -> Envelope:
"""
Send a message. If a message send is attempted and
``Channel`` is not ready, an exception is thrown.
:param message: an instance of a ``MessageBase`` subclass
"""
envelope: Envelope | None = None
with self._lock:
if not self.is_ready_to_send():
raise ChannelException(CEType.ME_LINK_NOT_READY, f"Link is not ready")
envelope = Envelope(self._outlet, message=message, sequence=self._next_sequence)
self._next_sequence = (self._next_sequence + 1) % Channel.SEQ_MODULUS
self._emplace_envelope(envelope, self._tx_ring)
if envelope is None:
raise BlockingIOError()
envelope.pack()
if len(envelope.raw) > self._outlet.mdu:
raise ChannelException(CEType.ME_TOO_BIG, f"Packed message too big for packet: {len(envelope.raw)} > {self._outlet.mdu}")
envelope.packet = self._outlet.send(envelope.raw)
envelope.tries += 1
self._outlet.set_packet_delivered_callback(envelope.packet, self._packet_delivered)
self._outlet.set_packet_timeout_callback(envelope.packet, self._packet_timeout, self._get_packet_timeout_time(envelope.tries))
self._update_packet_timeouts()
return envelope
@property
def mdu(self):
"""
Maximum Data Unit: the number of bytes available
for a message to consume in a single send. This
value is adjusted from the ``Link`` MDU to accommodate
message header information.
:return: number of bytes available
"""
mdu = self._outlet.mdu - 6 # sizeof(msgtype) + sizeof(length) + sizeof(sequence)
if mdu > 0xFFFF:
mdu = 0xFFFF
return mdu
class LinkChannelOutlet(ChannelOutletBase):
"""
An implementation of ChannelOutletBase for RNS.Link.
Allows Channel to send packets over an RNS Link with
Packets.
:param link: RNS Link to wrap
"""
def __init__(self, link: RNS.Link):
self.link = link
def send(self, raw: bytes) -> RNS.Packet:
packet = RNS.Packet(self.link, raw, context=RNS.Packet.CHANNEL)
if self.link.status == RNS.Link.ACTIVE:
packet.send()
return packet
def resend(self, packet: RNS.Packet) -> RNS.Packet:
receipt = packet.resend()
if not receipt:
RNS.log("Failed to resend packet", RNS.LOG_ERROR)
return packet
@property
def mdu(self):
return self.link.mdu
@property
def rtt(self):
return self.link.rtt
@property
def is_usable(self):
return True # had issues looking at Link.status
def get_packet_state(self, packet: TPacket) -> MessageState:
if packet.receipt == None:
return MessageState.MSGSTATE_FAILED
status = packet.receipt.get_status()
if status == RNS.PacketReceipt.SENT:
return MessageState.MSGSTATE_SENT
if status == RNS.PacketReceipt.DELIVERED:
return MessageState.MSGSTATE_DELIVERED
if status == RNS.PacketReceipt.FAILED:
return MessageState.MSGSTATE_FAILED
else:
raise Exception(f"Unexpected receipt state: {status}")
def timed_out(self):
self.link.teardown()
def __str__(self):
return f"{self.__class__.__name__}({self.link})"
def set_packet_timeout_callback(self, packet: RNS.Packet, callback: Callable[[RNS.Packet], None] | None,
timeout: float | None = None):
if timeout and packet.receipt:
packet.receipt.set_timeout(timeout)
def inner(receipt: RNS.PacketReceipt):
callback(packet)
if packet and packet.receipt:
packet.receipt.set_timeout_callback(inner if callback else None)
def set_packet_delivered_callback(self, packet: RNS.Packet, callback: Callable[[RNS.Packet], None] | None):
def inner(receipt: RNS.PacketReceipt):
callback(packet)
if packet and packet.receipt:
packet.receipt.set_delivery_callback(inner if callback else None)
def get_packet_id(self, packet: RNS.Packet) -> any:
if packet and hasattr(packet, "get_hash") and callable(packet.get_hash):
return packet.get_hash()
else:
return None
+55 -12
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -24,23 +32,57 @@ import RNS.Cryptography.Provider as cp
import RNS.vendor.platformutils as pu
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
from .aes import AES
from .aes import AES128
from .aes import AES256
elif cp.PROVIDER == cp.PROVIDER_PYCA:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
if pu.cryptography_old_api():
from cryptography.hazmat.backends import default_backend
if pu.cryptography_old_api(): from cryptography.hazmat.backends import default_backend
class AES_128_CBC:
@staticmethod
def encrypt(plaintext, key, iv):
if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
cipher = AES(key)
cipher = AES128(key)
return cipher.encrypt(plaintext, iv)
elif cp.PROVIDER == cp.PROVIDER_PYCA:
if not pu.cryptography_old_api():
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
else:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
return ciphertext
@staticmethod
def decrypt(ciphertext, key, iv):
if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
cipher = AES128(key)
return cipher.decrypt(ciphertext, iv)
elif cp.PROVIDER == cp.PROVIDER_PYCA:
if not pu.cryptography_old_api():
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
else:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
return plaintext
class AES_256_CBC:
@staticmethod
def encrypt(plaintext, key, iv):
if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
cipher = AES256(key)
return cipher.encrypt_cbc(plaintext, iv)
elif cp.PROVIDER == cp.PROVIDER_PYCA:
if not pu.cryptography_old_api():
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
@@ -53,9 +95,10 @@ class AES_128_CBC:
@staticmethod
def decrypt(ciphertext, key, iv):
if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
cipher = AES(key)
return cipher.decrypt(ciphertext, iv)
cipher = AES256(key)
return cipher.decrypt_cbc(ciphertext, iv)
elif cp.PROVIDER == cp.PROVIDER_PYCA:
if not pu.cryptography_old_api():
+30 -1
View File
@@ -1,3 +1,33 @@
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
from .pure25519 import ed25519_oop as ed25519
@@ -5,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):
-110
View File
@@ -1,110 +0,0 @@
# MIT License
#
# Copyright (c) 2022 Mark Qvist / unsigned.io
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import time
from RNS.Cryptography import HMAC
from RNS.Cryptography import PKCS7
from RNS.Cryptography.AES import AES_128_CBC
class Fernet():
"""
This class provides a slightly modified implementation of the Fernet spec
found at: https://github.com/fernet/spec/blob/master/Spec.md
According to the spec, a Fernet token includes a one byte VERSION and
eight byte TIMESTAMP field at the start of each token. These fields are
not relevant to Reticulum. They are therefore stripped from this
implementation, since they incur overhead and leak initiator metadata.
"""
FERNET_OVERHEAD = 48 # Bytes
@staticmethod
def generate_key():
return os.urandom(32)
def __init__(self, key = None):
if key == None:
raise ValueError("Fernet key cannot be None")
if len(key) != 32:
raise ValueError("Fernet key must be 32 bytes, not "+str(len(key)))
self._signing_key = key[:16]
self._encryption_key = key[16:]
def verify_hmac(self, token):
if len(token) <= 32:
raise ValueError("Cannot verify HMAC on token of only "+str(len(token))+" bytes")
else:
received_hmac = token[-32:]
expected_hmac = HMAC.new(self._signing_key, token[:-32]).digest()
if received_hmac == expected_hmac:
return True
else:
return False
def encrypt(self, data = None):
iv = os.urandom(16)
current_time = int(time.time())
if not isinstance(data, bytes):
raise TypeError("Fernet token plaintext input must be bytes")
ciphertext = AES_128_CBC.encrypt(
plaintext = PKCS7.pad(data),
key = self._encryption_key,
iv = iv,
)
signed_parts = iv+ciphertext
return signed_parts + HMAC.new(self._signing_key, signed_parts).digest()
def decrypt(self, token = None):
if not isinstance(token, bytes):
raise TypeError("Fernet token must be bytes")
if not self.verify_hmac(token):
raise ValueError("Fernet token HMAC was invalid")
iv = token[:16]
ciphertext = token[16:-32]
try:
plaintext = PKCS7.unpad(
AES_128_CBC.decrypt(
ciphertext,
self._encryption_key,
iv,
)
)
return plaintext
except Exception as e:
raise ValueError("Could not decrypt Fernet token")
+15 -10
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -33,15 +41,12 @@ def hkdf(length=None, derive_from=None, salt=None, context=None):
if length == None or length < 1:
raise ValueError("Invalid output key length")
if derive_from == "None" or derive_from == "":
if derive_from == None or derive_from == "":
raise ValueError("Cannot derive key from empty input material")
if salt == None or len(salt) == 0:
salt = bytes([0] * hash_len)
if salt == None:
salt = b""
if context == None:
context = b""
@@ -51,7 +56,7 @@ def hkdf(length=None, derive_from=None, salt=None, context=None):
derived = b""
for i in range(ceil(length / hash_len)):
block = hmac_sha256(pseudorandom_key, block + context + bytes([i + 1]))
block = hmac_sha256(pseudorandom_key, block + context + bytes([(i + 1)%(0xFF+1)]))
derived += block
return derived[:length]
return derived[:length]
+35 -1
View File
@@ -1,4 +1,34 @@
import importlib
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import importlib.util
if importlib.util.find_spec('hashlib') != None:
import hashlib
else:
@@ -32,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()
+12 -4
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+33 -2
View File
@@ -1,16 +1,47 @@
import importlib
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import importlib.util
PROVIDER_NONE = 0x00
PROVIDER_INTERNAL = 0x01
PROVIDER_PYCA = 0x02
FORCE_INTERNAL = False
PROVIDER = PROVIDER_NONE
pyca_v = None
use_pyca = False
try:
if importlib.util.find_spec('cryptography') != None:
if not FORCE_INTERNAL and importlib.util.find_spec('cryptography') != None:
import cryptography
pyca_v = cryptography.__version__
v = pyca_v.split(".")
+30
View File
@@ -1,3 +1,33 @@
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
+114
View File
@@ -0,0 +1,114 @@
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import time
from RNS.Cryptography import HMAC
from RNS.Cryptography import PKCS7
from RNS.Cryptography import AES
from RNS.Cryptography.AES import AES_128_CBC
from RNS.Cryptography.AES import AES_256_CBC
class Token():
"""
This class provides a slightly modified implementation of the Fernet spec
found at: https://github.com/fernet/spec/blob/master/Spec.md
According to the spec, a Fernet token includes a one byte VERSION and
eight byte TIMESTAMP field at the start of each token. These fields are
not relevant to Reticulum. They are therefore stripped from this
implementation, since they incur overhead and leak initiator metadata.
"""
TOKEN_OVERHEAD = 48 # Bytes
@staticmethod
def generate_key(mode=AES_256_CBC):
if mode == AES_128_CBC: return os.urandom(32)
elif mode == AES_256_CBC: return os.urandom(64)
else: raise TypeError(f"Invalid token mode: {mode}")
def __init__(self, key=None, mode=AES):
if key == None: raise ValueError("Token key cannot be None")
if mode == AES:
if len(key) == 32:
self.mode = AES_128_CBC
self._signing_key = key[:16]
self._encryption_key = key[16:]
elif len(key) == 64:
self.mode = AES_256_CBC
self._signing_key = key[:32]
self._encryption_key = key[32:]
else: raise ValueError("Token key must be 128 or 256 bits, not "+str(len(key)*8))
else: raise TypeError(f"Invalid token mode: {mode}")
def verify_hmac(self, token):
if len(token) <= 32: raise ValueError("Cannot verify HMAC on token of only "+str(len(token))+" bytes")
else:
received_hmac = token[-32:]
expected_hmac = HMAC.new(self._signing_key, token[:-32]).digest()
if received_hmac == expected_hmac: return True
else: return False
def encrypt(self, data = None):
if not isinstance(data, bytes): raise TypeError("Token plaintext input must be bytes")
iv = os.urandom(16)
ciphertext = self.mode.encrypt(
plaintext = PKCS7.pad(data),
key = self._encryption_key,
iv = iv)
signed_parts = iv+ciphertext
return signed_parts + HMAC.new(self._signing_key, signed_parts).digest()
def decrypt(self, token = None):
if not isinstance(token, bytes): raise TypeError("Token must be bytes")
if not self.verify_hmac(token): raise ValueError("Token HMAC was invalid")
iv = token[:16]
ciphertext = token[16:-32]
try:
return PKCS7.unpad(
self.mode.decrypt(
ciphertext = ciphertext,
key = self._encryption_key,
iv = iv))
except Exception as e: raise ValueError(f"Could not decrypt token: {e}")
+4 -1
View File
@@ -82,10 +82,13 @@ def _fix_secret(n):
n |= 64 << 8 * 31
return n
def _fix_base_point(n):
n &= ~(2**255)
return n
def curve25519(base_point_raw, secret_raw):
"""Raise the base point to a given power"""
base_point = _unpack_number(base_point_raw)
base_point = _fix_base_point(_unpack_number(base_point_raw))
secret = _fix_secret(_unpack_number(secret_raw))
return _pack_number(_raw_curve25519(base_point, secret))
+35 -3
View File
@@ -1,3 +1,33 @@
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import glob
@@ -5,7 +35,7 @@ from .Hashes import sha256
from .Hashes import sha512
from .HKDF import hkdf
from .PKCS7 import PKCS7
from .Fernet import Fernet
from .Token import Token
from .Provider import backend
import RNS.Cryptography.Provider as cp
@@ -20,5 +50,7 @@ elif cp.PROVIDER == cp.PROVIDER_PYCA:
from RNS.Cryptography.Proxies import Ed25519PrivateKeyProxy as Ed25519PrivateKey
from RNS.Cryptography.Proxies import Ed25519PublicKeyProxy as Ed25519PublicKey
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
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"))]))
+2 -1
View File
@@ -1 +1,2 @@
from .aes import AES
from .aes128 import AES128
from .aes256 import AES256
-271
View File
@@ -1,271 +0,0 @@
# MIT License
# Copyright (c) 2021 Or Gur Arie
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .utils import *
class AES:
# AES-128 block size
block_size = 16
# AES-128 encrypts messages with 10 rounds
_rounds = 10
# initiate the AES objecy
def __init__(self, key):
"""
Initializes the object with a given key.
"""
# make sure key length is right
assert len(key) == AES.block_size
# ExpandKey
self._round_keys = self._expand_key(key)
# will perform the AES ExpandKey phase
def _expand_key(self, master_key):
"""
Expands and returns a list of key matrices for the given master_key.
"""
# Initialize round keys with raw key material.
key_columns = bytes2matrix(master_key)
iteration_size = len(master_key) // 4
# Each iteration has exactly as many columns as the key material.
i = 1
while len(key_columns) < (self._rounds + 1) * 4:
# Copy previous word.
word = list(key_columns[-1])
# Perform schedule_core once every "row".
if len(key_columns) % iteration_size == 0:
# Circular shift.
word.append(word.pop(0))
# Map to S-BOX.
word = [s_box[b] for b in word]
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
word[0] ^= r_con[i]
i += 1
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
# Run word through S-box in the fourth iteration when using a
# 256-bit key.
word = [s_box[b] for b in word]
# XOR with equivalent word from previous iteration.
word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size]))
key_columns.append(word)
# Group key words in 4x4 byte matrices.
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
# encrypt a single block of data with AES
def _encrypt_block(self, plaintext):
"""
Encrypts a single block of 16 byte long plaintext.
"""
# length of a single block
assert len(plaintext) == AES.block_size
# perform on a matrix
state = bytes2matrix(plaintext)
# AddRoundKey
add_round_key(state, self._round_keys[0])
# 9 main rounds
for i in range(1, self._rounds):
# SubBytes
sub_bytes(state)
# ShiftRows
shift_rows(state)
# MixCols
mix_columns(state)
# AddRoundKey
add_round_key(state, self._round_keys[i])
# last round, w/t AddRoundKey step
sub_bytes(state)
shift_rows(state)
add_round_key(state, self._round_keys[-1])
# return the encrypted matrix as bytes
return matrix2bytes(state)
# decrypt a single block of data with AES
def _decrypt_block(self, ciphertext):
"""
Decrypts a single block of 16 byte long ciphertext.
"""
# length of a single block
assert len(ciphertext) == AES.block_size
# perform on a matrix
state = bytes2matrix(ciphertext)
# in reverse order, last round is first
add_round_key(state, self._round_keys[-1])
inv_shift_rows(state)
inv_sub_bytes(state)
for i in range(self._rounds - 1, 0, -1):
# nain rounds
add_round_key(state, self._round_keys[i])
inv_mix_columns(state)
inv_shift_rows(state)
inv_sub_bytes(state)
# initial AddRoundKey phase
add_round_key(state, self._round_keys[0])
# return bytes
return matrix2bytes(state)
# will encrypt the entire data
def encrypt(self, plaintext, iv):
"""
Encrypts `plaintext` using CBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
# iv length must be same as block size
assert len(iv) == AES.block_size
assert len(plaintext) % AES.block_size == 0
ciphertext_blocks = []
previous = iv
for plaintext_block in split_blocks(plaintext):
# in CBC mode every block is XOR'd with the previous block
xorred = xor_bytes(plaintext_block, previous)
# encrypt current block
block = self._encrypt_block(xorred)
previous = block
# append to ciphertext
ciphertext_blocks.append(block)
# return as bytes
return b''.join(ciphertext_blocks)
# will decrypt the entire data
def decrypt(self, ciphertext, iv):
"""
Decrypts `ciphertext` using CBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
# iv length must be same as block size
assert len(iv) == AES.block_size
plaintext_blocks = []
previous = iv
for ciphertext_block in split_blocks(ciphertext):
# in CBC mode every block is XOR'd with the previous block
xorred = xor_bytes(previous, self._decrypt_block(ciphertext_block))
# append plaintext
plaintext_blocks.append(xorred)
previous = ciphertext_block
return b''.join(plaintext_blocks)
def test():
# modules and classes requiered for test only
import os
class bcolors:
OK = '\033[92m' #GREEN
WARNING = '\033[93m' #YELLOW
FAIL = '\033[91m' #RED
RESET = '\033[0m' #RESET COLOR
# will test AES class by performing an encryption / decryption
print("AES Tests")
print("=========")
# generate a secret key and print details
key = os.urandom(AES.block_size)
_aes = AES(key)
print(f"Algorithm: AES-CBC-{AES.block_size*8}")
print(f"Secret Key: {key.hex()}")
print()
# test single block encryption / decryption
iv = os.urandom(AES.block_size)
single_block_text = b"SingleBlock Text"
print("Single Block Tests")
print("------------------")
print(f"iv: {iv.hex()}")
print(f"plain text: '{single_block_text.decode()}'")
ciphertext_block = _aes._encrypt_block(single_block_text)
plaintext_block = _aes._decrypt_block(ciphertext_block)
print(f"Ciphertext Hex: {ciphertext_block.hex()}")
print(f"Plaintext: {plaintext_block.decode()}")
assert plaintext_block == single_block_text
print(bcolors.OK + "Single Block Test Passed Successfully" + bcolors.RESET)
print()
# test a less than a block length phrase
iv = os.urandom(AES.block_size)
short_text = b"Just Text"
print("Short Text Tests")
print("----------------")
print(f"iv: {iv.hex()}")
print(f"plain text: '{short_text.decode()}'")
ciphertext_short = _aes.encrypt(short_text, iv)
plaintext_short = _aes.decrypt(ciphertext_short, iv)
print(f"Ciphertext Hex: {ciphertext_short.hex()}")
print(f"Plaintext: {plaintext_short.decode()}")
assert short_text == plaintext_short
print(bcolors.OK + "Short Text Test Passed Successfully" + bcolors.RESET)
print()
# test an arbitrary length phrase
iv = os.urandom(AES.block_size)
text = b"This Text is longer than one block"
print("Arbitrary Length Tests")
print("----------------------")
print(f"iv: {iv.hex()}")
print(f"plain text: '{text.decode()}'")
ciphertext = _aes.encrypt(text, iv)
plaintext = _aes.decrypt(ciphertext, iv)
print(f"Ciphertext Hex: {ciphertext.hex()}")
print(f"Plaintext: {plaintext.decode()}")
assert text == plaintext
print(bcolors.OK + "Arbitrary Length Text Test Passed Successfully" + bcolors.RESET)
print()
if __name__ == "__main__":
# test AES class
test()
+326
View File
@@ -0,0 +1,326 @@
# MIT License
# Copyright (c) 2021 Or Gur Arie
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
## AES lookup tables
# resource: https://en.wikipedia.org/wiki/Rijndael_S-box
s_box = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)
inv_s_box = (
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)
## AES AddRoundKey
# Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants
r_con = (
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)
def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]
## AES SubBytes
def sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = s_box[s[i][j]]
def inv_sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = inv_s_box[s[i][j]]
## AES ShiftRows
def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
def inv_shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
## AES MixColumns
# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
def mix_single_column(a):
# see Sec 4.1.2 in The Design of Rijndael
t = a[0] ^ a[1] ^ a[2] ^ a[3]
u = a[0]
a[0] ^= t ^ xtime(a[0] ^ a[1])
a[1] ^= t ^ xtime(a[1] ^ a[2])
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)
def mix_columns(s):
for i in range(4):
mix_single_column(s[i])
def inv_mix_columns(s):
# see Sec 4.1.3 in The Design of Rijndael
for i in range(4):
u = xtime(xtime(s[i][0] ^ s[i][2]))
v = xtime(xtime(s[i][1] ^ s[i][3]))
s[i][0] ^= u
s[i][1] ^= v
s[i][2] ^= u
s[i][3] ^= v
mix_columns(s)
## AES Bytes
def bytes2matrix(text):
""" Converts a 16-byte array into a 4x4 matrix. """
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
return bytes(sum(matrix, []))
def xor_bytes(a, b):
""" Returns a new byte array with the elements xor'ed. """
return bytes(i^j for i, j in zip(a, b))
def split_blocks(message, block_size=16, require_padding=True):
assert len(message) % block_size == 0 or not require_padding
return [message[i:i+16] for i in range(0, len(message), block_size)]
class AES128:
# AES-128 block size
block_size = 16
# AES-128 encrypts messages with 10 rounds
_rounds = 10
# initiate the AES objecy
def __init__(self, key):
"""
Initializes the object with a given key.
"""
# make sure key length is right
assert len(key) == AES128.block_size
# ExpandKey
self._round_keys = self._expand_key(key)
# will perform the AES ExpandKey phase
def _expand_key(self, master_key):
"""
Expands and returns a list of key matrices for the given master_key.
"""
# Initialize round keys with raw key material.
key_columns = bytes2matrix(master_key)
iteration_size = len(master_key) // 4
# Each iteration has exactly as many columns as the key material.
i = 1
while len(key_columns) < (self._rounds + 1) * 4:
# Copy previous word.
word = list(key_columns[-1])
# Perform schedule_core once every "row".
if len(key_columns) % iteration_size == 0:
# Circular shift.
word.append(word.pop(0))
# Map to S-BOX.
word = [s_box[b] for b in word]
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
word[0] ^= r_con[i]
i += 1
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
# Run word through S-box in the fourth iteration when using a
# 256-bit key.
word = [s_box[b] for b in word]
# XOR with equivalent word from previous iteration.
word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size]))
key_columns.append(word)
# Group key words in 4x4 byte matrices.
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
# encrypt a single block of data with AES
def _encrypt_block(self, plaintext):
"""
Encrypts a single block of 16 byte long plaintext.
"""
# length of a single block
assert len(plaintext) == AES128.block_size
# perform on a matrix
state = bytes2matrix(plaintext)
# AddRoundKey
add_round_key(state, self._round_keys[0])
# 9 main rounds
for i in range(1, self._rounds):
# SubBytes
sub_bytes(state)
# ShiftRows
shift_rows(state)
# MixCols
mix_columns(state)
# AddRoundKey
add_round_key(state, self._round_keys[i])
# last round, w/t AddRoundKey step
sub_bytes(state)
shift_rows(state)
add_round_key(state, self._round_keys[-1])
# return the encrypted matrix as bytes
return matrix2bytes(state)
# decrypt a single block of data with AES
def _decrypt_block(self, ciphertext):
"""
Decrypts a single block of 16 byte long ciphertext.
"""
# length of a single block
assert len(ciphertext) == AES128.block_size
# perform on a matrix
state = bytes2matrix(ciphertext)
# in reverse order, last round is first
add_round_key(state, self._round_keys[-1])
inv_shift_rows(state)
inv_sub_bytes(state)
for i in range(self._rounds - 1, 0, -1):
# nain rounds
add_round_key(state, self._round_keys[i])
inv_mix_columns(state)
inv_shift_rows(state)
inv_sub_bytes(state)
# initial AddRoundKey phase
add_round_key(state, self._round_keys[0])
# return bytes
return matrix2bytes(state)
# will encrypt the entire data
def encrypt(self, plaintext, iv):
"""
Encrypts `plaintext` using CBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
# iv length must be same as block size
assert len(iv) == AES128.block_size
assert len(plaintext) % AES128.block_size == 0
ciphertext_blocks = []
previous = iv
for plaintext_block in split_blocks(plaintext):
# in CBC mode every block is XOR'd with the previous block
xorred = xor_bytes(plaintext_block, previous)
# encrypt current block
block = self._encrypt_block(xorred)
previous = block
# append to ciphertext
ciphertext_blocks.append(block)
# return as bytes
return b''.join(ciphertext_blocks)
# will decrypt the entire data
def decrypt(self, ciphertext, iv):
"""
Decrypts `ciphertext` using CBC mode and PKCS#7 padding, with the given
initialization vector (iv).
"""
# iv length must be same as block size
assert len(iv) == AES128.block_size
plaintext_blocks = []
previous = iv
for ciphertext_block in split_blocks(ciphertext):
# in CBC mode every block is XOR'd with the previous block
xorred = xor_bytes(previous, self._decrypt_block(ciphertext_block))
# append plaintext
plaintext_blocks.append(xorred)
previous = ciphertext_block
return b''.join(plaintext_blocks)
@@ -1,17 +1,17 @@
# MIT License
# Copyright (c) 2021 Or Gur Arie
#
# Copyright (c) 2024 BoppreH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -20,12 +20,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
'''
Utils class for AES encryption / decryption
'''
## AES lookup tables
# resource: https://en.wikipedia.org/wiki/Rijndael_S-box
s_box = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
@@ -64,53 +58,33 @@ inv_s_box = (
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)
## AES AddRoundKey
# Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants
r_con = (
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)
def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]
## AES SubBytes
def sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = s_box[s[i][j]]
def inv_sub_bytes(s):
for i in range(4):
for j in range(4):
s[i][j] = inv_s_box[s[i][j]]
## AES ShiftRows
def shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
def inv_shift_rows(s):
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
def add_round_key(s, k):
for i in range(4):
for j in range(4):
s[i][j] ^= k[i][j]
## AES MixColumns
# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
def mix_single_column(a):
# see Sec 4.1.2 in The Design of Rijndael
t = a[0] ^ a[1] ^ a[2] ^ a[3]
@@ -120,12 +94,10 @@ def mix_single_column(a):
a[2] ^= t ^ xtime(a[2] ^ a[3])
a[3] ^= t ^ xtime(a[3] ^ u)
def mix_columns(s):
for i in range(4):
mix_single_column(s[i])
def inv_mix_columns(s):
# see Sec 4.1.3 in The Design of Rijndael
for i in range(4):
@@ -138,22 +110,127 @@ def inv_mix_columns(s):
mix_columns(s)
## AES Bytes
def bytes2matrix(text):
""" Converts a 16-byte array into a 4x4 matrix. """
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
def matrix2bytes(matrix):
""" Converts a 4x4 matrix into a 16-byte array. """
return bytes(sum(matrix, []))
r_con = (
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)
def xor_bytes(a, b):
""" Returns a new byte array with the elements xor'ed. """
return bytes(i^j for i, j in zip(a, b))
def bytes2matrix(text): return [list(text[i:i+4]) for i in range(0, len(text), 4)]
def matrix2bytes(matrix): return bytes(sum(matrix, []))
def xor_bytes(a, b): return bytes(i^j for i, j in zip(a, b))
def inc_bytes(a):
out = list(a)
for i in reversed(range(len(out))):
if out[i] == 0xFF:
out[i] = 0
else:
out[i] += 1
break
return bytes(out)
def split_blocks(message, block_size=16, require_padding=True):
assert len(message) % block_size == 0 or not require_padding
return [message[i:i+16] for i in range(0, len(message), block_size)]
assert len(message) % block_size == 0 or not require_padding
return [message[i:i+16] for i in range(0, len(message), block_size)]
class AES256:
rounds_by_key_size = {32: 14}
def __init__(self, master_key):
assert len(master_key) in AES256.rounds_by_key_size
self.n_rounds = AES256.rounds_by_key_size[len(master_key)]
self._key_matrices = self._expand_key(master_key)
def _expand_key(self, master_key):
# Initialize round keys with raw key material.
key_columns = bytes2matrix(master_key)
iteration_size = len(master_key) // 4
i = 1
while len(key_columns) < (self.n_rounds + 1) * 4:
# Copy previous word.
word = list(key_columns[-1])
# Perform schedule_core once every "row".
if len(key_columns) % iteration_size == 0:
# Circular shift.
word.append(word.pop(0))
# Map to S-BOX.
word = [s_box[b] for b in word]
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
word[0] ^= r_con[i]
i += 1
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
# Run word through S-box in the fourth iteration when using a
# 256-bit key.
word = [s_box[b] for b in word]
# XOR with equivalent word from previous iteration.
word = xor_bytes(word, key_columns[-iteration_size])
key_columns.append(word)
# Group key words in 4x4 byte matrices.
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
def encrypt_block(self, plaintext):
assert len(plaintext) == 16
plain_state = bytes2matrix(plaintext)
add_round_key(plain_state, self._key_matrices[0])
for i in range(1, self.n_rounds):
sub_bytes(plain_state)
shift_rows(plain_state)
mix_columns(plain_state)
add_round_key(plain_state, self._key_matrices[i])
sub_bytes(plain_state)
shift_rows(plain_state)
add_round_key(plain_state, self._key_matrices[-1])
return matrix2bytes(plain_state)
def decrypt_block(self, ciphertext):
assert len(ciphertext) == 16
cipher_state = bytes2matrix(ciphertext)
add_round_key(cipher_state, self._key_matrices[-1])
inv_shift_rows(cipher_state)
inv_sub_bytes(cipher_state)
for i in range(self.n_rounds - 1, 0, -1):
add_round_key(cipher_state, self._key_matrices[i])
inv_mix_columns(cipher_state)
inv_shift_rows(cipher_state)
inv_sub_bytes(cipher_state)
add_round_key(cipher_state, self._key_matrices[0])
return matrix2bytes(cipher_state)
def encrypt_cbc(self, plaintext, iv):
if len(iv) != 16: raise ValueError(f"Invalid IV length: {len(iv)}")
blocks = []
previous = iv
for plaintext_block in split_blocks(plaintext):
block = self.encrypt_block(xor_bytes(plaintext_block, previous))
blocks.append(block)
previous = block
return b''.join(blocks)
def decrypt_cbc(self, ciphertext, iv):
if len(iv) != 16: raise ValueError(f"Invalid IV length: {len(iv)}")
blocks = []
previous = iv
for ciphertext_block in split_blocks(ciphertext):
blocks.append(xor_bytes(previous, self.decrypt_block(ciphertext_block)))
previous = ciphertext_block
return b''.join(blocks)
__all__ = ["AES256"]
+296 -70
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -20,11 +28,14 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import math
import time
import threading
import RNS
from RNS.Cryptography import Fernet
from RNS.Cryptography import Token
from .vendor import umsgpack as umsgpack
class Callbacks:
def __init__(self):
@@ -38,14 +49,14 @@ class Destination:
instances are used both to create outgoing and incoming endpoints. The
destination type will decide if encryption, and what type, is used in
communication with the endpoint. A destination can also announce its
presence on the network, which will also distribute necessary keys for
presence on the network, which will distribute necessary keys for
encrypted communication with it.
:param identity: An instance of :ref:`RNS.Identity<api-identity>`. Can hold only public keys for an outgoing destination, or holding private keys for an ingoing.
:param direction: ``RNS.Destination.IN`` or ``RNS.Destination.OUT``.
:param type: ``RNS.Destination.SINGLE``, ``RNS.Destination.GROUP`` or ``RNS.Destination.PLAIN``.
:param app_name: A string specifying the app name.
:param \*aspects: Any non-zero number of string arguments.
:param \\*aspects: Any non-zero number of string arguments.
"""
# Constants
@@ -69,6 +80,18 @@ class Destination:
OUT = 0x12;
directions = [IN, OUT]
PR_TAG_WINDOW = 30
RATCHET_COUNT = 512
"""
The default number of generated ratchet keys a destination will retain, if it has ratchets enabled.
"""
RATCHET_INTERVAL = 30*60
"""
The minimum interval between rotating ratchet keys, in seconds.
"""
@staticmethod
def expand_name(identity, app_name, *aspects):
"""
@@ -97,7 +120,12 @@ class Destination:
name_hash = RNS.Identity.full_hash(Destination.expand_name(None, app_name, *aspects).encode("utf-8"))[:(RNS.Identity.NAME_HASH_LENGTH//8)]
addr_hash_material = name_hash
if identity != None:
addr_hash_material += identity.hash
if isinstance(identity, RNS.Identity):
addr_hash_material += identity.hash
elif isinstance(identity, bytes) and len(identity) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
addr_hash_material += identity
else:
raise TypeError("Invalid material supplied for destination hash calculation")
return RNS.Identity.full_hash(addr_hash_material)[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8]
@@ -130,14 +158,26 @@ class Destination:
self.type = type
self.direction = direction
self.proof_strategy = Destination.PROVE_NONE
self.ratchets = None
self.ratchets_path = None
self.ratchet_interval = Destination.RATCHET_INTERVAL
self.ratchet_file_lock = threading.Lock()
self.retained_ratchets = Destination.RATCHET_COUNT
self.latest_ratchet_time = None
self.latest_ratchet_id = None
self.__enforce_ratchets = False
self.mtu = 0
self.path_responses = {}
self.links = []
if identity == None and direction == Destination.IN and self.type != Destination.PLAIN:
identity = RNS.Identity()
aspects = aspects+(identity.hexhash,)
if identity == None and direction == Destination.OUT and self.type != Destination.PLAIN:
raise ValueError("Can't create outbound SINGLE destination without an identity")
if identity != None and self.type == Destination.PLAIN:
raise TypeError("Selected destination type PLAIN cannot hold an identity")
@@ -160,10 +200,47 @@ class Destination:
"""
:returns: A human-readable representation of the destination including addressable hash and full name.
"""
return "<"+self.name+"/"+self.hexhash+">"
return "<"+self.name+":"+self.hexhash+">"
def _clean_ratchets(self):
if self.ratchets != None:
if len (self.ratchets) > self.retained_ratchets:
self.ratchets = self.ratchets[:Destination.RATCHET_COUNT]
def announce(self, app_data=None, path_response=False, send=True):
def _persist_ratchets(self):
try:
with self.ratchet_file_lock:
temp_write_path = self.ratchets_path+".tmp"
packed_ratchets = umsgpack.packb(self.ratchets)
persisted_data = {"signature": self.sign(packed_ratchets), "ratchets": packed_ratchets}
ratchets_file = open(temp_write_path, "wb")
ratchets_file.write(umsgpack.packb(persisted_data))
ratchets_file.close()
if os.path.isfile(self.ratchets_path): os.unlink(self.ratchets_path)
os.rename(temp_write_path, self.ratchets_path)
except Exception as e:
RNS.trace_exception(e)
self.ratchets = None
self.ratchets_path = None
raise OSError("Could not write ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
def rotate_ratchets(self):
if self.ratchets != None:
now = time.time()
if now > self.latest_ratchet_time+self.ratchet_interval:
RNS.log("Rotating ratchets for "+str(self), RNS.LOG_DEBUG)
new_ratchet = RNS.Identity._generate_ratchet()
self.ratchets.insert(0, new_ratchet)
self.latest_ratchet_time = now
self._clean_ratchets()
self._persist_ratchets()
return True
else:
raise SystemError("Cannot rotate ratchet on "+str(self)+", ratchets are not enabled")
return False
def announce(self, app_data=None, path_response=False, attached_interface=None, tag=None, send=True):
"""
Creates an announce packet for this destination and broadcasts it on all
relevant interfaces. Application specific data can be added to the announce.
@@ -173,40 +250,71 @@ class Destination:
"""
if self.type != Destination.SINGLE:
raise TypeError("Only SINGLE destination types can be announced")
destination_hash = self.hash
random_hash = RNS.Identity.get_random_hash()[0:5]+int(time.time()).to_bytes(5, "big")
if app_data == None and self.default_app_data != None:
if isinstance(self.default_app_data, bytes):
app_data = self.default_app_data
elif callable(self.default_app_data):
returned_app_data = self.default_app_data()
if isinstance(returned_app_data, bytes):
app_data = returned_app_data
if self.direction != Destination.IN:
raise TypeError("Only IN destination types can be announced")
signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash
if app_data != None:
signed_data += app_data
ratchet = b""
now = time.time()
stale_responses = []
for entry_tag in self.path_responses:
entry = self.path_responses[entry_tag]
if now > entry[0]+Destination.PR_TAG_WINDOW:
stale_responses.append(entry_tag)
signature = self.identity.sign(signed_data)
for entry_tag in stale_responses:
self.path_responses.pop(entry_tag)
announce_data = self.identity.get_public_key()+self.name_hash+random_hash+signature
if app_data != None:
announce_data += app_data
if path_response:
announce_context = RNS.Packet.PATH_RESPONSE
if (path_response == True and tag != None) and tag in self.path_responses:
# This code is currently not used, since Transport will block duplicate
# path requests based on tags. When multi-path support is implemented in
# Transport, this will allow Transport to detect redundant paths to the
# same destination, and select the best one based on chosen criteria,
# since it will be able to detect that a single emitted announce was
# received via multiple paths. The difference in reception time will
# potentially also be useful in determining characteristics of the
# multiple available paths, and to choose the best one.
RNS.log("Using cached announce data for answering path request with tag "+RNS.prettyhexrep(tag), RNS.LOG_EXTREME)
announce_data = self.path_responses[tag][1]
else:
announce_context = RNS.Packet.NONE
destination_hash = self.hash
random_hash = RNS.Identity.get_random_hash()[0:5]+int(time.time()).to_bytes(5, "big")
announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context)
if self.ratchets != None:
self.rotate_ratchets()
ratchet = RNS.Identity._ratchet_public_bytes(self.ratchets[0])
RNS.Identity._remember_ratchet(self.hash, ratchet)
if send:
announce_packet.send()
else:
return announce_packet
if app_data == None and self.default_app_data != None:
if isinstance(self.default_app_data, bytes):
app_data = self.default_app_data
elif callable(self.default_app_data):
returned_app_data = self.default_app_data()
if isinstance(returned_app_data, bytes):
app_data = returned_app_data
signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash+ratchet
if app_data != None: signed_data += app_data
signature = self.identity.sign(signed_data)
announce_data = self.identity.get_public_key()+self.name_hash+random_hash+ratchet+signature
if app_data != None: announce_data += app_data
self.path_responses[tag] = [time.time(), announce_data]
if path_response: announce_context = RNS.Packet.PATH_RESPONSE
else: announce_context = RNS.Packet.NONE
if ratchet: context_flag = RNS.Packet.FLAG_SET
else: context_flag = RNS.Packet.FLAG_UNSET
announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context,
attached_interface = attached_interface, context_flag=context_flag)
if send: announce_packet.send()
else: return announce_packet
def accepts_links(self, accepts = None):
"""
@@ -215,13 +323,10 @@ class Destination:
:param accepts: If ``True`` or ``False``, this method sets whether the destination accepts incoming link requests. If not provided or ``None``, the method returns whether the destination currently accepts link requests.
:returns: ``True`` or ``False`` depending on whether the destination accepts incoming link requests, if the *accepts* parameter is not provided or ``None``.
"""
if accepts == None:
return self.accept_link_requests
if accepts == None: return self.accept_link_requests
if accepts:
self.accept_link_requests = True
else:
self.accept_link_requests = False
if accepts: self.accept_link_requests = True
else: self.accept_link_requests = False
def set_link_established_callback(self, callback):
"""
@@ -262,29 +367,25 @@ class Destination:
else:
self.proof_strategy = proof_strategy
def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None):
def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None, auto_compress = True):
"""
Registers a request handler.
:param path: The path for the request handler to be registered.
:param response_generator: A function or method with the signature *response_generator(path, data, request_id, remote_identity, requested_at)* to be called. Whatever this funcion returns will be sent as a response to the requester. If the function returns ``None``, no response will be sent.
:param response_generator: A function or method with the signature *response_generator(path, data, request_id, link_id, remote_identity, requested_at)* to be called. Whatever this funcion returns will be sent as a response to the requester. If the function returns ``None``, no response will be sent.
:param allow: One of ``RNS.Destination.ALLOW_NONE``, ``RNS.Destination.ALLOW_ALL`` or ``RNS.Destination.ALLOW_LIST``. If ``RNS.Destination.ALLOW_LIST`` is set, the request handler will only respond to requests for identified peers in the supplied list.
:param allowed_list: A list of *bytes-like* :ref:`RNS.Identity<api-identity>` hashes.
:param auto_compress: If ``True`` or ``False``, determines whether automatic compression of responses should be carried out. If set to an integer value, responses will only be auto-compressed if under this size in bytes. If omitted, the default compression settings will be followed.
:raises: ``ValueError`` if any of the supplied arguments are invalid.
"""
if path == None or path == "":
raise ValueError("Invalid path specified")
elif not callable(response_generator):
raise ValueError("Invalid response generator specified")
elif not allow in Destination.request_policies:
raise ValueError("Invalid request policy")
if path == None or path == "": raise ValueError("Invalid path specified")
elif not callable(response_generator): raise ValueError("Invalid response generator specified")
elif not allow in Destination.request_policies: raise ValueError("Invalid request policy")
else:
path_hash = RNS.Identity.truncated_hash(path.encode("utf-8"))
request_handler = [path, response_generator, allow, allowed_list]
request_handler = [path, response_generator, allow, allowed_list, auto_compress]
self.request_handlers[path_hash] = request_handler
def deregister_request_handler(self, path):
"""
Deregisters a request handler.
@@ -299,22 +400,22 @@ class Destination:
else:
return False
def receive(self, packet):
if packet.packet_type == RNS.Packet.LINKREQUEST:
plaintext = packet.data
self.incoming_link_request(plaintext, packet)
else:
plaintext = self.decrypt(packet.data)
if plaintext != None:
packet.ratchet_id = self.latest_ratchet_id
if plaintext == None: return False
else:
if packet.packet_type == RNS.Packet.DATA:
if self.callbacks.packet != None:
try:
self.callbacks.packet(plaintext, packet)
try: self.callbacks.packet(plaintext, packet)
except Exception as e:
RNS.log("Error while executing receive callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
return True
def incoming_link_request(self, data, packet):
if self.accept_link_requests:
@@ -322,6 +423,113 @@ class Destination:
if link != None:
self.links.append(link)
def _reload_ratchets(self, ratchets_path):
if os.path.isfile(ratchets_path):
with self.ratchet_file_lock:
def load_attempt():
ratchets_file = open(ratchets_path, "rb")
persisted_data = umsgpack.unpackb(ratchets_file.read())
if "signature" in persisted_data and "ratchets" in persisted_data:
if self.identity.validate(persisted_data["signature"], persisted_data["ratchets"]):
self.ratchets = umsgpack.unpackb(persisted_data["ratchets"])
self.ratchets_path = ratchets_path
else:
raise KeyError("Invalid ratchet file signature")
try:
try:
load_attempt()
except Exception as e:
RNS.trace_exception(e)
RNS.log(f"First ratchet reload attempt for {self} failed. Possible I/O conflict. Retrying in 500ms.", RNS.LOG_ERROR)
time.sleep(0.5)
load_attempt()
RNS.log(f"Ratchet reload retry succeeded", RNS.LOG_DEBUG)
except Exception as e:
self.ratchets = None
self.ratchets_path = None
RNS.trace_exception(e)
RNS.log(f"The ratchet file located at {ratchets_path} could not be loaded. This could indicate that the ratchet file has become corrupt.", RNS.LOG_CRITICAL)
RNS.log(f"You can attempt to manually recover the ratchet file, or simply remove it to have Reticulum recreate it on the next use.", RNS.LOG_CRITICAL)
RNS.log(f"If re-initialize this ratchet file, make sure to send an announce for the relevant destination as soon as possible,", RNS.LOG_CRITICAL)
RNS.log(f"so that the new ratchet information is synchronized to the network.", RNS.LOG_CRITICAL)
raise OSError("Could not read ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
RNS.log("No existing ratchet data found, initialising new ratchet file for "+str(self), RNS.LOG_DEBUG)
self.ratchets = []
self.ratchets_path = ratchets_path
self._persist_ratchets()
def enable_ratchets(self, ratchets_path):
"""
Enables ratchets on the destination. When ratchets are enabled, Reticulum will automatically rotate
the keys used to encrypt packets to this destination, and include the latest ratchet key in announces.
Enabling ratchets on a destination will provide forward secrecy for packets sent to that destination,
even when sent outside a ``Link``. The normal Reticulum ``Link`` establishment procedure already performs
its own ephemeral key exchange for each link establishment, which means that ratchets are not necessary
to provide forward secrecy for links.
Enabling ratchets will have a small impact on announce size, adding 32 bytes to every sent announce.
:param ratchets_path: The path to a file to store ratchet data in.
:returns: True if the operation succeeded, otherwise False.
"""
if ratchets_path != None:
self.latest_ratchet_time = 0
self._reload_ratchets(ratchets_path)
RNS.log("Ratchets enabled on "+str(self), RNS.LOG_DEBUG)
return True
else:
raise ValueError("No ratchet file path specified for "+str(self))
def enforce_ratchets(self):
"""
When ratchet enforcement is enabled, this destination will never accept packets that use its
base Identity key for encryption, but only accept packets encrypted with one of the retained
ratchet keys.
"""
if self.ratchets != None:
self.__enforce_ratchets = True
RNS.log("Ratchets enforced on "+str(self), RNS.LOG_DEBUG)
return True
else:
return False
def set_retained_ratchets(self, retained_ratchets):
"""
Sets the number of previously generated ratchet keys this destination will retain,
and try to use when decrypting incoming packets. Defaults to ``Destination.RATCHET_COUNT``.
:param retained_ratchets: The number of generated ratchets to retain.
:returns: True if the operation succeeded, False if not.
"""
if isinstance(retained_ratchets, int) and retained_ratchets > 0:
self.retained_ratchets = retained_ratchets
self._clean_ratchets()
return True
else:
return False
def set_ratchet_interval(self, interval):
"""
Sets the minimum interval in seconds between ratchet key rotation.
Defaults to ``Destination.RATCHET_INTERVAL``.
:param interval: The minimum interval in seconds.
:returns: True if the operation succeeded, False if not.
"""
if isinstance(interval, int) and interval > 0:
self.ratchet_interval = interval
return True
else:
return False
def create_keys(self):
"""
For a ``RNS.Destination.GROUP`` type destination, creates a new symmetric key.
@@ -335,9 +543,8 @@ class Destination:
raise TypeError("A single destination holds keys through an Identity instance")
if self.type == Destination.GROUP:
self.prv_bytes = Fernet.generate_key()
self.prv = Fernet(self.prv_bytes)
self.prv_bytes = Token.generate_key()
self.prv = Token(self.prv_bytes)
def get_private_key(self):
"""
@@ -352,7 +559,6 @@ class Destination:
else:
return self.prv_bytes
def load_private_key(self, key):
"""
For a ``RNS.Destination.GROUP`` type destination, loads a symmetric private key.
@@ -368,7 +574,7 @@ class Destination:
if self.type == Destination.GROUP:
self.prv_bytes = key
self.prv = Fernet(self.prv_bytes)
self.prv = Token(self.prv_bytes)
def load_public_key(self, key):
if self.type != Destination.SINGLE:
@@ -376,7 +582,6 @@ class Destination:
else:
raise TypeError("A single destination holds keys through an Identity instance")
def encrypt(self, plaintext):
"""
Encrypts information for ``RNS.Destination.SINGLE`` or ``RNS.Destination.GROUP`` type destination.
@@ -388,7 +593,10 @@ class Destination:
return plaintext
if self.type == Destination.SINGLE and self.identity != None:
return self.identity.encrypt(plaintext)
selected_ratchet = RNS.Identity.get_ratchet(self.hash)
if selected_ratchet:
self.latest_ratchet_id = RNS.Identity._get_ratchet_id(selected_ratchet)
return self.identity.encrypt(plaintext, ratchet=selected_ratchet)
if self.type == Destination.GROUP:
if hasattr(self, "prv") and self.prv != None:
@@ -400,8 +608,6 @@ class Destination:
else:
raise ValueError("No private key held by GROUP destination. Did you create or load one?")
def decrypt(self, ciphertext):
"""
Decrypts information for ``RNS.Destination.SINGLE`` or ``RNS.Destination.GROUP`` type destination.
@@ -413,7 +619,28 @@ class Destination:
return ciphertext
if self.type == Destination.SINGLE and self.identity != None:
return self.identity.decrypt(ciphertext)
if self.ratchets:
decrypted = None
try:
decrypted = self.identity.decrypt(ciphertext, ratchets=self.ratchets, enforce_ratchets=self.__enforce_ratchets, ratchet_id_receiver=self)
except:
decrypted = None
if not decrypted:
try:
RNS.log(f"Decryption with ratchets failed on {self}, reloading ratchets from storage and retrying", RNS.LOG_ERROR)
self._reload_ratchets(self.ratchets_path)
decrypted = self.identity.decrypt(ciphertext, ratchets=self.ratchets, enforce_ratchets=self.__enforce_ratchets, ratchet_id_receiver=self)
except Exception as e:
RNS.log(f"Decryption still failing after ratchet reload. The contained exception was: {e}", RNS.LOG_ERROR)
raise e
if decrypted: RNS.log("Decryption succeeded after ratchet reload", RNS.LOG_NOTICE)
return decrypted
else:
return self.identity.decrypt(ciphertext, ratchets=None, enforce_ratchets=self.__enforce_ratchets, ratchet_id_receiver=self)
if self.type == Destination.GROUP:
if hasattr(self, "prv") and self.prv != None:
@@ -425,7 +652,6 @@ class Destination:
else:
raise ValueError("No private key held by GROUP destination. Did you create or load one?")
def sign(self, message):
"""
Signs information for ``RNS.Destination.SINGLE`` type destination.
+790
View File
@@ -0,0 +1,790 @@
import os
import re
import RNS
import time
import random
import threading
import ipaddress
import subprocess
from threading import Lock
from .vendor import umsgpack as msgpack
NAME = 0xFF
TRANSPORT_ID = 0xFE
INTERFACE_TYPE = 0x00
TRANSPORT = 0x01
REACHABLE_ON = 0x02
LATITUDE = 0x03
LONGITUDE = 0x04
HEIGHT = 0x05
PORT = 0x06
IFAC_NETNAME = 0x07
IFAC_NETKEY = 0x08
FREQUENCY = 0x09
BANDWIDTH = 0x0A
SPREADINGFACTOR = 0x0B
CODINGRATE = 0x0C
MODULATION = 0x0D
CHANNEL = 0x0E
APP_NAME = "rnstransport"
class InterfaceAnnouncer():
JOB_INTERVAL = 60
DEFAULT_STAMP_VALUE = 14
WORKBLOCK_EXPAND_ROUNDS = 20
DISCOVERABLE_INTERFACE_TYPES = ["BackboneInterface", "TCPServerInterface", "TCPClientInterface",
"RNodeInterface", "WeaveInterface", "I2PInterface", "KISSInterface"]
def __init__(self, owner):
import importlib.util
if importlib.util.find_spec('LXMF') != None: from LXMF import LXStamper
else:
RNS.log("Using on-network interface discovery requires the LXMF module to be installed.", RNS.LOG_CRITICAL)
RNS.log("You can install it with the command: pip install lxmf", RNS.LOG_CRITICAL)
RNS.panic()
self.owner = owner
self.should_run = False
self.job_interval = self.JOB_INTERVAL
self.stamper = LXStamper
self.stamp_cache = {}
if self.owner.has_network_identity(): identity = self.owner.network_identity
else: identity = self.owner.identity
self.discovery_destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE,
APP_NAME, "discovery", "interface")
def start(self):
if not self.should_run:
self.should_run = True
threading.Thread(target=self.job, daemon=True).start()
def stop(self): self.should_run = False
def job(self):
while self.should_run:
time.sleep(self.job_interval)
try:
now = time.time()
due_interfaces = [i for i in self.owner.interfaces if i.supports_discovery and i.discoverable and now > (i.last_discovery_announce+i.discovery_announce_interval)]
due_interfaces.sort(key=lambda i: now-i.last_discovery_announce, reverse=True)
if len(due_interfaces) > 0:
selected_interface = due_interfaces[0]
selected_interface.last_discovery_announce = time.time()
RNS.log(f"Preparing interface discovery announce for {selected_interface.name}", RNS.LOG_DEBUG)
app_data = self.get_interface_announce_data(selected_interface)
if not app_data: RNS.log(f"Could not generate interface discovery announce data for {selected_interface.name}", RNS.LOG_ERROR)
else:
RNS.log(f"Sending interface discovery announce for {selected_interface.name} with {len(app_data)}B payload", RNS.LOG_DEBUG)
self.discovery_destination.announce(app_data=app_data)
except Exception as e:
RNS.log(f"Error while preparing interface discovery announces: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
def sanitize(self, in_str):
if in_str == None: return None
sanitized = in_str.replace("\n", "")
sanitized = sanitized.replace("\r", "")
sanitized = sanitized.strip()
return sanitized
def get_interface_announce_data(self, interface):
interface_type = type(interface).__name__
stamp_value = interface.discovery_stamp_value if interface.discovery_stamp_value else self.DEFAULT_STAMP_VALUE
if not interface_type in self.DISCOVERABLE_INTERFACE_TYPES: return None
else:
flags = 0x00
info = {INTERFACE_TYPE: interface_type,
TRANSPORT: RNS.Reticulum.transport_enabled(),
TRANSPORT_ID: RNS.Transport.identity.hash,
NAME: self.sanitize(interface.discovery_name),
LATITUDE: interface.discovery_latitude,
LONGITUDE: interface.discovery_longitude,
HEIGHT: interface.discovery_height}
if interface_type == "TCPClientInterface" and not interface.kiss_framing:
RNS.log(f"Invalid interface discovery configuration for {interface}, aborting discovery announce", RNS.LOG_ERROR)
return None
if interface_type in ["BackboneInterface", "TCPServerInterface"]:
reachable_on = self.sanitize(interface.reachable_on)
if not RNS.vendor.platformutils.is_windows():
try:
exec_path = os.path.expanduser(reachable_on)
if os.path.isfile(exec_path) and os.access(exec_path, os.X_OK):
RNS.log(f"Evaluating reachable_on from executable at {exec_path}", RNS.LOG_DEBUG)
exec_result = subprocess.run([exec_path], stdout=subprocess.PIPE)
exec_stdout = exec_result.stdout.decode("utf-8")
if exec_result.returncode != 0: raise ValueError("Non-zero exit code from subprocess")
reachable_on = self.sanitize(exec_stdout)
if not (is_ip_address(reachable_on) or is_hostname(reachable_on)):
raise ValueError(f"Valid IP address or hostname was not found in external script output \"{reachable_on}\"")
except Exception as e:
RNS.log(f"Error while getting reachable_on from executable at {interface.reachable_on}: {e}", RNS.LOG_ERROR)
RNS.log(f"Aborting discovery announce", RNS.LOG_ERROR)
return None
if not (is_ip_address(reachable_on) or is_hostname(reachable_on)):
RNS.log(f"The configured reachable_on parameter \"{reachable_on}\" for {interface} is not a valid IP address or hostname", RNS.LOG_ERROR)
RNS.log(f"Aborting discovery announce", RNS.LOG_ERROR)
return None
info[REACHABLE_ON] = reachable_on
info[PORT] = interface.bind_port
if interface_type == "I2PInterface" and interface.connectable and interface.b32:
info[REACHABLE_ON] = interface.b32
if interface_type == "RNodeInterface":
info[FREQUENCY] = interface.frequency
info[BANDWIDTH] = interface.bandwidth
info[SPREADINGFACTOR] = interface.sf
info[CODINGRATE] = interface.cr
if interface_type == "WeaveInterface":
info[FREQUENCY] = interface.discovery_frequency
info[BANDWIDTH] = interface.discovery_bandwidth
info[CHANNEL] = interface.discovery_channel
info[MODULATION] = interface.discovery_modulation
if interface_type == "KISSInterface" or (interface_type == "TCPClientInterface" and interface.kiss_framing):
info[INTERFACE_TYPE] = "KISSInterface"
info[FREQUENCY] = interface.discovery_frequency
info[BANDWIDTH] = interface.discovery_bandwidth
info[MODULATION] = self.sanitize(interface.discovery_modulation)
if interface.discovery_publish_ifac == True:
info[IFAC_NETNAME] = self.sanitize(interface.ifac_netname)
info[IFAC_NETKEY] = self.sanitize(interface.ifac_netkey)
packed = msgpack.packb(info)
infohash = RNS.Identity.full_hash(packed)
if infohash in self.stamp_cache: stamp = self.stamp_cache[infohash]
else: stamp, v = self.stamper.generate_stamp(infohash, stamp_cost=stamp_value, expand_rounds=self.WORKBLOCK_EXPAND_ROUNDS)
if not stamp: return None
else: self.stamp_cache[infohash] = stamp
if interface.discovery_encrypt:
flags |= InterfaceAnnounceHandler.FLAG_ENCRYPTED
if not self.owner.has_network_identity():
RNS.log(f"Discovery encryption requested for {interface}, but no network identity configured. Aborting discovery announce.", RNS.LOG_ERROR)
return None
else: payload = self.owner.network_identity.encrypt(packed+stamp)
else: payload = packed+stamp
return bytes([flags])+payload
class InterfaceAnnounceHandler:
FLAG_SIGNED = 0b00000001
FLAG_ENCRYPTED = 0b00000010
def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None):
import importlib.util
if importlib.util.find_spec('LXMF') != None: from LXMF import LXStamper
else:
RNS.log("Using on-network interface discovery requires the LXMF module to be installed.", RNS.LOG_CRITICAL)
RNS.log("You can install it with the command: pip install lxmf", RNS.LOG_CRITICAL)
RNS.panic()
self.aspect_filter = APP_NAME+".discovery.interface"
self.required_value = required_value
self.callback = callback
self.stamper = LXStamper
@staticmethod
def sanitize_name(name):
if not name: return None
name = name.encode("ascii", "ignore").decode("ascii").strip()
for i in [5,3,2]: name = name.replace(" "*i, " ")
while len(name) and name[0] not in san_map: name = name[1:]
while len(name) and name[-1] not in san_map+")": name = name[:-1]
return name
def received_announce(self, destination_hash, announced_identity, app_data):
try:
discovery_sources = RNS.Reticulum.interface_discovery_sources()
if discovery_sources and not announced_identity.hash in discovery_sources:
RNS.log(f"Interface discovered from non-authorized network identity {RNS.prettyhexrep(announced_identity.hash)}, ignoring", RNS.LOG_DEBUG)
return
if app_data and len(app_data) > self.stamper.STAMP_SIZE+1:
flags = app_data[0]
app_data = app_data[1:]
signed = flags & self.FLAG_SIGNED
encrypted = flags & self.FLAG_ENCRYPTED
if encrypted:
if not RNS.Transport.has_network_identity(): return
app_data = RNS.Transport.network_identity.decrypt(app_data)
if not app_data: return
stamp = app_data[-self.stamper.STAMP_SIZE:]
packed = app_data[:-self.stamper.STAMP_SIZE]
infohash = RNS.Identity.full_hash(packed)
workblock = self.stamper.stamp_workblock(infohash, expand_rounds=InterfaceAnnouncer.WORKBLOCK_EXPAND_ROUNDS)
value = self.stamper.stamp_value(workblock, stamp)
valid = self.stamper.stamp_valid(stamp, self.required_value, workblock)
if not valid:
RNS.log(f"Ignored discovered interface with invalid stamp", RNS.LOG_DEBUG)
return
if value < self.required_value: RNS.log(f"Ignored discovered interface with stamp value {value}", RNS.LOG_DEBUG)
else:
info = None
unpacked = msgpack.unpackb(packed)
if INTERFACE_TYPE in unpacked:
interface_type = unpacked[INTERFACE_TYPE]
name = self.sanitize_name(unpacked[NAME])
if type(unpacked[TRANSPORT]) != bool: raise ValueError("Invalid data in transport field of announce")
if type(unpacked[LATITUDE]) not in [type(None), float]: raise ValueError("Invalid data in latitude field of announce")
if type(unpacked[LONGITUDE]) not in [type(None), float]: raise ValueError("Invalid data in longitude field of announce")
if type(unpacked[HEIGHT]) not in [type(None), float]: raise ValueError("Invalid data in height field of announce")
if len(unpacked[TRANSPORT_ID]) != RNS.Identity.TRUNCATED_HASHLENGTH//8: raise ValueError("Invalid data in transport_id field of announce")
if not interface_type in InterfaceAnnouncer.DISCOVERABLE_INTERFACE_TYPES:
raise ValueError("Invalid interface type in announce data")
if REACHABLE_ON in unpacked:
if not (is_ip_address(unpacked[REACHABLE_ON]) or is_hostname(unpacked[REACHABLE_ON])):
raise ValueError("Invalid data in reachable_on field of announce")
info = {"type": interface_type,
"transport": unpacked[TRANSPORT],
"name": name or f"Discovered {interface_type}",
"received": time.time(),
"stamp": stamp,
"value": value,
"transport_id": RNS.hexrep(unpacked[TRANSPORT_ID], delimit=False),
"network_id": RNS.hexrep(announced_identity.hash, delimit=False),
"hops": RNS.Transport.hops_to(destination_hash),
"latitude": unpacked[LATITUDE],
"longitude": unpacked[LONGITUDE],
"height": unpacked[HEIGHT]}
if IFAC_NETNAME in unpacked: info["ifac_netname"] = str(unpacked[IFAC_NETNAME])
if IFAC_NETKEY in unpacked: info["ifac_netkey"] = str(unpacked[IFAC_NETKEY])
if interface_type in ["BackboneInterface", "TCPServerInterface"]:
backbone_support = not RNS.vendor.platformutils.is_windows()
info["reachable_on"] = unpacked[REACHABLE_ON]
info["port"] = unpacked[PORT]
connection_interface = "BackboneInterface" if backbone_support else "TCPClientInterface"
remote_str = "remote" if backbone_support else "target_host"
cfg_name = info["name"]
cfg_remote = info["reachable_on"]
cfg_port = info["port"]
cfg_identity = info["transport_id"]
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
info["config_entry"] = f"[[{cfg_name}]]\n type = {connection_interface}\n enabled = yes\n {remote_str} = {cfg_remote}\n target_port = {cfg_port}{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}"
if interface_type == "I2PInterface":
info["reachable_on"] = unpacked[REACHABLE_ON]
cfg_name = info["name"]
cfg_remote = info["reachable_on"]
cfg_identity = info["transport_id"]
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
info["config_entry"] = f"[[{cfg_name}]]\n type = I2PInterface\n enabled = yes\n peers = {cfg_remote}{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}"
if interface_type == "RNodeInterface":
info["frequency"] = unpacked[FREQUENCY]
info["bandwidth"] = unpacked[BANDWIDTH]
info["sf"] = unpacked[SPREADINGFACTOR]
info["cr"] = unpacked[CODINGRATE]
cfg_name = info["name"]
cfg_frequency = info["frequency"]
cfg_bandwidth = info["bandwidth"]
cfg_sf = info["sf"]
cfg_cr = info["cr"]
cfg_identity = info["transport_id"]
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
info["config_entry"] = f"[[{cfg_name}]]\n type = RNodeInterface\n enabled = yes\n port = \n frequency = {cfg_frequency}\n bandwidth = {cfg_bandwidth}\n spreadingfactor = {cfg_sf}\n codingrate = {cfg_cr}\n txpower = {cfg_netname_str}{cfg_netkey_str}"
if interface_type == "WeaveInterface":
info["frequency"] = unpacked[FREQUENCY]
info["bandwidth"] = unpacked[BANDWIDTH]
info["channel"] = unpacked[CHANNEL]
info["modulation"] = unpacked[MODULATION]
cfg_name = info["name"]
cfg_identity = info["transport_id"]
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
info["config_entry"] = f"[[{cfg_name}]]\n type = WeaveInterface\n enabled = yes\n port = {cfg_netname_str}{cfg_netkey_str}"
if interface_type == "KISSInterface":
info["frequency"] = unpacked[FREQUENCY]
info["bandwidth"] = unpacked[BANDWIDTH]
info["modulation"] = unpacked[MODULATION]
cfg_name = info["name"]
cfg_frequency = info["frequency"]
cfg_bandwidth = info["bandwidth"]
cfg_modulation = info["modulation"]
cfg_identity = info["transport_id"]
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
info["config_entry"] = f"[[{cfg_name}]]\n type = KISSInterface\n enabled = yes\n port = \n # Frequency: {cfg_frequency}\n # Bandwidth: {cfg_bandwidth}\n # Modulation: {cfg_modulation}{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}"
discovery_hash_material = info["transport_id"]+info["name"]
info["discovery_hash"] = RNS.Identity.full_hash(discovery_hash_material.encode("utf-8"))
if self.callback and callable(self.callback): self.callback(info)
except Exception as e:
RNS.log(f"An error occurred while trying to decode discovered interface. The contained exception was: {e}", RNS.LOG_DEBUG)
class InterfaceDiscovery():
THRESHOLD_UNKNOWN = 24*60*60
THRESHOLD_STALE = 3*24*60*60
THRESHOLD_REMOVE = 7*24*60*60
MONITOR_INTERVAL = 5
DETACH_THRESHOLD = 12
STATUS_STALE = 0
STATUS_UNKNOWN = 100
STATUS_AVAILABLE = 1000
STATUS_CODE_MAP = {"available": STATUS_AVAILABLE, "unknown": STATUS_UNKNOWN, "stale": STATUS_STALE}
AUTOCONNECT_TYPES = ["BackboneInterface", "TCPServerInterface"]
DISCOVERABLE_TYPES = ["BackboneInterface", "TCPServerInterface", "I2PInterface", "RNodeInterface", "WeaveInterface", "KISSInterface"]
discovery_lock = Lock()
def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None, discover_interfaces=True):
if not required_value: required_value = InterfaceAnnouncer.DEFAULT_STAMP_VALUE
self.required_value = required_value
self.discovery_callback = callback
self.rns_instance = RNS.Reticulum.get_instance()
self.monitored_interfaces = []
self.monitoring_autoconnects = False
self.monitor_interval = self.MONITOR_INTERVAL
self.detach_threshold = self.DETACH_THRESHOLD
self.initial_autoconnect_ran = False
if not self.rns_instance: raise SystemError("Attempt to start interface discovery listener without an active RNS instance")
self.storagepath = os.path.join(RNS.Reticulum.storagepath, "discovery", "interfaces")
if not os.path.isdir(self.storagepath): os.makedirs(self.storagepath)
if discover_interfaces:
self.handler = InterfaceAnnounceHandler(callback=self.interface_discovered, required_value=self.required_value)
RNS.Transport.register_announce_handler(self.handler)
threading.Thread(target=self.connect_discovered, daemon=True).start()
def list_discovered_interfaces(self, only_available=False, only_transport=False):
now = time.time()
discovered_interfaces = []
discovery_sources = RNS.Reticulum.interface_discovery_sources()
for filename in os.listdir(self.storagepath):
try:
with self.discovery_lock:
filepath = os.path.join(self.storagepath, filename)
with open(filepath, "rb") as f: info = msgpack.unpackb(f.read())
should_remove = False
heard_delta = now-info["last_heard"]
info["name"] = InterfaceAnnounceHandler.sanitize_name(info["name"])
if heard_delta > self.THRESHOLD_REMOVE: should_remove = True
elif discovery_sources and not "network_id" in info: should_remove = True
elif discovery_sources and not bytes.fromhex(info["network_id"]) in discovery_sources: should_remove = True
elif not "type" in info or ("type" in info and not info["type"] in self.DISCOVERABLE_TYPES): should_remove = True
elif "reachable_on" in info:
if not (is_ip_address(info["reachable_on"]) or is_hostname(info["reachable_on"])): should_remove = True
if should_remove:
os.unlink(filepath)
continue
else:
if heard_delta > self.THRESHOLD_STALE: info["status"] = "stale"
elif heard_delta > self.THRESHOLD_UNKNOWN: info["status"] = "unknown"
else: info["status"] = "available"
info["status_code"] = self.STATUS_CODE_MAP[info["status"]]
if not only_available and not only_transport: discovered_interfaces.append(info)
else:
should_append = True
status = info["status"]
transport = info["transport"]
if only_available and status != "available": should_append = False
if only_transport and not transport: should_append = False
if should_append: discovered_interfaces.append(info)
except Exception as e:
RNS.log(f"Error while loading discovered interface data: {e}", RNS.LOG_WARNING)
RNS.log(f"The interface data file {os.path.join(self.storagepath, filename)} may be corrupt", RNS.LOG_WARNING)
RNS.trace_exception(e)
discovered_interfaces.sort(key=lambda info: (info["status_code"], info["value"], info["last_heard"]), reverse=True)
return discovered_interfaces
def interface_discovered(self, info):
try:
name = info["name"]
value = info["value"]
interface_type = info["type"]
discovery_hash = info["discovery_hash"]
discovered_type = info["type"]
if not discovered_type in self.DISCOVERABLE_TYPES: return
hops = info["hops"]; ms = "" if hops == 1 else "s"
filename = RNS.hexrep(discovery_hash, delimit=False)
filepath = os.path.join(self.storagepath, filename)
RNS.log(f"Discovered {interface_type} {hops} hop{ms} away with stamp value {value}: {name}", RNS.LOG_DEBUG)
with self.discovery_lock:
if not os.path.isfile(filepath):
try:
with open(filepath, "wb") as f:
info["discovered"] = info["received"]
info["last_heard"] = info["received"]
info["heard_count"] = 0
f.write(msgpack.packb(info))
except Exception as e:
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
return
else:
discovered = None
heard_count = None
try:
try:
with open(filepath, "rb") as f:
last_info = msgpack.unpackb(f.read())
discovered = last_info["discovered"]
heard_count = last_info["heard_count"]
except Exception as e: RNS.log(f"Error while reading existing data for discovered interface, re-creating data", RNS.LOG_ERROR)
if discovered == None: discovered = info["received"]
if heard_count == None: heard_count = 0
with open(filepath, "wb") as f:
info["discovered"] = discovered
info["last_heard"] = info["received"]
info["heard_count"] = heard_count+1
f.write(msgpack.packb(info))
except Exception as e:
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
return
except Exception as e:
RNS.log(f"Error processing discovered interface data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
return
self.autoconnect(info)
try:
if self.discovery_callback and callable(self.discovery_callback): self.discovery_callback(info)
except Exception as e: RNS.log(f"Error while processing external interface discovery callback: {e}", RNS.LOG_ERROR)
def monitor_interface(self, interface):
if not interface in self.monitored_interfaces:
self.monitored_interfaces.append(interface)
if not self.monitoring_autoconnects:
self.monitoring_autoconnects = True
threading.Thread(target=self.__monitor_job, daemon=True).start()
def __monitor_job(self):
while self.monitoring_autoconnects and RNS.Transport._should_run:
time.sleep(self.monitor_interval)
detached_interfaces = []
online_interfaces = 0
autoconnected_interfaces = self.autoconnect_count()
for interface in self.monitored_interfaces:
try:
if interface.online:
online_interfaces += 1
if hasattr(interface, "autoconnect_down") and interface.autoconnect_down != None:
RNS.log(f"Auto-discovered interface {interface} reconnected")
interface.autoconnect_down = None
else:
if not hasattr(interface, "autoconnect_down") or interface.autoconnect_down == None:
RNS.log(f"Auto-discovered interface {interface} disconnected", RNS.LOG_DEBUG)
interface.autoconnect_down = time.time()
else:
down_for = time.time()-interface.autoconnect_down
if down_for >= self.detach_threshold:
RNS.log(f"Auto-discovered interface {interface} has been down for {RNS.prettytime(down_for)}, detaching", RNS.LOG_DEBUG)
detached_interfaces.append(interface)
except Exception as e:
RNS.log(f"Error while checking auto-connected interface state for {interface}: {e}", RNS.LOG_ERROR)
max_autoconnected_interfaces = RNS.Reticulum.max_autoconnected_interfaces()
free_slots = max(0, max_autoconnected_interfaces - autoconnected_interfaces)
reserved_slots = max_autoconnected_interfaces//4
if online_interfaces >= max_autoconnected_interfaces:
for interface in RNS.Transport.interfaces:
if hasattr(interface, "bootstrap_only") and interface.bootstrap_only == True:
RNS.log(f"Tearing down bootstrap-only {interface} since target connected auto-discovered interface count has been reached", RNS.LOG_INFO)
if not interface in detached_interfaces: detached_interfaces.append(interface)
if online_interfaces == 0:
if self.bootstrap_interface_count() == 0:
RNS.log(f"No auto-discovered interfaces connected, re-enabling bootstrap interfaces", RNS.LOG_NOTICE)
for config in RNS.Reticulum.get_instance().bootstrap_configs:
RNS.Reticulum.get_instance()._synthesize_interface(config, config["name"])
if self.initial_autoconnect_ran and free_slots > reserved_slots:
candidate_interfaces = self.list_discovered_interfaces(only_available=True, only_transport=True)
if len(candidate_interfaces) > 0:
random.shuffle(candidate_interfaces)
selected_interface = candidate_interfaces[0]
if not self.interface_exists(selected_interface): self.autoconnect(selected_interface)
for interface in detached_interfaces:
try: self.teardown_interface(interface)
except Exception as e:
RNS.log(f"Error while de-registering auto-connected interface from transport: {e}", RNS.LOG_ERROR)
def teardown_interface(self, interface):
interface.detach()
RNS.Transport.remove_interface(interface)
if interface in self.monitored_interfaces: self.monitored_interfaces.remove(interface)
def autoconnect_count(self):
return len([i for i in RNS.Transport.interfaces if hasattr(i, "autoconnect_hash")])
def bootstrap_interface_count(self):
return len([i for i in RNS.Transport.interfaces if hasattr(i, "bootstrap_only") and i.bootstrap_only == True])
def connect_discovered(self):
if RNS.Reticulum.should_autoconnect_discovered_interfaces():
try:
discovered_interfaces = self.list_discovered_interfaces(only_transport=True)
for info in discovered_interfaces:
if self.autoconnect_count() >= RNS.Reticulum.max_autoconnected_interfaces(): break
self.autoconnect(info)
self.initial_autoconnect_ran = True
except Exception as e:
RNS.log(f"Error while reconnecting discovered interfaces: {e}", RNS.LOG_ERROR)
def endpoint_hash(self, info):
endpoint_specifier = ""
if "reachable_on" in info: endpoint_specifier += str(info["reachable_on"])
if "port" in info: endpoint_specifier += ":"+str(info["port"])
endpoint_hash = RNS.Identity.full_hash(endpoint_specifier.encode("utf-8"))
return endpoint_hash
def interface_exists(self, info):
exists = False
for interface in RNS.Transport.interfaces:
if hasattr(interface, "autoconnect_hash") and interface.autoconnect_hash == self.endpoint_hash(info):
exists = True
break
else:
dest_match = "reachable_on" in info and hasattr(interface, "target_ip") and interface.target_ip == info["reachable_on"]
port_match = not "port" in info or (hasattr(interface, "target_port") and "port" in info and interface.target_port == info["port"])
b32d_match = "reachable_on" in info and hasattr(interface, "b32") and interface.b32 == info["reachable_on"]
if (dest_match and port_match) or b32d_match:
exists = True
break
return exists
def autoconnect(self, info):
try:
if RNS.Reticulum.should_autoconnect_discovered_interfaces():
autoconnected_count = self.autoconnect_count()
if autoconnected_count < RNS.Reticulum.max_autoconnected_interfaces():
interface_type = info["type"]
if interface_type in self.AUTOCONNECT_TYPES:
endpoint_hash = self.endpoint_hash(info)
exists = self.interface_exists(info)
if exists: RNS.log(f"Discovered {interface_type} already exists, not auto-connecting", RNS.LOG_DEBUG)
else:
if interface_type == "TCPClientInterface":
RNS.log(f"Your operating system does not support the Backbone interface type, and must degrade to using TCPClientInterface instead", RNS.LOG_WARNING)
RNS.log(f"Auto-connecting discovered TCPClient interfaces is not yet implemented, aborting auto-connect", RNS.LOG_WARNING)
RNS.log(f"You can obtain the configuration entry and add this interface manually instead using rnstatus -D", RNS.LOG_WARNING)
return
if interface_type == "I2PInterface":
RNS.log(f"Auto-connecting discovered I2P interfaces is not yet implemented, aborting auto-connect", RNS.LOG_WARNING)
RNS.log(f"You can obtain the configuration entry and add this interface manually instead using rnstatus -D", RNS.LOG_WARNING)
return
if is_ygg_ipv6(info["reachable_on"]):
# TODO: Somehow detect if yggdrasil is enabled on the system
return
interface_name = info["name"]
config_entry = info["config_entry"]
interface_config = {}
interface_config["name"] = f"{interface_name}"
ifac_netname = info["ifac_netname"] if "ifac_netname" in info else None
ifac_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
interface = None
if interface_type == "BackboneInterface":
from RNS.Interfaces import BackboneInterface
interface_config["target_host"] = info["reachable_on"]
interface_config["target_port"] = info["port"]
interface = BackboneInterface.BackboneClientInterface(RNS.Transport, interface_config)
if interface:
RNS.log(f"Auto-connecting discovered {interface_type} {interface_name}")
interface.autoconnect_hash = endpoint_hash
interface.autoconnect_source = info["network_id"]
mode = RNS.Interfaces.Interface.Interface.MODE_GATEWAY if RNS.Reticulum.transport_enabled() else None
ar_target = RNS.Reticulum.get_instance()._default_ar_target() if RNS.Reticulum.transport_enabled() else None
ar_penalty = RNS.Reticulum.get_instance()._default_ar_penalty() if RNS.Reticulum.transport_enabled() else None
ar_grace = RNS.Reticulum.get_instance()._default_ar_grace() if RNS.Reticulum.transport_enabled() else None
RNS.Reticulum.get_instance()._add_interface(interface, mode=mode, ifac_netname=ifac_netname, ifac_netkey=ifac_netkey, configured_bitrate=5E6,
announce_rate_target=ar_target, announce_rate_grace=ar_grace, announce_rate_penalty=ar_penalty)
self.monitor_interface(interface)
except Exception as e:
RNS.log(f"Error while auto-connecting discovered interface: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
class BlackholeUpdater():
INITIAL_WAIT = 20
JOB_INTERVAL = 60
UPDATE_INTERVAL = 1*60*60
SOURCE_TIMEOUT = 25
def __init__(self):
self.last_updates = {}
self.should_run = False
self.job_interval = self.JOB_INTERVAL
self.update_lock = threading.Lock()
def start(self):
if not self.should_run:
source_count = len(RNS.Reticulum.blackhole_sources())
ms = "" if source_count == 1 else "s"
RNS.log(f"Starting blackhole updater with {source_count} source{ms}", RNS.LOG_DEBUG)
self.should_run = True
threading.Thread(target=self.job, daemon=True).start()
def stop(self): self.should_run = False
def update_link_established(self, link):
remote_identity = link.get_remote_identity()
RNS.log(f"Link established for blackhole list update from {RNS.prettyhexrep(remote_identity.hash)}", RNS.LOG_DEBUG)
receipt = link.request("/list")
while not receipt.concluded(): time.sleep(0.2)
response = receipt.get_response()
link.teardown()
if type(response) == dict: blackhole_list = response
else: blackhole_list = None
if blackhole_list:
added = 0
for identity_hash in blackhole_list:
entry = blackhole_list[identity_hash]
if not identity_hash in RNS.Transport.blackholed_identities:
RNS.Transport.blackholed_identities[identity_hash] = entry
added += 1
if added > 0:
spec = "identity" if added == 1 else "identities"
RNS.log(f"Added {added} blackholed {spec} from {RNS.prettyhexrep(remote_identity.hash)}", RNS.LOG_DEBUG)
try:
sourcelistpath = os.path.join(RNS.Reticulum.blackholepath, RNS.hexrep(remote_identity.hash, delimit=False))
tmppath = f"{sourcelistpath}.tmp"
with open(tmppath, "wb") as f: f.write(msgpack.packb(blackhole_list))
if os.path.isfile(sourcelistpath): os.unlink(sourcelistpath)
os.rename(tmppath, sourcelistpath)
except Exception as e:
RNS.log(f"Error while persisting blackhole list from {RNS.prettyhexrep(remote_identity.hash)}: {e}", RNS.LOG_ERROR)
RNS.log(f"Blackhole list update from {RNS.prettyhexrep(remote_identity.hash)} completed", RNS.LOG_DEBUG)
def job(self):
time.sleep(self.INITIAL_WAIT)
while self.should_run:
try:
now = time.time()
for identity_hash in RNS.Reticulum.blackhole_sources():
if identity_hash in self.last_updates: last_update = self.last_updates[identity_hash]
else: last_update = 0
if now > last_update+self.UPDATE_INTERVAL:
try:
destination_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.info.blackhole", identity_hash)
RNS.log(f"Attempting blackhole list update from {RNS.prettyhexrep(identity_hash)}...", RNS.LOG_DEBUG)
if not RNS.Transport.await_path(destination_hash): RNS.log(f"No path available for blackhole list update from {RNS.prettyhexrep(identity_hash)}, retrying later", RNS.LOG_VERBOSE)
else:
remote_identity = RNS.Identity.recall(destination_hash)
destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "info", "blackhole")
RNS.Link(destination, established_callback=self.update_link_established)
self.last_updates[identity_hash] = time.time()
except Exception as e:
RNS.log(f"Error while establishing link for blackhole list update from {RNS.prettyhexrep(identity_hash)}: {e}", RNS.LOG_ERROR)
except Exception as e:
RNS.log(f"Error in blackhole list updater job: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
time.sleep(self.job_interval)
def is_ip_address(address_string):
try:
ipaddress.ip_address(address_string)
return True
except: return False
def is_ygg_ipv6(address_string):
try: return ipaddress.ip_address(address_string) in ipaddress.IPv6Network("200::/7")
except: return False
def is_hostname(hostname):
if hostname[-1] == ".": hostname = hostname[:-1]
if len(hostname) > 253: return False
components = hostname.split(".")
if re.match(r"[0-9]+$", components[-1]): return False
allowed = re.compile(r"(?!-)[a-z0-9-]{1,63}(?<!-)$", re.IGNORECASE)
return all(allowed.match(label) for label in components)
san_map = ""
for i in range(48, 58): san_map += bytes([i]).decode("ascii")
for i in range(65, 91): san_map += bytes([i]).decode("ascii")
for i in range(97, 123): san_map += bytes([i]).decode("ascii")
+487 -99
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -26,11 +34,12 @@ import RNS
import time
import atexit
import hashlib
import threading
from .vendor import umsgpack as umsgpack
from RNS.Cryptography import X25519PrivateKey, X25519PublicKey, Ed25519PrivateKey, Ed25519PublicKey
from RNS.Cryptography import Fernet
from RNS.Cryptography import Token
class Identity:
@@ -49,59 +58,106 @@ class Identity:
KEYSIZE = 256*2
"""
X25519 key size in bits. A complete key is the concatenation of a 256 bit encryption key, and a 256 bit signing key.
"""
X.25519 key size in bits. A complete key is the concatenation of a 256 bit encryption key, and a 256 bit signing key.
"""
RATCHETSIZE = 256
"""
X.25519 ratchet key size in bits.
"""
RATCHET_EXPIRY = 60*60*24*30
"""
The expiry time for received ratchets in seconds, defaults to 30 days. Reticulum will always use the most recently
announced ratchet, and remember it for up to ``RATCHET_EXPIRY`` since receiving it, after which it will be discarded.
If a newer ratchet is announced in the meantime, it will be replace the already known ratchet.
"""
# Non-configurable constants
FERNET_OVERHEAD = RNS.Cryptography.Fernet.FERNET_OVERHEAD
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
NAME_HASH_LENGTH = 80
TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH
NAME_HASH_LENGTH = 80
TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH
"""
Constant specifying the truncated hash length (in bits) used by Reticulum
for addressable hashes and other purposes. Non-configurable.
"""
DERIVED_KEY_LENGTH = 512//8
DERIVED_KEY_LENGTH_LEGACY = 256//8
# Storage
known_destinations = {}
known_ratchets = {}
ratchet_persist_lock = threading.Lock()
known_destinations_lock = threading.Lock()
@staticmethod
def remember(packet_hash, destination_hash, public_key, app_data = None):
if len(public_key) != Identity.KEYSIZE//8:
raise TypeError("Can't remember "+RNS.prettyhexrep(destination_hash)+", the public key size of "+str(len(public_key))+" is not valid.", RNS.LOG_ERROR)
else:
Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data]
with Identity.known_destinations_lock:
if not destination_hash in Identity.known_destinations:
Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data, 0]
else:
entry = Identity.known_destinations[destination_hash]
entry[0] = time.time()
entry[1] = packet_hash
entry[2] = public_key
entry[3] = app_data
@staticmethod
def recall(destination_hash):
def recall(target_hash, from_identity_hash=False, _no_use=False):
"""
Recall identity for a destination hash.
Recall identity for a destination or identity hash. By default, this function
will return the identity associated with a given *destination* hash. As an
example, if you know the ``lxmf.delivery`` destination hash of an endpoint,
this function will return the associated underlying identity. You can also
search for an identity from a known *identity hash*, by setting the
``from_identity_hash`` argument.
:param destination_hash: Destination hash as *bytes*.
:param target_hash: Destination or identity hash as *bytes*.
:param from_identity_hash: Whether to search based on identity hash instead of destination hash as *bool*.
:returns: An :ref:`RNS.Identity<api-identity>` instance that can be used to create an outgoing :ref:`RNS.Destination<api-destination>`, or *None* if the destination is unknown.
"""
if destination_hash in Identity.known_destinations:
identity_data = Identity.known_destinations[destination_hash]
identity = Identity(create_keys=False)
identity.load_public_key(identity_data[2])
identity.app_data = identity_data[3]
return identity
else:
for registered_destination in RNS.Transport.destinations:
if destination_hash == registered_destination.hash:
if from_identity_hash:
for destination_hash in Identity.known_destinations:
if target_hash == Identity.truncated_hash(Identity.known_destinations[destination_hash][2]):
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(destination_hash)
identity_data = Identity.known_destinations[destination_hash]
identity = Identity(create_keys=False)
identity.load_public_key(registered_destination.identity.get_public_key())
identity.app_data = None
identity.load_public_key(identity_data[2])
identity.app_data = identity_data[3]
return identity
return None
else:
if target_hash in Identity.known_destinations:
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(target_hash)
identity_data = Identity.known_destinations[target_hash]
identity = Identity(create_keys=False)
identity.load_public_key(identity_data[2])
identity.app_data = identity_data[3]
return identity
else:
for registered_destination in RNS.Transport.destinations:
if target_hash == registered_destination.hash:
identity = Identity(create_keys=False)
identity.load_public_key(registered_destination.identity.get_public_key())
identity.app_data = None
return identity
return None
@staticmethod
def recall_app_data(destination_hash):
def recall_app_data(destination_hash, _no_use=False):
"""
Recall last heard app_data for a destination hash.
@@ -109,13 +165,14 @@ class Identity:
:returns: *Bytes* containing app_data, or *None* if the destination is unknown.
"""
if destination_hash in Identity.known_destinations:
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(destination_hash)
app_data = Identity.known_destinations[destination_hash][3]
return app_data
else:
return None
else: return None
@staticmethod
def save_known_destinations():
def save_known_destinations(background=False, recombine=True):
# TODO: Improve the storage method so we don't have to
# deserialize and serialize the entire table on every
# save, but the only changes. It might be possible to
@@ -136,63 +193,166 @@ class Identity:
Identity.saving_known_destinations = True
save_start = time.time()
storage_known_destinations = {}
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
if recombine:
storage_known_destinations = {}
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
try:
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
storage_known_destinations = umsgpack.load(file)
except: pass
try:
file = open(RNS.Reticulum.storagepath+"/known_destinations","rb")
storage_known_destinations = umsgpack.load(file)
file.close()
except:
pass
for destination_hash in storage_known_destinations:
if not destination_hash in Identity.known_destinations:
with Identity.known_destinations_lock:
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
except Exception as e:
RNS.log("Skipped recombining known destinations from disk, since an error occurred: "+str(e), RNS.LOG_WARNING)
for destination_hash in storage_known_destinations:
if not destination_hash in Identity.known_destinations:
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_DEBUG)
file = open(RNS.Reticulum.storagepath+"/known_destinations","wb")
umsgpack.dump(Identity.known_destinations, file)
file.close()
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_VERBOSE)
with open(RNS.Reticulum.storagepath+"/known_destinations","wb") as file:
umsgpack.dump(Identity.known_destinations.copy(), file)
save_time = time.time() - save_start
if save_time < 1:
time_str = str(round(save_time*1000,2))+"ms"
else:
time_str = str(round(save_time,2))+"s"
if save_time < 1: time_str = str(round(save_time*1000,2))+"ms"
else: time_str = str(round(save_time,2))+"s"
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_DEBUG)
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_VERBOSE)
except Exception as e:
RNS.log("Error while saving known destinations to disk, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
Identity.saving_known_destinations = False
@staticmethod
def load_known_destinations():
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
st = time.time()
try:
file = open(RNS.Reticulum.storagepath+"/known_destinations","rb")
loaded_known_destinations = umsgpack.load(file)
file.close()
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
loaded_known_destinations = umsgpack.load(file)
Identity.known_destinations = {}
for known_destination in loaded_known_destinations:
if len(known_destination) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
Identity.known_destinations[known_destination] = loaded_known_destinations[known_destination]
if len(loaded_known_destinations[known_destination]) < 5:
e = loaded_known_destinations[known_destination]
loaded_known_destinations[known_destination] = [e[0], e[1], e[2], e[3], 0]
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE)
except:
with Identity.known_destinations_lock:
Identity.known_destinations[known_destination] = loaded_known_destinations[known_destination]
RNS.log(f"Loaded {len(Identity.known_destinations)} known destination from storage in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_VERBOSE)
except Exception as e:
RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR)
RNS.trace_exception(e)
else:
RNS.log("Destinations file does not exist, no known destinations loaded", RNS.LOG_VERBOSE)
@staticmethod
def _used_destination_data(destination_hash):
with Identity.known_destinations_lock:
if destination_hash in Identity.known_destinations:
if not Identity.known_destinations[destination_hash][4] < 0:
Identity.known_destinations[destination_hash][4] = time.time()
return True
return False
@staticmethod
def _retain_destination_data(destination_hash):
with Identity.known_destinations_lock:
if destination_hash in Identity.known_destinations:
Identity.known_destinations[destination_hash][4] = -1
return True
return False
@staticmethod
def _unretain_destination_data(destination_hash):
with Identity.known_destinations_lock:
if destination_hash in Identity.known_destinations:
Identity.known_destinations[destination_hash][4] = time.time()
return True
return False
@staticmethod
def _retain_identity(identity_hash):
try:
retained = False
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():
now = time.time()
st = now
total = len(Identity.known_destinations)
stale = []
no_path = 0
retained = 0
never_used = 0
for destination_hash in Identity.known_destinations:
try:
if RNS.Transport.has_path(destination_hash): has_path = True
else:
has_path = False
no_path += 1
with Identity.known_destinations_lock:
if destination_hash in Identity.known_destinations:
last_announce = Identity.known_destinations[destination_hash][0]
last_use = 0
was_used = False
is_retained = False
if Identity.known_destinations[destination_hash][4] > 0:
was_used = True
last_use = Identity.known_destinations[destination_hash][4]
elif Identity.known_destinations[destination_hash][4] == 0:
was_used = False
never_used += 1
elif Identity.known_destinations[destination_hash][4] == -1:
is_retained = True
retained += 1
unused_for = time.time() - Identity.known_destinations[destination_hash][4]
if not is_retained and not has_path:
if not was_used and now - last_announce > RNS.Transport.UNUSED_DESTINATION_LINGER: stale.append(destination_hash)
elif unused_for > RNS.Transport.DESTINATION_TIMEOUT*1.25: stale.append(destination_hash)
except Exception as e: RNS.log(f"Faulty entry for {RNS.prettyhexrep(destination_hash)} while cleaning known destinations: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
removed = 0
for destination_hash in stale:
with Identity.known_destinations_lock:
if destination_hash in Identity.known_destinations:
Identity.known_destinations.pop(destination_hash)
removed += 1
# RNS.log(f"Total destinations: {total}, stale: {len(stale)}, removed: {removed}, no path: {no_path}, never used: {never_used}, with path: {total-no_path}, used: {total-never_used}, retained: {retained}. Completed in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_WARNING) # TODO: Remove
if not RNS.Transport.owner.is_connected_to_shared_instance: Identity.save_known_destinations(recombine=False)
@staticmethod
def full_hash(data):
"""
Get a SHA-256 hash of passed data.
:param data: Data to be hashed as *bytes*.
:returns: SHA-256 hash as *bytes*
:returns: SHA-256 hash as *bytes*.
"""
return RNS.Cryptography.sha256(data)
@@ -202,7 +362,7 @@ class Identity:
Get a truncated SHA-256 hash of passed data.
:param data: Data to be hashed as *bytes*.
:returns: Truncated SHA-256 hash as *bytes*
:returns: Truncated SHA-256 hash as *bytes*.
"""
return Identity.full_hash(data)[:(Identity.TRUNCATED_HASHLENGTH//8)]
@@ -212,24 +372,175 @@ class Identity:
Get a random SHA-256 hash.
:param data: Data to be hashed as *bytes*.
:returns: Truncated SHA-256 hash of random data as *bytes*
:returns: Truncated SHA-256 hash of random data as *bytes*.
"""
return Identity.truncated_hash(os.urandom(Identity.TRUNCATED_HASHLENGTH//8))
@staticmethod
def validate_announce(packet):
def current_ratchet_id(destination_hash):
"""
Get the ID of the currently used ratchet key for a given destination hash
:param destination_hash: A destination hash as *bytes*.
:returns: A ratchet ID as *bytes* or *None*.
"""
ratchet = Identity.get_ratchet(destination_hash)
if ratchet == None:
return None
else:
return Identity._get_ratchet_id(ratchet)
@staticmethod
def _get_ratchet_id(ratchet_pub_bytes):
return Identity.full_hash(ratchet_pub_bytes)[:Identity.NAME_HASH_LENGTH//8]
@staticmethod
def _ratchet_public_bytes(ratchet):
return X25519PrivateKey.from_private_bytes(ratchet).public_key().public_bytes()
@staticmethod
def _generate_ratchet():
ratchet_prv = X25519PrivateKey.generate()
ratchet_pub = ratchet_prv.public_key()
return ratchet_prv.private_bytes()
@staticmethod
def _remember_ratchet(destination_hash, ratchet):
try:
if destination_hash in Identity.known_ratchets and Identity.known_ratchets[destination_hash] == ratchet:
ratchet_exists = True
else:
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) 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():
with Identity.ratchet_persist_lock:
hexhash = RNS.hexrep(destination_hash, delimit=False)
ratchet_data = {"ratchet": ratchet, "received": time.time()}
ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
if not os.path.isdir(ratchetdir):
os.makedirs(ratchetdir)
outpath = f"{ratchetdir}/{hexhash}.out"
finalpath = f"{ratchetdir}/{hexhash}"
with open(outpath, "wb") as ratchet_file:
ratchet_file.write(umsgpack.packb(ratchet_data))
os.replace(outpath, finalpath)
threading.Thread(target=persist_job, daemon=True).start()
except Exception as e:
RNS.log(f"Could not persist ratchet for {RNS.prettyhexrep(destination_hash)} to storage.", RNS.LOG_ERROR)
RNS.log(f"The contained exception was: {e}")
RNS.trace_exception(e)
@staticmethod
def _clean_ratchets():
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:
try:
ratchet_data = umsgpack.unpackb(rf.read())
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
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)
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):
if not destination_hash in Identity.known_ratchets:
ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
hexhash = RNS.hexrep(destination_hash, delimit=False)
ratchet_path = f"{ratchetdir}/{hexhash}"
if os.path.isfile(ratchet_path):
try:
with open(ratchet_path, "rb") as ratchet_file:
ratchet_data = umsgpack.unpackb(ratchet_file.read())
if time.time() < ratchet_data["received"]+Identity.RATCHET_EXPIRY and len(ratchet_data["ratchet"]) == Identity.RATCHETSIZE//8:
Identity.known_ratchets[destination_hash] = ratchet_data["ratchet"]
else:
return None
except Exception as e:
RNS.log(f"An error occurred while loading ratchet data for {RNS.prettyhexrep(destination_hash)} from storage.", RNS.LOG_ERROR)
RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR)
return None
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) if RNS.sl(RNS.LOG_DEBUG) else None
return None
@staticmethod
def validate_announce(packet, only_validate_signature=False):
try:
if packet.packet_type == RNS.Packet.ANNOUNCE:
keysize = Identity.KEYSIZE//8
ratchetsize = Identity.RATCHETSIZE//8
name_hash_len = Identity.NAME_HASH_LENGTH//8
sig_len = Identity.SIGLENGTH//8
destination_hash = packet.destination_hash
public_key = packet.data[:Identity.KEYSIZE//8]
name_hash = packet.data[Identity.KEYSIZE//8:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8]
random_hash = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10]
signature = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8]
app_data = b""
if len(packet.data) > Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:
app_data = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:]
signed_data = destination_hash+public_key+name_hash+random_hash+app_data
# Get public key bytes from announce
public_key = packet.data[:keysize]
# If the packet context flag is set,
# this announce contains a new ratchet
if packet.context_flag == RNS.Packet.FLAG_SET:
name_hash = packet.data[keysize:keysize+name_hash_len ]
random_hash = packet.data[keysize+name_hash_len:keysize+name_hash_len+10]
ratchet = packet.data[keysize+name_hash_len+10:keysize+name_hash_len+10+ratchetsize]
signature = packet.data[keysize+name_hash_len+10+ratchetsize:keysize+name_hash_len+10+ratchetsize+sig_len]
app_data = b""
if len(packet.data) > keysize+name_hash_len+10+sig_len+ratchetsize:
app_data = packet.data[keysize+name_hash_len+10+sig_len+ratchetsize:]
# If the packet context flag is not set,
# this announce does not contain a ratchet
else:
ratchet = b""
name_hash = packet.data[keysize:keysize+name_hash_len]
random_hash = packet.data[keysize+name_hash_len:keysize+name_hash_len+10]
signature = packet.data[keysize+name_hash_len+10:keysize+name_hash_len+10+sig_len]
app_data = b""
if len(packet.data) > keysize+name_hash_len+10+sig_len:
app_data = packet.data[keysize+name_hash_len+10+sig_len:]
signed_data = destination_hash+public_key+name_hash+random_hash+ratchet+app_data
if not len(packet.data) > Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:
app_data = None
@@ -237,7 +548,16 @@ class Identity:
announced_identity = Identity(create_keys=False)
announced_identity.load_public_key(public_key)
if len(RNS.Transport.blackholed_identities) > 0:
if announced_identity.hash in RNS.Transport.blackholed_identities:
RNS.log(f"Invalidated and dropped announce from blackholed identity {RNS.prettyhexrep(announced_identity.hash)}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
return False
if announced_identity.pub != None and announced_identity.validate(signature, signed_data):
if only_validate_signature:
del announced_identity
return True
hash_material = name_hash+announced_identity.hash
expected_hash = RNS.Identity.full_hash(hash_material)[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8]
@@ -255,19 +575,34 @@ class Identity:
RNS.Identity.remember(packet.get_hash(), destination_hash, public_key, app_data)
del announced_identity
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), RNS.LOG_EXTREME)
if packet.rssi != None or packet.snr != None:
signal_str = " ["
if packet.rssi != None:
signal_str += "RSSI "+str(packet.rssi)+"dBm"
if packet.snr != None:
signal_str += ", "
if packet.snr != None:
signal_str += "SNR "+str(packet.snr)+"dB"
signal_str += "]"
else:
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received on "+str(packet.receiving_interface), RNS.LOG_EXTREME)
signal_str = ""
if hasattr(packet, "transport_id") and packet.transport_id != None:
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received via "+RNS.prettyhexrep(packet.transport_id)+" on "+str(packet.receiving_interface)+signal_str, RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
else:
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received on "+str(packet.receiving_interface)+signal_str, RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
if ratchet:
Identity._remember_ratchet(destination_hash, ratchet)
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
@@ -276,9 +611,9 @@ class Identity:
return False
@staticmethod
def persist_data():
def persist_data(background=False):
if not RNS.Transport.owner.is_connected_to_shared_instance:
Identity.save_known_destinations()
Identity.save_known_destinations(background=background)
@staticmethod
def exit_handler():
@@ -334,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
@@ -373,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):
"""
@@ -440,7 +793,7 @@ class Identity:
return False
except Exception as e:
RNS.log("Error while loading identity from "+str(path), RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e))
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
def get_salt(self):
return self.hash
@@ -448,7 +801,7 @@ class Identity:
def get_context(self):
return None
def encrypt(self, plaintext):
def encrypt(self, plaintext, ratchet=None):
"""
Encrypts information for the identity.
@@ -460,25 +813,40 @@ class Identity:
ephemeral_key = X25519PrivateKey.generate()
ephemeral_pub_bytes = ephemeral_key.public_key().public_bytes()
shared_key = ephemeral_key.exchange(self.pub)
if ratchet != None:
target_public_key = X25519PublicKey.from_public_bytes(ratchet)
else:
target_public_key = self.pub
shared_key = ephemeral_key.exchange(target_public_key)
derived_key = RNS.Cryptography.hkdf(
length=32,
length=Identity.DERIVED_KEY_LENGTH,
derive_from=shared_key,
salt=self.get_salt(),
context=self.get_context(),
)
fernet = Fernet(derived_key)
ciphertext = fernet.encrypt(plaintext)
token = Token(derived_key)
ciphertext = token.encrypt(plaintext)
token = ephemeral_pub_bytes+ciphertext
return token
else:
raise KeyError("Encryption failed because identity does not hold a public key")
def __decrypt(self, shared_key, ciphertext):
derived_key = RNS.Cryptography.hkdf(
length=Identity.DERIVED_KEY_LENGTH,
derive_from=shared_key,
salt=self.get_salt(),
context=self.get_context())
def decrypt(self, ciphertext_token):
token = Token(derived_key)
plaintext = token.decrypt(ciphertext)
return plaintext
def decrypt(self, ciphertext_token, ratchets=None, enforce_ratchets=False, ratchet_id_receiver=None):
"""
Decrypts information for the identity.
@@ -486,32 +854,52 @@ class Identity:
:returns: Plaintext as *bytes*, or *None* if decryption fails.
:raises: *KeyError* if the instance does not hold a private key.
"""
if self.prv != None:
if len(ciphertext_token) > Identity.KEYSIZE//8//2:
plaintext = None
try:
peer_pub_bytes = ciphertext_token[:Identity.KEYSIZE//8//2]
peer_pub = X25519PublicKey.from_public_bytes(peer_pub_bytes)
shared_key = self.prv.exchange(peer_pub)
derived_key = RNS.Cryptography.hkdf(
length=32,
derive_from=shared_key,
salt=self.get_salt(),
context=self.get_context(),
)
fernet = Fernet(derived_key)
ciphertext = ciphertext_token[Identity.KEYSIZE//8//2:]
plaintext = fernet.decrypt(ciphertext)
if ratchets:
for ratchet in ratchets:
try:
ratchet_prv = X25519PrivateKey.from_private_bytes(ratchet)
ratchet_id = Identity._get_ratchet_id(ratchet_prv.public_key().public_bytes())
shared_key = ratchet_prv.exchange(peer_pub)
plaintext = self.__decrypt(shared_key, ciphertext)
if ratchet_id_receiver:
ratchet_id_receiver.latest_ratchet_id = ratchet_id
break
except Exception as e:
pass
if enforce_ratchets and plaintext == None:
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
if plaintext == None:
shared_key = self.prv.exchange(peer_pub)
plaintext = self.__decrypt(shared_key, ciphertext)
if ratchet_id_receiver:
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;
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")
+44 -14
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -20,7 +28,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .Interface import Interface
from RNS.Interfaces.Interface import Interface
from time import sleep
import sys
import threading
@@ -59,6 +67,7 @@ class AX25():
class AX25KISSInterface(Interface):
MAX_CHUNK = 32768
BITRATE_GUESS = 1200
DEFAULT_IFAC_SIZE = 8
owner = None
port = None
@@ -68,8 +77,8 @@ class AX25KISSInterface(Interface):
stopbits = None
serial = None
def __init__(self, owner, name, callsign, ssid, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control):
import importlib
def __init__(self, owner, configuration):
import importlib.util
if importlib.util.find_spec('serial') != None:
import serial
else:
@@ -77,8 +86,26 @@ class AX25KISSInterface(Interface):
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
RNS.panic()
self.rxb = 0
self.txb = 0
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
preamble = int(c["preamble"]) if "preamble" in c else None
txtail = int(c["txtail"]) if "txtail" in c else None
persistence = int(c["persistence"]) if "persistence" in c else None
slottime = int(c["slottime"]) if "slottime" in c else None
flow_control = c.as_bool("flow_control") if "flow_control" in c else False
port = c["port"] if "port" in c else None
speed = int(c["speed"]) if "speed" in c else 9600
databits = int(c["databits"]) if "databits" in c else 8
parity = c["parity"] if "parity" in c else "N"
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
callsign = c["callsign"] if "callsign" in c else ""
ssid = int(c["ssid"]) if "ssid" in c else -1
if port == None:
raise ValueError("No port specified for serial interface")
self.HW_MTU = 564
@@ -97,7 +124,7 @@ class AX25KISSInterface(Interface):
self.stopbits = stopbits
self.timeout = 100
self.online = False
self.bitrate = KISSInterface.BITRATE_GUESS
self.bitrate = AX25KISSInterface.BITRATE_GUESS
self.packet_queue = []
self.flow_control = flow_control
@@ -226,13 +253,13 @@ class AX25KISSInterface(Interface):
raise IOError("Could not enable AX.25 KISS interface flow control")
def processIncoming(self, data):
def process_incoming(self, data):
if (len(data) > AX25.HEADER_SIZE):
self.rxb += len(data)
self.owner.inbound(data[AX25.HEADER_SIZE:], self)
def processOutgoing(self,data):
def process_outgoing(self,data):
datalen = len(data)
if self.online:
if self.interface_ready:
@@ -282,7 +309,7 @@ class AX25KISSInterface(Interface):
if len(self.packet_queue) > 0:
data = self.packet_queue.pop(0)
self.interface_ready = True
self.processOutgoing(data)
self.process_outgoing(data)
elif len(self.packet_queue) == 0:
self.interface_ready = True
@@ -301,7 +328,7 @@ class AX25KISSInterface(Interface):
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
in_frame = False
self.processIncoming(data_buffer)
self.process_incoming(data_buffer)
elif (byte == KISS.FEND):
in_frame = True
command = KISS.CMD_UNKNOWN
@@ -367,5 +394,8 @@ class AX25KISSInterface(Interface):
RNS.log("Reconnected serial port for "+str(self))
def should_ingress_limit(self):
return False
def __str__(self):
return "AX25KISSInterface["+self.name+"]"
+45 -13
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -52,6 +60,7 @@ class KISS():
class KISSInterface(Interface):
MAX_CHUNK = 32768
BITRATE_GUESS = 1200
DEFAULT_IFAC_SIZE = 8
owner = None
port = None
@@ -61,8 +70,8 @@ class KISSInterface(Interface):
stopbits = None
serial = None
def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control, beacon_interval, beacon_data):
import importlib
def __init__(self, owner, configuration):
import importlib.util
if RNS.vendor.platformutils.is_android():
self.on_android = True
if importlib.util.find_spec('usbserial4a') != None:
@@ -82,8 +91,22 @@ class KISSInterface(Interface):
else:
raise SystemError("Android-specific interface was used on non-Android OS")
self.rxb = 0
self.txb = 0
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
preamble = int(c["preamble"]) if "preamble" in c else None
txtail = int(c["txtail"]) if "txtail" in c else None
persistence = int(c["persistence"]) if "persistence" in c else None
slottime = int(c["slottime"]) if "slottime" in c else None
flow_control = c.as_bool("flow_control") if "flow_control" in c else False
port = c["port"] if "port" in c else None
speed = int(c["speed"]) if "speed" in c else 9600
databits = int(c["databits"]) if "databits" in c else 8
parity = c["parity"] if "parity" in c else "N"
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
beacon_interval = int(c["beacon_interval"]) if "beacon_interval" in c and c["beacon_interval"] != None else None
beacon_data = c["beacon_data"] if "beacon_data" in c else None
self.HW_MTU = 564
@@ -268,13 +291,13 @@ class KISSInterface(Interface):
raise IOError("Could not enable KISS interface flow control")
def processIncoming(self, data):
def process_incoming(self, data):
self.rxb += len(data)
def af():
self.owner.inbound(data, self)
threading.Thread(target=af, daemon=True).start()
def processOutgoing(self,data):
def process_outgoing(self,data):
datalen = len(data)
if self.online:
if self.interface_ready:
@@ -308,7 +331,7 @@ class KISSInterface(Interface):
if len(self.packet_queue) > 0:
data = self.packet_queue.pop(0)
self.interface_ready = True
self.processOutgoing(data)
self.process_outgoing(data)
elif len(self.packet_queue) == 0:
self.interface_ready = True
@@ -329,7 +352,7 @@ class KISSInterface(Interface):
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
in_frame = False
self.processIncoming(data_buffer)
self.process_incoming(data_buffer)
elif (byte == KISS.FEND):
in_frame = True
command = KISS.CMD_UNKNOWN
@@ -374,7 +397,13 @@ class KISSInterface(Interface):
if time.time() > self.first_tx + self.beacon_i:
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.beacon_d.decode("utf-8")), RNS.LOG_DEBUG)
self.first_tx = None
self.processOutgoing(self.beacon_d)
# Pad to minimum length
frame = bytearray(self.beacon_d)
while len(frame) < 15:
frame.append(0x00)
self.process_outgoing(bytes(frame))
except Exception as e:
self.online = False
@@ -403,5 +432,8 @@ class KISSInterface(Interface):
RNS.log("Reconnected serial port for "+str(self))
def should_ingress_limit(self):
return False
def __str__(self):
return "KISSInterface["+self.name+"]"
File diff suppressed because it is too large Load Diff
+33 -11
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -42,6 +50,7 @@ class HDLC():
class SerialInterface(Interface):
MAX_CHUNK = 32768
DEFAULT_IFAC_SIZE = 8
owner = None
port = None
@@ -51,8 +60,8 @@ class SerialInterface(Interface):
stopbits = None
serial = None
def __init__(self, owner, name, port, speed, databits, parity, stopbits):
import importlib
def __init__(self, owner, configuration):
import importlib.util
if RNS.vendor.platformutils.is_android():
self.on_android = True
if importlib.util.find_spec('usbserial4a') != None:
@@ -72,8 +81,18 @@ class SerialInterface(Interface):
else:
raise SystemError("Android-specific interface was used on non-Android OS")
self.rxb = 0
self.txb = 0
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
port = c["port"] if "port" in c else None
speed = int(c["speed"]) if "speed" in c else 9600
databits = int(c["databits"]) if "databits" in c else 8
parity = c["parity"] if "parity" in c else "N"
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
if port == None:
raise ValueError("No port specified for serial interface")
self.HW_MTU = 564
@@ -173,13 +192,13 @@ class SerialInterface(Interface):
RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
def processIncoming(self, data):
def process_incoming(self, data):
self.rxb += len(data)
def af():
self.owner.inbound(data, self)
threading.Thread(target=af, daemon=True).start()
def processOutgoing(self,data):
def process_outgoing(self,data):
if self.online:
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
written = self.serial.write(data)
@@ -203,7 +222,7 @@ class SerialInterface(Interface):
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.processIncoming(data_buffer)
self.process_incoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
@@ -254,5 +273,8 @@ class SerialInterface(Interface):
RNS.log("Reconnected serial port for "+str(self))
def should_ingress_limit(self):
return False
def __str__(self):
return "SerialInterface["+self.name+"]"
+16 -6
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -23,5 +31,7 @@
import os
import glob
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
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"))]))
+445 -142
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -20,9 +28,11 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .Interface import Interface
from RNS.Interfaces.Interface import Interface
from collections import deque
import socketserver
import threading
import re
import socket
import struct
import time
@@ -31,9 +41,13 @@ import RNS
class AutoInterface(Interface):
HW_MTU = 1196
FIXED_MTU = True
DEFAULT_DISCOVERY_PORT = 29716
DEFAULT_DATA_PORT = 42671
DEFAULT_GROUP_ID = "reticulum".encode("utf-8")
DEFAULT_IFAC_SIZE = 16
SCOPE_LINK = "2"
SCOPE_ADMIN = "4"
@@ -41,45 +55,100 @@ class AutoInterface(Interface):
SCOPE_ORGANISATION = "8"
SCOPE_GLOBAL = "e"
PEERING_TIMEOUT = 7.5
MULTICAST_PERMANENT_ADDRESS_TYPE = "0"
MULTICAST_TEMPORARY_ADDRESS_TYPE = "1"
PEERING_TIMEOUT = 22.0
ANNOUNCE_INTERVAL = 1.6
PEER_JOB_INTERVAL = 4.0
MCAST_ECHO_TIMEOUT = 6.5
ALL_IGNORE_IFS = ["lo0"]
DARWIN_IGNORE_IFS = ["awdl0", "llw0", "lo0", "en5"]
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
def __init__(self, owner, name, group_id=None, discovery_scope=None, discovery_port=None, data_port=None, allowed_interfaces=None, ignored_interfaces=None, configured_bitrate=None):
import importlib
if importlib.util.find_spec('netifaces') != None:
import netifaces
else:
RNS.log("Using AutoInterface requires the netifaces module.", RNS.LOG_CRITICAL)
RNS.log("You can install it with the command: python3 -m pip install netifaces", RNS.LOG_CRITICAL)
RNS.panic()
MULTI_IF_DEQUE_LEN = 48
MULTI_IF_DEQUE_TTL = 0.75
self.netifaces = netifaces
self.rxb = 0
self.txb = 0
def handler_factory(self, callback):
def create_handler(*args, **keys):
return AutoInterfaceHandler(callback, *args, **keys)
return create_handler
self.HW_MTU = 1064
def descope_linklocal(self, link_local_addr):
# Drop scope specifier expressd as %ifname (macOS)
link_local_addr = link_local_addr.split("%")[0]
# Drop embedded scope specifier (NetBSD, OpenBSD)
link_local_addr = re.sub(r"fe80:[0-9a-f]*::","fe80::", link_local_addr)
return link_local_addr
def list_interfaces(self):
ifs = self.netinfo.interfaces()
return ifs
def list_addresses(self, ifname):
ifas = self.netinfo.ifaddresses(ifname)
return ifas
def interface_name_to_index(self, ifname):
# socket.if_nametoindex doesn't work with uuid interface names on windows, it wants the ethernet_0 style
# we will just get the index from netinfo instead as it seems to work
if RNS.vendor.platformutils.is_windows():
return self.netinfo.interface_names_to_indexes()[ifname]
return socket.if_nametoindex(ifname)
def __init__(self, owner, configuration):
c = Interface.get_config_obj(configuration)
name = c["name"]
group_id = c["group_id"] if "group_id" in c else None
discovery_scope = c["discovery_scope"] if "discovery_scope" in c else None
discovery_port = int(c["discovery_port"]) if "discovery_port" in c else None
multicast_address_type = c["multicast_address_type"] if "multicast_address_type" in c else None
data_port = int(c["data_port"]) if "data_port" in c else None
allowed_interfaces = c.as_list("devices") if "devices" in c else None
ignored_interfaces = c.as_list("ignored_devices") if "ignored_devices" in c else None
configured_bitrate = c["configured_bitrate"] if "configured_bitrate" in c else None
from RNS.Interfaces import netinfo
super().__init__()
self.netinfo = netinfo
self.HW_MTU = AutoInterface.HW_MTU
self.IN = True
self.OUT = False
self.name = name
self.owner = owner
self.online = False
self.final_init_done = False
self.peers = {}
self.link_local_addresses = []
self.adopted_interfaces = {}
self.interface_servers = {}
self.multicast_echoes = {}
self.initial_echoes = {}
self.timed_out_interfaces = {}
self.spawned_interfaces = {}
self.write_lock = threading.Lock()
self.mif_deque = deque(maxlen=AutoInterface.MULTI_IF_DEQUE_LEN)
self.mif_deque_times = deque(maxlen=AutoInterface.MULTI_IF_DEQUE_LEN)
self.carrier_changed = False
self.outbound_udp_socket = None
self.announce_rate_target = None
self.announce_interval = AutoInterface.PEERING_TIMEOUT/6.0
self.peer_job_interval = AutoInterface.PEERING_TIMEOUT*1.1
self.peering_timeout = AutoInterface.PEERING_TIMEOUT
self.multicast_echo_timeout = AutoInterface.PEERING_TIMEOUT/2
self.announce_rate_target = None
self.announce_interval = AutoInterface.ANNOUNCE_INTERVAL
self.peer_job_interval = AutoInterface.PEER_JOB_INTERVAL
self.peering_timeout = AutoInterface.PEERING_TIMEOUT
self.multicast_echo_timeout = AutoInterface.MCAST_ECHO_TIMEOUT
self.reverse_peering_interval = self.announce_interval*3.25
# Increase peering timeout on Android, due to potential
# low-power modes implemented on many chipsets.
if RNS.vendor.platformutils.is_android():
self.peering_timeout *= 1.25
if allowed_interfaces == None:
self.allowed_interfaces = []
@@ -101,6 +170,17 @@ class AutoInterface(Interface):
else:
self.discovery_port = discovery_port
self.unicast_discovery_port = self.discovery_port+1
if multicast_address_type == None:
self.multicast_address_type = AutoInterface.MULTICAST_TEMPORARY_ADDRESS_TYPE
elif str(multicast_address_type).lower() == "temporary":
self.multicast_address_type = AutoInterface.MULTICAST_TEMPORARY_ADDRESS_TYPE
elif str(multicast_address_type).lower() == "permanent":
self.multicast_address_type = AutoInterface.MULTICAST_PERMANENT_ADDRESS_TYPE
else:
self.multicast_address_type = AutoInterface.MULTICAST_TEMPORARY_ADDRESS_TYPE
if data_port == None:
self.data_port = AutoInterface.DEFAULT_DATA_PORT
else:
@@ -129,124 +209,164 @@ class AutoInterface(Interface):
gt += ":"+"{:02x}".format(g[9]+(g[8]<<8))
gt += ":"+"{:02x}".format(g[11]+(g[10]<<8))
gt += ":"+"{:02x}".format(g[13]+(g[12]<<8))
self.mcast_discovery_address = "ff1"+self.discovery_scope+":"+gt
self.mcast_discovery_address = "ff"+self.multicast_address_type+self.discovery_scope+":"+gt
suitable_interfaces = 0
for ifname in self.netifaces.interfaces():
if RNS.vendor.platformutils.is_darwin() and ifname in AutoInterface.DARWIN_IGNORE_IFS and not ifname in self.allowed_interfaces:
RNS.log(str(self)+" skipping Darwin AWDL or tethering interface "+str(ifname), RNS.LOG_EXTREME)
elif RNS.vendor.platformutils.is_darwin() and ifname == "lo0":
RNS.log(str(self)+" skipping Darwin loopback interface "+str(ifname), RNS.LOG_EXTREME)
elif RNS.vendor.platformutils.is_android() and ifname in AutoInterface.ANDROID_IGNORE_IFS and not ifname in self.allowed_interfaces:
RNS.log(str(self)+" skipping Android system interface "+str(ifname), RNS.LOG_EXTREME)
elif ifname in self.ignored_interfaces:
RNS.log(str(self)+" ignoring disallowed interface "+str(ifname), RNS.LOG_EXTREME)
else:
if len(self.allowed_interfaces) > 0 and not ifname in self.allowed_interfaces:
RNS.log(str(self)+" ignoring interface "+str(ifname)+" since it was not allowed", RNS.LOG_EXTREME)
for ifname in self.list_interfaces():
try:
if RNS.vendor.platformutils.is_darwin() and ifname in AutoInterface.DARWIN_IGNORE_IFS and not ifname in self.allowed_interfaces:
RNS.log(str(self)+" skipping Darwin AWDL or tethering interface "+str(ifname), RNS.LOG_EXTREME)
elif RNS.vendor.platformutils.is_darwin() and ifname == "lo0":
RNS.log(str(self)+" skipping Darwin loopback interface "+str(ifname), RNS.LOG_EXTREME)
elif RNS.vendor.platformutils.is_android() and ifname in AutoInterface.ANDROID_IGNORE_IFS and not ifname in self.allowed_interfaces:
RNS.log(str(self)+" skipping Android system interface "+str(ifname), RNS.LOG_EXTREME)
elif ifname in self.ignored_interfaces:
RNS.log(str(self)+" ignoring disallowed interface "+str(ifname), RNS.LOG_EXTREME)
elif ifname in AutoInterface.ALL_IGNORE_IFS:
RNS.log(str(self)+" skipping interface "+str(ifname), RNS.LOG_EXTREME)
else:
addresses = self.netifaces.ifaddresses(ifname)
if self.netifaces.AF_INET6 in addresses:
link_local_addr = None
for address in addresses[self.netifaces.AF_INET6]:
if "addr" in address:
if address["addr"].startswith("fe80:"):
link_local_addr = address["addr"]
self.link_local_addresses.append(link_local_addr.split("%")[0])
self.adopted_interfaces[ifname] = link_local_addr.split("%")[0]
self.multicast_echoes[ifname] = time.time()
RNS.log(str(self)+" Selecting link-local address "+str(link_local_addr)+" for interface "+str(ifname), RNS.LOG_EXTREME)
if len(self.allowed_interfaces) > 0 and not ifname in self.allowed_interfaces:
RNS.log(str(self)+" ignoring interface "+str(ifname)+" since it was not allowed", RNS.LOG_EXTREME)
else:
addresses = self.list_addresses(ifname)
if self.netinfo.AF_INET6 in addresses:
link_local_addr = None
for address in addresses[self.netinfo.AF_INET6]:
if "addr" in address:
if address["addr"].startswith("fe80:"):
link_local_addr = self.descope_linklocal(address["addr"])
self.link_local_addresses.append(link_local_addr)
self.adopted_interfaces[ifname] = link_local_addr
self.multicast_echoes[ifname] = time.time()
nice_name = self.netinfo.interface_name_to_nice_name(ifname)
if nice_name != None and nice_name != ifname:
RNS.log(f"{self} Selecting link-local address {link_local_addr} for interface {nice_name} / {ifname}", RNS.LOG_EXTREME)
else:
RNS.log(f"{self} Selecting link-local address {link_local_addr} for interface {ifname}", RNS.LOG_EXTREME)
if link_local_addr == None:
RNS.log(str(self)+" No link-local IPv6 address configured for "+str(ifname)+", skipping interface", RNS.LOG_EXTREME)
else:
mcast_addr = self.mcast_discovery_address
RNS.log(str(self)+" Creating multicast discovery listener on "+str(ifname)+" with address "+str(mcast_addr), RNS.LOG_EXTREME)
if link_local_addr == None:
RNS.log(str(self)+" No link-local IPv6 address configured for "+str(ifname)+", skipping interface", RNS.LOG_EXTREME)
else:
RNS.log(str(self)+" Creating unicast discovery listener on "+str(ifname)+" with address "+str(link_local_addr), RNS.LOG_EXTREME)
# Struct with interface index
if_struct = struct.pack("I", socket.if_nametoindex(ifname))
# Struct with interface index
if_struct = struct.pack("I", self.interface_name_to_index(ifname))
# Set up multicast socket
discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, "SO_REUSEPORT"):
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, if_struct)
# Set up unicast discovery socket
unicast_discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
unicast_discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, "SO_REUSEPORT"): unicast_discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
# Join multicast group
mcast_group = socket.inet_pton(socket.AF_INET6, mcast_addr) + if_struct
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mcast_group)
# Bind unicast discovery socket
if RNS.vendor.platformutils.is_windows():
# Windows throws "[WinError 10049] The requested address is not valid in its context"
# when trying to use the multicast address as host, or when providing interface index
# passing an empty host appears to work, but probably not exactly how we want it to...
unicast_discovery_socket.bind(('', self.unicast_discovery_port))
# Bind socket
addr_info = socket.getaddrinfo(mcast_addr+"%"+ifname, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
discovery_socket.bind(addr_info[0][4])
else:
addr_info = socket.getaddrinfo(link_local_addr+"%"+ifname, self.unicast_discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
unicast_discovery_socket.bind(addr_info[0][4])
# Set up thread for discovery packets
def discovery_loop():
self.discovery_handler(discovery_socket, ifname)
mcast_addr = self.mcast_discovery_address
RNS.log(str(self)+" Creating multicast discovery listener on "+str(ifname)+" with address "+str(mcast_addr), RNS.LOG_EXTREME)
thread = threading.Thread(target=discovery_loop)
thread.daemon = True
thread.start()
# Set up multicast discovery socket
discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, "SO_REUSEPORT"): discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, if_struct)
suitable_interfaces += 1
# Join multicast group
mcast_group = socket.inet_pton(socket.AF_INET6, mcast_addr) + if_struct
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mcast_group)
# Bind multicast socket
if RNS.vendor.platformutils.is_windows():
# Windows throws "[WinError 10049] The requested address is not valid in its context"
# when trying to use the multicast address as host, or when providing interface index
# passing an empty host appears to work, but probably not exactly how we want it to...
discovery_socket.bind(('', self.discovery_port))
else:
if self.discovery_scope == AutoInterface.SCOPE_LINK:
addr_info = socket.getaddrinfo(mcast_addr+"%"+ifname, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
else:
addr_info = socket.getaddrinfo(mcast_addr, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
discovery_socket.bind(addr_info[0][4])
# Set up thread for multicast discovery packets
def discovery_loop(): self.discovery_handler(discovery_socket, ifname)
thread = threading.Thread(target=discovery_loop, daemon=True).start()
# Set up thread for unicast discovery packets
def unicast_discovery_loop(): self.discovery_handler(unicast_discovery_socket, ifname, announce=False)
thread = threading.Thread(target=unicast_discovery_loop, daemon=True).start()
suitable_interfaces += 1
except Exception as e:
nice_name = self.netinfo.interface_name_to_nice_name(ifname)
if nice_name != None and nice_name != ifname:
RNS.log(f"Could not configure the system interface {nice_name} / {ifname} for use with {self}, skipping it. The contained exception was: {e}", RNS.LOG_ERROR)
else:
RNS.log(f"Could not configure the system interface {ifname} for use with {self}, skipping it. The contained exception was: {e}", RNS.LOG_ERROR)
if suitable_interfaces == 0:
RNS.log(str(self)+" could not autoconfigure. This interface currently provides no connectivity.", RNS.LOG_WARNING)
else:
self.receives = True
peering_wait = self.announce_interval*1.2
RNS.log(str(self)+" discovering peers for "+str(round(peering_wait, 2))+" seconds...", RNS.LOG_VERBOSE)
def handlerFactory(callback):
def createHandler(*args, **keys):
return AutoInterfaceHandler(callback, *args, **keys)
return createHandler
self.owner = owner
socketserver.UDPServer.address_family = socket.AF_INET6
for ifname in self.adopted_interfaces:
local_addr = self.adopted_interfaces[ifname]+"%"+ifname
addr_info = socket.getaddrinfo(local_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
address = addr_info[0][4]
self.server = socketserver.UDPServer(address, handlerFactory(self.processIncoming))
thread = threading.Thread(target=self.server.serve_forever)
thread.daemon = True
thread.start()
job_thread = threading.Thread(target=self.peer_jobs)
job_thread.daemon = True
job_thread.start()
time.sleep(peering_wait)
if configured_bitrate != None:
self.bitrate = configured_bitrate
else:
self.bitrate = AutoInterface.BITRATE_GUESS
self.online = True
def final_init(self):
peering_wait = self.announce_interval*1.2
RNS.log(str(self)+" discovering peers for "+str(round(peering_wait, 2))+" seconds...", RNS.LOG_VERBOSE)
socketserver.UDPServer.address_family = socket.AF_INET6
def discovery_handler(self, socket, ifname):
def announce_loop():
self.announce_handler(ifname)
for ifname in self.adopted_interfaces:
local_addr = self.adopted_interfaces[ifname]+"%"+str(self.interface_name_to_index(ifname))
addr_info = socket.getaddrinfo(local_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
address = addr_info[0][4]
udp_server = socketserver.UDPServer(address, self.handler_factory(self.process_incoming))
self.interface_servers[ifname] = udp_server
thread = threading.Thread(target=announce_loop)
thread.daemon = True
thread.start()
thread = threading.Thread(target=udp_server.serve_forever)
thread.daemon = True
thread.start()
job_thread = threading.Thread(target=self.peer_jobs)
job_thread.daemon = True
job_thread.start()
time.sleep(peering_wait)
self.online = True
self.final_init_done = True
def discovery_handler(self, socket, ifname, announce=True):
def announce_loop(): self.announce_handler(ifname)
if announce:
thread = threading.Thread(target=announce_loop)
thread.daemon = True
thread.start()
while True:
data, ipv6_src = socket.recvfrom(1024)
expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
if data == expected_hash:
self.add_peer(ipv6_src[0], ifname)
else:
RNS.log(str(self)+" received peering packet on "+str(ifname)+" from "+str(ipv6_src[0])+", but authentication hash was incorrect.", RNS.LOG_DEBUG)
if self.final_init_done:
peering_hash = data[:RNS.Identity.HASHLENGTH//8]
expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
if peering_hash == expected_hash:
self.add_peer(ipv6_src[0], ifname)
else:
RNS.log(str(self)+" received peering packet on "+str(ifname)+" from "+str(ipv6_src[0])+", but authentication hash was incorrect.", RNS.LOG_DEBUG)
def peer_jobs(self):
while True:
@@ -264,37 +384,89 @@ class AutoInterface(Interface):
# Remove any timed out peers
for peer_addr in timed_out_peers:
removed_peer = self.peers.pop(peer_addr)
if peer_addr in self.spawned_interfaces:
spawned_interface = self.spawned_interfaces[peer_addr]
spawned_interface.detach()
spawned_interface.teardown()
RNS.log(str(self)+" removed peer "+str(peer_addr)+" on "+str(removed_peer[0]), RNS.LOG_DEBUG)
# Send reverse peering packets
for peer_addr in self.peers:
try:
peer = self.peers[peer_addr]
ifname = peer[0]
last_outbound = peer[2]
if now > last_outbound+self.reverse_peering_interval:
self.reverse_announce(ifname, peer_addr)
peer[2] = time.time()
except Exception as e:
RNS.log(f"Error while sending reverse peering packet to {peer_addr}: {e}", RNS.LOG_ERROR)
for ifname in self.adopted_interfaces:
# Check that the link-local address has not changed
try:
addresses = self.netifaces.ifaddresses(ifname)
if self.netifaces.AF_INET6 in addresses:
addresses = self.list_addresses(ifname)
if self.netinfo.AF_INET6 in addresses:
link_local_addr = None
for address in addresses[self.netifaces.AF_INET6]:
for address in addresses[self.netinfo.AF_INET6]:
if "addr" in address:
if address["addr"].startswith("fe80:"):
link_local_addr = address["addr"].split("%")[0]
link_local_addr = self.descope_linklocal(address["addr"])
if link_local_addr != self.adopted_interfaces[ifname]:
old_link_local_address = self.adopted_interfaces[ifname]
RNS.log("Replacing link-local address "+str(old_link_local_address)+" for "+str(ifname)+" with "+str(link_local_addr), RNS.LOG_DEBUG)
self.adopted_interfaces[ifname] = link_local_addr
self.link_local_addresses.append(link_local_addr)
if old_link_local_address in self.link_local_addresses:
self.link_local_addresses.remove(old_link_local_address)
local_addr = link_local_addr+"%"+ifname
addr_info = socket.getaddrinfo(local_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
listen_address = addr_info[0][4]
if ifname in self.interface_servers:
RNS.log("Shutting down previous UDP listener for "+str(self)+" "+str(ifname), RNS.LOG_DEBUG)
previous_server = self.interface_servers[ifname]
def shutdown_server():
previous_server.shutdown()
threading.Thread(target=shutdown_server, daemon=True).start()
RNS.log("Starting new UDP listener for "+str(self)+" "+str(ifname), RNS.LOG_DEBUG)
udp_server = socketserver.UDPServer(listen_address, self.handler_factory(self.process_incoming))
self.interface_servers[ifname] = udp_server
thread = threading.Thread(target=udp_server.serve_forever)
thread.daemon = True
thread.start()
self.carrier_changed = True
except Exception as e:
RNS.log("Could not get device information while updating link-local addresses for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
# Check multicast echo timeouts
last_multicast_echo = 0
if ifname in self.multicast_echoes:
last_multicast_echo = self.multicast_echoes[ifname]
last_multicast_echo = 0
multicast_echo_received = False
if ifname in self.multicast_echoes: last_multicast_echo = self.multicast_echoes[ifname]
if ifname in self.initial_echoes: multicast_echo_received = True
if now - last_multicast_echo > self.multicast_echo_timeout:
if ifname in self.timed_out_interfaces and self.timed_out_interfaces[ifname] == False:
self.carrier_changed = True
RNS.log("Multicast echo timeout for "+str(ifname)+". Carrier lost.", RNS.LOG_WARNING)
self.timed_out_interfaces[ifname] = True
else:
if ifname in self.timed_out_interfaces and self.timed_out_interfaces[ifname] == True:
self.carrier_changed = True
RNS.log(str(self)+" Carrier recovered on "+str(ifname), RNS.LOG_WARNING)
self.timed_out_interfaces[ifname] = False
if not multicast_echo_received:
RNS.log(f"{self} No multicast echoes received on {ifname}. The networking hardware or a firewall may be blocking multicast traffic.", RNS.LOG_ERROR)
# else:
# RNS.log(f"{self} Initial multicast echo on {ifname} received {RNS.prettytime(time.time()-self.initial_echoes[ifname])} ago.", RNS.LOG_DEBUG)
def announce_handler(self, ifname):
@@ -302,6 +474,20 @@ class AutoInterface(Interface):
self.peer_announce(ifname)
time.sleep(self.announce_interval)
def reverse_announce(self, ifname, peer_addr):
try:
link_local_address = self.adopted_interfaces[ifname]
discovery_token = RNS.Identity.full_hash(self.group_id+link_local_address.encode("utf-8"))
announce_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
addr_info = socket.getaddrinfo(f"{peer_addr}%{ifname}", self.unicast_discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
ifis = struct.pack("I", self.interface_name_to_index(ifname))
announce_socket.sendto(discovery_token, addr_info[0][4])
announce_socket.close()
except Exception as e:
RNS.log(f"Could not send reverse peering packet to {peer_addr} on {ifname}: {e}", RNS.LOG_ERROR)
def peer_announce(self, ifname):
try:
link_local_address = self.adopted_interfaces[ifname]
@@ -309,7 +495,7 @@ class AutoInterface(Interface):
announce_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
addr_info = socket.getaddrinfo(self.mcast_discovery_address, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
ifis = struct.pack("I", socket.if_nametoindex(ifname))
ifis = struct.pack("I", self.interface_name_to_index(ifname))
announce_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifis)
announce_socket.sendto(discovery_token, addr_info[0][4])
announce_socket.close()
@@ -320,6 +506,10 @@ class AutoInterface(Interface):
else:
pass
@property
def peer_count(self):
return len(self.spawned_interfaces)
def add_peer(self, addr, ifname):
if addr in self.link_local_addresses:
ifname = None
@@ -329,42 +519,154 @@ class AutoInterface(Interface):
if ifname != None:
self.multicast_echoes[ifname] = time.time()
if not ifname in self.initial_echoes: self.initial_echoes[ifname] = time.time()
else:
RNS.log(str(self)+" received multicast echo on unexpected interface "+str(ifname), RNS.LOG_WARNING)
else:
if not addr in self.peers:
self.peers[addr] = [ifname, time.time()]
self.peers[addr] = [ifname, time.time(), time.time()]
spawned_interface = AutoInterfacePeer(self, addr, ifname)
spawned_interface.OUT = self.OUT
spawned_interface.IN = self.IN
spawned_interface.ingress_control = self.ingress_control
spawned_interface.ic_max_held_announces = self.ic_max_held_announces
spawned_interface.ic_burst_hold = self.ic_burst_hold
spawned_interface.ic_burst_freq = self.ic_burst_freq
spawned_interface.ic_burst_freq_new = self.ic_burst_freq_new
spawned_interface.ic_new_time = self.ic_new_time
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
spawned_interface.egress_control = self.egress_control
spawned_interface.ec_pr_freq = self.ec_pr_freq
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
spawned_interface.parent_interface = self
spawned_interface.bitrate = self.bitrate
spawned_interface.ifac_size = self.ifac_size
spawned_interface.ifac_netname = self.ifac_netname
spawned_interface.ifac_netkey = self.ifac_netkey
if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None:
ifac_origin = b""
if spawned_interface.ifac_netname != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
if spawned_interface.ifac_netkey != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
spawned_interface.ifac_key = RNS.Cryptography.hkdf(
length=64,
derive_from=ifac_origin_hash,
salt=RNS.Reticulum.IFAC_SALT,
context=None
)
spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key)
spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key))
spawned_interface.announce_rate_target = self.announce_rate_target
spawned_interface.announce_rate_grace = self.announce_rate_grace
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
spawned_interface.mode = self.mode
spawned_interface.HW_MTU = self.HW_MTU
spawned_interface.online = True
RNS.Transport.add_interface(spawned_interface)
if addr in self.spawned_interfaces:
self.spawned_interfaces[addr].detach()
self.spawned_interfaces[addr].teardown()
if addr in self.spawned_interfaces: self.spawned_interfaces.pop(addr)
self.spawned_interfaces[addr] = spawned_interface
RNS.log(str(self)+" added peer "+str(addr)+" on "+str(ifname), RNS.LOG_DEBUG)
else:
self.refresh_peer(addr)
def refresh_peer(self, addr):
self.peers[addr][1] = time.time()
try: self.peers[addr][1] = time.time()
except Exception as e: RNS.log(f"An error occurred while refreshing peer {addr} on {self}: {e}", RNS.LOG_ERROR)
def processIncoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self)
def process_incoming(self, data, addr=None):
if self.online and addr in self.spawned_interfaces:
self.spawned_interfaces[addr].process_incoming(data, addr)
def processOutgoing(self,data):
for peer in self.peers:
def process_outgoing(self, data): pass
def detach(self): self.online = False
def __str__(self): return f"AutoInterface[{self.name}]"
class AutoInterfacePeer(Interface):
def __init__(self, owner, addr, ifname):
super().__init__()
self.owner = owner
self.parent_interface = owner
self.addr = addr
self.ifname = ifname
self.peer_addr = None
self.addr_info = None
self.HW_MTU = self.owner.HW_MTU
self.FIXED_MTU = self.owner.FIXED_MTU
def __str__(self):
return f"AutoInterfacePeer[{self.ifname}/{self.addr}]"
def process_incoming(self, data, addr=None):
if self.online and self.owner.online:
data_hash = RNS.Identity.full_hash(data)
deque_hit = False
if data_hash in self.owner.mif_deque:
for te in self.owner.mif_deque_times:
if te[0] == data_hash and time.time() < te[1]+AutoInterface.MULTI_IF_DEQUE_TTL:
deque_hit = True
break
if not deque_hit:
self.owner.refresh_peer(self.addr)
self.owner.mif_deque.append(data_hash)
self.owner.mif_deque_times.append([data_hash, time.time()])
self.rxb += len(data)
self.owner.rxb += len(data)
self.owner.owner.inbound(data, self)
def process_outgoing(self, data):
if self.online:
with self.owner.write_lock:
try:
if self.outbound_udp_socket == None:
self.outbound_udp_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
peer_addr = str(peer)+"%"+str(self.peers[peer][0])
addr_info = socket.getaddrinfo(peer_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
self.outbound_udp_socket.sendto(data, addr_info[0][4])
if self.owner.outbound_udp_socket == None: self.owner.outbound_udp_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
if self.peer_addr == None: self.peer_addr = str(self.addr)+"%"+str(self.owner.interface_name_to_index(self.ifname))
if self.addr_info == None: self.addr_info = socket.getaddrinfo(self.peer_addr, self.owner.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
self.owner.outbound_udp_socket.sendto(data, self.addr_info[0][4])
self.txb += len(data)
self.owner.txb += len(data)
except Exception as e:
RNS.log("Could not transmit on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
self.txb += len(data)
def detach(self):
self.online = False
self.detached = True
def teardown(self):
if not self.detached:
RNS.log(f"The interface {self} experienced an unrecoverable error and is being torn down.", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error: RNS.panic()
def __str__(self):
return "AutoInterface["+self.name+"]"
else: RNS.log(f"The interface {self} is being torn down.", RNS.LOG_VERBOSE)
self.online = False
self.OUT = False
self.IN = False
if self.addr in self.owner.spawned_interfaces:
try: self.owner.spawned_interfaces.pop(self.addr)
except Exception as e:
RNS.log(f"Could not remove {self} from parent interface on detach. The contained exception was: {e}", RNS.LOG_ERROR)
RNS.Transport.remove_interface(self)
class AutoInterfaceHandler(socketserver.BaseRequestHandler):
def __init__(self, callback, *args, **keys):
@@ -373,4 +675,5 @@ class AutoInterfaceHandler(socketserver.BaseRequestHandler):
def handle(self):
data = self.request[0]
self.callback(data)
addr = self.client_address[0]
self.callback(data, addr)
+779
View File
@@ -0,0 +1,779 @@
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from RNS.Interfaces.Interface import Interface
import threading
import socket
import select
import time
import sys
import os
import RNS
class HDLC():
FLAG = 0x7E
ESC = 0x7D
ESC_MASK = 0x20
@staticmethod
def escape(data):
data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
return data
class BackboneInterface(Interface):
HW_MTU = 1048576
BITRATE_GUESS = 1_000_000_000
DEFAULT_IFAC_SIZE = 16
AUTOCONFIGURE_MTU = True
epoll = None
listener_filenos = {}
spawned_interface_filenos = {}
epoll = None
_job_active = False
_job_lock = threading.Lock()
@staticmethod
def get_address_for_if(name, bind_port, prefer_ipv6=False):
from RNS.Interfaces import netinfo
ifaddr = netinfo.ifaddresses(name)
if len(ifaddr) < 1:
raise SystemError(f"No addresses available on specified kernel interface \"{name}\" for BackboneInterface to bind to")
if (prefer_ipv6 or not netinfo.AF_INET in ifaddr) and netinfo.AF_INET6 in ifaddr:
bind_ip = ifaddr[netinfo.AF_INET6][0]["addr"]
if bind_ip.lower().startswith("fe80::"):
# We'll need to add the interface as scope for link-local addresses
return BackboneInterface.get_address_for_host(f"{bind_ip}%{name}", bind_port, prefer_ipv6)
else:
return BackboneInterface.get_address_for_host(bind_ip, bind_port, prefer_ipv6)
elif netinfo.AF_INET in ifaddr:
bind_ip = ifaddr[netinfo.AF_INET][0]["addr"]
return (bind_ip, bind_port)
else:
raise SystemError(f"No addresses available on specified kernel interface \"{name}\" for BackboneInterface to bind to")
@staticmethod
def get_address_for_host(name, bind_port, prefer_ipv6=False):
address_infos = socket.getaddrinfo(name, bind_port, proto=socket.IPPROTO_TCP)
address_info = address_infos[0]
for entry in address_infos:
if prefer_ipv6 and entry[0] == socket.AF_INET6:
address_info = entry; break
elif not prefer_ipv6 and entry[0] == socket.AF_INET:
address_info = entry; break
if address_info[0] == socket.AF_INET6:
return (name, bind_port, address_info[4][2], address_info[4][3])
elif address_info[0] == socket.AF_INET:
return (name, bind_port)
else:
raise SystemError(f"No suitable kernel interface available for address \"{name}\" for BackboneInterface to bind to")
@property
def clients(self):
return len(self.spawned_interfaces)
def __init__(self, owner, configuration):
if not RNS.vendor.platformutils.is_linux() and not RNS.vendor.platformutils.is_android():
raise OSError("BackboneInterface is only supported on Linux-based operating systems")
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
device = c["device"] if "device" in c else None
port = int(c["port"]) if "port" in c else None
bindip = c["listen_ip"] if "listen_ip" in c else None
bindport = int(c["listen_port"]) if "listen_port" in c else None
prefer_ipv6 = c.as_bool("prefer_ipv6") if "prefer_ipv6" in c else False
if port != None: bindport = port
self.HW_MTU = BackboneInterface.HW_MTU
self.online = False
self.IN = True
self.OUT = False
self.name = name
self.detached = False
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
self.spawned_interfaces = []
self.supports_discovery = True
if bindport == None:
raise SystemError(f"No TCP port configured for interface \"{name}\"")
else:
self.bind_port = bindport
bind_address = None
if device != None:
bind_address = self.get_address_for_if(device, self.bind_port, prefer_ipv6)
else:
if bindip == None:
raise SystemError(f"No TCP bind IP configured for interface \"{name}\"")
bind_address = self.get_address_for_host(bindip, self.bind_port, prefer_ipv6)
if bind_address != None:
self.receives = True
self.bind_ip = bind_address[0]
self.owner = owner
if len(bind_address) == 2 : BackboneInterface.add_listener(self, bind_address, socket_type=socket.AF_INET)
elif len(bind_address) == 4: BackboneInterface.add_listener(self, bind_address, socket_type=socket.AF_INET6)
self.bitrate = self.BITRATE_GUESS
self.online = True
else:
raise SystemError("Insufficient parameters to create listener")
__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()
@staticmethod
def ensure_epoll():
if not BackboneInterface.epoll: BackboneInterface.epoll = select.epoll()
@staticmethod
def add_listener(interface, bind_address, socket_type=socket.AF_INET):
BackboneInterface.ensure_epoll()
if socket_type == socket.AF_INET:
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(bind_address)
elif socket_type == socket.AF_INET6:
server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(bind_address)
elif socket_type == socket.AF_UNIX:
server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server_socket.bind(bind_address)
else: raise TypeError(f"Invalid socket type {socket_type} for {interface}")
server_socket.listen(1)
server_socket.setblocking(0)
BackboneInterface.listener_filenos[server_socket.fileno()] = (interface, server_socket)
BackboneInterface.epoll.register(server_socket.fileno(), select.EPOLLIN)
BackboneInterface.start()
@staticmethod
def add_client_socket(client_socket, interface):
BackboneInterface.ensure_epoll()
BackboneInterface.spawned_interface_filenos[client_socket.fileno()] = interface
BackboneInterface.register_in(client_socket.fileno())
BackboneInterface.start()
@staticmethod
def register_in(fileno):
if fileno < 0:
RNS.log(f"Attempt to register invalid file descriptor {fileno}", RNS.LOG_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_WARNING)
@staticmethod
def deregister_fileno(fileno):
if fileno < 0:
RNS.log(f"Attempt to deregister invalid file descriptor {fileno}", RNS.LOG_DEBUG)
return
try: BackboneInterface.epoll.unregister(fileno)
except Exception as e:
RNS.log(f"An error occurred while deregistering file descriptor {fileno}: {e}", RNS.LOG_DEBUG)
@staticmethod
def deregister_listeners():
for fileno in BackboneInterface.listener_filenos:
owner_interface, server_socket = BackboneInterface.listener_filenos[fileno]
fileno = server_socket.fileno()
BackboneInterface.deregister_fileno(fileno)
server_socket.close()
BackboneInterface.listener_filenos.clear()
@staticmethod
def tx_ready(interface):
if interface.socket:
fileno = interface.socket.fileno()
if fileno in BackboneInterface.spawned_interface_filenos:
try: BackboneInterface.epoll.modify(fileno, select.EPOLLOUT)
except Exception as e:
RNS.log(f"Error occurred on {interface} while modifying socket EPOLL state: {e}", RNS.LOG_WARNING)
raise e
@staticmethod
def __job():
with BackboneInterface._job_lock:
if BackboneInterface._job_active: return
else:
BackboneInterface._job_active = True
BackboneInterface.ensure_epoll()
try:
while True:
events = BackboneInterface.epoll.poll(1)
for fileno, event in BackboneInterface.epoll.poll(1):
if fileno in BackboneInterface.spawned_interface_filenos:
spawned_interface = BackboneInterface.spawned_interface_filenos[fileno]
client_socket = spawned_interface.socket
if client_socket and fileno == client_socket.fileno() and (event & select.EPOLLIN):
try: received_bytes = client_socket.recv(spawned_interface.HW_MTU)
except Exception as e:
RNS.log(f"Error while reading from {spawned_interface}: {e}", RNS.LOG_DEBUG)
received_bytes = b""
if len(received_bytes): spawned_interface.receive(received_bytes)
else:
BackboneInterface.deregister_fileno(fileno); client_socket.close()
try:
if fileno in BackboneInterface.spawned_interface_filenos: BackboneInterface.spawned_interface_filenos.pop(fileno)
except Exception as e: RNS.log(f"Error while removing spawned interface file descriptor from BackboneInterface I/O handler: {e}", RNS.LOG_ERROR)
try:
if spawned_interface.parent_interface:
pif = spawned_interface.parent_interface
if pif.spawned_interfaces != None:
while spawned_interface in pif.spawned_interfaces: pif.spawned_interfaces.remove(spawned_interface)
except Exception as e: RNS.log(f"Error while removing spawned interface from {pif}: {e}", RNS.LOG_ERROR)
spawned_interface.receive(received_bytes)
elif client_socket and fileno == client_socket.fileno() and (event & select.EPOLLOUT):
try: written = client_socket.send(spawned_interface.transmit_buffer)
except Exception as e:
written = 0
if not spawned_interface.detached: RNS.log(f"Error while writing to {spawned_interface}: {e}", RNS.LOG_DEBUG)
BackboneInterface.deregister_fileno(fileno)
try:
if fileno in BackboneInterface.spawned_interface_filenos: BackboneInterface.spawned_interface_filenos.pop(fileno)
except Exception as e: RNS.log(f"Error while removing spawned interface file descriptor from BackboneInterface I/O handler: {e}", RNS.LOG_ERROR)
try:
if spawned_interface.parent_interface:
pif = spawned_interface.parent_interface
if pif.spawned_interfaces != None:
while spawned_interface in pif.spawned_interfaces: pif.spawned_interfaces.remove(spawned_interface)
except Exception as e: RNS.log(f"Error while removing spawned interface from {pif}: {e}", RNS.LOG_ERROR)
try: client_socket.close()
except Exception as e: RNS.log(f"Error while closing socket for {spawned_interface}: {e}", RNS.LOG_WARNING)
spawned_interface.receive(b"")
spawned_interface.transmit_buffer = spawned_interface.transmit_buffer[written:]
try:
if len(spawned_interface.transmit_buffer) == 0: BackboneInterface.epoll.modify(fileno, select.EPOLLIN)
except Exception as e:
RNS.log(f"Error while setting EPOLLIN on {spawned_interface}: {e}", RNS.LOG_ERROR)
spawned_interface.txb += written
if spawned_interface.parent_interface: spawned_interface.parent_interface.txb += written
elif client_socket and fileno == client_socket.fileno() and event & (select.EPOLLHUP):
BackboneInterface.deregister_fileno(fileno)
try:
if fileno in BackboneInterface.spawned_interface_filenos: BackboneInterface.spawned_interface_filenos.pop(fileno)
except Exception as e: RNS.log(f"Error while removing spawned interface file descriptor from BackboneInterface I/O handler: {e}", RNS.LOG_ERROR)
try:
if spawned_interface.parent_interface:
pif = spawned_interface.parent_interface
if pif.spawned_interfaces != None:
while spawned_interface in pif.spawned_interfaces: pif.spawned_interfaces.remove(spawned_interface)
except Exception as e: RNS.log(f"Error while removing spawned interface from {pif}: {e}", RNS.LOG_ERROR)
try: client_socket.close()
except Exception as e: RNS.log(f"Error while closing socket for {spawned_interface}: {e}", RNS.LOG_ERROR)
spawned_interface.receive(b"")
elif fileno in BackboneInterface.listener_filenos:
owner_interface, server_socket = BackboneInterface.listener_filenos[fileno]
if fileno == server_socket.fileno() and (event & select.EPOLLIN):
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 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_WARNING)
except Exception as e:
RNS.log(f"BackboneInterface error: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
finally:
BackboneInterface.deregister_listeners()
def incoming_connection(self, socket):
RNS.log("Accepting incoming connection", RNS.LOG_VERBOSE)
try:
spawned_configuration = {"name": "Client on "+self.name, "target_host": None, "target_port": None}
spawned_interface = BackboneClientInterface(self.owner, spawned_configuration, connected_socket=socket)
spawned_interface.OUT = self.OUT
spawned_interface.IN = self.IN
spawned_interface.ingress_control = self.ingress_control
spawned_interface.ic_max_held_announces = self.ic_max_held_announces
spawned_interface.ic_burst_hold = self.ic_burst_hold
spawned_interface.ic_burst_freq = self.ic_burst_freq
spawned_interface.ic_burst_freq_new = self.ic_burst_freq_new
spawned_interface.ic_new_time = self.ic_new_time
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
spawned_interface.egress_control = self.egress_control
spawned_interface.ec_pr_freq = self.ec_pr_freq
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
spawned_interface.socket = socket
spawned_interface.target_ip = socket.getpeername()[0]
spawned_interface.target_port = str(socket.getpeername()[1])
spawned_interface.parent_interface = self
spawned_interface.bitrate = self.bitrate
spawned_interface.optimise_mtu()
spawned_interface.ifac_size = self.ifac_size
spawned_interface.ifac_netname = self.ifac_netname
spawned_interface.ifac_netkey = self.ifac_netkey
if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None:
ifac_origin = b""
if spawned_interface.ifac_netname != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
if spawned_interface.ifac_netkey != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
spawned_interface.ifac_key = RNS.Cryptography.hkdf(
length=64,
derive_from=ifac_origin_hash,
salt=RNS.Reticulum.IFAC_SALT,
context=None
)
spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key)
spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key))
spawned_interface.announce_rate_target = self.announce_rate_target
spawned_interface.announce_rate_grace = self.announce_rate_grace
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
spawned_interface.mode = self.mode
spawned_interface.HW_MTU = self.HW_MTU
spawned_interface.online = True
RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
RNS.Transport.add_interface(spawned_interface)
while spawned_interface in self.spawned_interfaces: self.spawned_interfaces.remove(spawned_interface)
self.spawned_interfaces.append(spawned_interface)
BackboneInterface.add_client_socket(socket, spawned_interface)
except Exception as e:
RNS.log(f"An error occurred while accepting incoming connection on {self}: {e}", RNS.LOG_ERROR)
return False
return True
def received_announce(self, from_spawned=False):
if from_spawned: self.ia_freq_deque.append(time.time())
def sent_announce(self, from_spawned=False):
if from_spawned: self.oa_freq_deque.append(time.time())
def 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
def detach(self):
self.detached = True
self.online = False
detached = []
for fileno in BackboneInterface.listener_filenos:
owner_interface, listener_socket = BackboneInterface.listener_filenos[fileno]
if owner_interface == self:
if hasattr(listener_socket, "shutdown"):
if callable(listener_socket.shutdown):
try: listener_socket.shutdown(socket.SHUT_RDWR)
except Exception as e:
if str(e).endswith("Transport endpoint is not connected"): pass
else: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e), RNS.LOG_ERROR)
def __str__(self):
if ":" in self.bind_ip:
ip_str = f"[{self.bind_ip}]"
else:
ip_str = f"{self.bind_ip}"
return "BackboneInterface["+self.name+"/"+ip_str+":"+str(self.bind_port)+"]"
class BackboneClientInterface(Interface):
BITRATE_GUESS = 100_000_000
DEFAULT_IFAC_SIZE = 16
AUTOCONFIGURE_MTU = True
RECONNECT_WAIT = 5
RECONNECT_MAX_TRIES = None
# TCP socket options
TCP_USER_TIMEOUT = 24
TCP_PROBE_AFTER = 5
TCP_PROBE_INTERVAL = 2
TCP_PROBES = 12
INITIAL_CONNECT_TIMEOUT = 5
SYNCHRONOUS_START = True
def __init__(self, owner, configuration, connected_socket=None):
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
target_ip = c["target_host"] if "target_host" in c and c["target_host"] != None else None
target_port = int(c["target_port"]) if "target_port" in c and c["target_host"] != None else None
i2p_tunneled = c.as_bool("i2p_tunneled") if "i2p_tunneled" in c else False
connect_timeout = c.as_int("connect_timeout") if "connect_timeout" in c else None
max_reconnect_tries = c.as_int("max_reconnect_tries") if "max_reconnect_tries" in c else None
prefer_ipv6 = c.as_bool("prefer_ipv6") if "prefer_ipv6" in c else False
self.HW_MTU = BackboneInterface.HW_MTU
self.IN = True
self.OUT = False
self.socket = None
self.parent_interface = None
self.name = name
self.initiator = False
self.reconnecting = False
self.never_connected = True
self.owner = owner
self.online = False
self.detached = False
self.prefer_ipv6 = prefer_ipv6
self.i2p_tunneled = i2p_tunneled
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
self.bitrate = BackboneClientInterface.BITRATE_GUESS
self.frame_buffer = b""
self.transmit_buffer = b""
if max_reconnect_tries == None:
self.max_reconnect_tries = BackboneClientInterface.RECONNECT_MAX_TRIES
else:
self.max_reconnect_tries = max_reconnect_tries
if connected_socket != None:
self.receives = True
self.target_ip = None
self.target_port = None
self.socket = connected_socket
self.set_timeouts_linux()
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
elif target_ip != None and target_port != None:
self.receives = True
self.target_ip = target_ip
self.target_port = target_port
self.initiator = True
if connect_timeout != None:
self.connect_timeout = connect_timeout
else:
self.connect_timeout = BackboneClientInterface.INITIAL_CONNECT_TIMEOUT
if BackboneClientInterface.SYNCHRONOUS_START:
self.initial_connect()
else:
thread = threading.Thread(target=self.initial_connect)
thread.daemon = True
thread.start()
def initial_connect(self):
if not self.connect(initial=True):
thread = threading.Thread(target=self.reconnect)
thread.daemon = True
thread.start()
else:
self.wants_tunnel = True
def set_timeouts_linux(self):
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(BackboneClientInterface.TCP_USER_TIMEOUT * 1000))
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(BackboneClientInterface.TCP_PROBE_AFTER))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(BackboneClientInterface.TCP_PROBE_INTERVAL))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(BackboneClientInterface.TCP_PROBES))
def detach(self):
self.online = False
if self.socket != None:
if hasattr(self.socket, "close"):
if callable(self.socket.close):
self.detached = True
try:
if self.socket != None: self.socket.shutdown(socket.SHUT_RDWR)
except Exception as e:
if str(e).endswith("Transport endpoint is not connected"): pass
else: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e), RNS.LOG_ERROR)
try:
if self.socket != None: self.socket.close()
except Exception as e: RNS.log("Error while closing socket for "+str(self)+": "+str(e), RNS.LOG_ERROR)
self.socket = None
def connect(self, initial=False):
try:
if initial:
RNS.log("Establishing TCP connection for "+str(self)+"...", RNS.LOG_DEBUG)
address_infos = socket.getaddrinfo(self.target_ip, self.target_port, proto=socket.IPPROTO_TCP)
address_info = address_infos[0]
for entry in address_infos:
if self.prefer_ipv6 and entry[0] == socket.AF_INET6:
address_info = entry; break
elif not self.prefer_ipv6 and entry[0] == socket.AF_INET:
address_info = entry; break
address_family = address_info[0]
target_address = address_info[4]
self.socket = socket.socket(address_family, socket.SOCK_STREAM)
self.socket.settimeout(BackboneClientInterface.INITIAL_CONNECT_TIMEOUT)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.socket.connect(target_address)
self.socket.settimeout(None)
BackboneInterface.add_client_socket(self.socket, self)
self.online = True
if initial:
RNS.log("TCP connection for "+str(self)+" established", RNS.LOG_DEBUG)
except Exception as e:
if initial:
RNS.log("Initial connection for "+str(self)+" could not be established: "+str(e), RNS.LOG_WARNING)
RNS.log("Leaving unconnected and retrying connection in "+str(BackboneClientInterface.RECONNECT_WAIT)+" seconds.", RNS.LOG_WARNING)
return False
else:
raise e
self.set_timeouts_linux()
self.online = True
self.never_connected = False
return True
def reconnect(self):
if self.initiator:
if not self.reconnecting:
self.reconnecting = True
attempts = 0
while not self.online and not self.detached:
time.sleep(BackboneClientInterface.RECONNECT_WAIT)
attempts += 1
if self.max_reconnect_tries != None and attempts > self.max_reconnect_tries:
RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_WARNING)
self.teardown()
break
try: self.connect()
except Exception as e:
RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
if not self.online: return
if not self.never_connected:
RNS.log("Reconnected socket for "+str(self)+".", RNS.LOG_INFO)
self.reconnecting = False
RNS.Transport.synthesize_tunnel(self)
else:
RNS.log("Attempt to reconnect on a non-initiator TCP interface. This should not happen.", RNS.LOG_ERROR)
raise IOError("Attempt to reconnect on a non-initiator TCP interface")
def process_incoming(self, data):
if self.online and not self.detached:
self.rxb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.rxb += len(data)
self.owner.inbound(data, self)
def process_outgoing(self, data):
if self.online and not self.detached:
try:
self.transmit_buffer += bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
BackboneInterface.tx_ready(self)
except Exception as e:
RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
self.teardown()
def receive(self, data_in):
try:
if len(data_in) > 0:
self.frame_buffer += data_in
flags_remaining = True
while flags_remaining:
frame_start = self.frame_buffer.find(HDLC.FLAG)
if frame_start != -1:
frame_end = self.frame_buffer.find(HDLC.FLAG, frame_start+1)
if frame_end != -1:
frame = self.frame_buffer[frame_start+1:frame_end]
frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG]))
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
if len(frame) > RNS.Reticulum.HEADER_MINSIZE:
self.process_incoming(frame)
self.frame_buffer = self.frame_buffer[frame_end:]
else:
flags_remaining = False
else:
flags_remaining = False
else:
self.online = False
if self.initiator and not self.detached:
RNS.log("The socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
def job(): self.reconnect()
threading.Thread(target=job, daemon=True).start()
else:
RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_DEBUG)
self.teardown()
except Exception as e:
self.online = False
RNS.log("An interface error occurred for "+str(self)+", the contained exception was: "+str(e), RNS.LOG_WARNING)
if self.initiator:
RNS.log("Attempting to reconnect...", RNS.LOG_WARNING)
def job(): self.reconnect()
threading.Thread(target=job, daemon=True).start()
else:
self.teardown()
def teardown(self):
if self.initiator and not self.detached:
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error:
RNS.panic()
else:
RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_VERBOSE)
self.online = False
self.OUT = False
self.IN = False
if hasattr(self, "parent_interface") and self.parent_interface != None:
while self in self.parent_interface.spawned_interfaces:
self.parent_interface.spawned_interfaces.remove(self)
if not self.initiator:
RNS.Transport.remove_interface(self)
def __str__(self):
if ":" in self.target_ip: ip_str = f"[{self.target_ip}]"
else: ip_str = f"{self.target_ip}"
return "BackboneInterface["+str(self.name)+"/"+ip_str+":"+str(self.target_port)+"]"
+197 -50
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -20,7 +28,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .Interface import Interface
from RNS.Interfaces.Interface import Interface
import socketserver
import threading
import platform
@@ -161,8 +169,6 @@ class I2PController:
raise tn.status["exception"]
else:
self.client_tunnels[i2p_destination] = True
owner.awaiting_i2p_tunnel = False
if owner.socket != None:
if hasattr(owner.socket, "close"):
if callable(owner.socket.close):
@@ -175,6 +181,8 @@ class I2PController:
owner.socket.close()
except Exception as e:
RNS.log("Error while closing socket for "+str(owner)+": "+str(e))
self.client_tunnels[i2p_destination] = True
owner.awaiting_i2p_tunnel = False
RNS.log(str(owner)+" tunnel setup complete", RNS.LOG_VERBOSE)
@@ -383,10 +391,14 @@ class I2PInterfacePeer(Interface):
I2P_PROBE_AFTER = 10
I2P_PROBE_INTERVAL = 9
I2P_PROBES = 5
I2P_READ_TIMEOUT = (I2P_PROBE_INTERVAL * I2P_PROBES + I2P_PROBE_AFTER)*2
TUNNEL_STATE_INIT = 0x00
TUNNEL_STATE_ACTIVE = 0x01
TUNNEL_STATE_STALE = 0x02
def __init__(self, parent_interface, owner, name, target_i2p_dest=None, connected_socket=None, max_reconnect_tries=None):
self.rxb = 0
self.txb = 0
super().__init__()
self.HW_MTU = 1064
@@ -409,6 +421,30 @@ class I2PInterfacePeer(Interface):
self.i2p_tunnel_ready = False
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
self.bitrate = I2PInterface.BITRATE_GUESS
self.last_read = 0
self.last_write = 0
self.wd_reset = False
self.i2p_tunnel_state = I2PInterfacePeer.TUNNEL_STATE_INIT
self.ifac_size = self.parent_interface.ifac_size
self.ifac_netname = self.parent_interface.ifac_netname
self.ifac_netkey = self.parent_interface.ifac_netkey
if self.ifac_netname != None or self.ifac_netkey != None:
ifac_origin = b""
if self.ifac_netname != None:
ifac_origin += RNS.Identity.full_hash(self.ifac_netname.encode("utf-8"))
if self.ifac_netkey != None:
ifac_origin += RNS.Identity.full_hash(self.ifac_netkey.encode("utf-8"))
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
self.ifac_key = RNS.Cryptography.hkdf(
length=64,
derive_from=ifac_origin_hash,
salt=RNS.Reticulum.IFAC_SALT,
context=None
)
self.ifac_identity = RNS.Identity.from_bytes(self.ifac_key)
self.ifac_signature = self.ifac_identity.sign(RNS.Identity.full_hash(self.ifac_key))
self.announce_rate_target = None
self.announce_rate_grace = None
@@ -454,7 +490,7 @@ class I2PInterfacePeer(Interface):
RNS.log("Error while while configuring "+str(self)+": "+str(e), RNS.LOG_ERROR)
RNS.log("Check that I2P is installed and running, and that SAM is enabled. Retrying tunnel setup later.", RNS.LOG_ERROR)
time.sleep(15)
time.sleep(8)
thread = threading.Thread(target=tunnel_job)
thread.daemon = True
@@ -463,6 +499,7 @@ class I2PInterfacePeer(Interface):
def wait_job():
while self.awaiting_i2p_tunnel:
time.sleep(0.25)
time.sleep(2)
if not self.kiss_framing:
self.wants_tunnel = True
@@ -482,18 +519,11 @@ class I2PInterfacePeer(Interface):
def set_timeouts_linux(self):
if not self.i2p_tunneled:
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(I2PInterfacePeer.TCP_USER_TIMEOUT * 1000))
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(I2PInterfacePeer.TCP_PROBE_AFTER))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(I2PInterfacePeer.TCP_PROBE_INTERVAL))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(I2PInterfacePeer.TCP_PROBES))
else:
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(I2PInterfacePeer.I2P_USER_TIMEOUT * 1000))
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(I2PInterfacePeer.I2P_PROBE_AFTER))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(I2PInterfacePeer.I2P_PROBE_INTERVAL))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(I2PInterfacePeer.I2P_PROBES))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(I2PInterfacePeer.I2P_USER_TIMEOUT * 1000))
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(I2PInterfacePeer.I2P_PROBE_AFTER))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(I2PInterfacePeer.I2P_PROBE_INTERVAL))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(I2PInterfacePeer.I2P_PROBES))
def set_timeouts_osx(self):
if hasattr(socket, "TCP_KEEPALIVE"):
@@ -502,22 +532,19 @@ class I2PInterfacePeer(Interface):
TCP_KEEPIDLE = 0x10
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
if not self.i2p_tunneled:
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(I2PInterfacePeer.TCP_PROBE_AFTER))
else:
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(I2PInterfacePeer.I2P_PROBE_AFTER))
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(I2PInterfacePeer.I2P_PROBE_AFTER))
def shutdown_socket(self, socket):
if callable(socket.close):
def shutdown_socket(self, target_socket):
if callable(target_socket.close):
try:
socket.shutdown(socket.SHUT_RDWR)
if socket != None:
target_socket.shutdown(socket.SHUT_RDWR)
except Exception as e:
RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
try:
socket.close()
if socket != None:
target_socket.close()
except Exception as e:
RNS.log("Error while closing socket for "+str(self)+": "+str(e))
@@ -571,7 +598,6 @@ class I2PInterfacePeer(Interface):
return True
def reconnect(self):
if self.initiator:
if not self.reconnecting:
@@ -609,14 +635,14 @@ class I2PInterfacePeer(Interface):
RNS.log("Attempt to reconnect on a non-initiator I2P interface. This should not happen.", RNS.LOG_ERROR)
raise IOError("Attempt to reconnect on a non-initiator I2P interface")
def processIncoming(self, data):
def process_incoming(self, data):
self.rxb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None and self.parent_count:
self.parent_interface.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self, data):
def process_outgoing(self, data):
if self.online:
while self.writing:
time.sleep(0.001)
@@ -632,6 +658,7 @@ class I2PInterfacePeer(Interface):
self.socket.sendall(data)
self.writing = False
self.txb += len(data)
self.last_write = time.time()
if hasattr(self, "parent_interface") and self.parent_interface != None and self.parent_count:
self.parent_interface.txb += len(data)
@@ -642,8 +669,59 @@ class I2PInterfacePeer(Interface):
self.teardown()
def read_watchdog(self):
while self.wd_reset:
time.sleep(0.25)
should_run = True
try:
while should_run and not self.wd_reset:
time.sleep(1)
if (time.time()-self.last_read > I2PInterfacePeer.I2P_PROBE_AFTER*2):
if self.i2p_tunnel_state != I2PInterfacePeer.TUNNEL_STATE_STALE:
RNS.log("I2P tunnel became unresponsive", RNS.LOG_DEBUG)
self.i2p_tunnel_state = I2PInterfacePeer.TUNNEL_STATE_STALE
else:
self.i2p_tunnel_state = I2PInterfacePeer.TUNNEL_STATE_ACTIVE
if (time.time()-self.last_write > I2PInterfacePeer.I2P_PROBE_AFTER*1):
try:
if self.socket != None:
self.socket.sendall(bytes([HDLC.FLAG, HDLC.FLAG]))
except Exception as e:
RNS.log("An error ocurred while sending I2P keepalive. The contained exception was: "+str(e), RNS.LOG_ERROR)
self.shutdown_socket(self.socket)
should_run = False
if (time.time()-self.last_read > I2PInterfacePeer.I2P_READ_TIMEOUT):
RNS.log("I2P socket is unresponsive, restarting...", RNS.LOG_WARNING)
if self.socket != None:
try:
self.socket.shutdown(socket.SHUT_RDWR)
except Exception as e:
RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
try:
self.socket.close()
except Exception as e:
RNS.log("Error while closing socket for "+str(self)+": "+str(e))
should_run = False
self.wd_reset = False
finally:
self.wd_reset = False
def read_loop(self):
try:
self.last_read = time.time()
self.last_write = time.time()
wd_thread = threading.Thread(target=self.read_watchdog, daemon=True).start()
in_frame = False
escape = False
data_buffer = b""
@@ -653,6 +731,7 @@ class I2PInterfacePeer(Interface):
data_in = self.socket.recv(4096)
if len(data_in) > 0:
pointer = 0
self.last_read = time.time()
while pointer < len(data_in):
byte = data_in[pointer]
pointer += 1
@@ -661,7 +740,7 @@ class I2PInterfacePeer(Interface):
# Read loop for KISS framing
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
in_frame = False
self.processIncoming(data_buffer)
self.process_incoming(data_buffer)
elif (byte == KISS.FEND):
in_frame = True
command = KISS.CMD_UNKNOWN
@@ -688,7 +767,7 @@ class I2PInterfacePeer(Interface):
# Read loop for HDLC framing
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.processIncoming(data_buffer)
self.process_incoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
@@ -705,6 +784,11 @@ class I2PInterfacePeer(Interface):
data_buffer = data_buffer+bytes([byte])
else:
self.online = False
self.wd_reset = True
time.sleep(2)
self.wd_reset = False
if self.initiator and not self.detached:
RNS.log("Socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
self.reconnect()
@@ -739,12 +823,11 @@ class I2PInterfacePeer(Interface):
self.IN = False
if hasattr(self, "parent_interface") and self.parent_interface != None:
if self.parent_interface.clients > 0:
self.parent_interface.clients -= 1
while self in self.parent_interface.spawned_interfaces:
self.parent_interface.spawned_interfaces.remove(self)
if self in RNS.Transport.interfaces:
if not self.initiator:
RNS.Transport.interfaces.remove(self)
if not self.initiator:
RNS.Transport.remove_interface(self)
def __str__(self):
@@ -753,15 +836,28 @@ class I2PInterfacePeer(Interface):
class I2PInterface(Interface):
BITRATE_GUESS = 256*1000
DEFAULT_IFAC_SIZE = 16
def __init__(self, owner, name, rns_storagepath, peers, connectable = False):
self.rxb = 0
self.txb = 0
@property
def clients(self):
return len(self.spawned_interfaces)
def __init__(self, owner, configuration):
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
rns_storagepath = c["storagepath"]
peers = c.as_list("peers") if "peers" in c else None
connectable = c.as_bool("connectable") if "connectable" in c else False
ifac_size = c["ifac_size"] if "ifac_size" in c else None
ifac_netname = c["ifac_netname"] if "ifac_netname" in c else None
ifac_netkey = c["ifac_netkey"] if "ifac_netkey" in c else None
self.HW_MTU = 1064
self.online = False
self.clients = 0
self.spawned_interfaces = []
self.owner = owner
self.connectable = connectable
self.i2p_tunneled = True
@@ -780,6 +876,10 @@ class I2PInterface(Interface):
self.bind_port = self.i2p.get_free_port()
self.address = (self.bind_ip, self.bind_port)
self.bitrate = I2PInterface.BITRATE_GUESS
self.ifac_size = ifac_size
self.ifac_netname = ifac_netname
self.ifac_netkey = ifac_netkey
self.supports_discovery = True
self.online = False
@@ -839,7 +939,7 @@ class I2PInterface(Interface):
peer_interface.IN = True
peer_interface.parent_interface = self
peer_interface.parent_count = False
RNS.Transport.interfaces.append(peer_interface)
RNS.Transport.add_interface(peer_interface)
def incoming_connection(self, handler):
RNS.log("Accepting incoming I2P connection", RNS.LOG_VERBOSE)
@@ -847,25 +947,72 @@ class I2PInterface(Interface):
spawned_interface = I2PInterfacePeer(self, self.owner, interface_name, connected_socket=handler.request)
spawned_interface.OUT = True
spawned_interface.IN = True
spawned_interface.ingress_control = self.ingress_control
spawned_interface.ic_max_held_announces = self.ic_max_held_announces
spawned_interface.ic_burst_hold = self.ic_burst_hold
spawned_interface.ic_burst_freq = self.ic_burst_freq
spawned_interface.ic_burst_freq_new = self.ic_burst_freq_new
spawned_interface.ic_new_time = self.ic_new_time
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
spawned_interface.egress_control = self.egress_control
spawned_interface.ec_pr_freq = self.ec_pr_freq
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
spawned_interface.parent_interface = self
spawned_interface.online = True
spawned_interface.bitrate = self.bitrate
spawned_interface.ifac_size = self.ifac_size
spawned_interface.ifac_netname = self.ifac_netname
spawned_interface.ifac_netkey = self.ifac_netkey
if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None:
ifac_origin = b""
if spawned_interface.ifac_netname != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
if spawned_interface.ifac_netkey != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
spawned_interface.ifac_key = RNS.Cryptography.hkdf(
length=64,
derive_from=ifac_origin_hash,
salt=RNS.Reticulum.IFAC_SALT,
context=None
)
spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key)
spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key))
spawned_interface.announce_rate_target = self.announce_rate_target
spawned_interface.announce_rate_grace = self.announce_rate_grace
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
spawned_interface.mode = self.mode
spawned_interface.HW_MTU = self.HW_MTU
RNS.log("Spawned new I2PInterface Peer: "+str(spawned_interface), RNS.LOG_VERBOSE)
RNS.Transport.interfaces.append(spawned_interface)
self.clients += 1
RNS.Transport.add_interface(spawned_interface)
while spawned_interface in self.spawned_interfaces:
self.spawned_interfaces.remove(spawned_interface)
self.spawned_interfaces.append(spawned_interface)
spawned_interface.read_loop()
def processOutgoing(self, data):
def process_outgoing(self, data):
pass
def received_announce(self, from_spawned=False):
if from_spawned: self.ia_freq_deque.append(time.time())
def sent_announce(self, from_spawned=False):
if from_spawned: self.oa_freq_deque.append(time.time())
def 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()
+296 -12
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -23,6 +31,8 @@
import RNS
import time
import threading
from collections import deque
from RNS.vendor.configobj import ConfigObj
class Interface:
IN = False
@@ -39,18 +49,277 @@ class Interface:
MODE_BOUNDARY = 0x05
MODE_GATEWAY = 0x06
# Which interface modes a Transport Node
# should actively discover paths for.
DISCOVER_PATHS_FOR = [MODE_ACCESS_POINT, MODE_GATEWAY]
# Which interface modes a Transport Node should
# actively discover paths for.
DISCOVER_PATHS_FOR = [MODE_ACCESS_POINT, MODE_GATEWAY, MODE_ROAMING]
# How many samples to use for announce
# frequency calculations
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.
MAX_HELD_ANNOUNCES = 256
# How long a spawned interface will be
# considered to be newly created. Two
# hours by default.
IC_NEW_TIME = 2*60*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 = 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
def __init__(self):
self.rxb = 0
self.txb = 0
self.online = False
self.rxb = 0
self.txb = 0
self.created = time.time()
self.detached = False
self.online = False
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.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"))
# This is a generic function for determining when an interface
# should activate ingress limiting. Since this can vary for
# different interface types, this function should be overwritten
# in case a particular interface requires a different approach.
def should_ingress_limit(self):
if self.ingress_control:
freq_threshold = self.ic_burst_freq_new if self.age() < self.ic_new_time else self.ic_burst_freq
ia_freq = self.incoming_announce_frequency()
if self.ic_burst_active:
if ia_freq < freq_threshold and time.time() > self.ic_burst_activated+self.ic_burst_hold:
if len(self.ia_freq_deque) >= self.IC_BURST_MIN_SAMPLES: self.ic_burst_active = False
return True
else:
if ia_freq > freq_threshold:
self.ic_burst_active = True
self.ic_burst_activated = time.time()
self.ic_held_release = time.time() + self.ic_burst_penalty
return True
else: return False
else: return False
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:
self.HW_MTU = 524288
elif self.bitrate > 750_000_000:
self.HW_MTU = 262144
elif self.bitrate > 400_000_000:
self.HW_MTU = 131072
elif self.bitrate > 200_000_000:
self.HW_MTU = 65536
elif self.bitrate > 100_000_000:
self.HW_MTU = 32768
elif self.bitrate > 10_000_000:
self.HW_MTU = 16384
elif self.bitrate > 5_000_000:
self.HW_MTU = 8192
elif self.bitrate > 2_000_000:
self.HW_MTU = 4096
elif self.bitrate > 1_000_000:
self.HW_MTU = 2048
elif self.bitrate > 62_500:
self.HW_MTU = 1024
else:
self.HW_MTU = None
RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_DEBUG)
def age(self):
return time.time()-self.created
def hold_announce(self, announce_packet):
if announce_packet.destination_hash in self.held_announces:
self.held_announces[announce_packet.destination_hash] = announce_packet
elif not len(self.held_announces) >= self.ic_max_held_announces:
self.held_announces[announce_packet.destination_hash] = announce_packet
def process_held_announces(self):
try:
if len(self.held_announces) > 0 and time.time() > self.ic_held_release:
freq_threshold = self.ic_burst_freq_new if self.age() < self.ic_new_time else self.ic_burst_freq
ia_freq = self.incoming_announce_frequency()
if ia_freq < freq_threshold:
selected_announce_packet = None
min_hops = RNS.Transport.PATHFINDER_M
for destination_hash in self.held_announces:
announce_packet = self.held_announces[destination_hash]
if announce_packet.hops < min_hops:
min_hops = announce_packet.hops
selected_announce_packet = announce_packet
if selected_announce_packet != None:
RNS.log("Releasing held announce packet "+str(selected_announce_packet)+" from "+str(self), RNS.LOG_EXTREME)
self.ic_held_release = time.time() + self.ic_held_release_interval
self.held_announces.pop(selected_announce_packet.destination_hash)
def release(): RNS.Transport.inbound(selected_announce_packet.raw, selected_announce_packet.receiving_interface)
threading.Thread(target=release, daemon=True).start()
except Exception as e:
RNS.log("An error occurred while processing held announces for "+str(self), RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
def received_announce(self, from_spawned=False):
self.ia_freq_deque.append(time.time())
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.received_announce(from_spawned=True)
def sent_announce(self, from_spawned=False):
self.oa_freq_deque.append(time.time())
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.sent_announce(from_spawned=True)
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
def outgoing_announce_frequency(self):
n = len(self.oa_freq_deque)
if not len(self.oa_freq_deque) > 1: return 0
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
def process_announce_queue(self):
if not hasattr(self, "announce_cap"):
self.announce_cap = RNS.Reticulum.ANNOUNCE_CAP
@@ -78,7 +347,8 @@ class Interface:
wait_time = (tx_time / self.announce_cap)
self.announce_allowed_at = now + wait_time
self.processOutgoing(selected["raw"])
self.process_outgoing(selected["raw"])
self.sent_announce()
if selected in self.announce_queue:
self.announce_queue.remove(selected)
@@ -92,5 +362,19 @@ class Interface:
RNS.log("Error while processing announce queue on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("The announce queue for this interface has been cleared.", RNS.LOG_ERROR)
def final_init(self):
pass
def detach(self):
pass
pass
@staticmethod
def get_config_obj(config_in):
if type(config_in) == ConfigObj:
return config_in
else:
try:
return ConfigObj(config_in)
except Exception as e:
RNS.log(f"Could not parse supplied configuration data. The contained exception was: {e}", RNS.LOG_ERROR)
raise SystemError("Invalid configuration data supplied")
+49 -14
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -20,7 +28,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .Interface import Interface
from RNS.Interfaces.Interface import Interface
from time import sleep
import sys
import threading
@@ -52,6 +60,7 @@ class KISS():
class KISSInterface(Interface):
MAX_CHUNK = 32768
BITRATE_GUESS = 1200
DEFAULT_IFAC_SIZE = 8
owner = None
port = None
@@ -61,8 +70,8 @@ class KISSInterface(Interface):
stopbits = None
serial = None
def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control, beacon_interval, beacon_data):
import importlib
def __init__(self, owner, configuration):
import importlib.util
if importlib.util.find_spec('serial') != None:
import serial
else:
@@ -70,8 +79,25 @@ class KISSInterface(Interface):
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
RNS.panic()
self.rxb = 0
self.txb = 0
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
preamble = int(c["preamble"]) if "preamble" in c else None
txtail = int(c["txtail"]) if "txtail" in c else None
persistence = int(c["persistence"]) if "persistence" in c else None
slottime = int(c["slottime"]) if "slottime" in c else None
flow_control = c.as_bool("flow_control") if "flow_control" in c else False
port = c["port"] if "port" in c else None
speed = int(c["speed"]) if "speed" in c else 9600
databits = int(c["databits"]) if "databits" in c else 8
parity = c["parity"] if "parity" in c else "N"
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
beacon_interval = int(c["id_interval"]) if "id_interval" in c else None
beacon_data = c["id_callsign"] if "id_callsign" in c else None
if port == None:
raise ValueError("No port specified for serial interface")
self.HW_MTU = 564
@@ -218,12 +244,12 @@ class KISSInterface(Interface):
raise IOError("Could not enable KISS interface flow control")
def processIncoming(self, data):
def process_incoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self,data):
def process_outgoing(self,data):
datalen = len(data)
if self.online:
if self.interface_ready:
@@ -257,7 +283,7 @@ class KISSInterface(Interface):
if len(self.packet_queue) > 0:
data = self.packet_queue.pop(0)
self.interface_ready = True
self.processOutgoing(data)
self.process_outgoing(data)
elif len(self.packet_queue) == 0:
self.interface_ready = True
@@ -276,7 +302,7 @@ class KISSInterface(Interface):
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
in_frame = False
self.processIncoming(data_buffer)
self.process_incoming(data_buffer)
elif (byte == KISS.FEND):
in_frame = True
command = KISS.CMD_UNKNOWN
@@ -320,7 +346,13 @@ class KISSInterface(Interface):
if time.time() > self.first_tx + self.beacon_i:
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.beacon_d.decode("utf-8")), RNS.LOG_DEBUG)
self.first_tx = None
self.processOutgoing(self.beacon_d)
# Pad to minimum length
frame = bytearray(self.beacon_d)
while len(frame) < 15:
frame.append(0x00)
self.process_outgoing(bytes(frame))
except Exception as e:
self.online = False
@@ -349,5 +381,8 @@ class KISSInterface(Interface):
RNS.log("Reconnected serial port for "+str(self))
def should_ingress_limit(self):
return False
def __str__(self):
return "KISSInterface["+self.name+"]"
+281 -113
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -20,7 +28,8 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .Interface import Interface
from RNS.Interfaces.Interface import Interface
from RNS.Interfaces.BackboneInterface import BackboneInterface
import socketserver
import threading
import socket
@@ -28,6 +37,7 @@ import time
import sys
import os
import RNS
from threading import Lock
class HDLC():
FLAG = 0x7E
@@ -41,21 +51,28 @@ class HDLC():
return data
class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
def server_bind(self):
if RNS.vendor.platformutils.is_windows():
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
else:
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(self.server_address)
self.server_address = self.socket.getsockname()
class LocalClientInterface(Interface):
RECONNECT_WAIT = 3
RECONNECT_WAIT = 8
AUTOCONFIGURE_MTU = True
CLIENT_SLEEP_PAUSE_TIMEOUT = 12
def __init__(self, owner, name, target_port = None, connected_socket=None):
self.rxb = 0
self.txb = 0
def __init__(self, owner, name, target_port = None, connected_socket=None, socket_path=None):
super().__init__()
# TODO: Remove at some point
# self.rxptime = 0
self.epoll_backend = False
self.HW_MTU = 262144
self.online = False
self.HW_MTU = 1064
self.online = False
if socket_path != None and RNS.Reticulum.get_instance().use_af_unix: self.socket_path = f"\0rns/{socket_path}"
else: self.socket_path = None
self.IN = True
self.OUT = False
@@ -66,6 +83,12 @@ class LocalClientInterface(Interface):
self.detached = False
self.name = name
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
self.frame_buffer = b""
self.transmit_buffer = b""
if RNS.vendor.platformutils.use_epoll(): self.epoll_backend = True
self.pause_on_client_sleep = False
if connected_socket != None:
self.receives = True
@@ -73,8 +96,21 @@ class LocalClientInterface(Interface):
self.target_port = None
self.socket = connected_socket
if self.socket.family == socket.AF_INET:
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
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
self.target_port = None
self.connect()
elif target_port != None:
self.receives = True
self.target_ip = "127.0.0.1"
@@ -82,27 +118,42 @@ class LocalClientInterface(Interface):
self.connect()
self.owner = owner
self.bitrate = 1000*1000*1000
self.bitrate = 1_000_000_000
self.online = True
self.writing = False
self._force_bitrate = False
self.announce_rate_target = None
self.announce_rate_grace = None
self.announce_rate_penalty = None
if connected_socket == None:
thread = threading.Thread(target=self.read_loop)
thread.daemon = True
thread.start()
if not self.epoll_backend:
thread = threading.Thread(target=self.read_loop)
thread.daemon = True
thread.start()
def should_ingress_limit(self):
return False
def connect(self):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((self.target_ip, self.target_port))
if self.socket_path != None:
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.socket.connect(self.socket_path)
else:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.socket.connect((self.target_ip, self.target_port))
self.online = True
self.is_connected_to_shared_instance = True
self.never_connected = False
if RNS.vendor.platformutils.is_android(): self.phy_keepalive = True
if self.epoll_backend: BackboneInterface.add_client_socket(self.socket, self)
return True
@@ -126,89 +177,144 @@ class LocalClientInterface(Interface):
RNS.log("Reconnected socket for "+str(self)+".", RNS.LOG_INFO)
self.reconnecting = False
thread = threading.Thread(target=self.read_loop)
thread.daemon = True
thread.start()
RNS.Transport.shared_connection_reappeared()
if not self.epoll_backend:
thread = threading.Thread(target=self.read_loop)
thread.daemon = True
thread.start()
def job():
time.sleep(LocalClientInterface.RECONNECT_WAIT+2)
RNS.Transport.shared_connection_reappeared()
threading.Thread(target=job, daemon=True).start()
else:
RNS.log("Attempt to reconnect on a non-initiator shared local interface. This should not happen.", RNS.LOG_ERROR)
raise IOError("Attempt to reconnect on a non-initiator local interface")
def processIncoming(self, data):
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 hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.rxb += len(data)
if self.parent_interface != None: self.parent_interface.rxb += len(data)
# TODO: Remove at some point
# processing_start = time.time()
self.owner.inbound(data, self)
try: self.owner.inbound(data, self)
except Exception as e:
RNS.log(f"An error occurred in the processing of an incoming frame for {self}: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
# TODO: Remove at some point
# duration = time.time() - processing_start
# self.rxptime += duration
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
def processOutgoing(self, data):
if self.online:
try:
self.writing = True
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
self.socket.sendall(data)
self.writing = False
self.txb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.txb += len(data)
if self.epoll_backend:
self.transmit_buffer += bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
BackboneInterface.tx_ready(self)
else:
self.writing = True
if self._force_bitrate:
if not hasattr(self, "send_lock"):
self.send_lock = Lock()
with self.send_lock:
# RNS.log(f"Simulating latency of {RNS.prettytime(s)} for {len(data)} bytes", RNS.LOG_EXTREME)
s = len(data) / self.bitrate * 8
time.sleep(s)
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
self.socket.sendall(data)
self.writing = False
self.txb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.txb += len(data)
except Exception as e:
RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
self.teardown()
def handle_hdlc(self, data_in):
self.frame_buffer += data_in
flags_remaining = True
while flags_remaining:
frame_start = self.frame_buffer.find(HDLC.FLAG)
if frame_start != -1:
frame_end = self.frame_buffer.find(HDLC.FLAG, frame_start+1)
if frame_end != -1:
frame = self.frame_buffer[frame_start+1:frame_end]
frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG]))
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
if len(frame) > RNS.Reticulum.HEADER_MINSIZE: self.process_incoming(frame)
self.frame_buffer = self.frame_buffer[frame_end:]
else: flags_remaining = False
else: flags_remaining = False
def receive(self, data_in):
try:
if len(data_in) > 0: self.handle_hdlc(data_in)
else:
self.online = False
if self.is_connected_to_shared_instance and not self.detached:
RNS.log("Socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
RNS.Transport.shared_connection_disappeared()
# TODO: Potentially run this in a thread, but since if we get here,
# there's no other connectivity left to block anyway, it might be
# unnecessary.
self.reconnect()
else:
self.teardown(nowarning=True)
except Exception as e:
self.online = False
RNS.log("An interface error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("Tearing down "+str(self), RNS.LOG_ERROR)
self.teardown()
if self.pause_on_client_sleep: self.pause_timeout = time.time() + self.CLIENT_SLEEP_PAUSE_TIMEOUT
def read_loop(self):
try:
in_frame = False
escape = False
data_buffer = b""
self.frame_buffer = b""
data_in = b""
while True:
data_in = self.socket.recv(4096)
if len(data_in) > 0:
pointer = 0
while pointer < len(data_in):
byte = data_in[pointer]
pointer += 1
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.processIncoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
elif (in_frame and len(data_buffer) < self.HW_MTU):
if (byte == HDLC.ESC):
escape = True
else:
if (escape):
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
byte = HDLC.FLAG
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
byte = HDLC.ESC
escape = False
data_buffer = data_buffer+bytes([byte])
if len(data_in) > 0: self.handle_hdlc(data_in)
else:
self.online = False
if self.is_connected_to_shared_instance and not self.detached:
RNS.log("Socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
RNS.Transport.shared_connection_disappeared()
# TODO: Potentially run this in a thread, but since if we get here,
# there's no other connectivity left to block anyway, it might be
# unnecessary.
self.reconnect()
else:
self.teardown(nowarning=True)
break
except Exception as e:
self.online = False
RNS.log("An interface error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
@@ -223,12 +329,14 @@ class LocalClientInterface(Interface):
self.detached = True
try:
self.socket.shutdown(socket.SHUT_RDWR)
if self.socket != None:
self.socket.shutdown(socket.SHUT_RDWR)
except Exception as e:
RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
try:
self.socket.close()
if self.socket != None:
self.socket.close()
except Exception as e:
RNS.log("Error while closing socket for "+str(self)+": "+str(e))
@@ -239,14 +347,15 @@ class LocalClientInterface(Interface):
self.OUT = False
self.IN = False
if self in RNS.Transport.interfaces:
RNS.Transport.interfaces.remove(self)
RNS.Transport.remove_interface(self)
if self in RNS.Transport.local_client_interfaces:
RNS.Transport.local_client_interfaces.remove(self)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.clients -= 1
RNS.Transport.owner._should_persist_data()
if hasattr(RNS.Transport, "owner") and RNS.Transport.owner != None:
background = not self.detached
RNS.Transport.owner._should_persist_data(background=background)
if nowarning == False:
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
@@ -261,73 +370,132 @@ class LocalClientInterface(Interface):
def __str__(self):
return "LocalInterface["+str(self.target_port)+"]"
if self.socket_path: return "LocalInterface["+str(self.socket_path.replace("\0", ""))+"]"
else: return "LocalInterface["+str(self.target_port)+"]"
class LocalServerInterface(Interface):
AUTOCONFIGURE_MTU = True
def __init__(self, owner, bindport=None):
self.rxb = 0
self.txb = 0
def __init__(self, owner, bindport=None, socket_path=None):
super().__init__()
self.epoll_backend = False
self.online = False
self.clients = 0
if socket_path != None and RNS.Reticulum.get_instance().use_af_unix: self.socket_path = f"\0rns/{socket_path}"
else: self.socket_path = None
self.IN = True
self.OUT = False
self.name = "Reticulum"
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
if (bindport != None):
if RNS.vendor.platformutils.use_epoll():
self.epoll_backend = True
if socket_path != None and self.epoll_backend:
self.receives = True
self.bind_ip = None
self.bind_port = None
self.owner = owner
self.is_local_shared_instance = True
BackboneInterface.add_listener(self, self.socket_path, socket_type=socket.AF_UNIX)
elif bindport != None:
self.receives = True
self.bind_ip = "127.0.0.1"
self.bind_port = bindport
def handlerFactory(callback):
def createHandler(*args, **keys):
return LocalInterfaceHandler(callback, *args, **keys)
return createHandler
self.owner = owner
self.is_local_shared_instance = True
address = (self.bind_ip, self.bind_port)
if self.epoll_backend: BackboneInterface.add_listener(self, address)
else:
def handlerFactory(callback):
def createHandler(*args, **keys):
return LocalInterfaceHandler(callback, *args, **keys)
return createHandler
ThreadingTCPServer.allow_reuse_address = True
self.server = ThreadingTCPServer(address, handlerFactory(self.incoming_connection))
thread = threading.Thread(target=self.server.serve_forever)
thread.daemon = True
thread.start()
self.announce_rate_target = None
self.announce_rate_grace = None
self.announce_rate_penalty = None
self.bitrate = 1000*1000*1000
self.online = True
self.server = ThreadingTCPServer(address, handlerFactory(self.incoming_connection))
self.server.daemon_threads = True
thread = threading.Thread(target=self.server.serve_forever)
thread.daemon = True
thread.start()
self.announce_rate_target = None
self.announce_rate_grace = None
self.announce_rate_penalty = None
self.bitrate = 1000*1000*1000
self.online = True
def incoming_connection(self, handler):
interface_name = str(str(handler.client_address[1]))
spawned_interface = LocalClientInterface(self.owner, name=interface_name, connected_socket=handler.request)
spawned_interface.OUT = self.OUT
spawned_interface.IN = self.IN
spawned_interface.target_ip = handler.client_address[0]
spawned_interface.target_port = str(handler.client_address[1])
spawned_interface.parent_interface = self
spawned_interface.bitrate = self.bitrate
RNS.log("Accepting new connection to shared instance: "+str(spawned_interface), RNS.LOG_EXTREME)
RNS.Transport.interfaces.append(spawned_interface)
RNS.Transport.local_client_interfaces.append(spawned_interface)
self.clients += 1
spawned_interface.read_loop()
if self.epoll_backend:
client_socket = handler
if client_socket.family == socket.AF_INET:
interface_name = str(str(client_socket.getpeername()[1]))
elif client_socket.family == socket.AF_UNIX:
interface_name = f"{self.clients}@{self.socket_path}"
def processOutgoing(self, data):
spawned_interface = LocalClientInterface(self.owner, name=interface_name, connected_socket=client_socket)
spawned_interface.OUT = self.OUT
spawned_interface.IN = self.IN
spawned_interface.socket = client_socket
spawned_interface.parent_interface = self
spawned_interface.bitrate = self.bitrate
if client_socket.family == socket.AF_INET:
spawned_interface.target_ip = client_socket.getpeername()[0]
spawned_interface.target_port = str(client_socket.getpeername()[1])
elif client_socket.family == socket.AF_UNIX:
spawned_interface.target_ip = None
spawned_interface.target_port = interface_name
spawned_interface.socket_path = self.socket_path
if hasattr(self, "_force_bitrate"): spawned_interface._force_bitrate = self._force_bitrate
RNS.Transport.add_interface(spawned_interface)
RNS.Transport.local_client_interfaces.append(spawned_interface)
BackboneInterface.add_client_socket(client_socket, spawned_interface)
self.clients += 1
return True
else:
interface_name = str(str(handler.client_address[1]))
spawned_interface = LocalClientInterface(self.owner, name=interface_name, connected_socket=handler.request)
spawned_interface.OUT = self.OUT
spawned_interface.IN = self.IN
spawned_interface.target_ip = handler.client_address[0]
spawned_interface.target_port = str(handler.client_address[1])
spawned_interface.parent_interface = self
spawned_interface.bitrate = self.bitrate
if hasattr(self, "_force_bitrate"): spawned_interface._force_bitrate = self._force_bitrate
RNS.Transport.add_interface(spawned_interface)
RNS.Transport.local_client_interfaces.append(spawned_interface)
self.clients += 1
spawned_interface.read_loop()
def process_outgoing(self, data):
pass
def received_announce(self, from_spawned=False):
if from_spawned: self.ia_freq_deque.append(time.time())
def sent_announce(self, from_spawned=False):
if from_spawned: self.oa_freq_deque.append(time.time())
def 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):
return "Shared Instance["+str(self.bind_port)+"]"
if self.socket_path: return "Shared Instance["+str(self.socket_path.replace("\0", ""))+"]"
else: return "Shared Instance["+str(self.bind_port)+"]"
class LocalInterfaceHandler(socketserver.BaseRequestHandler):
def __init__(self, callback, *args, **keys):
+28 -12
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -20,7 +28,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .Interface import Interface
from RNS.Interfaces.Interface import Interface
from time import sleep
import sys
import threading
@@ -46,17 +54,25 @@ class HDLC():
class PipeInterface(Interface):
MAX_CHUNK = 32768
BITRATE_GUESS = 1*1000*1000
DEFAULT_IFAC_SIZE = 8
owner = None
command = None
def __init__(self, owner, name, command, respawn_delay):
def __init__(self, owner, configuration):
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
command = c["command"] if "command" in c else None
respawn_delay = c.as_float("respawn_delay") if "respawn_delay" in c else None
if command == None:
raise ValueError("No command specified for PipeInterface")
if respawn_delay == None:
respawn_delay = 5
self.rxb = 0
self.txb = 0
self.HW_MTU = 1064
self.owner = owner
@@ -102,12 +118,12 @@ class PipeInterface(Interface):
RNS.log("Subprocess pipe for "+str(self)+" is now connected", RNS.LOG_VERBOSE)
def processIncoming(self, data):
def process_incoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self,data):
def process_outgoing(self,data):
if self.online:
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
written = self.process.stdin.write(data)
@@ -135,7 +151,7 @@ class PipeInterface(Interface):
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.processIncoming(data_buffer)
self.process_incoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+34 -12
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -20,7 +28,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .Interface import Interface
from RNS.Interfaces.Interface import Interface
from time import sleep
import sys
import threading
@@ -42,6 +50,7 @@ class HDLC():
class SerialInterface(Interface):
MAX_CHUNK = 32768
DEFAULT_IFAC_SIZE = 8
owner = None
port = None
@@ -51,8 +60,8 @@ class SerialInterface(Interface):
stopbits = None
serial = None
def __init__(self, owner, name, port, speed, databits, parity, stopbits):
import importlib
def __init__(self, owner, configuration):
import importlib.util
if importlib.util.find_spec('serial') != None:
import serial
else:
@@ -60,8 +69,18 @@ class SerialInterface(Interface):
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
RNS.panic()
self.rxb = 0
self.txb = 0
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
port = c["port"] if "port" in c else None
speed = int(c["speed"]) if "speed" in c else 9600
databits = int(c["databits"]) if "databits" in c else 8
parity = c["parity"] if "parity" in c else "N"
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
if port == None:
raise ValueError("No port specified for serial interface")
self.HW_MTU = 564
@@ -122,12 +141,12 @@ class SerialInterface(Interface):
RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
def processIncoming(self, data):
def process_incoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self,data):
def process_outgoing(self,data):
if self.online:
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
written = self.serial.write(data)
@@ -150,7 +169,7 @@ class SerialInterface(Interface):
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.processIncoming(data_buffer)
self.process_incoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
@@ -201,5 +220,8 @@ class SerialInterface(Interface):
RNS.log("Reconnected serial port for "+str(self))
def should_ingress_limit(self):
return False
def __str__(self):
return "SerialInterface["+self.name+"]"
+253 -100
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -20,7 +28,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .Interface import Interface
from RNS.Interfaces.Interface import Interface
import socketserver
import threading
import platform
@@ -30,6 +38,9 @@ import sys
import os
import RNS
class TCPInterface():
HW_MTU = 262144
class HDLC():
FLAG = 0x7E
ESC = 0x7D
@@ -58,8 +69,13 @@ class KISS():
class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
class ThreadingTCP6Server(socketserver.ThreadingMixIn, socketserver.TCPServer):
address_family = socket.AF_INET6
class TCPClientInterface(Interface):
BITRATE_GUESS = 10*1000*1000
DEFAULT_IFAC_SIZE = 16
AUTOCONFIGURE_MTU = True
RECONNECT_WAIT = 5
RECONNECT_MAX_TRIES = None
@@ -78,12 +94,26 @@ class TCPClientInterface(Interface):
I2P_PROBE_INTERVAL = 9
I2P_PROBES = 5
def __init__(self, owner, name, target_ip=None, target_port=None, connected_socket=None, max_reconnect_tries=None, kiss_framing=False, i2p_tunneled = False, connect_timeout = None):
self.rxb = 0
self.txb = 0
self.HW_MTU = 1064
def __init__(self, owner, configuration, connected_socket=None):
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
target_ip = c["target_host"] if "target_host" in c and c["target_host"] != None else None
target_port = int(c["target_port"]) if "target_port" in c and c["target_host"] != None else None
kiss_framing = False
if "kiss_framing" in c and c.as_bool("kiss_framing") == True:
kiss_framing = True
i2p_tunneled = c.as_bool("i2p_tunneled") if "i2p_tunneled" in c else False
connect_timeout = c.as_int("connect_timeout") if "connect_timeout" in c else None
max_reconnect_tries = c.as_int("max_reconnect_tries") if "max_reconnect_tries" in c else None
fixed_mtu = c.as_int("fixed_mtu") if "fixed_mtu" in c else None
if fixed_mtu:
if fixed_mtu < RNS.Reticulum.MTU: raise ValueError(f"Configured MTU of {fixed_mtu} bytes is too small")
self.AUTOCONFIGURE_MTU = False
self.FIXED_MTU = True
self.HW_MTU = TCPInterface.HW_MTU if not fixed_mtu else fixed_mtu
self.IN = True
self.OUT = False
self.socket = None
@@ -101,10 +131,9 @@ class TCPClientInterface(Interface):
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
self.bitrate = TCPClientInterface.BITRATE_GUESS
if max_reconnect_tries == None:
self.max_reconnect_tries = TCPClientInterface.RECONNECT_MAX_TRIES
else:
self.max_reconnect_tries = max_reconnect_tries
self.supports_discovery = True
if max_reconnect_tries == None: self.max_reconnect_tries = TCPClientInterface.RECONNECT_MAX_TRIES
else: self.max_reconnect_tries = max_reconnect_tries
if connected_socket != None:
self.receives = True
@@ -117,6 +146,8 @@ class TCPClientInterface(Interface):
elif platform.system() == "Darwin":
self.set_timeouts_osx()
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
elif target_ip != None and target_port != None:
self.receives = True
self.target_ip = target_ip
@@ -176,19 +207,21 @@ class TCPClientInterface(Interface):
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(TCPClientInterface.I2P_PROBE_AFTER))
def detach(self):
self.online = False
if self.socket != None:
if hasattr(self.socket, "close"):
if callable(self.socket.close):
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
self.detached = True
try:
self.socket.shutdown(socket.SHUT_RDWR)
if self.socket != None:
self.socket.shutdown(socket.SHUT_RDWR)
except Exception as e:
RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
try:
self.socket.close()
if self.socket != None:
self.socket.close()
except Exception as e:
RNS.log("Error while closing socket for "+str(self)+": "+str(e))
@@ -199,9 +232,14 @@ class TCPClientInterface(Interface):
if initial:
RNS.log("Establishing TCP connection for "+str(self)+"...", RNS.LOG_DEBUG)
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
address_info = socket.getaddrinfo(self.target_ip, self.target_port, proto=socket.IPPROTO_TCP)[0]
address_family = address_info[0]
target_address = address_info[4]
self.socket = socket.socket(address_family, socket.SOCK_STREAM)
self.socket.settimeout(TCPClientInterface.INITIAL_CONNECT_TIMEOUT)
self.socket.connect((self.target_ip, self.target_port))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.socket.connect(target_address)
self.socket.settimeout(None)
self.online = True
@@ -263,15 +301,16 @@ class TCPClientInterface(Interface):
RNS.log("Attempt to reconnect on a non-initiator TCP interface. This should not happen.", RNS.LOG_ERROR)
raise IOError("Attempt to reconnect on a non-initiator TCP interface")
def processIncoming(self, data):
self.rxb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.rxb += len(data)
self.owner.inbound(data, self)
def process_incoming(self, data):
if self.online and not self.detached:
self.rxb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self, data):
if self.online:
def process_outgoing(self, data):
if self.online and not self.detached:
# while self.writing:
# time.sleep(0.01)
@@ -299,22 +338,23 @@ class TCPClientInterface(Interface):
try:
in_frame = False
escape = False
frame_buffer = b""
data_in = b""
data_buffer = b""
command = KISS.CMD_UNKNOWN
while True:
data_in = self.socket.recv(4096)
if self.socket: data_in = self.socket.recv(4096)
else: data_in = b""
if len(data_in) > 0:
pointer = 0
while pointer < len(data_in):
byte = data_in[pointer]
pointer += 1
if self.kiss_framing:
# Read loop for KISS framing
if self.kiss_framing:
# Read loop for KISS framing
pointer = 0
while pointer < len(data_in):
byte = data_in[pointer]
pointer += 1
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
in_frame = False
self.processIncoming(data_buffer)
self.process_incoming(data_buffer)
elif (byte == KISS.FEND):
in_frame = True
command = KISS.CMD_UNKNOWN
@@ -337,32 +377,33 @@ class TCPClientInterface(Interface):
escape = False
data_buffer = data_buffer+bytes([byte])
else:
# Read loop for HDLC framing
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.processIncoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
elif (in_frame and len(data_buffer) < self.HW_MTU):
if (byte == HDLC.ESC):
escape = True
else:
# Read loop for standard HDLC framing
frame_buffer += data_in
flags_remaining = True
while flags_remaining:
frame_start = frame_buffer.find(HDLC.FLAG)
if frame_start != -1:
frame_end = frame_buffer.find(HDLC.FLAG, frame_start+1)
if frame_end != -1:
frame = frame_buffer[frame_start+1:frame_end]
frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG]))
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
if len(frame) > RNS.Reticulum.HEADER_MINSIZE:
self.process_incoming(frame)
frame_buffer = frame_buffer[frame_end:]
else:
if (escape):
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
byte = HDLC.FLAG
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
byte = HDLC.ESC
escape = False
data_buffer = data_buffer+bytes([byte])
flags_remaining = False
else:
flags_remaining = False
else:
self.online = False
if self.initiator and not self.detached:
RNS.log("The socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
self.reconnect()
else:
RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_VERBOSE)
RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_DEBUG)
self.teardown()
break
@@ -392,50 +433,89 @@ class TCPClientInterface(Interface):
self.IN = False
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.clients -= 1
while self in self.parent_interface.spawned_interfaces:
self.parent_interface.spawned_interfaces.remove(self)
if self in RNS.Transport.interfaces:
if not self.initiator:
RNS.Transport.interfaces.remove(self)
if not self.initiator:
RNS.Transport.remove_interface(self)
def __str__(self):
return "TCPInterface["+str(self.name)+"/"+str(self.target_ip)+":"+str(self.target_port)+"]"
if ":" in self.target_ip:
ip_str = f"[{self.target_ip}]"
else:
ip_str = f"{self.target_ip}"
return "TCPInterface["+str(self.name)+"/"+ip_str+":"+str(self.target_port)+"]"
class TCPServerInterface(Interface):
BITRATE_GUESS = 10*1000*1000
BITRATE_GUESS = 10_000_000
DEFAULT_IFAC_SIZE = 16
AUTOCONFIGURE_MTU = True
@staticmethod
def get_address_for_if(name):
import importlib
if importlib.util.find_spec('netifaces') != None:
import netifaces
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['addr']
def get_address_for_if(name, bind_port, prefer_ipv6=False):
from RNS.Interfaces import netinfo
ifaddr = netinfo.ifaddresses(name)
if len(ifaddr) < 1:
raise SystemError(f"No addresses available on specified kernel interface \"{name}\" for TCPServerInterface to bind to")
if (prefer_ipv6 or not netinfo.AF_INET in ifaddr) and netinfo.AF_INET6 in ifaddr:
bind_ip = ifaddr[netinfo.AF_INET6][0]["addr"]
if bind_ip.lower().startswith("fe80::"):
# We'll need to add the interface as scope for link-local addresses
return TCPServerInterface.get_address_for_host(f"{bind_ip}%{name}", bind_port, prefer_ipv6)
else:
return TCPServerInterface.get_address_for_host(bind_ip, bind_port, prefer_ipv6)
elif netinfo.AF_INET in ifaddr:
bind_ip = ifaddr[netinfo.AF_INET][0]["addr"]
return (bind_ip, bind_port)
else:
RNS.log("Getting interface addresses from device names requires the netifaces module.", RNS.LOG_CRITICAL)
RNS.log("You can install it with the command: python3 -m pip install netifaces", RNS.LOG_CRITICAL)
RNS.panic()
raise SystemError(f"No addresses available on specified kernel interface \"{name}\" for TCPServerInterface to bind to")
@staticmethod
def get_broadcast_for_if(name):
import importlib
if importlib.util.find_spec('netifaces') != None:
import netifaces
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['broadcast']
def get_address_for_host(name, bind_port, prefer_ipv6=False):
address_infos = socket.getaddrinfo(name, bind_port, proto=socket.IPPROTO_TCP)
address_info = address_infos[0]
for entry in address_infos:
if prefer_ipv6 and entry[0] == socket.AF_INET6:
address_info = entry; break
elif not prefer_ipv6 and entry[0] == socket.AF_INET:
address_info = entry; break
if address_info[0] == socket.AF_INET6:
return (name, bind_port, address_info[4][2], address_info[4][3])
elif address_info[0] == socket.AF_INET:
return (name, bind_port)
else:
RNS.log("Getting interface addresses from device names requires the netifaces module.", RNS.LOG_CRITICAL)
RNS.log("You can install it with the command: python3 -m pip install netifaces", RNS.LOG_CRITICAL)
RNS.panic()
raise SystemError(f"No suitable kernel interface available for address \"{name}\" for TCPServerInterface to bind to")
def __init__(self, owner, name, device=None, bindip=None, bindport=None, i2p_tunneled=False):
self.rxb = 0
self.txb = 0
self.HW_MTU = 1064
@property
def clients(self):
return len(self.spawned_interfaces)
def __init__(self, owner, configuration):
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
device = c["device"] if "device" in c else None
port = int(c["port"]) if "port" in c else None
bindip = c["listen_ip"] if "listen_ip" in c else None
bindport = int(c["listen_port"]) if "listen_port" in c else None
i2p_tunneled = c.as_bool("i2p_tunneled") if "i2p_tunneled" in c else False
prefer_ipv6 = c.as_bool("prefer_ipv6") if "prefer_ipv6" in c else False
if port != None:
bindport = port
self.supports_discovery = True
self.HW_MTU = TCPInterface.HW_MTU
self.online = False
self.clients = 0
self.spawned_interfaces = []
self.IN = True
self.OUT = False
@@ -445,24 +525,41 @@ class TCPServerInterface(Interface):
self.i2p_tunneled = i2p_tunneled
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
if device != None:
bindip = TCPServerInterface.get_address_for_if(device)
if (bindip != None and bindport != None):
self.receives = True
self.bind_ip = bindip
if bindport == None:
raise SystemError(f"No TCP port configured for interface \"{name}\"")
else:
self.bind_port = bindport
bind_address = None
if device != None:
bind_address = TCPServerInterface.get_address_for_if(device, self.bind_port, prefer_ipv6)
else:
if bindip == None:
raise SystemError(f"No TCP bind IP configured for interface \"{name}\"")
bind_address = TCPServerInterface.get_address_for_host(bindip, self.bind_port, prefer_ipv6)
if bind_address != None:
self.receives = True
self.bind_ip = bind_address[0]
def handlerFactory(callback):
def createHandler(*args, **keys):
return TCPInterfaceHandler(callback, *args, **keys)
return createHandler
self.owner = owner
address = (self.bind_ip, self.bind_port)
ThreadingTCPServer.allow_reuse_address = True
self.server = ThreadingTCPServer(address, handlerFactory(self.incoming_connection))
if len(bind_address) == 4:
try:
ThreadingTCP6Server.allow_reuse_address = True
self.server = ThreadingTCP6Server(bind_address, handlerFactory(self.incoming_connection))
except Exception as e:
RNS.log(f"Error while binding IPv6 socket for interface, the contained exception was: {e}", RNS.LOG_ERROR)
raise SystemError("Could not bind IPv6 socket for interface. Please check the specified \"listen_ip\" configuration option")
else:
ThreadingTCPServer.allow_reuse_address = True
self.server = ThreadingTCPServer(bind_address, handlerFactory(self.incoming_connection))
self.server.daemon_threads = True
self.bitrate = TCPServerInterface.BITRATE_GUESS
@@ -472,20 +569,56 @@ class TCPServerInterface(Interface):
self.online = True
else:
raise SystemError("Insufficient parameters to create TCP listener")
def incoming_connection(self, handler):
RNS.log("Accepting incoming TCP connection", RNS.LOG_VERBOSE)
interface_name = "Client on "+self.name
spawned_interface = TCPClientInterface(self.owner, interface_name, target_ip=None, target_port=None, connected_socket=handler.request, i2p_tunneled=self.i2p_tunneled)
spawned_configuration = {"name": "Client on "+self.name, "target_host": None, "target_port": None, "i2p_tunneled": self.i2p_tunneled}
spawned_interface = TCPClientInterface(self.owner, spawned_configuration, connected_socket=handler.request)
spawned_interface.OUT = self.OUT
spawned_interface.IN = self.IN
spawned_interface.ingress_control = self.ingress_control
spawned_interface.ic_max_held_announces = self.ic_max_held_announces
spawned_interface.ic_burst_hold = self.ic_burst_hold
spawned_interface.ic_burst_freq = self.ic_burst_freq
spawned_interface.ic_burst_freq_new = self.ic_burst_freq_new
spawned_interface.ic_new_time = self.ic_new_time
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
spawned_interface.egress_control = self.egress_control
spawned_interface.ec_pr_freq = self.ec_pr_freq
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
spawned_interface.target_ip = handler.client_address[0]
spawned_interface.target_port = str(handler.client_address[1])
spawned_interface.parent_interface = self
spawned_interface.bitrate = self.bitrate
spawned_interface.optimise_mtu()
spawned_interface.ifac_size = self.ifac_size
spawned_interface.ifac_netname = self.ifac_netname
spawned_interface.ifac_netkey = self.ifac_netkey
if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None:
ifac_origin = b""
if spawned_interface.ifac_netname != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
if spawned_interface.ifac_netkey != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
spawned_interface.ifac_key = RNS.Cryptography.hkdf(
length=64,
derive_from=ifac_origin_hash,
salt=RNS.Reticulum.IFAC_SALT,
context=None
)
spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key)
spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key))
spawned_interface.announce_rate_target = self.announce_rate_target
spawned_interface.announce_rate_grace = self.announce_rate_grace
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
@@ -493,22 +626,37 @@ class TCPServerInterface(Interface):
spawned_interface.HW_MTU = self.HW_MTU
spawned_interface.online = True
RNS.log("Spawned new TCPClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
RNS.Transport.interfaces.append(spawned_interface)
self.clients += 1
RNS.Transport.add_interface(spawned_interface)
while spawned_interface in self.spawned_interfaces:
self.spawned_interfaces.remove(spawned_interface)
self.spawned_interfaces.append(spawned_interface)
spawned_interface.read_loop()
def processOutgoing(self, data):
def received_announce(self, from_spawned=False):
if from_spawned: self.ia_freq_deque.append(time.time())
def sent_announce(self, from_spawned=False):
if from_spawned: self.oa_freq_deque.append(time.time())
def 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
def detach(self):
self.detached = True
self.online = False
if self.server != None:
if hasattr(self.server, "shutdown"):
if callable(self.server.shutdown):
try:
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
self.server.shutdown()
self.detached = True
self.server.server_close()
self.server = None
except Exception as e:
@@ -516,7 +664,12 @@ class TCPServerInterface(Interface):
def __str__(self):
return "TCPServerInterface["+self.name+"/"+self.bind_ip+":"+str(self.bind_port)+"]"
if ":" in self.bind_ip:
ip_str = f"[{self.bind_ip}]"
else:
ip_str = f"{self.bind_ip}"
return "TCPServerInterface["+self.name+"/"+ip_str+":"+str(self.bind_port)+"]"
class TCPInterfaceHandler(socketserver.BaseRequestHandler):
+40 -27
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -20,7 +28,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .Interface import Interface
from RNS.Interfaces.Interface import Interface
import socketserver
import threading
import socket
@@ -31,32 +39,37 @@ import RNS
class UDPInterface(Interface):
BITRATE_GUESS = 10*1000*1000
DEFAULT_IFAC_SIZE = 16
@staticmethod
def get_address_for_if(name):
import importlib
if importlib.util.find_spec('netifaces') != None:
import netifaces
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['addr']
else:
RNS.log("Getting interface addresses from device names requires the netifaces module.", RNS.LOG_CRITICAL)
RNS.log("You can install it with the command: python3 -m pip install netifaces", RNS.LOG_CRITICAL)
RNS.panic()
from RNS.Interfaces import netinfo
ifaddr = netinfo.ifaddresses(name)
return ifaddr[netinfo.AF_INET][0]["addr"]
@staticmethod
def get_broadcast_for_if(name):
import importlib
if importlib.util.find_spec('netifaces') != None:
import netifaces
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['broadcast']
else:
RNS.log("Getting interface addresses from device names requires the netifaces module.", RNS.LOG_CRITICAL)
RNS.log("You can install it with the command: python3 -m pip install netifaces", RNS.LOG_CRITICAL)
RNS.panic()
from RNS.Interfaces import netinfo
ifaddr = netinfo.ifaddresses(name)
return ifaddr[netinfo.AF_INET][0]["broadcast"]
def __init__(self, owner, name, device=None, bindip=None, bindport=None, forwardip=None, forwardport=None):
self.rxb = 0
self.txb = 0
def __init__(self, owner, configuration):
super().__init__()
c = Interface.get_config_obj(configuration)
name = c["name"]
device = c["device"] if "device" in c else None
port = int(c["port"]) if "port" in c else None
bindip = c["listen_ip"] if "listen_ip" in c else None
bindport = int(c["listen_port"]) if "listen_port" in c else None
forwardip = c["forward_ip"] if "forward_ip" in c else None
forwardport = int(c["forward_port"]) if "forward_port" in c else None
if port != None:
if bindport == None:
bindport = port
if forwardport == None:
forwardport = port
self.HW_MTU = 1064
@@ -86,7 +99,7 @@ class UDPInterface(Interface):
self.owner = owner
address = (self.bind_ip, self.bind_port)
socketserver.UDPServer.address_family = socket.AF_INET
self.server = socketserver.UDPServer(address, handlerFactory(self.processIncoming))
self.server = socketserver.UDPServer(address, handlerFactory(self.process_incoming))
thread = threading.Thread(target=self.server.serve_forever)
thread.daemon = True
@@ -100,11 +113,11 @@ class UDPInterface(Interface):
self.forward_port = forwardport
def processIncoming(self, data):
def process_incoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self,data):
def process_outgoing(self,data):
try:
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
File diff suppressed because it is too large Load Diff
+18 -6
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -23,6 +31,10 @@
import os
import glob
import RNS.Interfaces.Android
import RNS.Interfaces.util
import RNS.Interfaces.util.netinfo as netinfo
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
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"))]))
+7
View File
@@ -0,0 +1,7 @@
import os
import glob
py_modules = glob.glob(os.path.dirname(__file__)+"/*.py")
pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
modules = py_modules+pyc_modules
__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))]))
+325
View File
@@ -0,0 +1,325 @@
# MIT License
#
# Copyright (c) 2014 Stefan C. Mueller
# Copyright (c) 2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import socket
import ipaddress
import platform
import ctypes.util
import collections
from typing import List, Iterable, Optional, Tuple, Union
AF_INET6 = socket.AF_INET6.value
AF_INET = socket.AF_INET.value
def interfaces() -> List[str]:
adapters = get_adapters(include_unconfigured=True)
return [a.name for a in adapters]
def interface_names_to_indexes() -> dict:
adapters = get_adapters(include_unconfigured=True)
results = {}
for adapter in adapters:
results[adapter.name] = adapter.index
return results
def interface_name_to_nice_name(ifname) -> str:
try:
adapters = get_adapters(include_unconfigured=True)
for adapter in adapters:
if adapter.name == ifname:
if hasattr(adapter, "nice_name"):
return adapter.nice_name
except: return None
return None
def ifaddresses(ifname) -> dict:
adapters = get_adapters(include_unconfigured=True)
ifa = {}
for a in adapters:
if a.name == ifname:
ipv4s = []
ipv6s = []
for ip in a.ips:
t = {}
if ip.is_IPv4:
net = ipaddress.ip_network(str(ip.ip)+"/"+str(ip.network_prefix), strict=False)
t["addr"] = ip.ip
t["prefix"] = ip.network_prefix
t["broadcast"] = str(net.broadcast_address)
ipv4s.append(t)
if ip.is_IPv6:
t["addr"] = ip.ip[0]
ipv6s.append(t)
if len(ipv4s) > 0: ifa[AF_INET] = ipv4s
if len(ipv6s) > 0: ifa[AF_INET6] = ipv6s
return ifa
def get_adapters(include_unconfigured=False):
if os.name == "posix": return _get_adapters_posix(include_unconfigured=include_unconfigured)
elif os.name == "nt": return _get_adapters_win(include_unconfigured=include_unconfigured)
else: raise RuntimeError(f"Unsupported Operating System: {os.name}")
class Adapter(object):
def __init__(self, name: str, nice_name: str, ips: List["IP"], index: Optional[int] = None) -> None:
self.name = name
self.nice_name = nice_name
self.ips = ips
self.index = index
def __repr__(self) -> str:
return "Adapter(name={name}, nice_name={nice_name}, ips={ips}, index={index})".format(
name=repr(self.name), nice_name=repr(self.nice_name), ips=repr(self.ips), index=repr(self.index))
_IPv4Address = str
_IPv6Address = Tuple[str, int, int]
class IP(object):
def __init__(self, ip: Union[_IPv4Address, _IPv6Address], network_prefix: int, nice_name: str) -> None:
self.ip = ip
self.network_prefix = network_prefix
self.nice_name = nice_name
@property
def is_IPv4(self) -> bool: return not isinstance(self.ip, tuple)
@property
def is_IPv6(self) -> bool: return isinstance(self.ip, tuple)
def __repr__(self) -> str:
return "IP(ip={ip}, network_prefix={network_prefix}, nice_name={nice_name})".format(ip=repr(self.ip), network_prefix=repr(self.network_prefix), nice_name=repr(self.nice_name))
if platform.system() == "Darwin" or "BSD" in platform.system():
class sockaddr(ctypes.Structure):
_fields_ = [
("sa_len", ctypes.c_uint8),
("sa_familiy", ctypes.c_uint8),
("sa_data", ctypes.c_uint8 * 14)]
class sockaddr_in(ctypes.Structure):
_fields_ = [
("sa_len", ctypes.c_uint8),
("sa_familiy", ctypes.c_uint8),
("sin_port", ctypes.c_uint16),
("sin_addr", ctypes.c_uint8 * 4),
("sin_zero", ctypes.c_uint8 * 8)]
class sockaddr_in6(ctypes.Structure):
_fields_ = [
("sa_len", ctypes.c_uint8),
("sa_familiy", ctypes.c_uint8),
("sin6_port", ctypes.c_uint16),
("sin6_flowinfo", ctypes.c_uint32),
("sin6_addr", ctypes.c_uint8 * 16),
("sin6_scope_id", ctypes.c_uint32)]
else:
class sockaddr(ctypes.Structure): # type: ignore
_fields_ = [("sa_familiy", ctypes.c_uint16), ("sa_data", ctypes.c_uint8 * 14)]
class sockaddr_in(ctypes.Structure): # type: ignore
_fields_ = [
("sin_familiy", ctypes.c_uint16),
("sin_port", ctypes.c_uint16),
("sin_addr", ctypes.c_uint8 * 4),
("sin_zero", ctypes.c_uint8 * 8)]
class sockaddr_in6(ctypes.Structure): # type: ignore
_fields_ = [
("sin6_familiy", ctypes.c_uint16),
("sin6_port", ctypes.c_uint16),
("sin6_flowinfo", ctypes.c_uint32),
("sin6_addr", ctypes.c_uint8 * 16),
("sin6_scope_id", ctypes.c_uint32)]
def sockaddr_to_ip(sockaddr_ptr: "ctypes.pointer[sockaddr]") -> Optional[Union[_IPv4Address, _IPv6Address]]:
if sockaddr_ptr:
if sockaddr_ptr[0].sa_familiy == socket.AF_INET:
ipv4 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in))
ippacked = bytes(bytearray(ipv4[0].sin_addr))
ip = str(ipaddress.ip_address(ippacked))
return ip
elif sockaddr_ptr[0].sa_familiy == socket.AF_INET6:
ipv6 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in6))
flowinfo = ipv6[0].sin6_flowinfo
ippacked = bytes(bytearray(ipv6[0].sin6_addr))
ip = str(ipaddress.ip_address(ippacked))
scope_id = ipv6[0].sin6_scope_id
return (ip, flowinfo, scope_id)
return None
def ipv6_prefixlength(address: ipaddress.IPv6Address) -> int:
prefix_length = 0
for i in range(address.max_prefixlen):
if int(address) >> i & 1: prefix_length = prefix_length + 1
return prefix_length
if os.name == "posix":
class ifaddrs(ctypes.Structure): pass
ifaddrs._fields_ = [
("ifa_next", ctypes.POINTER(ifaddrs)),
("ifa_name", ctypes.c_char_p),
("ifa_flags", ctypes.c_uint),
("ifa_addr", ctypes.POINTER(sockaddr)),
("ifa_netmask", ctypes.POINTER(sockaddr)),]
libc = ctypes.CDLL(ctypes.util.find_library("socket" if os.uname()[0] == "SunOS" else "c"), use_errno=True) # type: ignore
def _get_adapters_posix(include_unconfigured: bool = False) -> Iterable[Adapter]:
addr0 = addr = ctypes.POINTER(ifaddrs)()
retval = libc.getifaddrs(ctypes.byref(addr))
if retval != 0:
eno = ctypes.get_errno()
raise OSError(eno, os.strerror(eno))
ips = collections.OrderedDict()
def add_ip(adapter_name: str, ip: Optional[IP]) -> None:
if adapter_name not in ips:
index = None # type: Optional[int]
try:
index = socket.if_nametoindex(adapter_name) # type: ignore
except (OSError, AttributeError): pass
ips[adapter_name] = Adapter(adapter_name, adapter_name, [], index=index)
if ip is not None:
ips[adapter_name].ips.append(ip)
while addr:
name = addr[0].ifa_name.decode(encoding="UTF-8")
ip_addr = sockaddr_to_ip(addr[0].ifa_addr)
if ip_addr:
if addr[0].ifa_netmask and not addr[0].ifa_netmask[0].sa_familiy:
addr[0].ifa_netmask[0].sa_familiy = addr[0].ifa_addr[0].sa_familiy
netmask = sockaddr_to_ip(addr[0].ifa_netmask)
if isinstance(netmask, tuple):
netmaskStr = str(netmask[0])
prefixlen = ipv6_prefixlength(ipaddress.IPv6Address(netmaskStr))
else:
assert netmask is not None, f"sockaddr_to_ip({addr[0].ifa_netmask}) returned None"
netmaskStr = str("0.0.0.0/" + netmask)
prefixlen = ipaddress.IPv4Network(netmaskStr).prefixlen
ip = IP(ip_addr, prefixlen, name)
add_ip(name, ip)
else:
if include_unconfigured:
add_ip(name, None)
addr = addr[0].ifa_next
libc.freeifaddrs(addr0)
return ips.values()
elif os.name == "nt":
from ctypes import wintypes
NO_ERROR = 0
ERROR_BUFFER_OVERFLOW = 111
MAX_ADAPTER_NAME_LENGTH = 256
MAX_ADAPTER_DESCRIPTION_LENGTH = 128
MAX_ADAPTER_ADDRESS_LENGTH = 8
AF_UNSPEC = 0
class SOCKET_ADDRESS(ctypes.Structure): _fields_ = [("lpSockaddr", ctypes.POINTER(sockaddr)), ("iSockaddrLength", wintypes.INT)]
class IP_ADAPTER_UNICAST_ADDRESS(ctypes.Structure): pass
IP_ADAPTER_UNICAST_ADDRESS._fields_ = [
("Length", wintypes.ULONG),
("Flags", wintypes.DWORD),
("Next", ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)),
("Address", SOCKET_ADDRESS),
("PrefixOrigin", ctypes.c_uint),
("SuffixOrigin", ctypes.c_uint),
("DadState", ctypes.c_uint),
("ValidLifetime", wintypes.ULONG),
("PreferredLifetime", wintypes.ULONG),
("LeaseLifetime", wintypes.ULONG),
("OnLinkPrefixLength", ctypes.c_uint8)]
class IP_ADAPTER_ADDRESSES(ctypes.Structure): pass
IP_ADAPTER_ADDRESSES._fields_ = [
("Length", wintypes.ULONG),
("IfIndex", wintypes.DWORD),
("Next", ctypes.POINTER(IP_ADAPTER_ADDRESSES)),
("AdapterName", ctypes.c_char_p),
("FirstUnicastAddress", ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)),
("FirstAnycastAddress", ctypes.c_void_p),
("FirstMulticastAddress", ctypes.c_void_p),
("FirstDnsServerAddress", ctypes.c_void_p),
("DnsSuffix", ctypes.c_wchar_p),
("Description", ctypes.c_wchar_p),
("FriendlyName", ctypes.c_wchar_p)]
iphlpapi = ctypes.windll.LoadLibrary("Iphlpapi") # type: ignore
def _enumerate_interfaces_of_adapter_win(nice_name: str, address: IP_ADAPTER_UNICAST_ADDRESS) -> Iterable[IP]:
# Iterate through linked list and fill list
addresses = [] # type: List[IP_ADAPTER_UNICAST_ADDRESS]
while True:
addresses.append(address)
if not address.Next: break
address = address.Next[0]
for address in addresses:
ip = sockaddr_to_ip(address.Address.lpSockaddr)
assert ip is not None, f"sockaddr_to_ip({address.Address.lpSockaddr}) returned None"
network_prefix = address.OnLinkPrefixLength
yield IP(ip, network_prefix, nice_name)
def _get_adapters_win(include_unconfigured: bool = False) -> Iterable[Adapter]:
addressbuffersize = wintypes.ULONG(15 * 1024)
retval = ERROR_BUFFER_OVERFLOW
while retval == ERROR_BUFFER_OVERFLOW:
addressbuffer = ctypes.create_string_buffer(addressbuffersize.value)
retval = iphlpapi.GetAdaptersAddresses(
wintypes.ULONG(AF_UNSPEC),
wintypes.ULONG(0),
None,
ctypes.byref(addressbuffer),
ctypes.byref(addressbuffersize))
if retval != NO_ERROR:
raise ctypes.WinError() # type: ignore
# Iterate through adapters and fill array
address_infos = [] # type: List[IP_ADAPTER_ADDRESSES]
address_info = IP_ADAPTER_ADDRESSES.from_buffer(addressbuffer)
while True:
address_infos.append(address_info)
if not address_info.Next: break
address_info = address_info.Next[0]
# Iterate through unicast addresses
result = [] # type: List[Adapter]
for adapter_info in address_infos:
name = adapter_info.AdapterName.decode()
nice_name = adapter_info.Description
index = adapter_info.IfIndex
if adapter_info.FirstUnicastAddress:
ips = _enumerate_interfaces_of_adapter_win(adapter_info.FriendlyName, adapter_info.FirstUnicastAddress[0])
ips = list(ips)
result.append(Adapter(name, nice_name, ips, index=index))
elif include_unconfigured: result.append(Adapter(name, nice_name, [], index=index))
return result
+697 -272
View File
File diff suppressed because it is too large Load Diff
+106 -32
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -30,15 +38,15 @@ class Packet:
"""
The Packet class is used to create packet instances that can be sent
over a Reticulum network. Packets will automatically be encrypted if
they are adressed to a ``RNS.Destination.SINGLE`` destination,
they are addressed to a ``RNS.Destination.SINGLE`` destination,
``RNS.Destination.GROUP`` destination or a :ref:`RNS.Link<api-link>`.
For ``RNS.Destination.GROUP`` destinations, Reticulum will use the
pre-shared key configured for the destination. All packets to group
destinations are encrypted with the same AES-128 key.
destinations are encrypted with the same AES-256 key.
For ``RNS.Destination.SINGLE`` destinations, Reticulum will use a newly
derived ephemeral AES-128 key for every packet.
derived ephemeral AES-256 key for every packet.
For :ref:`RNS.Link<api-link>` destinations, Reticulum will use per-link
ephemeral keys, and offers **Forward Secrecy**.
@@ -58,9 +66,7 @@ class Packet:
# Header types
HEADER_1 = 0x00 # Normal header format
HEADER_2 = 0x01 # Header format used for packets in transport
HEADER_3 = 0x02 # Reserved
HEADER_4 = 0x03 # Reserved
header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4]
header_types = [HEADER_1, HEADER_2]
# Packet context types
NONE = 0x00 # Generic data packet
@@ -77,6 +83,7 @@ class Packet:
PATH_RESPONSE = 0x0B # Packet is a response to a path request
COMMAND = 0x0C # Packet is a command
COMMAND_STATUS = 0x0D # Packet is a status of an executed command
CHANNEL = 0x0E # Packet contains link channel data
KEEPALIVE = 0xFA # Packet is a keepalive packet
LINKIDENTIFY = 0xFB # Packet is a link peer identification proof
LINKCLOSE = 0xFC # Packet is a link close message
@@ -84,6 +91,10 @@ class Packet:
LRRTT = 0xFE # Packet is a link request round-trip time measurement
LRPROOF = 0xFF # Packet is a link request proof
# Context flag values
FLAG_SET = 0x01
FLAG_UNSET = 0x00
# This is used to calculate allowable
# payload sizes
HEADER_MAXSIZE = RNS.Reticulum.HEADER_MAXSIZE
@@ -92,7 +103,7 @@ class Packet:
# With an MTU of 500, the maximum of data we can
# send in a single encrypted packet is given by
# the below calculation; 383 bytes.
ENCRYPTED_MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.FERNET_OVERHEAD-RNS.Identity.KEYSIZE//16)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
ENCRYPTED_MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.TOKEN_OVERHEAD-RNS.Identity.KEYSIZE//16)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
"""
The maximum size of the payload data in a single encrypted packet
"""
@@ -103,7 +114,14 @@ class Packet:
TIMEOUT_PER_HOP = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT
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):
__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", "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):
if destination != None:
if transport_type == None:
transport_type = RNS.Transport.BROADCAST
@@ -112,6 +130,7 @@ class Packet:
self.packet_type = packet_type
self.transport_type = transport_type
self.context = context
self.context_flag = context_flag
self.hops = 0;
self.destination = destination
@@ -131,20 +150,28 @@ class Packet:
self.fromPacked = True
self.create_receipt = False
self.MTU = RNS.Reticulum.MTU
if destination and destination.type == RNS.Destination.LINK:
self.MTU = destination.mtu
else:
self.MTU = RNS.Reticulum.MTU
self.sent_at = None
self.packet_hash = None
self.ratchet_id = None
self.attached_interface = attached_interface
self.receiving_interface = None
self.is_outbound_pr = False
self.rssi = None
self.snr = None
self.q = None
def get_packed_flags(self):
if self.context == Packet.LRPROOF:
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | RNS.Destination.LINK | self.packet_type
packed_flags = (self.header_type << 6) | (self.context_flag << 5) | (self.transport_type << 4) | (RNS.Destination.LINK << 2) | self.packet_type
else:
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
packed_flags = (self.header_type << 6) | (self.context_flag << 5) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
return packed_flags
def pack(self):
@@ -173,8 +200,8 @@ class Packet:
# Packet proofs over links are not encrypted
self.ciphertext = self.data
elif self.context == Packet.RESOURCE:
# A resource takes care of symmetric
# encryption by itself
# A resource takes care of encryption
# by itself
self.ciphertext = self.data
elif self.context == Packet.KEEPALIVE:
# Keepalive packets contain no actual
@@ -187,6 +214,8 @@ class Packet:
# In all other cases, we encrypt the packet
# with the destination's encryption method
self.ciphertext = self.destination.encrypt(self.data)
if hasattr(self.destination, "latest_ratchet_id"):
self.ratchet_id = self.destination.latest_ratchet_id
if self.header_type == Packet.HEADER_2:
if self.transport_id != None:
@@ -215,8 +244,9 @@ class Packet:
self.flags = self.raw[0]
self.hops = self.raw[1]
self.header_type = (self.flags & 0b11000000) >> 6
self.transport_type = (self.flags & 0b00110000) >> 4
self.header_type = (self.flags & 0b01000000) >> 6
self.context_flag = (self.flags & 0b00100000) >> 5
self.transport_type = (self.flags & 0b00010000) >> 4
self.destination_type = (self.flags & 0b00001100) >> 2
self.packet_type = (self.flags & 0b00000011)
@@ -238,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):
@@ -250,19 +280,21 @@ class Packet:
if not self.sent:
if self.destination.type == RNS.Destination.LINK:
if self.destination.status == RNS.Link.CLOSED:
raise IOError("Attempt to transmit over a closed link")
RNS.log("Attempt to transmit over a closed link, dropping packet", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
self.sent = False
self.receipt = None
return False
else:
self.destination.last_outbound = time.time()
self.destination.tx += 1
self.destination.txbytes += len(self.data)
if not self.packed:
self.pack()
if not self.packed: self.pack()
if RNS.Transport.outbound(self):
return self.receipt
if RNS.Transport.outbound(self): return self.receipt
else:
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
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
@@ -277,10 +309,14 @@ class Packet:
:returns: A :ref:`RNS.PacketReceipt<api-packetreceipt>` instance if *create_receipt* was set to *True* when the packet was instantiated, if not returns *None*. If the packet could not be sent *False* is returned.
"""
if self.sent:
# Re-pack the packet to obtain new ciphertext for
# encrypted destinations
self.pack()
if RNS.Transport.outbound(self):
return self.receipt
else:
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
RNS.log("Re-send failed. No interfaces could process the outbound packet", RNS.LOG_WARNING)
self.sent = False
self.receipt = None
return False
@@ -325,6 +361,33 @@ class Packet:
return hashable_part
def get_rssi(self):
"""
:returns: The physical layer *Received Signal Strength Indication* if available, otherwise ``None``.
"""
if self.rssi != None:
return self.rssi
else:
return reticulum.get_packet_rssi(self.packet_hash)
def get_snr(self):
"""
:returns: The physical layer *Signal-to-Noise Ratio* if available, otherwise ``None``.
"""
if self.snr != None:
return self.snr
else:
return reticulum.get_packet_snr(self.packet_hash)
def get_q(self):
"""
:returns: The physical layer *Link Quality* if available, otherwise ``None``.
"""
if self.q != None:
return self.q
else:
return reticulum.get_packet_q(self.packet_hash)
class ProofDestination:
def __init__(self, packet):
self.hash = packet.get_hash()[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8];
@@ -365,10 +428,10 @@ class PacketReceipt:
self.proof_packet = None
if packet.destination.type == RNS.Destination.LINK:
self.timeout = packet.destination.rtt * packet.destination.traffic_timeout_factor
self.timeout = max(packet.destination.rtt * packet.destination.traffic_timeout_factor, RNS.Link.TRAFFIC_TIMEOUT_MIN_MS/1000)
else:
self.timeout = Packet.TIMEOUT_PER_HOP * RNS.Transport.hops_to(self.destination.hash)
self.timeout = RNS.Reticulum.get_instance().get_first_hop_timeout(self.destination.hash)
self.timeout += Packet.TIMEOUT_PER_HOP * RNS.Transport.hops_to(self.destination.hash)
def get_status(self):
"""
@@ -397,9 +460,16 @@ class PacketReceipt:
self.proved = True
self.concluded_at = time.time()
self.proof_packet = proof_packet
link.last_proof = self.concluded_at
if self.callbacks.delivery != None:
self.callbacks.delivery(self)
try:
self.callbacks.delivery(self)
except Exception as e:
RNS.log("An error occurred while evaluating external delivery callback for "+str(link), RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e)
return True
else:
return False
@@ -430,7 +500,7 @@ class PacketReceipt:
# This is an explicit proof
proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
if proof_hash == self.hash:
if proof_hash == self.hash and hasattr(self.destination, "identity") and self.destination.identity != None:
proof_valid = self.destination.identity.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
@@ -451,6 +521,10 @@ class PacketReceipt:
return False
elif len(proof) == PacketReceipt.IMPL_LENGTH:
# This is an implicit proof
if not hasattr(self.destination, "identity"):
return False
if self.destination.identity == None:
return False
+35
View File
@@ -0,0 +1,35 @@
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
class Resolver:
@staticmethod
def resolve_identity(full_name):
pass
+533 -236
View File
File diff suppressed because it is too large Load Diff
+1251 -661
View File
File diff suppressed because it is too large Load Diff
+2392 -1095
View File
File diff suppressed because it is too large Load Diff
+16 -6
View File
@@ -1,6 +1,6 @@
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -9,8 +9,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -23,5 +31,7 @@
import os
import glob
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
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"))]))
+639 -121
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -24,6 +32,8 @@
import RNS
import argparse
import threading
import shutil
import time
import sys
import os
@@ -32,38 +42,119 @@ from RNS._version import __version__
APP_NAME = "rncp"
allow_all = False
allow_fetch = False
allow_overwrite_on_receive = False
fetch_auto_compress = True
fetch_jail = None
save_path = None
show_phy_rates = False
allowed_identity_hashes = []
identity = None
def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_identity = False, limit = None, disable_auth = None, disable_announce = False):
global allow_all, allowed_identity_hashes
identity = None
def prepare_identity(identity_path):
global identity
if identity_path == None:
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
targetloglevel = 3+verbosity-quietness
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
if os.path.isfile(identity_path):
identity = RNS.Identity.from_file(identity_path)
identity = RNS.Identity.from_file(identity_path)
if identity == None:
RNS.log(f"Could not load identity for rncp. The identity file at \"{identity_path}\" may be corrupt or unreadable.", RNS.LOG_ERROR)
RNS.exit(2)
if identity == None:
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
identity = RNS.Identity()
identity.to_file(identity_path)
REQ_FETCH_NOT_ALLOWED = 0xF0
es = " "
erase_str = "\33[2K\r"
def listen(configdir, identitypath = None, verbosity = 0, quietness = 0, allowed = [], display_identity = False,
limit = None, disable_auth = None, fetch_allowed = False, no_compress=False,
jail = None, save = None, announce = False, allow_overwrite=False):
global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path, identity
global fetch_auto_compress, allow_overwrite_on_receive
allow_fetch = fetch_allowed
fetch_auto_compress = not no_compress
allow_overwrite_on_receive = allow_overwrite
identity = None
if announce < 0:
announce = False
targetloglevel = 3+verbosity-quietness
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
if jail != None:
fetch_jail = os.path.abspath(os.path.expanduser(jail))
RNS.log("Restricting fetch requests to paths under \""+fetch_jail+"\"", RNS.LOG_VERBOSE)
if save != None:
sp = os.path.abspath(os.path.expanduser(save))
if os.path.isdir(sp):
if os.access(sp, os.W_OK):
save_path = sp
else:
RNS.log("Output directory not writable", RNS.LOG_ERROR)
RNS.exit(4)
else:
RNS.log("Output directory not found", RNS.LOG_ERROR)
RNS.exit(3)
RNS.log("Saving received files in \""+save_path+"\"", RNS.LOG_VERBOSE)
prepare_identity(identitypath)
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "receive")
if display_identity:
print("Identity : "+str(identity))
print("Receiving on : "+RNS.prettyhexrep(destination.hash))
exit(0)
print("Listening on : "+RNS.prettyhexrep(destination.hash))
RNS.exit(0)
if disable_auth:
allow_all = True
else:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
try:
allowed_file_name = "allowed_identities"
allowed_file = None
if os.path.isfile(os.path.expanduser("/etc/rncp/"+allowed_file_name)):
allowed_file = os.path.expanduser("/etc/rncp/"+allowed_file_name)
elif os.path.isfile(os.path.expanduser("~/.config/rncp/"+allowed_file_name)):
allowed_file = os.path.expanduser("~/.config/rncp/"+allowed_file_name)
elif os.path.isfile(os.path.expanduser("~/.rncp/"+allowed_file_name)):
allowed_file = os.path.expanduser("~/.rncp/"+allowed_file_name)
if allowed_file != None:
af = open(allowed_file, "r")
al = af.read().replace("\r", "").split("\n")
ali = []
for a in al:
if len(a) == dest_len:
ali.append(a)
if len(ali) > 0:
if not allowed:
allowed = ali
else:
allowed.extend(ali)
if len(ali) == 1:
ms = "y"
else:
ms = "ies"
RNS.log("Loaded "+str(len(ali))+" allowed identit"+ms+" from "+str(allowed_file), RNS.LOG_VERBOSE)
except Exception as e:
RNS.log("Error while parsing allowed_identities file. The contained exception was: "+str(e), RNS.LOG_ERROR)
if allowed != 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:
@@ -72,22 +163,75 @@ def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_ident
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
exit(1)
RNS.log(f"Could not apply allowed identity: {e}", RNS.LOG_ERROR)
RNS.exit(1)
if len(allowed_identity_hashes) < 1 and not disable_auth:
print("Warning: No allowed identities configured, rncp will not accept any files!")
RNS.log("No allowed identities configured, rncp will not accept any files!", RNS.LOG_WARNING)
destination.set_link_established_callback(receive_link_established)
print("rncp ready to receive on "+RNS.prettyhexrep(destination.hash))
def fetch_request(path, data, request_id, link_id, remote_identity, requested_at):
global allow_fetch, fetch_jail, fetch_auto_compress
if not allow_fetch:
return REQ_FETCH_NOT_ALLOWED
if not disable_announce:
destination.announce()
if fetch_jail:
if data.startswith(fetch_jail+"/"):
data = data.replace(fetch_jail+"/", "")
file_path = os.path.abspath(os.path.expanduser(f"{fetch_jail}/{data}"))
if not file_path.startswith(fetch_jail+"/"):
RNS.log(f"Disallowing fetch request for {file_path} outside of fetch jail {fetch_jail}", RNS.LOG_WARNING)
return REQ_FETCH_NOT_ALLOWED
else:
file_path = os.path.abspath(os.path.expanduser(f"{data}"))
target_link = None
for link in RNS.Transport.active_links:
if link.link_id == link_id:
target_link = link
if not os.path.isfile(file_path):
RNS.log("Client-requested file not found: "+str(file_path), RNS.LOG_VERBOSE)
return False
else:
if target_link != None:
RNS.log("Sending file "+str(file_path)+" to client", RNS.LOG_VERBOSE)
try:
metadata = {"name": os.path.basename(file_path).encode("utf-8") }
fetch_resource = RNS.Resource(open(file_path, "rb"), target_link, metadata=metadata, auto_compress=fetch_auto_compress)
return True
except Exception as e:
RNS.log(f"Could not send file to client. The contained exception was: {e}", RNS.LOG_ERROR)
return False
else:
return None
destination.set_link_established_callback(client_link_established)
if allow_fetch:
if allow_all:
RNS.log("Allowing unauthenticated fetch requests", RNS.LOG_WARNING)
destination.register_request_handler("fetch_file", response_generator=fetch_request, allow=RNS.Destination.ALLOW_ALL)
else:
destination.register_request_handler("fetch_file", response_generator=fetch_request, allow=RNS.Destination.ALLOW_LIST, allowed_list=allowed_identity_hashes)
RNS.log("rncp listening on "+RNS.prettyhexrep(destination.hash), RNS.LOG_INFO)
if announce >= 0:
def job():
destination.announce()
if announce > 0:
while True:
time.sleep(announce)
destination.announce()
threading.Thread(target=job, daemon=True).start()
while True:
time.sleep(1)
while True: time.sleep(1)
def receive_link_established(link):
def client_link_established(link):
RNS.log("Incoming link established", RNS.LOG_VERBOSE)
link.set_remote_identified_callback(receive_sender_identified)
link.set_resource_strategy(RNS.Link.ACCEPT_APP)
@@ -127,64 +271,108 @@ def receive_resource_started(resource):
else:
id_str = ""
print("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str)
RNS.log("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str, RNS.LOG_INFO)
def receive_resource_concluded(resource):
global save_path, allow_overwrite_on_receive
if resource.status == RNS.Resource.COMPLETE:
print(str(resource)+" completed")
RNS.log(f"Incoming resource {resource} completed", RNS.LOG_INFO)
if resource.total_size > 4:
filename_len = int.from_bytes(resource.data.read(2), "big")
filename = resource.data.read(filename_len).decode("utf-8")
counter = 0
saved_filename = filename
while os.path.isfile(saved_filename):
counter += 1
saved_filename = filename+"."+str(counter)
file = open(saved_filename, "wb")
file.write(resource.data.read())
file.close()
if resource.metadata == None:
RNS.log("Invalid data received, ignoring resource", RNS.LOG_WARNING)
return
else:
print("Invalid data received, ignoring resource")
try:
filename = os.path.basename(resource.metadata["name"].decode("utf-8"))
counter = 0
if save_path:
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
if not saved_filename.startswith(save_path+"/"):
RNS.log(f"Invalid save path {saved_filename}, ignoring", RNS.LOG_ERROR)
return
else:
saved_filename = filename
full_save_path = saved_filename
if allow_overwrite_on_receive:
if os.path.isfile(full_save_path):
try: os.unlink(full_save_path)
except Exception as e:
RNS.log(f"Could not overwrite existing file {full_save_path}, renaming instead", RNS.LOG_ERROR)
while os.path.isfile(full_save_path):
counter += 1
full_save_path = saved_filename+"."+str(counter)
shutil.move(resource.data.name, full_save_path)
RNS.log("Saved received file to "+full_save_path, RNS.LOG_NOTICE)
except Exception as e:
RNS.log(f"An error occurred while saving received resource: {e}", RNS.LOG_ERROR)
return
else:
print("Resource failed")
RNS.log("Resource failed", RNS.LOG_INFO)
resource_done = False
current_resource = None
stats = []
speed = 0.0
phy_speed = 0.0
phy_got_total = 0
def sender_progress(resource):
stats_max = 32
global current_resource, stats, speed, resource_done
global current_resource, stats, speed, phy_speed, phy_got_total, resource_done
current_resource = resource
now = time.time()
got = current_resource.get_progress()*current_resource.total_size
entry = [now, got]
got = current_resource.get_progress()*current_resource.get_data_size()
phy_got = current_resource.get_segment_progress()*current_resource.get_transfer_size()
entry = [now, got, phy_got]
stats.append(entry)
while len(stats) > stats_max:
stats.pop(0)
span = now - stats[0][0]
if span == 0:
speed = 0
phy_speed = 0
else:
diff = got - stats[0][1]
speed = diff/span
phy_diff = phy_got - stats[0][2]
if phy_diff > 0:
phy_speed = phy_diff/span
# phy_got_total += phy_diff
if resource.status < RNS.Resource.COMPLETE:
resource_done = False
else:
resource_done = True
link = None
def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT):
global current_resource, resource_done, link, speed
from tempfile import TemporaryFile
def fetch(configdir, identitypath = None, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, save=None, allow_overwrite=False):
global current_resource, resource_done, link, speed, show_phy_rates, save_path, allow_overwrite_on_receive, identity
targetloglevel = 3+verbosity-quietness
show_phy_rates = phy_rates
allow_overwrite_on_receive = allow_overwrite
if save:
sp = os.path.abspath(os.path.expanduser(save))
if os.path.isdir(sp):
if os.access(sp, os.W_OK):
save_path = sp
else:
RNS.log("Output directory not writable", RNS.LOG_ERROR)
RNS.exit(4)
else:
RNS.log("Output directory not found", RNS.LOG_ERROR)
RNS.exit(3)
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
@@ -196,62 +384,297 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
exit(1)
file_path = os.path.expanduser(file)
if not os.path.isfile(file_path):
print("File not found")
exit(1)
temp_file = TemporaryFile()
real_file = open(file_path, "rb")
filename_bytes = os.path.basename(file_path).encode("utf-8")
filename_len = len(filename_bytes)
if filename_len > 0xFFFF:
print("Filename exceeds max size, cannot send")
exit(1)
else:
print("Preparing file...", end=" ")
temp_file.write(filename_len.to_bytes(2, "big"))
temp_file.write(filename_bytes)
temp_file.write(real_file.read())
temp_file.seek(0)
print("\r \r", end="")
RNS.exit(1)
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
if os.path.isfile(identity_path):
identity = RNS.Identity.from_file(identity_path)
if identity == None:
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
identity = RNS.Identity()
identity.to_file(identity_path)
prepare_identity(identitypath)
if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash)
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
if silent:
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested")
else:
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=es)
sys.stdout.flush()
i = 0
syms = "⢄⢂⢁⡁⡈⡐⡠"
estab_timeout = time.time()+timeout
while not RNS.Transport.has_path(destination_hash) and time.time() < estab_timeout:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
if not silent:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
if not RNS.Transport.has_path(destination_hash):
print("\r \rPath not found")
exit(1)
if silent:
print("Path not found")
else:
print(f"{erase_str}Path not found")
RNS.exit(1)
else:
print("\r \rEstablishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=" ")
if silent:
print("Establishing link with "+RNS.prettyhexrep(destination_hash))
else:
print(f"{erase_str}Establishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=es)
listener_identity = RNS.Identity.recall(destination_hash)
listener_destination = RNS.Destination(
listener_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
APP_NAME,
"receive"
)
link = RNS.Link(listener_destination)
while link.status != RNS.Link.ACTIVE and time.time() < estab_timeout:
if not silent:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
if not RNS.Transport.has_path(destination_hash):
if silent:
print("Could not establish link with "+RNS.prettyhexrep(destination_hash))
else:
print(f"{erase_str}Could not establish link with "+RNS.prettyhexrep(destination_hash))
RNS.exit(1)
else:
if silent:
print("Requesting file from remote...")
else:
print(f"{erase_str}Requesting file from remote ", end=es)
link.identify(identity)
request_resolved = False
request_status = "unknown"
resource_resolved = False
resource_status = "unrequested"
current_resource = None
current_transfer_started = None
def request_response(request_receipt):
nonlocal request_resolved, request_status
if request_receipt.response == False:
request_status = "not_found"
elif request_receipt.response == None:
request_status = "remote_error"
elif request_receipt.response == REQ_FETCH_NOT_ALLOWED:
request_status = "fetch_not_allowed"
else:
request_status = "found"
request_resolved = True
def request_failed(request_receipt):
nonlocal request_resolved, request_status
request_status = "unknown"
request_resolved = True
def fetch_resource_started(resource):
nonlocal resource_status, current_transfer_started
current_resource = resource
current_resource.progress_callback(sender_progress)
resource_status = "started"
if not current_transfer_started: current_transfer_started = time.time()
def fetch_resource_concluded(resource):
nonlocal resource_resolved, resource_status
global save_path, allow_overwrite_on_receive
if resource.status == RNS.Resource.COMPLETE:
if resource.metadata == None:
print("Invalid data received, ignoring resource")
return
else:
try:
filename = os.path.basename(resource.metadata["name"].decode("utf-8"))
counter = 0
if save_path:
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
if not saved_filename.startswith(save_path+"/"):
print(f"Invalid save path {saved_filename}, ignoring")
return
else:
saved_filename = filename
full_save_path = saved_filename
if allow_overwrite_on_receive:
if os.path.isfile(full_save_path):
try: os.unlink(full_save_path)
except Exception as e:
print(f"Could not overwrite existing file {full_save_path}, renaming instead")
while os.path.isfile(full_save_path):
counter += 1
full_save_path = saved_filename+"."+str(counter)
shutil.move(resource.data.name, full_save_path)
except Exception as e:
print(f"An error occurred while saving received resource: {e}")
return
else:
print("Resource failed")
resource_status = "failed"
resource_resolved = True
link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
link.set_resource_started_callback(fetch_resource_started)
link.set_resource_concluded_callback(fetch_resource_concluded)
link.request("fetch_file", data=file, response_callback=request_response, failed_callback=request_failed)
syms = "⢄⢂⢁⡁⡈⡐⡠"
while not request_resolved:
if not silent:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
if request_status == "fetch_not_allowed":
if not silent: print(f"{erase_str}", end="")
print("Fetch request failed, fetching the file "+str(file)+" was not allowed by the remote")
link.teardown()
time.sleep(0.15)
RNS.exit(0)
elif request_status == "not_found":
if not silent: print(f"{erase_str}", end="")
print("Fetch request failed, the file "+str(file)+" was not found on the remote")
link.teardown()
time.sleep(0.15)
RNS.exit(0)
elif request_status == "remote_error":
if not silent: print(f"{erase_str}", end="")
print("Fetch request failed due to an error on the remote system")
link.teardown()
time.sleep(0.15)
RNS.exit(0)
elif request_status == "unknown":
if not silent: print(f"{erase_str}", end="")
print("Fetch request failed due to an unknown error (probably not authorised)")
link.teardown()
time.sleep(0.15)
RNS.exit(0)
elif request_status == "found":
if not silent: print(f"{erase_str}", end="")
while not resource_resolved:
if not silent:
time.sleep(0.1)
if current_resource:
prg = current_resource.get_progress()
percent = round(prg * 100.0, 1)
if show_phy_rates:
pss = size_str(phy_speed, "b")
phy_str = f" ({pss}ps at physical layer)"
else:
phy_str = ""
ps = size_str(int(prg*current_resource.total_size))
ts = size_str(current_resource.total_size)
ss = size_str(speed, "b")
stat_str = f"{percent}% - {ps} of {ts} - {ss}ps{phy_str}"
if prg != 1.0:
print(f"{erase_str}Transferring file {syms[i]} {stat_str}", end=es)
else:
end_time = time.time(); delta_time = end_time - current_transfer_started
speed = current_resource.total_size/delta_time; dt_str = RNS.prettytime(delta_time)
ss = size_str(speed, "b")
stat_str = f"{percent}% - {ps} of {ts} in {dt_str} - {ss}ps{phy_str}"
print(f"{erase_str}Transfer complete {stat_str}", end=es)
else:
print(f"{erase_str}Waiting for transfer to start {syms[i]} ", end=es)
sys.stdout.flush()
i = (i+1)%len(syms)
if not current_resource or current_resource.status != RNS.Resource.COMPLETE:
if silent:
print("The transfer failed")
else:
print(f"{erase_str}The transfer failed")
RNS.exit(1)
else:
if silent:
print(str(file)+" fetched from "+RNS.prettyhexrep(destination_hash))
else:
print("\n"+str(file)+" fetched from "+RNS.prettyhexrep(destination_hash))
link.teardown()
time.sleep(0.1)
RNS.exit(0)
link.teardown()
time.sleep(0.1)
RNS.exit(0)
def send(configdir, identitypath = None, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, no_compress=False):
global current_resource, resource_done, link, speed, show_phy_rates, phy_got_total, phy_speed, identity
targetloglevel = 3+verbosity-quietness
show_phy_rates = phy_rates
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination) != 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(destination)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
RNS.exit(1)
file_path = os.path.expanduser(file)
if not os.path.isfile(file_path):
print("File not found")
sys.exit(1)
metadata = {"name": os.path.basename(file_path).encode("utf-8") }
print(f"{erase_str}", end="")
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
if identity == None:
prepare_identity(identitypath)
if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash)
if silent:
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested")
else:
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=es)
sys.stdout.flush()
i = 0
syms = "⢄⢂⢁⡁⡈⡐⡠"
estab_timeout = time.time()+timeout
while not RNS.Transport.has_path(destination_hash) and time.time() < estab_timeout:
if not silent:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
if not RNS.Transport.has_path(destination_hash):
if silent:
print("Path not found")
else:
print(f"{erase_str}Path not found")
RNS.exit(1)
else:
if silent:
print("Establishing link with "+RNS.prettyhexrep(destination_hash))
else:
print(f"{erase_str}Establishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=es)
receiver_identity = RNS.Identity.recall(destination_hash)
receiver_destination = RNS.Destination(
@@ -264,53 +687,109 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
link = RNS.Link(receiver_destination)
while link.status != RNS.Link.ACTIVE and time.time() < estab_timeout:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
if not silent:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
if not RNS.Transport.has_path(destination_hash):
print("\r \rCould not establish link with "+RNS.prettyhexrep(destination_hash))
exit(1)
if time.time() > estab_timeout:
if silent:
print("Link establishment with "+RNS.prettyhexrep(destination_hash)+" timed out")
else:
print(f"{erase_str}Link establishment with "+RNS.prettyhexrep(destination_hash)+" timed out")
RNS.exit(1)
elif not RNS.Transport.has_path(destination_hash):
if silent:
print("No path found to "+RNS.prettyhexrep(destination_hash))
else:
print(f"{erase_str}No path found to "+RNS.prettyhexrep(destination_hash))
RNS.exit(1)
else:
print("\r \rAdvertising file resource ", end=" ")
if silent:
print("Advertising file resource...")
else:
print(f"{erase_str}Advertising file resource ", end=es)
link.identify(identity)
resource = RNS.Resource(temp_file, link, callback = sender_progress, progress_callback = sender_progress)
auto_compress = True
if no_compress: auto_compress = False
try: resource = RNS.Resource(open(file_path, "rb"), link, metadata=metadata, callback = sender_progress, progress_callback = sender_progress, auto_compress = auto_compress)
except Exception as e:
print(f"Could not start transfer: {e}")
RNS.exit(1)
current_resource = resource
while resource.status < RNS.Resource.TRANSFERRING:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
if not silent:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
resource_started_at = time.time()
if resource.status > RNS.Resource.COMPLETE:
print("\r \rFile was not accepted by "+RNS.prettyhexrep(destination_hash))
exit(1)
if silent:
print("File was not accepted by "+RNS.prettyhexrep(destination_hash))
else:
print(f"{erase_str}File was not accepted by "+RNS.prettyhexrep(destination_hash))
RNS.exit(1)
else:
print("\r \rTransferring file ", end=" ")
if silent:
print("Transferring file...")
else:
print(f"{erase_str}Transferring file ", end=es)
while not resource_done:
def progress_update(i, done=False):
time.sleep(0.1)
prg = current_resource.get_progress()
percent = round(prg * 100.0, 1)
stat_str = str(percent)+"% - " + size_str(int(prg*current_resource.total_size)) + " of " + size_str(current_resource.total_size) + " - " +size_str(speed, "b")+"ps"
print("\r \rTransferring file "+syms[i]+" "+stat_str, end=" ")
if show_phy_rates and not resource_done:
pss = size_str(phy_speed, "b")
phy_str = f" ({pss}ps at physical layer)"
else:
phy_str = ""
es = " "
cs = size_str(int(prg*current_resource.total_size))
ts = size_str(current_resource.total_size)
ss = size_str(speed, "b")
stat_str = f"{percent}% - {cs} of {ts} - {ss}ps{phy_str}"
if not done:
print(f"{erase_str}Transferring file "+syms[i]+" "+stat_str, end=es)
else:
print(f"{erase_str}Transfer complete "+stat_str, end=es)
sys.stdout.flush()
i = (i+1)%len(syms)
return i
while not resource_done:
if not silent:
i = progress_update(i)
resource_concluded_at = time.time()
transfer_time = resource_concluded_at - resource_started_at
speed = current_resource.total_size/transfer_time
# phy_speed = phy_got_total/transfer_time
if not silent:
i = progress_update(i, done=True)
if current_resource.status != RNS.Resource.COMPLETE:
print("\r \rThe transfer failed")
exit(1)
if silent:
print("The transfer failed")
else:
print(f"{erase_str}The transfer failed")
RNS.exit(1)
else:
print("\r \r"+str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
if silent:
print(str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
else:
print("\n"+str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
link.teardown()
time.sleep(0.25)
real_file.close()
temp_file.close()
exit(0)
RNS.exit(0)
def main():
try:
@@ -320,37 +799,76 @@ def main():
parser.add_argument("--config", metavar="path", action="store", default=None, help="path to alternative Reticulum config directory", 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("-S", '--silent', action='store_true', default=False, help="disable transfer progress output")
parser.add_argument("-l", '--listen', action='store_true', default=False, help="listen for incoming transfer requests")
parser.add_argument("-C", '--no-compress', action='store_true', default=False, help="disable automatic compression")
parser.add_argument("-F", '--allow-fetch', action='store_true', default=False, help="allow authenticated clients to fetch files")
parser.add_argument("-f", '--fetch', action='store_true', default=False, help="fetch file from remote listener instead of sending")
parser.add_argument("-j", "--jail", metavar="path", action="store", default=None, help="restrict fetch requests to specified path", type=str)
parser.add_argument("-s", "--save", metavar="path", action="store", default=None, help="save received files in specified path", type=str)
parser.add_argument('-O', '--overwrite', action='store_true', default=False, help="Allow overwriting received files, instead of adding postfix")
parser.add_argument("-b", action='store', metavar="seconds", default=-1, help="announce interval, 0 to only announce at startup", type=int)
parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="allow this identity (or add in ~/.rncp/allowed_identities)", type=str)
parser.add_argument('-n', '--no-auth', action='store_true', default=False, help="accept requests from anyone")
parser.add_argument('-p', '--print-identity', action='store_true', default=False, help="print identity and destination info and exit")
parser.add_argument("-r", '--receive', action='store_true', default=False, help="wait for incoming files")
parser.add_argument("-b", '--no-announce', action='store_true', default=False, help="don't announce at program start")
parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="accept from this identity", type=str)
parser.add_argument('-n', '--no-auth', action='store_true', default=False, help="accept files from anyone")
parser.add_argument('-i', metavar="identity", action='store', dest="identity", default=None, help="path to identity to use", type=str)
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="sender timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
parser.add_argument('-P', '--phy-rates', action='store_true', default=False, help="display physical layer transfer rates")
# parser.add_argument("--limit", action="store", metavar="files", type=float, help="maximum number of files to accept", default=None)
parser.add_argument("--version", action="version", version="rncp {version}".format(version=__version__))
args = parser.parse_args()
if args.receive or args.print_identity:
receive(
if args.listen or args.print_identity:
listen(
configdir = args.config,
identitypath = args.identity,
verbosity=args.verbose,
quietness=args.quiet,
allowed = args.allowed,
fetch_allowed = args.allow_fetch,
no_compress = args.no_compress,
jail = args.jail,
save = args.save,
display_identity=args.print_identity,
# limit=args.limit,
disable_auth=args.no_auth,
disable_announce=args.no_announce,
announce=args.b,
allow_overwrite=args.overwrite,
)
elif args.fetch:
if args.destination != None and args.file != None:
fetch(
configdir = args.config,
identitypath = args.identity,
verbosity = args.verbose,
quietness = args.quiet,
destination = args.destination,
file = args.file,
timeout = args.w,
silent = args.silent,
phy_rates = args.phy_rates,
save = args.save,
allow_overwrite=args.overwrite,
)
else:
print("")
parser.print_help()
print("")
elif args.destination != None and args.file != None:
send(
configdir = args.config,
identitypath = args.identity,
verbosity = args.verbose,
quietness = args.quiet,
destination = args.destination,
file = args.file,
timeout = args.w,
silent = args.silent,
phy_rates = args.phy_rates,
no_compress = args.no_compress,
)
else:
@@ -364,7 +882,7 @@ def main():
resource.cancel()
if link != None:
link.teardown()
exit()
RNS.exit()
def size_str(num, suffix='B'):
units = ['','K','M','G','T','P','E','Z']
+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
+769
View File
@@ -0,0 +1,769 @@
# Reticulum License
#
# Copyright (c) 2016-2026 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import re
import RNS
# Validate ref names according to https://git-scm.com/docs/git-check-ref-format
# This may be a bit overkill, since git validates names as well, but why not.
def san_ref(ref):
if ref.startswith("-"): return None
if ref.startswith("/"): return None
if ref.endswith("/"): return None
if ref.endswith("."): return None
if " " in ref: return None
if not "/" in ref: return None
if ".." in ref: return None
if "/." in ref: return None
if "//" in ref: return None
if "\\" in ref: return None
for comp in ref.split("/"):
if comp.endswith(".lock"): return None
if not all(ord(c) >= 40 for c in ref): return None # Any control character
if "\x7f" in ref: return None # ASCII DEL (177)
if "~" in ref: return None
if "^" in ref: return None
if ":" in ref: return None
if "?" in ref: return None
if "*" in ref: return None
if "[" in ref: return None
if "@{" in ref: return None
if "@" == ref: return None
return ref
def san_refs(refs):
if not type(refs) == list: return None
for ref in refs:
if not san_ref(ref): return None
return refs
# Git SHA format validation
def san_sha(sha):
if len(sha) < 40: return None
try: bytes.fromhex(sha)
except: return None
return sha
class MarkdownToMicron:
BOLD = "`!"
BOLD_END = "`!"
ITALIC = "`*"
ITALIC_END = "`*"
UNDERLINE = "`_"
UNDERLINE_END = "`_"
CODE_BG = "`BT282828"
CODE_BG_INLINE = "`BT383838"
CODE_FG = "`Fddd"
CODE_RESET = "`f`b"
LITERAL_START = "`="
LITERAL_END = "`="
BULLET = ""
# Regex patterns for markdown elements
HEADER_RE = re.compile(r'^(#{1,6})\s+(.+)$')
CODE_FENCE_RE = re.compile(r'^(\s*)```(.*)$')
HORIZONTAL_RULE_RE = re.compile(r'^(\s*)(---+|===+|\*\*\*+|___+)\s*$')
UNORDERED_LIST_RE = re.compile(r'^(\s*)([-*+])\s+(.+)$')
# Table patterns
TABLE_ROW_RE = re.compile(r'^\s*\|?(.+?)\|?\s*$')
TABLE_SEP_RE = re.compile(r'^\s*\|?(?:\s*:?-+:?\s*\|)+\s*$')
# Quote pattern
QUOTE_RE = re.compile(r'^>\s?(.*)$')
# Inline patterns (processed in order of specificity)
LINK_RE = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
INLINE_CODE_RE = re.compile(r'`([^`]+)`')
BOLD_RE = re.compile(r'\*\*(.+?)\*\*|__(.+?)__')
ITALIC_RE = re.compile(r'\*(.+?)\*|_(.+?)_')
TABLE_H = ""
TABLE_V = ""
TABLE_TL = ""
TABLE_TR = ""
TABLE_BL = ""
TABLE_BR = ""
TABLE_ML = ""
TABLE_MR = ""
TABLE_TM = ""
TABLE_BM = ""
TABLE_MM = ""
TABLE_MIN_COL_WIDTH = 3
def __init__(self, max_width=100, syntax_highlighter=None, url_scope=None):
self.max_width = max_width
self.local_url_scope = url_scope or ":/page/"
self.__local_url_scope = self.local_url_scope
self.syntax_highlighter = syntax_highlighter
self.wcwidth = None
self.bold_links = True
self.underline_links = True
self.link_color = None
try:
import wcwidth
self.wcwidth = wcwidth
except: RNS.log(f"The wcwidth module is unavailable, display width calculations for some glyphs will be incorrect", RNS.LOG_WARNING)
def set_url_scope(self, url_scope): self.local_url_scope = url_scope
def restore_url_scope(self, url_scope): self.local_url_scope = self.__local_url_scope
def display_width(self, text):
if not self.wcwidth: return len(text)
else:
# wcswidth returns -1 for non-printable strings,
# fallback to len in this case
w = self.wcwidth.wcswidth(text)
return w if w is not None and w >= 0 else len(text)
def format_block(self, text, url_scope=None):
# text = text.replace("\\", "\\\\") # Now handled in format_line instead
lines = text.split('\n')
result_lines = []
in_code_block = False
code_block_lang = None
code_buffer = []
in_table = False
table_buffer = []
in_quote = False
quote_buffer = []
def flush_quote_buffer():
nonlocal result_lines, quote_buffer, in_quote
if not quote_buffer:
in_quote = False
return
para = " ".join(quote_buffer)
formatted = self._format_inline(para)
effective_width = self.max_width - 3
if effective_width < 1: effective_width = 1
wrapped_lines = self._wrap_text(formatted, effective_width)
for wrapped_line in wrapped_lines: result_lines.append(f"{wrapped_line}")
quote_buffer = []
in_quote = False
def flush_table_buffer():
nonlocal result_lines, table_buffer, in_table
if not table_buffer:
in_table = False
return
if len(table_buffer) >= 2 and self._is_table_separator(table_buffer[1]):
formatted_lines = self.format_table(table_buffer)
result_lines.extend(formatted_lines)
else:
for line in table_buffer: result_lines.append(self.format_line(line))
table_buffer = []
in_table = False
def flush_code_block():
nonlocal result_lines, code_buffer, code_block_lang
if not code_buffer:
return
code_content = '\n'.join(code_buffer)
if self.syntax_highlighter and code_block_lang:
try:
highlighted = self.syntax_highlighter.highlight(code_content, language=code_block_lang)
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
result_lines.append(highlighted)
result_lines.append(self.CODE_RESET)
except Exception:
# Fallback to plain literal block on any error
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
result_lines.append(self.LITERAL_START)
result_lines.append(self._escape_literals(code_content))
result_lines.append(self.LITERAL_END)
result_lines.append(self.CODE_RESET)
else:
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
result_lines.append(self.LITERAL_START)
result_lines.append(self._escape_literals(code_content))
result_lines.append(self.LITERAL_END)
result_lines.append(self.CODE_RESET)
code_buffer = []
for line in lines:
is_fence, lang_hint = self._detect_code_fence(line)
if is_fence:
# Flush any pending structures before code fence
flush_quote_buffer()
flush_table_buffer()
if not in_code_block:
# Opening fence, start buffering
in_code_block = True
code_block_lang = lang_hint.strip() if lang_hint else None
code_buffer = []
else:
# Closing fence, flush highlighted code
flush_code_block()
in_code_block = False
code_block_lang = None
else:
# Buffer code lines for later highlighting
if in_code_block: code_buffer.append(line)
else:
quote_match = self.QUOTE_RE.match(line)
if quote_match:
if not in_quote:
flush_table_buffer()
in_quote = True
quote_buffer = []
quote_buffer.append(quote_match.group(1))
else:
if in_quote:
flush_quote_buffer()
if line.strip() != "":
if self._is_table_row(line):
in_table = True
table_buffer = [line]
else:
formatted = self.format_line(line)
result_lines.append(formatted)
# Pass through blank line as separator
else: result_lines.append("")
else:
if self._is_table_row(line):
if not in_table:
in_table = True
table_buffer = [line]
else: table_buffer.append(line)
else:
# Line breaks table, flush buffer
if in_table: flush_table_buffer()
formatted = self.format_line(line)
result_lines.append(formatted)
# Handle unclosed structures
if in_quote: flush_quote_buffer()
if in_table: flush_table_buffer()
if in_code_block: flush_code_block()
return '\n'.join(result_lines)
def format_line(self, line, mode="normal"):
if mode == "codeblock": return self._escape_literals(line)
line = line.replace("\\", "\\\\")
if line.startswith("-") and not line.startswith("---") and not line.startswith("- "): line = f"\\{line}"
if line.startswith("<"): line = f"\\{line}"
# if line.startswith(">"): line = f"\\{line}" # Now handled by blockquotes
if self.HORIZONTAL_RULE_RE.match(line): return self._format_horizontal_rule()
header_match = self.HEADER_RE.match(line)
if header_match: return self._format_header(header_match)
list_match = self.UNORDERED_LIST_RE.match(line)
if list_match: return self._format_list_item(list_match)
line = self._format_inline(line)
return line
def _format_inline(self, text):
code_blocks = []
def extract_code(match):
code_blocks.append(match.group(1))
return f"\x00CODE{len(code_blocks)-1}\x00"
links = []
def extract_link(match):
links.append((match.group(1), match.group(2)))
return f"\x00LINK{len(links)-1}\x00"
text = self.LINK_RE.sub(extract_link, text)
text = self.INLINE_CODE_RE.sub(extract_code, text)
text = self.BOLD_RE.sub(self._bold_sub, text)
text = self.ITALIC_RE.sub(self._italic_sub, text)
def restore_link(match):
idx = int(match.group(1))
text, url = links[idx]
anchor_components = url.split("#")
url = anchor_components[0]
anchor = anchor_components[1] if len(anchor_components) > 1 else ""
if not ":/" in url:
url = f"{self.local_url_scope}{url}"
if anchor: url = f"{url}|anchor={anchor}"
undl = "`_" if self.underline_links else ""
bold = "`!" if self.bold_links else ""
text = text.replace('`', '')
link = f"{undl}{bold}`[{text}`{url}]{bold}{undl}"
if self.link_color and len(self.link_color) == 3: link = f"`F{self.link_color}{link}`f"
if self.link_color and len(self.link_color) == 6: link = f"`FT{self.link_color}{link}`f"
return link
text = re.sub(r'\x00LINK(\d+)\x00', restore_link, text)
def restore_code(match):
idx = int(match.group(1))
content = code_blocks[idx]
content = content.replace('`', '\\`')
return f"{self.CODE_BG_INLINE}{self.CODE_FG}{content}{self.CODE_RESET}"
text = re.sub(r'\x00CODE(\d+)\x00', restore_code, text)
return text
def _highlight_inline_code(self, content):
if not self.syntax_highlighter: return None
return self.syntax_highlighter.highlight(content, language=None)
def _bold_sub(self, match):
content = match.group(1) or match.group(2)
return f"{self.BOLD}{content}{self.BOLD_END}"
def _italic_sub(self, match):
content = match.group(1) or match.group(2)
return f"{self.ITALIC}{content}{self.ITALIC_END}"
def _format_header(self, match):
hashes = match.group(1)
content = match.group(2)
level = len(hashes)
prefix = ">" * min(level, 6)
return f"{prefix}{self._format_inline(content)}"
def _format_list_item(self, match):
indent = match.group(1)
content = match.group(3)
content = self._format_inline(content)
return f"{indent} {self.BULLET} {content}"
def _format_horizontal_rule(self):
return "-"
def _detect_code_fence(self, line):
match = self.CODE_FENCE_RE.match(line)
if match:
# match.group(2) contains everything after the backticks (language hint)
return True, match.group(2)
return False, ""
def _is_table_row(self, line):
if '|' not in line: return False
match = self.TABLE_ROW_RE.match(line)
if match is None: return False
content = match.group(1)
return '|' in content or line.strip().startswith('|')
def _is_table_separator(self, line):
if '|' not in line: return False
match = self.TABLE_SEP_RE.match(line)
return match is not None
def _escape_literals(self, text):
return text.replace('`', '\\`')
def format_table(self, rows, align="c"):
if len(rows) < 2: return rows
# Parse header and separator
header_cells = self._parse_table_row(rows[0])
alignments = self._parse_table_alignments(rows[1])
# Ensure alignment count matches header cells
while len(alignments) < len(header_cells): alignments.append('left')
alignments = alignments[:len(header_cells)]
# Parse data rows
data_rows = []
for i in range(2, len(rows)):
cells = self._parse_table_row(rows[i])
while len(cells) < len(header_cells): cells.append("")
cells = cells[:len(header_cells)]
data_rows.append(cells)
# Calculate column widths based on content
num_cols = len(header_cells)
col_widths = [0] * num_cols
all_rows = [header_cells] + data_rows
for row in all_rows:
for i, cell in enumerate(row):
formatted = self._format_inline(cell)
width = self._visible_width(formatted)
col_widths[i] = max(col_widths[i], width)
# Apply minimum width and calculate total
col_widths = [max(w, self.TABLE_MIN_COL_WIDTH) for w in col_widths]
# Check max_width constraint
# Total = sum of columns + 3 chars per column (space + 2 borders) + 1 for final border
total_width = sum(col_widths) + (num_cols * 3) + 1
if total_width > self.max_width:
# Reduce widest columns proportionally
excess = total_width - self.max_width
indexed_widths = [(i, w) for i, w in enumerate(col_widths)]
indexed_widths.sort(key=lambda x: -x[1])
for i, w in indexed_widths:
if excess <= 0: break
reduction = min(excess, w - self.TABLE_MIN_COL_WIDTH)
col_widths[i] -= reduction
excess -= reduction
# Build formatted table
result = []
# Alignment start
if align: result.append(f"`{align}")
# Top border
border = self.TABLE_TL
for i, w in enumerate(col_widths):
border += self.TABLE_H * (w + 2)
if i < len(col_widths) - 1: border += self.TABLE_TM
else: border += self.TABLE_TR
result.append(self._escape_literals(border))
# Header row
header_line = self.TABLE_V
for i, cell in enumerate(header_cells):
formatted = self._format_inline(cell)
padded = self._pad_cell(formatted, col_widths[i], 'left')
header_line += f" {padded} {self.TABLE_V}"
result.append(self._escape_literals(header_line))
# Separator row
sep_line = self.TABLE_ML
for i, w in enumerate(col_widths):
cell_width = w + 2
sep_line += self.TABLE_H * cell_width
if i < len(col_widths) - 1: sep_line += self.TABLE_MM
else: sep_line += self.TABLE_MR
result.append(self._escape_literals(sep_line))
# Data rows
for row in data_rows:
row_line = self.TABLE_V
for i, cell in enumerate(row):
formatted = self._format_inline(cell)
padded = self._pad_cell(formatted, col_widths[i], alignments[i])
row_line += f" {padded} {self.TABLE_V}"
result.append(row_line)
# Bottom border
border = self.TABLE_BL
for i, w in enumerate(col_widths):
border += self.TABLE_H * (w + 2)
if i < len(col_widths) - 1: border += self.TABLE_BM
else: border += self.TABLE_BR
result.append(self._escape_literals(border))
# End alignment
if align: result.append("`a")
return result
def format_table_raw(self, rows, align="c"):
if len(rows) < 2: return rows
# Parse header and separator
header_cells = self._parse_table_row(rows[0])
alignments = self._parse_table_alignments(rows[1])
# Ensure alignment count matches header cells
while len(alignments) < len(header_cells): alignments.append('left')
alignments = alignments[:len(header_cells)]
# Parse data rows
data_rows = []
for i in range(2, len(rows)):
cells = self._parse_table_row(rows[i])
while len(cells) < len(header_cells): cells.append("")
cells = cells[:len(header_cells)]
data_rows.append(cells)
# Calculate column widths based on raw content
num_cols = len(header_cells)
col_widths = [0] * num_cols
all_rows = [header_cells] + data_rows
for row in all_rows:
for i, cell in enumerate(row):
width = self._visible_width(cell)
col_widths[i] = max(col_widths[i], width)
# Apply minimum width and calculate total
col_widths = [max(w, self.TABLE_MIN_COL_WIDTH) for w in col_widths]
# Check max_width constraint
total_width = sum(col_widths) + (num_cols * 3) + 1
if total_width > self.max_width:
# Reduce widest columns proportionally
excess = total_width - self.max_width
indexed_widths = [(i, w) for i, w in enumerate(col_widths)]
indexed_widths.sort(key=lambda x: -x[1])
for i, w in indexed_widths:
if excess <= 0: break
reduction = min(excess, w - self.TABLE_MIN_COL_WIDTH)
col_widths[i] -= reduction
excess -= reduction
# Build formatted table
result = []
# Alignment start
if align: result.append(f"`{align}")
# Top border
border = self.TABLE_TL
for i, w in enumerate(col_widths):
border += self.TABLE_H * (w + 2)
if i < len(col_widths) - 1: border += self.TABLE_TM
else: border += self.TABLE_TR
result.append(self._escape_literals(border))
# Header row
header_line = self.TABLE_V
for i, cell in enumerate(header_cells):
padded = self._pad_cell(cell, col_widths[i], 'left')
header_line += f" {padded} {self.TABLE_V}"
result.append(header_line)
# Separator row - clean horizontal lines without alignment markers
sep_line = self.TABLE_ML
for i, w in enumerate(col_widths):
cell_width = w + 2
sep_line += self.TABLE_H * cell_width
if i < len(col_widths) - 1: sep_line += self.TABLE_MM
else: sep_line += self.TABLE_MR
result.append(self._escape_literals(sep_line))
# Data rows (with alignment)
for row in data_rows:
row_line = self.TABLE_V
for i, cell in enumerate(row):
padded = self._pad_cell(cell, col_widths[i], alignments[i])
row_line += f" {padded} {self.TABLE_V}"
result.append(row_line)
# Bottom border
border = self.TABLE_BL
for i, w in enumerate(col_widths):
border += self.TABLE_H * (w + 2)
if i < len(col_widths) - 1: border += self.TABLE_BM
else: border += self.TABLE_BR
result.append(self._escape_literals(border))
# End alignment
if align: result.append("`a")
return result
def _parse_table_row(self, line):
line = line.strip()
if line.startswith('|'): line = line[1:]
if line.endswith('|'): line = line[:-1]
cells = []
current = ""
escaped = False
for char in line:
if escaped:
current += char
escaped = False
elif char == '\\':
escaped = True
elif char == '|':
cells.append(current.strip())
current = ""
else:
current += char
cells.append(current.strip())
return cells
def _parse_table_alignments(self, line):
cells = self._parse_table_row(line)
alignments = []
for cell in cells:
cell = cell.strip()
if cell.startswith(':') and cell.endswith(':'): alignments.append('center')
elif cell.endswith(':'): alignments.append('right')
else: alignments.append('left')
return alignments
def _visible_width(self, text):
text = re.sub(r'`[FB][0-9a-fA-F]{3}', '', text)
text = re.sub(r'`[FB]T[0-9a-fA-F]{6}', '', text)
text = re.sub(r'`[!*_=]', '', text)
text = re.sub(r'`f`b', '', text)
text = re.sub(r'`f', '', text)
text = re.sub(r'`b', '', text)
return self.display_width(text)
def _pad_cell(self, text, width, align):
text = self._truncate_cell(text, width)
text_width = self._visible_width(text)
padding = width - text_width
if align == 'right':
return " " * padding + text
elif align == 'center':
left = padding // 2
right = padding - left
return " " * left + text + " " * right
else:
return text + " " * padding
def _truncate_cell(self, text, width):
if self._visible_width(text) <= width: return text
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)
+987
View File
@@ -0,0 +1,987 @@
#!/usr/bin/env python3
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import RNS
import argparse
import time
import sys
import os
import io
import base64
from RNS._version import __version__
from RNS.vendor import umsgpack as mp
from RNS.Cryptography.Hashes import sha256
from RNS.Cryptography.Hashes import file_sha256
APP_NAME = "rns"
DEFAULT_ASPECTS = f"{APP_NAME}.id"
NO_MESSAGE = 0x01
PRV_EXT = "rid"
PUB_EXT = "pub"
SIG_EXT = "rsg"
MSG_EXT = "rsm"
ENCRYPT_EXT = "rfe"
CHUNK_BLOCKS = 1024*1024
ENC_CHUNK = CHUNK_BLOCKS*RNS.Identity.AES256_BLOCKSIZE
DEC_CHUNK = ENC_CHUNK + RNS.Cryptography.Token.TOKEN_OVERHEAD*2
RSG_HASHTYPES = ["sha256"]
R_OK = 0
R_NO_SIG_FILE = 1
R_NO_IDENTITY = 2
R_NO_PUBKEY = 3
R_NO_PRVKEY = 4
R_NO_KEYS = 5
R_NO_FILE = 6
R_INVALID_FILE = 7
R_INVALID_IDENTITY = 8
R_INVALID_ASPECTS = 9
R_INVALID_SIGNATURE = 10
R_FILE_EXISTS = 11
R_DECRYPT_FAILED = 12
R_INVALID_ARGS = 250
R_SEQUENCE_ERROR = 251
R_READ_ERROR = 252
R_WRITE_ERROR = 253
R_UNKNOWN_ERROR = 254
R_INTERRUPTED = 255
reticulum = None
def validate_args(args):
ops = 0;
for o in [args.encrypt, args.decrypt, args.validate, args.sign, args.sign_message]:
if o: ops += 1
if ops > 1: print("This utility currently only supports one of the encrypt, decrypt, sign or verify operations per invocation"); exit(1)
g = 0;
for a in [args.import_pub, args.import_prv, args.identity, args.generate]:
if a: g += 1
if g > 1: print("The -i, -g, -m and -M args are mutually exclusive"); exit(1)
g = 0;
for a in [args.base64, args.base32, args.base256, args.hex]:
if a: g += 1
if g > 1: print("The -b, -B, --hex and --base256 args are mutually exclusive"); exit(1)
return True
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum Identity & Encryption Utility")
# Identity Resolution
parser.add_argument("--config", metavar="path", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("-i", "--identity", metavar="rid", action="store", default=None, help="hexadecimal Reticulum identity or destination hash, or path to Identity file", type=str)
parser.add_argument("-g", "--generate", metavar="path", action="store", default=None, help="generate a new Identity and save to path")
parser.add_argument("-m", "--import-pub", dest="import_pub", metavar="rid", action="store", default=None, help="import public Reticulum identity in hex, base32 or base64 format, or from file", type=str)
parser.add_argument("-M", "--import-prv", dest="import_prv", metavar="rid", action="store", default=None, help="import Reticulum identity in hex, base32 or base64 format, or from file", type=str)
parser.add_argument("-x", "--export-pub", action="store_true", default=None, help="export public identity to hex, base32 or base64 format")
parser.add_argument("-X", "--export-prv", action="store_true", default=None, help="export private identity to hex, base32 or base64 format, or to file")
# Verbosity Control
parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity")
parser.add_argument("-q", "--quiet", action="count", default=0, help="decrease verbosity")
# Operations
parser.add_argument("-a", "--announce", metavar="aspects", action="store", nargs="?", const=DEFAULT_ASPECTS, default=None, help="announce a destination based on this Identity")
parser.add_argument("-H", "--hash", metavar="aspects", action="store", default=None, help="show destination hashes for other aspects for this Identity")
parser.add_argument("-d", "--decrypt", metavar="file", action="store", nargs="*", default=None, help="decrypt file")
parser.add_argument("-e", "--encrypt", metavar="file", action="store", nargs="*", default=None, help="encrypt file")
parser.add_argument("-V", "--validate", metavar="path", action="store", nargs="*", default=None, help="validate signature")
parser.add_argument("-s", "--sign", metavar="path", action="store", nargs="*", default=None, help="sign file")
parser.add_argument("-S", "--sign-message", metavar="path", action="store", nargs="?", const=NO_MESSAGE, default=None, help="create embedded signed message")
parser.add_argument("--raw", action="store_true", default=False, help="sign raw input data instead of hashing first")
# I/O Control
parser.add_argument("-w", "--write", metavar="file", action="store", default=None, help="output file path", type=str)
parser.add_argument("-f", "--force", action="store_true", default=None, help="write output even if it overwrites existing files")
parser.add_argument("-I", "--stdin", action="store_true", default=False, help=argparse.SUPPRESS) # help="read input from STDIN instead of file"
parser.add_argument("-O", "--stdout", action="store_true", default=False, help=argparse.SUPPRESS) # help="write output to STDOUT instead of file"
# Information Flow
parser.add_argument("-R", "--request", action="store_true", default=False, help="request unknown Identities from the network")
parser.add_argument("-N", "--no-cache", action="store_true", default=False, help="never used cached or network-sourced information")
parser.add_argument("-t", action="store", metavar="seconds", type=float, help="identity request timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
parser.add_argument("-p", "--print-identity", action="store_true", default=False, help="print identity info and exit")
parser.add_argument("-P", "--print-private", action="store_true", default=False, help="allow displaying private keys")
# Formatting Control
parser.add_argument("-b", "--base64", action="store_true", default=False, help="Use base64-encoded input and output")
parser.add_argument("-B", "--base32", action="store_true", default=False, help="Use base32-encoded input and output")
parser.add_argument("--hex", action="store_true", default=False, help="Use hex-encoded input and output")
parser.add_argument("--base256", action="store_true", default=False, help="Use base256-encoded input and output")
parser.add_argument("--version", action="version", version="rnid {version}".format(version=__version__))
args = parser.parse_args()
validate_args(args)
op_requires_identity = (args.sign or args.encrypt or args.decrypt or args.announce or args.write
or args.print_identity or args.print_identity or args.export_pub or args.export_prv)
identity = get_operating_identity(args, allow_none=not op_requires_identity, no_cache=args.no_cache); op = False
if not identity and op_requires_identity: print("Could not get working identity"); exit(R_NO_IDENTITY)
if args.print_identity: print_identity_information(args, identity); op = True
if args.export_pub: export_pub_identity(args, identity); op = True
if args.export_prv: export_prv_identity(args, identity); op = True
if args.hash: print_hash_information(args, identity or args.identity); op = True
if args.announce: announce(args, identity); op = True
if args.validate: validate(args, identity or args.identity); op = True
if args.sign: sign(args, identity); op = True
if args.sign_message: sign_message(args, identity); op = True
if args.encrypt: encrypt(args, identity); op = True
if args.decrypt: decrypt(args, identity); op = True
if args.write: write_identity(args, identity); op = True
if args.generate: op = True
if not op: parser.print_help()
exit(0)
except KeyboardInterrupt: print(""); exit(R_INTERRUPTED)
#####################
# Reticulum Helpers #
#####################
def ensure_reticulum(args):
global reticulum
if not reticulum:
targetloglevel = 4; verbosity = args.verbose; quietness = args.quiet
if verbosity != 0 or quietness != 0: targetloglevel = targetloglevel+verbosity-quietness
reticulum = RNS.Reticulum(configdir=args.config, loglevel=targetloglevel)
RNS.compact_log_fmt = True
if args.stdout: RNS.loglevel = -1
#################################
# Identity Loading & Resolution #
#################################
def get_operating_identity(args, allow_none=False, no_cache=False):
global reticulum
identity = None
if args.generate:
identity = RNS.Identity()
if not args.force and os.path.isfile(args.generate):
print("Identity file "+str(args.generate)+" already exists. Not overwriting.")
exit(R_FILE_EXISTS)
else:
try: identity.to_file(args.generate); print(f"New identity {identity} written to {args.generate}")
except Exception as e: print(f"An error ocurred while saving the generated Identity: {e}"); exit(R_WRITE_ERROR)
elif args.identity:
load_path = None
try: load_path = os.path.expanduser(args.identity)
except: pass
# Attempt to load Identity from .rid file
if load_path and os.path.isfile(load_path):
try:
identity = RNS.Identity.from_file(load_path)
print(f"Loaded Identity {identity} from {load_path}")
if not identity.get_private_key() or not identity.get_public_key():
raise SystemError("Missing key data in loaded identity")
except Exception as e: print(f"Could not load Identity from specified file: {e}"); exit(R_INVALID_IDENTITY)
elif no_cache:
if allow_none: return None
else: print("Could not resolve identity"); exit(R_NO_IDENTITY)
# Attempt to recall Identity from hex-encoded hash
elif len(args.identity) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2:
try:
ensure_reticulum(args)
requested_hash = bytes.fromhex(args.identity)
identity = RNS.Identity.recall(requested_hash) or RNS.Identity.recall(requested_hash, from_identity_hash=True)
if identity == None:
if allow_none and not args.request: return None
elif not args.request:
print("Could not recall Identity for "+RNS.prettyhexrep(requested_hash)+".")
print("You can query the network for unknown Identities with the -R option.")
exit(R_NO_IDENTITY)
else:
id_destination_hash = RNS.Destination.hash_from_name_and_identity(DEFAULT_ASPECTS, requested_hash)
RNS.Transport.request_path(requested_hash) # Acquire if this was a destination hash
RNS.Transport.request_path(id_destination_hash) # Acquire if this was an identity hash
def spincheck():
received = RNS.Identity.recall(requested_hash) or RNS.Identity.recall(requested_hash, from_identity_hash=True)
return received != None
spin(spincheck, "Requesting unknown Identity for "+RNS.prettyhexrep(requested_hash), args.t)
if not spincheck():
print("Identity request timed out");
if not allow_none: exit(R_NO_IDENTITY)
else: return None
else:
identity = RNS.Identity.recall(requested_hash) or RNS.Identity.recall(requested_hash, from_identity_hash=True)
print("Received Identity "+str(identity)+" for destination "+RNS.prettyhexrep(requested_hash)+" from the network")
else:
ident_str = str(identity)
hash_str = RNS.prettyhexrep(requested_hash)
if ident_str == hash_str: print(f"Recalled Identity {ident_str}")
else: print(f"Recalled Identity {ident_str} for destination {hash_str}")
if identity and identity.hash: reticulum._retain_identity(identity.hash)
except Exception as e: print(f"Invalid hexadecimal hash provided: {e}"); exit(R_INVALID_IDENTITY)
elif args.import_pub or args.import_prv:
prvsize = RNS.Identity.KEYSIZE//8
pubsize = prvsize
identity_bytes = None
if args.import_pub:
try:
identity_bytes = None
import_path = os.path.expanduser(args.import_pub)
if os.path.isfile(import_path):
try:
with open(import_path, "rb") as fh: file_input = fh.read()
if file_input and len(file_input) == pubsize:
identity_bytes = file_input
print(f"Reticulum Identity imported from {import_path}")
except: pass
if not identity_bytes:
if len(args.import_pub) == pubsize*2:
try:
identity_bytes = bytes.fromhex(args.import_pub)
print("Reticulum Identity imported from hex input")
except: pass
if not identity_bytes:
try:
b32_decoded = base64.b32decode(args.import_pub)
if len(b32_decoded) == pubsize:
identity_bytes = b32_decoded
print("Reticulum Identity imported from base32 input")
except: pass
if not identity_bytes:
try:
b64_decoded = base64.urlsafe_b64decode(args.import_pub)
if len(b64_decoded) == pubsize:
identity_bytes = b64_decoded
print("Reticulum Identity imported from base64 input")
except: pass
if not identity_bytes: print("Could not decode specified data to a valid public Reticulum Identity"); exit(R_INVALID_IDENTITY)
except Exception as e: print("Invalid identity data specified for private identity import: "+str(e)); exit(R_INVALID_IDENTITY)
elif args.import_prv:
try:
identity_bytes = None
import_path = os.path.expanduser(args.import_prv)
if os.path.isfile(import_path):
try:
with open(import_path, "rb") as fh: file_input = fh.read()
if file_input and len(file_input) == prvsize:
identity_bytes = file_input
print(f"Reticulum Identity imported from {import_path}")
except: pass
if not identity_bytes:
if len(args.import_prv) == prvsize*2:
try:
identity_bytes = bytes.fromhex(args.import_prv)
print("Reticulum Identity imported from hex input")
except: pass
if not identity_bytes:
try:
b32_decoded = base64.b32decode(args.import_prv)
if len(b32_decoded) == prvsize:
identity_bytes = b32_decoded
print("Reticulum Identity imported from base32 input")
except: pass
if not identity_bytes:
try:
b64_decoded = base64.urlsafe_b64decode(args.import_prv)
if len(b64_decoded) == prvsize:
identity_bytes = b64_decoded
print("Reticulum Identity imported from base64 input")
except: pass
if not identity_bytes: print("Could not decode specified data to a valid private Reticulum Identity"); exit(R_INVALID_IDENTITY)
except Exception as e: print("Invalid identity data specified for private identity import: "+str(e)); exit(R_INVALID_IDENTITY)
if args.import_prv:
try: identity = RNS.Identity.from_bytes(identity_bytes)
except Exception as e: print("Could not create Reticulum identity from specified data: "+str(e)); exit(R_INVALID_IDENTITY)
elif args.import_pub:
try:
identity = RNS.Identity(create_keys=False)
identity.load_public_key(identity_bytes)
except Exception as e: print("Could not create Reticulum identity from specified data: "+str(e)); exit(R_INVALID_IDENTITY)
return identity
######################
# Network Operations #
######################
def announce(args, identity):
try:
ensure_reticulum(args)
aspects = args.announce.split(".")
if not len(aspects) > 1: print("Invalid destination aspects specified"); exit(R_INVALID_ASPECTS)
else:
app_name = aspects[0]; aspects = aspects[1:]
if not identity.get_private_key(): print("Cannot announce this destination, since the private key is not held"); exit(R_NO_PRVKEY)
else:
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, app_name, *aspects)
print(f"Announcing {args.announce} destination {RNS.prettyhexrep(destination.hash)} for identity {identity}")
destination.announce(); time.sleep(0.25)
except Exception as e: print(f"An error ocurred while attempting to send the announce: {e}"); exit(R_UNKNOWN_ERROR)
########################################
# Canonical RSG Manipulation Functions #
########################################
def get_rsg_data(rsg):
rsg_data = None
if type(rsg) == bytes: rsg_data = rsg
elif type(rsg) == io.BufferedReader: rsg_data = rsg.read()
elif type(rsg) == str:
try: rsg_data = base64.urlsafe_b64decode(rsg)
except: pass
try: rsg_data = base64.b32decode(rsg.strip(RSG_PADDING))
except: pass
try: rsg_data = bytes.fromhex(rsg.strip(RSG_PADDING))
except: pass
try: rsg_data = RNS.b256_to_bytes(rsg.strip(RSG_PADDING.decode("utf-8")))
except: pass
return rsg_data
def extract_signed_rsg_data(rsg):
siglen = RNS.Identity.SIGLENGTH//8
rsg_data = get_rsg_data(rsg)
envelope = rsg_data[siglen:]
try: return mp.unpackb(envelope)
except: return None
def get_rsg_hash(message):
sha = None
if type(message) == bytes: sha = sha256(message)
elif type(message) == str: sha = sha256(message.encode("utf-8"))
elif type(message) == io.BufferedReader: sha = file_sha256(message)
else: raise TypeError(f"Invalid input type {type(message)} for rsg creation")
if not sha: raise ValueError("Hash calculation for rsg signature input failed")
return sha
def rsg_is_legacy_format(rsg):
rsg_data = get_rsg_data(rsg)
if not rsg_data: return False
return True if len(rsg_data) == RNS.Identity.SIGLENGTH//8 else False
def validate_rsg(rsg, message=None, required_signer=None):
if not message: raise ValueError(f"No message specified for rsg validation")
if not type(required_signer) in [RNS.Identity, bytes, type(None)]: raise TypeError(f"Invalid required signer type {type(required_signer)}")
if type(required_signer) == RNS.Identity: required_signer_hash = required_signer.hash
elif type(required_signer) == bytes: required_signer_hash = required_signer
else: required_signer_hash = None
siglen = RNS.Identity.SIGLENGTH//8
rsg_data = get_rsg_data(rsg)
rsg_hash = get_rsg_hash(message)
if len(rsg_data) == siglen: raise ValueError(f"Cannot validate legacy rsg format")
if not rsg_data: return False, None, None
else:
if len(rsg_data) < siglen+1: return False, None, None
else:
signing_identity = None
signature = rsg_data[:siglen]
envelope = rsg_data[siglen:]
try: signed_data = mp.unpackb(envelope)
except: return False, None, None
if not "hashtype" in signed_data or not "hash" in signed_data: return False, None, None
if not signed_data["hashtype"] in RSG_HASHTYPES: return False, None, None
if not "meta" in signed_data: return False, None, None
if not "signer" in signed_data["meta"]: return False, None, None
if not "pubkey" in signed_data["meta"]: return False, None, None
if not "note" in signed_data["meta"]: return False, None, None
try:
if type(required_signer) == RNS.Identity:
signing_identity = required_signer
else:
signing_identity = RNS.Identity(create_keys=False)
signing_identity.load_public_key(signed_data["meta"]["pubkey"])
except: return False, None, None
if required_signer_hash == None: required_signer_hash = signing_identity.hash
if not signing_identity: return False, None, None
if not signing_identity.hash == required_signer_hash: return False, None, signing_identity
if signed_data["hash"] != rsg_hash: return False, None, signing_identity
else:
if not signing_identity.validate(signature, envelope): return False, signed_data, signing_identity
else: return True, signed_data, signing_identity
return False, signed_data, signing_identity
def create_rsg(signer_identity, message, embed=False, note=None, meta=None, output="bin"):
if not output in ["bin", "hex", "base32", "base256", "base64"]: raise TypeError(f"Invalid output format for rsg creation")
if not type(signer_identity) == RNS.Identity: raise TypeError(f"{signer_identity} is not a Reticulum Identity")
if not signer_identity.get_private_key(): raise ValueError(f"{signer_identity} does not hold a private key")
signed_data = { "hashtype": "sha256", "hash": get_rsg_hash(message),
"meta": { "signer": signer_identity.hash,
"pubkey": signer_identity.get_public_key(),
"note" : note } }
if embed:
if type(message) == str: message = message.encode("utf-8")
signed_data["message"] = message
if meta and type(meta) == dict:
for key in meta:
if not key in signed_data["meta"]: signed_data["meta"]["key"] = meta["key"]
envelope = mp.packb(signed_data)
signature = signer_identity.sign(envelope)
rsg_data = signature+envelope
if output == "bin": rsg = rsg_data
elif output == "hex": rsg = RNS.hexrep(rsg_data, delimit=False).encode("ascii")
elif output == "base32": rsg = base64.b32encode(rsg_data)
elif output == "base64": rsg = base64.urlsafe_b64encode(rsg_data)
elif output == "base256": rsg = RNS.b256rep(rsg_data)
else: return None
return rsg
RSG_ASCII_HEADER = b"#### Start of rsg data "
RSG_ASCII_FOOTER = b" End of rsg data ####"
RSG_ASCII_ROW_WIDTH = 64
RSG_PADDING = b"="
def wrap_rsg(rsg):
if type(rsg) == str: return wrap_rsg_str(rsg)
def pad(chunk): return chunk+(RSG_ASCII_ROW_WIDTH-len(chunk))*RSG_PADDING
header = RSG_ASCII_HEADER+b"#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_HEADER))
footer = b"#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_FOOTER))+RSG_ASCII_FOOTER
wrapped = header+b"\n"
read = 0
while len(rsg):
chunk = rsg[:RSG_ASCII_ROW_WIDTH]
if len(chunk) < RSG_ASCII_ROW_WIDTH: chunk = pad(chunk)
wrapped += chunk+b"\n"; read += len(chunk)
rsg = rsg[len(chunk):]
wrapped += footer
return wrapped.decode("ascii")
def wrap_rsg_str(rsg):
def pad(chunk): return chunk+(RSG_ASCII_ROW_WIDTH-len(chunk))*RSG_PADDING.decode("utf-8")
header = RSG_ASCII_HEADER.decode("utf-8")+"#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_HEADER.decode("utf-8")))
footer = "#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_FOOTER.decode("utf-8")))+RSG_ASCII_FOOTER.decode("utf-8")
wrapped = header+"\n"
read = 0
while len(rsg):
chunk = rsg[:RSG_ASCII_ROW_WIDTH]
if len(chunk) < RSG_ASCII_ROW_WIDTH: chunk = pad(chunk)
wrapped += chunk+"\n"; read += len(chunk)
rsg = rsg[len(chunk):]
wrapped += footer
return wrapped
def unwrap_rsg(wrapped_rsg):
unwrapped = ""
if type(wrapped_rsg) == bytes: wrapped_rsg = wrapped_rsg.decode("ascii")
elif type(wrapped_rsg) == str: pass
else: return None
for line in wrapped_rsg.splitlines():
if not line.strip(): continue
if line.startswith("#"): continue
unwrapped += line
return unwrapped if unwrapped else None
###################################
# Signing & Validation Operations #
###################################
def validate(args, identity, __recursive=False):
if type(args.validate) == list:
paths = args.validate.copy()
validated = 0
for path in paths:
args.validate = path
code = validate(args, identity, __recursive=True)
if code != 0: print(f"Sequence error on recursive signature validation"); exit(R_SEQUENCE_ERROR)
else: validated += 1
if len(paths) != validated: print(f"Sequence error on recursive signature validation"); exit(R_SEQUENCE_ERROR)
else: exit(R_OK)
msg_ext = f".{MSG_EXT}"
sig_ext = f".{SIG_EXT}"
validate_path = os.path.expanduser(args.validate)
path_is_msgfile = validate_path.lower().endswith(msg_ext)
path_is_sigfile = validate_path.lower().endswith(sig_ext)
if path_is_sigfile: signature_path = validate_path; file_path = validate_path[:-len(sig_ext)]
else: signature_path = f"{validate_path}{sig_ext}"; file_path = validate_path
signature_exists = os.path.isfile(signature_path)
file_exists = os.path.isfile(file_path)
if path_is_msgfile: return validate_message(args, identity, __recursive=__recursive)
if not file_exists: print(f"The validation target \"{file_path}\" does not exist"); exit(R_NO_FILE)
if not signature_exists: print(f"No signature file exists for \"{file_path}\""); exit(R_NO_FILE)
try:
with open(signature_path, "rb") as fh: is_legacy_format = rsg_is_legacy_format(fh)
except Exception as e: print(f"Could not detect rsg format: {e}"); exit(R_UNKNOWN_ERROR)
if not is_legacy_format:
try:
with open(signature_path, "rb") as fh: rsg = fh.read()
except Exception as e: print(f"Could not read rsg: {e}"); exit(R_READ_ERROR)
if type(identity) == str:
if not len(identity) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2: print("Invalid identity hash length"); exit(R_INVALID_IDENTITY)
try: identity = bytes.fromhex(identity)
except Exception as e: print(f"Invalid identity hash: {e}"); exit(R_INVALID_IDENTITY)
try:
with open(file_path, "rb") as fh:
try:
valid, signed_data, signing_identity = validate_rsg(rsg, message=fh, required_signer=identity)
identity_str = RNS.prettyhexrep(identity) if type(identity) == bytes else f"{identity}"
signer_description = f"\nThis file was NOT signed by {identity_str or signing_identity}" if identity else ""
if not valid: print(f"Invalid signature {signature_path} for file {file_path}{signer_description}"); exit(R_INVALID_SIGNATURE)
else: print(f"Signature is valid, the file {file_path} was signed by {signing_identity}"); return exit(R_OK) if not __recursive else R_OK
except Exception as e: print(f"Error while validating {signature_path}: {e}"); exit(R_UNKNOWN_ERROR)
except Exception as e: print(f"Could not read {file_path}: {e}"); exit(R_READ_ERROR)
else:
if type(identity) != RNS.Identity: print(f"Cannot validate legacy rsg signatures without an explicit required identity"); exit(R_NO_IDENTITY)
try:
with open(signature_path, "rb") as fh: signature = fh.read()
except Exception as e: print(f"Could not read signature: {e}"); exit(R_READ_ERROR)
try:
with open(file_path, "rb") as fh: valid = identity.validate(signature, fh.read())
if not valid: print(f"Invalid signature {signature_path} for file {file_path}\nThis file was NOT signed by {identity}"); exit(R_INVALID_SIGNATURE)
else: print(f"Signature is valid, the file {file_path} was signed by {identity}"); exit(R_OK)
except Exception as e: print(f"Could not validate signature: {e}"); exit(R_READ_ERROR)
def validate_message(args, identity, __recursive=False):
msg_ext = f".{MSG_EXT}"
validate_path = os.path.expanduser(args.validate)
path_is_msgfile = validate_path.lower().endswith(msg_ext)
if path_is_msgfile: signature_path = validate_path
signature_exists = os.path.isfile(signature_path)
if not signature_exists: print(f"The signature file \"{signature_path}\" does not exist"); exit(R_NO_FILE)
try:
with open(signature_path, "rb") as fh: rsg = fh.read()
except Exception as e: print(f"Could not read rsg: {e}"); exit(R_READ_ERROR)
if type(identity) == str:
if not len(identity) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2: print("Invalid identity hash length"); exit(R_INVALID_IDENTITY)
try: identity = bytes.fromhex(identity)
except Exception as e: print(f"Invalid identity hash: {e}"); exit(R_INVALID_IDENTITY)
try:
rsm_contents = extract_signed_rsg_data(rsg)
if not "message" in rsm_contents: print(f"No embedded message in {signature_path}"); exit(R_INVALID_SIGNATURE)
valid, signed_data, signing_identity = validate_rsg(rsg, message=rsm_contents["message"], required_signer=identity)
identity_str = RNS.prettyhexrep(identity) if type(identity) == bytes else f"{identity}"
signer_description = f"\nThe message was NOT signed by {identity_str or signing_identity}" if identity else ""
if not valid: print(f"Invalid signature in {signature_path}{signer_description}"); exit(R_INVALID_SIGNATURE)
else:
print(f"\nSignature is valid, the following message was signed by {signing_identity}:\n")
print(signed_data["message"].decode("utf-8"))
return exit(R_OK) if not __recursive else R_OK
except Exception as e: print(f"Error while validating {signature_path}: {e}"); exit(R_UNKNOWN_ERROR)
def sign(args, identity, __recursive=False):
if type(args.sign) == list:
paths = args.sign.copy()
signed = 0
for path in paths:
args.sign = path
code = sign(args, identity, __recursive=True)
if code != 0: print(f"Sequence error on recursive signature creation"); exit(R_SEQUENCE_ERROR)
else: signed += 1
if len(paths) != signed: print(f"Sequence error on recursive signature creation"); exit(R_SEQUENCE_ERROR)
else: exit(R_OK)
sig_ext = f".{SIG_EXT}"
sign_path = os.path.expanduser(args.sign)
rsg_path = f"{sign_path}{sig_ext}"
file_exists = os.path.isfile(sign_path)
signature_exists = os.path.isfile(rsg_path)
if args.base32: output = "base32"
elif args.base64: output = "base64"
elif args.base256: output = "base256"
elif args.hex: output = "hex"
else: output = "bin"
if not identity.get_private_key(): print(f"Cannot sign \"{sign_path}\", the identity does not hold a private key"); exit(R_NO_PRVKEY)
if not file_exists: print(f"The file \"{sign_path}\" does not exist"); exit(R_NO_FILE)
if output == "bin" and signature_exists and not args.force:
print(f"The signature file \"{rsg_path}\" already exists, not overwriting"); exit(R_FILE_EXISTS)
try:
if args.raw:
with open(sign_path, "rb") as fh: data = fh.read()
with open(rsg_path, "wb") as fh: fh.write(identity.sign(data))
else:
with open(sign_path, "rb") as in_file: rsg = create_rsg(identity, in_file, output=output)
if not rsg: print(f"No signature created, not writing"); exit(R_UNKNOWN_ERROR)
if output == "bin":
with open(rsg_path, "wb") as out_file: out_file.write(rsg)
elif output in ["base32", "base64", "base256", "hex"]: print(f"\n{wrap_rsg(rsg)}\n")
else: print("No valid output format specified"); exit(R_INVALID_ARGS)
print(f"Signed file {sign_path} with {identity}"); return exit(R_OK) if not __recursive else R_OK
except Exception as e: print(f"Could not sign {sign_path}: {e}"); exit(R_UNKNOWN_ERROR)
def sign_message(args, identity):
message = args.sign_message
if args.base32: output = "base32"
elif args.base64: output = "base64"
elif args.base256: output = "base256"
elif args.hex: output = "hex"
else: output = "bin"
if output == "bin" and not args.write: print("No write path specified"); exit(R_INVALID_ARGS)
if not identity.get_private_key(): print(f"Cannot sign \"{sign_path}\", the identity does not hold a private key"); exit(R_NO_PRVKEY)
if message == NO_MESSAGE: message = get_editor_content()
if not message: print("No message specified"); exit(R_INVALID_ARGS)
try:
rsg = create_rsg(identity, message, embed=True, output=output)
if not rsg: print(f"No signature created, not writing"); exit(R_UNKNOWN_ERROR)
if output == "bin":
sig_ext = f".{MSG_EXT}"
rsg_path = os.path.expanduser(args.write)
rsg_path = f"{rsg_path}{sig_ext}" if not rsg_path.endswith(sig_ext) else rsg_path
signature_exists = os.path.isfile(rsg_path)
if signature_exists and not args.force: print(f"The signature file \"{rsg_path}\" already exists, not overwriting"); exit(R_FILE_EXISTS)
with open(rsg_path, "wb") as out_file: out_file.write(rsg)
print(f"Message signed with {identity} saved to {rsg_path}"); exit(R_OK)
elif output in ["base32", "base64", "base256", "hex"]: print(f"\n{wrap_rsg(rsg)}\n")
else: print("No valid output format specified"); exit(R_INVALID_ARGS)
print(f"Message signed with {identity}"); exit(R_OK)
except Exception as e: print(f"Could not sign message: {e}"); exit(R_UNKNOWN_ERROR)
######################################
# Encryption & Decryption Operations #
######################################
def encrypt(args, identity, __recursive=False):
if type(args.encrypt) == list:
paths = args.encrypt.copy()
encrypted = 0
for path in paths:
args.encrypt = path
code = encrypt(args, identity, __recursive=True)
if code != 0: print(f"Sequence error on recursive file encryption"); exit(R_SEQUENCE_ERROR)
else: encrypted += 1
if len(paths) != encrypted: print(f"Sequence error on recursive file encryption"); exit(R_SEQUENCE_ERROR)
else: exit(R_OK)
enc_ext = f".{ENCRYPT_EXT}"
encrypt_path = os.path.expanduser(args.encrypt)
rfe_path = args.write if args.write else f"{encrypt_path}{enc_ext}"
file_exists = os.path.isfile(encrypt_path)
rfe_exists = os.path.isfile(rfe_path)
if not identity: print(f"Cannot encrypt \"{encrypt_path}\", no identity specified"); exit(R_NO_IDENTITY)
if not identity.get_public_key(): print(f"Cannot encrypt \"{encrypt_path}\", the identity does not hold a public key"); exit(R_NO_PUBKEY)
if not file_exists: print(f"The file \"{encrypt_path}\" does not exist"); exit(R_NO_FILE)
if rfe_exists and not args.force:
print(f"The encryption output file \"{rfe_path}\" already exists, not overwriting"); exit(R_FILE_EXISTS)
try:
with open(encrypt_path, "rb") as input_fh:
try:
with open(rfe_path, "wb") as output_fh:
wrote = 0
data_remaining = True
while data_remaining:
chunk = input_fh.read(ENC_CHUNK)
if chunk: wrote += output_fh.write(identity.encrypt(chunk))
else: data_remaining = False
print(f"\rWrote {RNS.prettysize(wrote)} to {rfe_path} ", end="")
except Exception as e: print(f"\nError writing encrypted output to {rfe_path}: {e}"); exit(R_WRITE_ERROR)
except Exception as e: print(f"\nError reading {encrypt_path} for encryption: {e}"); exit(R_WRITE_ERROR)
print(f"\nFile {encrypt_path} encrypted for {identity} to {rfe_path}"); return exit(R_OK) if not __recursive else R_OK
def decrypt(args, identity, __recursive=False):
if type(args.decrypt) == list:
paths = args.decrypt.copy()
decrypted = 0
for path in paths:
args.decrypt = path
code = decrypt(args, identity, __recursive=True)
if code != 0: print(f"Sequence error on recursive file decryption"); exit(R_SEQUENCE_ERROR)
else: decrypted += 1
if len(paths) != decrypted: print(f"Sequence error on recursive file decryption"); exit(R_SEQUENCE_ERROR)
else: exit(R_OK)
enc_ext = f".{ENCRYPT_EXT}"
rfe_path = os.path.expanduser(args.decrypt)
if not rfe_path.endswith(enc_ext): print(f"The file {rfe_path} does not appear to be a Reticulum encrypted file"); exit(R_INVALID_FILE)
decrypt_path = os.path.expanduser(args.write) if args.write else f"{rfe_path[:-len(enc_ext)]}"
if not decrypt_path: print(f"Invalid output filename"); exit(R_INVALID_FILE)
rfe_exists = os.path.isfile(rfe_path)
file_exists = os.path.isfile(decrypt_path)
if not identity: print(f"Cannot decrypt \"{rfe_path}\", no identity specified"); exit(R_NO_IDENTITY)
if not identity.get_private_key(): print(f"Cannot decrypt \"{rfe_path}\", the identity does not hold a private key"); exit(R_NO_PRVKEY)
if not rfe_exists: print(f"The file \"{rfe_path}\" does not exist"); exit(R_NO_FILE)
if file_exists and not args.force:
print(f"The decryption output file \"{decrypt_path}\" already exists, not overwriting"); exit(R_FILE_EXISTS)
try:
with open(rfe_path, "rb") as input_fh:
try:
with open(decrypt_path, "wb") as output_fh:
data_remaining = True
wrote = 0
while data_remaining:
chunk = input_fh.read(DEC_CHUNK)
if chunk:
decrypted = identity.decrypt(chunk)
if not decrypted: print(f"The provided identity could not decrypt the file"); exit(R_DECRYPT_FAILED)
else: wrote += output_fh.write(decrypted)
print(f"\rWrote {RNS.prettysize(wrote)} to {decrypt_path} ", end="")
else: data_remaining = False
except Exception as e: print(f"\nError writing decrypted output to {decrypt_path}: {e}"); exit(R_WRITE_ERROR)
except Exception as e: print(f"\nError reading {rfe_path} for decryption: {e}"); exit(R_WRITE_ERROR)
print(f"\nFile {rfe_path} decrypted to {decrypt_path}"); return exit(R_OK) if not __recursive else R_OK
################
# File Output #
################
def write_identity(args, identity):
try:
wp = os.path.expanduser(args.write)
args.write = False
if identity.get_private_key() and args.export_prv:
if not os.path.isfile(wp) or args.force:
identity.to_file(wp)
print("Wrote private identity to "+str(wp))
else: print("File "+str(wp)+" already exists, not overwriting"); exit(R_FILE_EXISTS)
elif identity.get_public_key():
if not wp.lower().endswith(f".{PUB_EXT}"): wp += f".{PUB_EXT}"
if not os.path.isfile(wp) or args.force:
identity.pub_to_file(wp)
print("Wrote public identity to "+str(wp))
else: print("File "+str(wp)+" already exists, not overwriting"); exit(R_FILE_EXISTS)
else: print("Identity holds neither a public nor private key"); exit(R_NO_KEYS)
except Exception as e: print("Error while writing imported identity to file: "+str(e)); exit(R_WRITE_ERROR)
###################
# Terminal Output #
###################
def print_identity_information(args, identity):
print("Identity Hash : "+RNS.prettyhexrep(identity.hash))
if args.base64: print("Public Key : "+base64.urlsafe_b64encode(identity.get_public_key()).decode("utf-8"))
elif args.base32: print("Public Key : "+base64.b32encode(identity.get_public_key()).decode("utf-8"))
else: print("Public Key : "+RNS.hexrep(identity.get_public_key(), delimit=False))
if identity.prv:
if args.print_private:
if args.base64: print("Private Key : "+base64.urlsafe_b64encode(identity.get_private_key()).decode("utf-8"))
elif args.base32: print("Private Key : "+base64.b32encode(identity.get_private_key()).decode("utf-8"))
else: print("Private Key : "+RNS.hexrep(identity.get_private_key(), delimit=False))
else: print("Private Key : Hidden")
def print_hash_information(args, identity):
try:
hashlen = RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2
if not type(identity) in [RNS.Identity, str]: print("Invalid identity"); exit(R_INVALID_IDENTITY)
if type(identity) == str and len(identity) != hashlen: print("Invalid identity hash length"); exit(R_INVALID_IDENTITY)
aspects = args.hash.split(".")
if not len(aspects) > 0: print("Invalid destination aspects specified"); exit(R_INVALID_ASPECTS)
else:
app_name = aspects[0]; aspects = aspects[1:]
if type(identity) == RNS.Identity and not identity.get_public_key(): print("Identity does not hold a public key"); exit(R_NO_PUBKEY)
else:
if type(identity) == RNS.Identity:
identity_hash = identity.hash
destination = RNS.Destination(identity, RNS.Destination.OUT, RNS.Destination.SINGLE, app_name, *aspects)
elif type(identity) == str:
destination = None
try: identity_hash = bytes.fromhex(identity)
except Exception as e: print(f"Invalid identity: {e}"); exit(R_INVALID_IDENTITY)
else: print("Invalid identity"); exit(R_INVALID_IDENTITY)
destination_hash = RNS.Destination.hash_from_name_and_identity(args.hash, identity_hash)
print(f"The {args.hash} destination for this Identity is {RNS.prettyhexrep(destination_hash)}")
if destination: print("The full destination specifier is "+str(destination))
except Exception as e: print(f"An error ocurred while attempting to get hash information: {e}"); exit(R_UNKNOWN_ERROR)
def export_pub_identity(args, identity):
k = identity.get_public_key()
if not k: print("Identity doesn't hold a public key, cannot export"); exit(R_NO_PUBKEY)
else:
if args.base64: print("Public Identity Keys : "+base64.urlsafe_b64encode(k).decode("utf-8"))
elif args.base32: print("Public Identity Keys : "+base64.b32encode(k).decode("utf-8"))
else: print("Public Identity Keys : "+RNS.hexrep(k, delimit=False))
def export_prv_identity(args, identity):
k = identity.get_private_key()
if not k: print("Identity doesn't hold a private key, cannot export"); exit(R_NO_PRVKEY)
else:
if args.base64: print("Private Identity Keys : "+base64.urlsafe_b64encode(k).decode("utf-8"))
elif args.base32: print("Private Identity Keys : "+base64.b32encode(k).decode("utf-8"))
else: print("Private Identity Keys : "+RNS.hexrep(k, delimit=False))
##############################
# Helper & Utility Functions #
##############################
def get_editor_content():
import subprocess
from tempfile import NamedTemporaryFile
template = ""
editor = os.environ.get("EDITOR", "")
if not editor:
for fallback in ["nano", "vim", "vi"]:
try:
subprocess.run(["which", fallback], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
editor = fallback
break
except subprocess.CalledProcessError: continue
if not editor: print("Could not launch editor"); exit(R_READ_ERROR);
try:
with NamedTemporaryFile(mode="w+", suffix=".tmp", delete=False) as tmp:
tmp_path = tmp.name
tmp.write(template)
result = subprocess.run([editor, tmp_path])
if result.returncode != 0: print(f"Editor exited with error code {result.returncode}"); os.unlink(tmp_path); exit(R_READ_ERROR)
with open(tmp_path, "r") as f: content = f.read()
os.unlink(tmp_path)
return content.encode("utf-8")
except Exception as e: print(f"Could not get content from editor: {e}"); exit(R_READ_ERROR)
def spin(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():
time.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
if __name__ == "__main__":
main()
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import RNS
import argparse
import time
from RNS._version import __version__
def program_setup(configdir, verbosity = 0, quietness = 0, service = False):
targetverbosity = verbosity-quietness
if service:
targetlogdest = RNS.LOG_FILE
targetverbosity = None
else:
targetlogdest = RNS.LOG_STDOUT
reticulum = RNS.Reticulum(configdir=configdir, verbosity=targetverbosity, logdest=targetlogdest)
exit(0)
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum Distributed Identity Resolver")
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument('-v', '--verbose', action='count', default=0)
parser.add_argument('-q', '--quiet', action='count', default=0)
parser.add_argument("--exampleconfig", action='store_true', default=False, help="print verbose configuration example to stdout and exit")
parser.add_argument("--version", action="version", version="rnir {version}".format(version=__version__))
args = parser.parse_args()
if args.exampleconfig:
print(__example_rns_config__)
exit()
if args.config:
configarg = args.config
else:
configarg = None
program_setup(configdir = configarg, verbosity=args.verbose, quietness=args.quiet)
except KeyboardInterrupt:
print("")
exit()
if __name__ == "__main__":
main()
Regular → Executable
+2575 -433
View File
File diff suppressed because one or more lines are too long
+425 -214
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -23,154 +31,419 @@
# SOFTWARE.
import RNS
import os
import sys
import time
import argparse
from RNS._version import __version__
remote_link = None
output_rst_str = "\r \r"
def connect_remote(destination_hash, auth_identity, timeout, no_output = False, purpose="management"):
global remote_link, reticulum
if not RNS.Transport.has_path(destination_hash):
if not no_output:
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested", end=" ")
sys.stdout.flush()
RNS.Transport.request_path(destination_hash)
pr_time = time.time()
while not RNS.Transport.has_path(destination_hash):
time.sleep(0.1)
if time.time() - pr_time > timeout:
if not no_output:
print(output_rst_str, end="")
print("Path request timed out")
exit(12)
def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity, timeout, drop_queues):
if table:
remote_identity = RNS.Identity.recall(destination_hash)
def remote_link_closed(link):
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")
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
if not no_output:
print(output_rst_str, end="")
print("The link was closed by the server, exiting now")
else:
if not no_output:
print(output_rst_str, end="")
print("Link closed unexpectedly, exiting now")
exit(10)
def remote_link_established(link):
global remote_link
if purpose == "management": link.identify(auth_identity)
remote_link = link
if not no_output:
print(output_rst_str, end="")
print("Establishing link with remote transport instance...", end=" ")
sys.stdout.flush()
if purpose == "management": remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
elif purpose == "blackhole": remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "info", "blackhole")
link = RNS.Link(remote_destination)
link.set_link_established_callback(remote_link_established)
link.set_link_closed_callback(remote_link_closed)
def parse_hash(input_str):
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(input_str) != dest_len: raise ValueError("Hash length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try:
hash_bytes = bytes.fromhex(input_str)
return hash_bytes
except Exception as e: raise ValueError("Invalid hash entered. Check your input.")
def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity, timeout, drop_queues,
drop_via, max_hops, remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT,
blackholed=False, blackhole=False, unblackhole=False, blackhole_duration=None, blackhole_reason=None,
remote_blackhole_list=False, remote_blackhole_list_filter=None, no_output=False, json=False):
global remote_link, reticulum
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
if remote:
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(remote) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try:
identity_hash = bytes.fromhex(remote)
remote_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.remote.management", identity_hash)
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
identity = RNS.Identity.from_file(os.path.expanduser(management_identity))
if identity == None: raise ValueError("Could not load management identity from "+str(management_identity))
try: connect_remote(remote_hash, identity, remote_timeout, no_output)
except Exception as e: raise e
except Exception as e:
print(str(e))
exit(20)
while remote_link == None: time.sleep(0.1)
if blackholed or remote_blackhole_list:
blackholed_list = None
if blackholed:
if remote_link:
if not no_output:
print(output_rst_str, end="")
print("Listing blackholed identities on remote instances not yet implemented")
exit(255)
try: blackholed_list = reticulum.get_blackholed_identities()
except Exception as e:
print(f"Could not get blackholed identities from RNS instance: {e}")
exit(20)
elif remote_blackhole_list:
try: identity_hash = parse_hash(destination_hexhash)
except Exception as e:
print(f"{e}")
exit(20)
remote_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.info.blackhole", identity_hash)
connect_remote(remote_hash, None, remote_timeout, no_output, purpose="blackhole")
while remote_link == None: time.sleep(0.1)
if not no_output:
print(output_rst_str, end="")
print("Sending request...", end=" ")
sys.stdout.flush()
receipt = remote_link.request("/list")
while not receipt.concluded(): time.sleep(0.1)
response = receipt.get_response()
if type(response) == dict:
blackholed_list = response
print(output_rst_str, end="")
else:
if not no_output:
print(output_rst_str, end="")
print("The remote request failed.")
exit(10)
else:
print(f"Nowhere to fetch blackhole list from")
exit(255)
if not blackholed_list:
print("No blackholed identity data available")
exit(20)
else:
rmlen = 64
def trunc(input_str):
if len(input_str) <= rmlen: return input_str
else: return f"{input_str[:rmlen-1]}"
try:
now = time.time()
for identity_hash in blackholed_list:
until = blackholed_list[identity_hash]["until"]
reason = blackholed_list[identity_hash]["reason"]
source = blackholed_list[identity_hash]["source"]
until_str = f"for {RNS.prettytime(max(0, until-now))}" if until else "indefinitely"
reason_str = f" ({trunc(reason)})" if reason else ""
by_str = f" by {RNS.prettyhexrep(source)}" if source != RNS.Transport.identity.hash else ""
filter_str = f"{RNS.prettyhexrep(identity_hash)} {until_str} {reason_str} {by_str}"
if not remote_blackhole_list:
if destination_hexhash and not destination_hexhash in filter_str: continue
else:
if remote_blackhole_list_filter and not remote_blackhole_list_filter in filter_str: continue
print(f"{RNS.prettyhexrep(identity_hash)} blackholed {until_str}{reason_str}{by_str}")
except Exception as e:
print(f"Error while displaying collected blackhole data: {e}")
exit(20)
elif blackhole:
if remote_link:
if not no_output:
print(output_rst_str, end="")
print("Blackholing identity on remote instances not yet implemented")
exit(255)
try:
identity_hash = parse_hash(destination_hexhash)
until = time.time()+blackhole_duration*60*60 if blackhole_duration else None
result = reticulum.blackhole_identity(identity_hash, until=until, reason=blackhole_reason)
if result == True: print(f"Blackholed identity {destination_hexhash}")
elif result == None: print(f"Identity {destination_hexhash} already blackholed")
else: print(f"Could not blackhole identity {destination_hexhash}")
except Exception as e:
print(f"Could not blackhole identity: {e}")
exit(20)
elif unblackhole:
if remote_link:
if not no_output:
print(output_rst_str, end="")
print("Blackholing identity on remote instances not yet implemented")
exit(255)
try:
identity_hash = parse_hash(destination_hexhash)
result = reticulum.unblackhole_identity(identity_hash)
if result == True: print(f"Lifted blackhole for identity {destination_hexhash}")
elif result == None: print(f"Identity {destination_hexhash} not blackholed")
else: print(f"Could not unblackhole identity {destination_hexhash}")
except Exception as e:
print(f"Could not unblackhole identity: {e}")
exit(20)
elif table:
destination_hash = None
if destination_hexhash != None:
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try:
destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
if len(destination_hexhash) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try: destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
sys.exit(1)
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
table = sorted(reticulum.get_path_table(), key=lambda e: (e["interface"], e["hops"]) )
if not remote_link: table = sorted(reticulum.get_path_table(max_hops=max_hops), key=lambda e: (e["interface"], e["hops"]) )
else:
if not no_output:
print(output_rst_str, end="")
print("Sending request...", end=" ")
sys.stdout.flush()
receipt = remote_link.request("/path", data = ["table", destination_hash, max_hops])
while not receipt.concluded(): time.sleep(0.1)
response = receipt.get_response()
if response:
table = response
print(output_rst_str, end="")
else:
if not no_output:
print(output_rst_str, end="")
print("The remote request failed. Likely authentication failure.")
exit(10)
displayed = 0
for path in table:
if destination_hash == None or destination_hash == path["hash"]:
displayed += 1
exp_str = RNS.timestamp_str(path["expires"])
if path["hops"] == 1:
m_str = " "
else:
m_str = "s"
print(RNS.prettyhexrep(path["hash"])+" is "+str(path["hops"])+" hop"+m_str+" away via "+RNS.prettyhexrep(path["via"])+" on "+path["interface"]+" expires "+RNS.timestamp_str(path["expires"]))
if json:
import json
for p in table:
for k in p:
if isinstance(p[k], bytes): p[k] = RNS.hexrep(p[k], delimit=False)
if destination_hash != None and displayed == 0:
print("No path known")
sys.exit(1)
print(json.dumps(table))
exit()
else:
for path in table:
if destination_hash == None or destination_hash == path["hash"]:
displayed += 1
exp_str = RNS.timestamp_str(path["expires"])
if path["hops"] == 1: m_str = " "
else: m_str = "s"
print(RNS.prettyhexrep(path["hash"])+" is "+str(path["hops"])+" hop"+m_str+" away via "+RNS.prettyhexrep(path["via"])+" on "+path["interface"]+" expires "+RNS.timestamp_str(path["expires"]))
if destination_hash != None and displayed == 0:
print("No path known")
sys.exit(1)
elif rates:
destination_hash = None
if destination_hexhash != None:
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try:
destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
if len(destination_hexhash) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try: destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
sys.exit(1)
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
table = sorted(reticulum.get_rate_table(), key=lambda e: e["last"] )
if len(table) == 0:
print("No information available")
if not remote_link: table = reticulum.get_rate_table()
else:
displayed = 0
for entry in table:
if destination_hash == None or destination_hash == entry["hash"]:
displayed += 1
try:
last_str = pretty_date(int(entry["last"]))
start_ts = entry["timestamps"][0]
span = max(time.time() - start_ts, 3600.0)
span_hours = span/3600.0
span_str = pretty_date(int(entry["timestamps"][0]))
hour_rate = round(len(entry["timestamps"])/span_hours, 3)
if hour_rate-int(hour_rate) == 0:
hour_rate = int(hour_rate)
if entry["rate_violations"] > 0:
if entry["rate_violations"] == 1:
s_str = ""
if not no_output:
print(output_rst_str, end="")
print("Sending request...", end=" ")
sys.stdout.flush()
receipt = remote_link.request("/path", data = ["rates", destination_hash])
while not receipt.concluded():
time.sleep(0.1)
response = receipt.get_response()
if response:
table = response
print(output_rst_str, end="")
else:
if not no_output:
print(output_rst_str, end="")
print("The remote request failed. Likely authentication failure.")
exit(10)
table = sorted(table, key=lambda e: e["last"])
if json:
import json
for p in table:
for k in p:
if isinstance(p[k], bytes): p[k] = RNS.hexrep(p[k], delimit=False)
print(json.dumps(table))
exit()
else:
if len(table) == 0: print("No information available")
else:
displayed = 0
for entry in table:
if destination_hash == None or destination_hash == entry["hash"]:
displayed += 1
try:
last_str = pretty_date(int(entry["last"]))
start_ts = entry["timestamps"][0]
span = max(time.time() - start_ts, 3600.0)
span_hours = span/3600.0
span_str = pretty_date(int(entry["timestamps"][0]))
hour_rate = round(len(entry["timestamps"])/span_hours, 3)
if hour_rate-int(hour_rate) == 0:
hour_rate = int(hour_rate)
if entry["rate_violations"] > 0:
if entry["rate_violations"] == 1:
s_str = ""
else:
s_str = "s"
rv_str = ", "+str(entry["rate_violations"])+" active rate violation"+s_str
else:
s_str = "s"
rv_str = ""
if entry["blocked_until"] > time.time():
bli = time.time()-(int(entry["blocked_until"])-time.time())
bl_str = ", new announces allowed in "+pretty_date(int(bli))
else:
bl_str = ""
rv_str = ", "+str(entry["rate_violations"])+" active rate violation"+s_str
else:
rv_str = ""
if entry["blocked_until"] > time.time():
bli = time.time()-(int(entry["blocked_until"])-time.time())
bl_str = ", new announces allowed in "+pretty_date(int(bli))
else:
bl_str = ""
print(RNS.prettyhexrep(entry["hash"])+" last heard "+last_str+" ago, "+str(hour_rate)+" announces/hour in the last "+span_str+rv_str+bl_str)
print(RNS.prettyhexrep(entry["hash"])+" last heard "+last_str+" ago, "+str(hour_rate)+" announces/hour in the last "+span_str+rv_str+bl_str)
except Exception as e:
print("Error while processing entry for "+RNS.prettyhexrep(entry["hash"]))
print(str(e))
except Exception as e:
print("Error while processing entry for "+RNS.prettyhexrep(entry["hash"]))
print(str(e))
if destination_hash != None and displayed == 0:
print("No information available")
sys.exit(1)
if destination_hash != None and displayed == 0:
print("No information available")
sys.exit(1)
elif drop_queues:
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
RNS.log("Dropping announce queues on all interfaces...")
if remote_link:
if not no_output:
print(output_rst_str, end="")
print("Dropping announce queues on remote instances not yet implemented")
exit(255)
print("Dropping announce queues on all interfaces...")
reticulum.drop_announce_queues()
elif drop:
if remote_link:
if not no_output:
print(output_rst_str, end="")
print("Dropping path on remote instances not yet implemented")
exit(255)
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try:
destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
if len(destination_hexhash) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try: destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
sys.exit(1)
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
if reticulum.drop_path(destination_hash):
print("Dropped path to "+RNS.prettyhexrep(destination_hash))
if reticulum.drop_path(destination_hash): print("Dropped path to "+RNS.prettyhexrep(destination_hash))
else:
print("Unable to drop path to "+RNS.prettyhexrep(destination_hash)+". Does it exist?")
sys.exit(1)
elif drop_via:
if remote_link:
if not no_output:
print(output_rst_str, end="")
print("Dropping all paths via specific transport instance on remote instances yet not implemented")
exit(255)
else:
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try:
destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
if len(destination_hexhash) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try: destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
sys.exit(1)
if reticulum.drop_all_via(destination_hash): print("Dropped all paths via "+RNS.prettyhexrep(destination_hash))
else:
print("Unable to drop paths via "+RNS.prettyhexrep(destination_hash)+". Does the transport instance exist?")
sys.exit(1)
else:
if remote_link:
if not no_output:
print(output_rst_str, end="")
print("Requesting paths on remote instances not implemented")
exit(255)
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try: destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
sys.exit(1)
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash)
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
@@ -187,111 +460,65 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
if RNS.Transport.has_path(destination_hash):
hops = RNS.Transport.hops_to(destination_hash)
next_hop = RNS.prettyhexrep(reticulum.get_next_hop(destination_hash))
next_hop_interface = reticulum.get_next_hop_if_name(destination_hash)
if hops != 1:
ms = "s"
next_hop_bytes = reticulum.get_next_hop(destination_hash)
if next_hop_bytes == None:
print("\r \rError: Invalid path data returned")
sys.exit(1)
else:
ms = ""
next_hop = RNS.prettyhexrep(next_hop_bytes)
next_hop_interface = reticulum.get_next_hop_if_name(destination_hash)
print("\rPath found, destination "+RNS.prettyhexrep(destination_hash)+" is "+str(hops)+" hop"+ms+" away via "+next_hop+" on "+next_hop_interface)
if hops != 1: ms = "s"
else: ms = ""
print("\rPath found, destination "+RNS.prettyhexrep(destination_hash)+" is "+str(hops)+" hop"+ms+" away via "+next_hop+" on "+next_hop_interface)
else:
print("\r \rPath not found")
sys.exit(1)
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum Path Discovery Utility")
parser.add_argument("--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
parser.add_argument(
"--version",
action="version",
version="rnpath {version}".format(version=__version__)
)
parser.add_argument(
"-t",
"--table",
action="store_true",
help="show all known paths",
default=False
)
parser.add_argument(
"-r",
"--rates",
action="store_true",
help="show announce rate info",
default=False
)
parser.add_argument(
"-d",
"--drop",
action="store_true",
help="remove the path to a destination",
default=False
)
parser.add_argument(
"-D",
"--drop-announces",
action="store_true",
help="drop all queued announces",
default=False
)
parser.add_argument(
"-w",
action="store",
metavar="seconds",
type=float,
help="timeout before giving up",
default=RNS.Transport.PATH_REQUEST_TIMEOUT
)
parser.add_argument(
"destination",
nargs="?",
default=None,
help="hexadecimal hash of the destination",
type=str
)
parser = argparse.ArgumentParser(description="Reticulum Path Management Utility")
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("--version", action="version", version="rnpath {version}".format(version=__version__))
parser.add_argument("-t", "--table", action="store_true", help="show all known paths", default=False)
parser.add_argument("-m", "--max", action="store", metavar="hops", type=int, help="maximum hops to filter path table by", default=None)
parser.add_argument("-r", "--rates", action="store_true", help="show announce rate info", default=False)
parser.add_argument("-d", "--drop", action="store_true", help="remove the path to a destination", default=False)
parser.add_argument("-D", "--drop-announces", action="store_true", help="drop all queued announces", default=False)
parser.add_argument("-x", "--drop-via", action="store_true", help="drop all paths via specified transport instance", default=False)
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
parser.add_argument("-R", action="store", metavar="hash", help="transport identity hash of remote instance to manage", default=None, type=str)
parser.add_argument("-i", action="store", metavar="path", help="path to identity used for remote management", default=None, type=str)
parser.add_argument("-W", action="store", metavar="seconds", type=float, help="timeout before giving up on remote queries", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
parser.add_argument("-b", "--blackholed", action="store_true", help="list blackholed identities", default=False)
parser.add_argument("-B", "--blackhole", action="store_true", help="blackhole identity", default=False)
parser.add_argument("-U", "--unblackhole", action="store_true", help="unblackhole identity", default=False)
parser.add_argument( "--duration", action="store", type=float, help="duration of blackhole enforcement in hours", default=None)
parser.add_argument( "--reason", action="store", type=str, help="reason for blackholing identity", default=None)
parser.add_argument("-p", "--blackholed-list", action="store_true", help="view published blackhole list for remote transport instance", default=False)
parser.add_argument("-j", "--json", action="store_true", help="output in JSON format", default=False)
parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the destination", type=str)
parser.add_argument("list_filter", nargs="?", default=None, help="filter for remote blackhole list view", type=str)
parser.add_argument('-v', '--verbose', action='count', default=0)
args = parser.parse_args()
if args.config:
configarg = args.config
else:
configarg = None
if args.config: configarg = args.config
else: configarg = None
if not args.drop_announces and not args.table and not args.rates and not args.destination:
if not args.drop_announces and not args.table and not args.rates and not args.destination and not args.drop_via and not args.blackholed:
print("")
parser.print_help()
print("")
else:
program_setup(
configdir = configarg,
table = args.table,
rates = args.rates,
drop = args.drop,
destination_hexhash = args.destination,
verbosity = args.verbose,
timeout = args.w,
drop_queues = args.drop_announces,
)
program_setup(configdir = configarg, table = args.table, rates = args.rates, drop = args.drop, destination_hexhash = args.destination,
verbosity = args.verbose, timeout = args.w, drop_queues = args.drop_announces, drop_via = args.drop_via, max_hops = args.max,
remote=args.R, management_identity=args.i, remote_timeout=args.W, blackholed=args.blackholed, blackhole=args.blackhole,
unblackhole=args.unblackhole, blackhole_duration=args.duration, blackhole_reason=args.reason, remote_blackhole_list=args.blackholed_list,
remote_blackhole_list_filter=args.list_filter, json=args.json)
sys.exit(0)
except KeyboardInterrupt:
@@ -301,38 +528,22 @@ def main():
def pretty_date(time=False):
from datetime import datetime
now = datetime.now()
if type(time) is int:
diff = now - datetime.fromtimestamp(time)
elif isinstance(time,datetime):
diff = now - time
elif not time:
diff = now - now
if type(time) is int: diff = now - datetime.fromtimestamp(time)
elif isinstance(time,datetime): diff = now - time
elif not time: diff = now - now
second_diff = diff.seconds
day_diff = diff.days
if day_diff < 0:
return ''
if day_diff < 0: return ''
if day_diff == 0:
if second_diff < 10:
return str(second_diff) + " seconds"
if second_diff < 60:
return str(second_diff) + " seconds"
if second_diff < 120:
return "1 minute"
if second_diff < 3600:
return str(int(second_diff / 60)) + " minutes"
if second_diff < 7200:
return "an hour"
if second_diff < 86400:
return str(int(second_diff / 3600)) + " hours"
if day_diff == 1:
return "1 day"
if day_diff < 7:
return str(day_diff) + " days"
if day_diff < 31:
return str(int(day_diff / 7)) + " weeks"
if day_diff < 365:
return str(int(day_diff / 30)) + " months"
if second_diff < 10: return str(second_diff) + " seconds"
if second_diff < 60: return str(second_diff) + " seconds"
if second_diff < 70: return "1 minute"
if second_diff < 7200: return str(int(second_diff / 60)) + " minutes"
if second_diff < 86400: return str(int(second_diff / 3600)) + " hours"
if day_diff == 1: return "1 day"
if day_diff < 7: return str(day_diff) + " days"
if day_diff < 31: return str(int(day_diff / 7)) + " weeks"
if day_diff < 365: return str(int(day_diff / 30)) + " months"
return str(int(day_diff / 365)) + " years"
if __name__ == "__main__":
main()
if __name__ == "__main__": main()
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
# Reticulum License
#
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import RNS
import argparse
import time
from RNS._version import __version__
def program_setup(configdir, verbosity = 0, quietness = 0, service = False):
targetverbosity = verbosity-quietness
if service:
targetlogdest = RNS.LOG_FILE
targetverbosity = None
else:
targetlogdest = RNS.LOG_STDOUT
reticulum = RNS.Reticulum(configdir=configdir, verbosity=targetverbosity, logdest=targetlogdest)
exit(0)
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum Meta Package Manager")
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument('-v', '--verbose', action='count', default=0)
parser.add_argument('-q', '--quiet', action='count', default=0)
parser.add_argument("--exampleconfig", action='store_true', default=False, help="print verbose configuration example to stdout and exit")
parser.add_argument("--version", action="version", version="rnpkg {version}".format(version=__version__))
args = parser.parse_args()
if args.exampleconfig:
print(__example_rnpkg_config__)
exit()
if args.config: configarg = args.config
else: configarg = None
program_setup(configdir = configarg, verbosity=args.verbose, quietness=args.quiet)
except KeyboardInterrupt:
print("")
exit()
__example_rnpkg_config__ = '''# This is an example package manager configuration file.
'''
if __name__ == "__main__": main()
+124 -86
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -31,8 +39,10 @@ import argparse
from RNS._version import __version__
DEFAULT_PROBE_SIZE = 16
DEFAULT_TIMEOUT = 12
def program_setup(configdir, destination_hexhash, size=DEFAULT_PROBE_SIZE, full_name = None, verbosity = 0):
def program_setup(configdir, destination_hexhash, size=None, full_name = None, verbosity = 0, timeout=None, wait=0, probes=1):
if size == None: size = DEFAULT_PROBE_SIZE
if full_name == None:
print("The full destination name including application name aspects must be specified for the destination")
exit()
@@ -71,14 +81,19 @@ def program_setup(configdir, destination_hexhash, size=DEFAULT_PROBE_SIZE, full_
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
sys.stdout.flush()
_timeout = time.time() + (timeout or DEFAULT_TIMEOUT+reticulum.get_first_hop_timeout(destination_hash))
i = 0
syms = "⢄⢂⢁⡁⡈⡐⡠"
while not RNS.Transport.has_path(destination_hash):
while not RNS.Transport.has_path(destination_hash) and not time.time() > _timeout:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
if time.time() > _timeout:
print("\r \rPath request timed out")
exit(1)
server_identity = RNS.Identity.recall(destination_hash)
request_destination = RNS.Destination(
@@ -89,101 +104,120 @@ def program_setup(configdir, destination_hexhash, size=DEFAULT_PROBE_SIZE, full_
*aspects
)
probe = RNS.Packet(request_destination, os.urandom(size))
receipt = probe.send()
sent = 0
replies = 0
while probes:
if more_output:
more = " via "+RNS.prettyhexrep(reticulum.get_next_hop(destination_hash))+" on "+str(reticulum.get_next_hop_if_name(destination_hash))
else:
more = ""
if sent > 0:
time.sleep(wait)
print("\rSent "+str(size)+" byte probe to "+RNS.prettyhexrep(destination_hash)+more+" ", end=" ")
try:
probe = RNS.Packet(request_destination, os.urandom(size))
probe.pack()
except OSError:
print("Error: Probe packet size of "+str(len(probe.raw))+" bytes exceed MTU of "+str(RNS.Reticulum.MTU)+" bytes")
exit(3)
i = 0
while not receipt.status == RNS.PacketReceipt.DELIVERED:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
receipt = probe.send()
sent += 1
print("\b\b ")
sys.stdout.flush()
if more_output:
nhd = reticulum.get_next_hop(destination_hash)
via_str = " via "+RNS.prettyhexrep(nhd) if nhd != None else ""
if_str = " on "+str(reticulum.get_next_hop_if_name(destination_hash)) if reticulum.get_next_hop_if_name(destination_hash) != "None" else ""
more = via_str+if_str
else:
more = ""
hops = RNS.Transport.hops_to(destination_hash)
if hops != 1:
ms = "s"
else:
ms = ""
print("\rSent probe "+str(sent)+" ("+str(size)+" bytes) to "+RNS.prettyhexrep(destination_hash)+more+" ", end=" ")
rtt = receipt.get_rtt()
if (rtt >= 1):
rtt = round(rtt, 3)
rttstring = str(rtt)+" seconds"
else:
rtt = round(rtt*1000, 3)
rttstring = str(rtt)+" milliseconds"
_timeout = time.time() + (timeout or DEFAULT_TIMEOUT+reticulum.get_first_hop_timeout(destination_hash))
i = 0
while receipt.status == RNS.PacketReceipt.SENT and not time.time() > _timeout:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
reception_stats = ""
if reticulum.is_connected_to_shared_instance:
reception_rssi = reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
reception_snr = reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
if reception_rssi != None:
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
if time.time() > _timeout:
print("\r \rProbe timed out")
if reception_snr != None:
reception_stats += " [SNR "+str(reception_snr)+" dB]"
else:
print("\b\b ")
sys.stdout.flush()
if receipt.status == RNS.PacketReceipt.DELIVERED:
replies += 1
hops = RNS.Transport.hops_to(destination_hash)
if hops != 1:
ms = "s"
else:
ms = ""
rtt = receipt.get_rtt()
if (rtt >= 1):
rtt = round(rtt, 3)
rttstring = str(rtt)+" seconds"
else:
rtt = round(rtt*1000, 3)
rttstring = str(rtt)+" milliseconds"
reception_stats = ""
if reticulum.is_connected_to_shared_instance:
reception_rssi = reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
reception_snr = reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
reception_q = reticulum.get_packet_q(receipt.proof_packet.packet_hash)
if reception_rssi != None:
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
if reception_snr != None:
reception_stats += " [SNR "+str(reception_snr)+" dB]"
if reception_q != None:
reception_stats += " [Link Quality "+str(reception_q)+"%]"
else:
if receipt.proof_packet != None:
if receipt.proof_packet.rssi != None:
reception_stats += " [RSSI "+str(receipt.proof_packet.rssi)+" dBm]"
if receipt.proof_packet.snr != None:
reception_stats += " [SNR "+str(receipt.proof_packet.snr)+" dB]"
print(
"Valid reply from "+
RNS.prettyhexrep(receipt.destination.hash)+
"\nRound-trip time is "+rttstring+
" over "+str(hops)+" hop"+ms+
reception_stats+"\n"
)
else:
print("\r \rProbe timed out")
probes -= 1
loss = round((1-(replies/sent))*100, 2)
print(f"Sent {sent}, received {replies}, packet loss {loss}%")
if loss > 0:
exit(2)
else:
if receipt.proof_packet != None:
if receipt.proof_packet.rssi != None:
reception_stats += " [RSSI "+str(receipt.proof_packet.rssi)+" dBm]"
if receipt.proof_packet.snr != None:
reception_stats += " [SNR "+str(receipt.proof_packet.snr)+" dB]"
print(
"Valid reply received from "+
RNS.prettyhexrep(receipt.destination.hash)+
"\nRound-trip time is "+rttstring+
" over "+str(hops)+" hop"+ms+
reception_stats
)
exit(0)
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum Probe Utility")
parser.add_argument("--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
parser.add_argument(
"--version",
action="version",
version="rnprobe {version}".format(version=__version__)
)
parser.add_argument(
"full_name",
nargs="?",
default=None,
help="full destination name in dotted notation",
type=str
)
parser.add_argument(
"destination_hash",
nargs="?",
default=None,
help="hexadecimal hash of the destination",
type=str
)
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("-s", "--size", action="store", default=None, help="size of probe packet payload in bytes", type=int)
parser.add_argument("-n", "--probes", action="store", default=1, help="number of probes to send", type=int)
parser.add_argument("-t", "--timeout", metavar="seconds", action="store", default=None, help="timeout before giving up", type=float)
parser.add_argument("-w", "--wait", metavar="seconds", action="store", default=0, help="time between each probe", type=float)
parser.add_argument("--version", action="version", version="rnprobe {version}".format(version=__version__))
parser.add_argument("full_name", nargs="?", default=None, help="full destination name in dotted notation", type=str)
parser.add_argument("destination_hash", nargs="?", default=None, help="hexadecimal hash of the destination", type=str)
parser.add_argument('-v', '--verbose', action='count', default=0)
@@ -202,8 +236,12 @@ def main():
program_setup(
configdir = configarg,
destination_hexhash = args.destination_hash,
size = args.size,
full_name = args.full_name,
verbosity = args.verbose
verbosity = args.verbose,
probes = args.probes,
wait = args.wait,
timeout = args.timeout,
)
except KeyboardInterrupt:
+164 -18
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# MIT License
# Reticulum License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2025 Mark Qvist
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -11,8 +11,16 @@
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@@ -29,23 +37,27 @@ import time
from RNS._version import __version__
def program_setup(configdir, verbosity = 0, quietness = 0, service = False):
targetloglevel = 3+verbosity-quietness
def program_setup(configdir, verbosity = 0, quietness = 0, service = False, interactive=False):
targetverbosity = verbosity-quietness
if service:
targetlogdest = RNS.LOG_FILE
targetloglevel = None
targetverbosity = None
else:
targetlogdest = RNS.LOG_STDOUT
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel, logdest=targetlogdest)
reticulum = RNS.Reticulum(configdir=configdir, verbosity=targetverbosity, logdest=targetlogdest)
if reticulum.is_connected_to_shared_instance:
RNS.log("Started rnsd version {version} connected to another shared local instance, this is probably NOT what you want!".format(version=__version__), RNS.LOG_WARNING)
else:
# TODO: Rethink why this was added
# if RNS.Reticulum.get_instance().shared_instance_interface:
# RNS.Reticulum.get_instance().shared_instance_interface.server.daemon_threads = True
RNS.log("Started rnsd version {version}".format(version=__version__), RNS.LOG_NOTICE)
while True:
time.sleep(1)
if interactive: import code; code.interact(local=globals())
else:
while True: time.sleep(1)
def main():
try:
@@ -54,6 +66,7 @@ def main():
parser.add_argument('-v', '--verbose', action='count', default=0)
parser.add_argument('-q', '--quiet', action='count', default=0)
parser.add_argument('-s', '--service', action='store_true', default=False, help="rnsd is running as a service and should log to file")
parser.add_argument('-i', '--interactive', action='store_true', default=False, help="drop into interactive shell after initialisation")
parser.add_argument("--exampleconfig", action='store_true', default=False, help="print verbose configuration example to stdout and exit")
parser.add_argument("--version", action="version", version="rnsd {version}".format(version=__version__))
@@ -68,7 +81,7 @@ def main():
else:
configarg = None
program_setup(configdir = configarg, verbosity=args.verbose, quietness=args.quiet, service=args.service)
program_setup(configdir = configarg, verbosity=args.verbose, quietness=args.quiet, service=args.service, interactive=args.interactive)
except KeyboardInterrupt:
print("")
@@ -87,7 +100,7 @@ __example_rns_config__ = '''# This is an example Reticulum config file.
# always-on. This directive is optional and can be removed
# for brevity.
enable_transport = False
enable_transport = No
# By default, the first program to launch the Reticulum
@@ -104,12 +117,97 @@ share_instance = Yes
# If you want to run multiple *different* shared instances
# on the same system, you will need to specify different
# shared instance ports for each. The defaults are given
# below, and again, these options can be left out if you
# don't need them.
# instance names for each. On platforms supporting domain
# sockets, this can be done with the instance_name option:
instance_name = default
# Some platforms don't support domain sockets, and if that
# is the case, you can isolate different instances by
# specifying a unique set of ports for each:
# shared_instance_port = 37428
# instance_control_port = 37429
# If you want to explicitly use TCP for shared instance
# communication, instead of domain sockets, this is also
# possible, by using the following option:
# shared_instance_type = tcp
# On systems where running instances may not have access
# to the same shared Reticulum configuration directory,
# it is still possible to allow full interactivity for
# running instances, by manually specifying a shared RPC
# key. In almost all cases, this option is not needed, but
# it can be useful on operating systems such as Android.
# The key must be specified as bytes in hexadecimal.
# rpc_key = e5c032d3ec4e64a6aca9927ba8ab73336780f6d71790
# It is possible to allow remote management of Reticulum
# systems using the various built-in utilities, such as
# rnstatus and rnpath. You will need to specify one or
# more Reticulum Identity hashes for authenticating the
# queries from client programs. For this purpose, you can
# use existing identity files, or generate new ones with
# the rnid utility.
# enable_remote_management = yes
# remote_management_allowed = 9fb6d773498fb3feda407ed8ef2c3229, 2d882c5586e548d79b5af27bca1776dc
# For easier management, discovery and configuration of
# networks with many individual transport instances,
# you can specify a network identity to be used across
# a set of instances. If sending interface discovery
# announces, these will all be signed by the specified
# network identity, and other nodes discovering your
# interfaces will be able to identify that they belong
# to the same network, even though they exist on different
# transport nodes.
# network_identity = ~/.reticulum/storage/identity/network
# You can configure whether Reticulum should discover
# available interfaces from other Transport Instances over
# the network. If this option is enabled, Reticulum will
# collect interface information discovered from the network.
# discover_interfaces = No
# If you only want to discover interfaces from specific
# networks, you can provide a list of network identities
# from which to discover interfaces. If this option is not
# provided, interfaces will be discovered from all transport
# instances on all connected networks.
# interface_discovery_sources = 78616ff7c4b8d3886d67d494b440f333, cb127015e13aa6ea1e0a606cdc9123d0
# It is possible to automatically bring up and connect new
# interfaces discovered over the network. This option is
# disabled by default, but allows you to specify a maximum
# number of discovered interfaces to automatically connect.
# Additionally, if this option is enabled, Reticulum will
# also try to autoconnect available auto-discovered inter-
# faces on startup, up to the maximum number specified.
# autoconnect_discovered_interfaces = 0
# To prevent interface discovery spamming, a valid crypto-
# graphic stamp is required per announced interface. You
# can configure the minimum required value to accept as
# valid for discovered interfaces.
# required_discovery_value = 14
shared_instance_port = 37428
instance_control_port = 37429
# You can configure Reticulum to panic and forcibly close
# if an unrecoverable interface error occurs, such as the
@@ -117,7 +215,38 @@ instance_control_port = 37429
# an optional directive, and can be left out for brevity.
# This behaviour is disabled by default.
panic_on_interface_error = No
# panic_on_interface_error = No
# When Transport is enabled, it is possible to allow the
# Transport Instance to respond to probe requests from
# the rnprobe utility. This can be a useful tool to test
# connectivity. When this option is enabled, the probe
# destination will be generated from the Identity of the
# Transport Instance, and printed to the log at startup.
# Optional, and disabled by default.
# respond_to_probes = No
# You can publish your local list of blackholed identities
# for other transport instances to use for automatic,
# network-wide blackhole management.
# publish_blackhole = No
# List of remote transport identities from which to auto-
# matically source lists of blackholed identities.
#
# If you're connecting to a large external network, you
# can use one or more external blackhole list to block
# spammy and excessive announces onto your network. This
# funtionality is especially useful if you're hosting public
# entrypoints or gateways. The list source below provides a
# functional example, but better, more timely maintained
# lists probably exist in the community.
# blackhole_sources = 521c87a83afb8f29e4455e77930b973b
[logging]
@@ -259,6 +388,23 @@ loglevel = 4
# Serial port for the device
port = /dev/ttyUSB0
# It is also possible to use BLE devices
# instead of wired serial ports. The
# target RNode must be paired with the
# host device before connecting. BLE
# devices can be connected by name,
# BLE MAC address or by any available.
# Connect to specific device by name
# port = ble://RNode 3B87
# Or by BLE MAC address
# port = ble://F4:12:73:29:4E:89
# Or connect to the first available,
# paired device
# port = ble://
# Set frequency to 867.2 MHz
frequency = 867200000
+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()

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