Compare commits

...

799 Commits

Author SHA1 Message Date
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
Mark Qvist 4ca32c039d Updated documentation 2022-11-03 12:08:23 +01:00
Mark Qvist 81ec701240 Updated version 2022-11-03 12:05:10 +01:00
Mark Qvist b16d614495 Updated readme 2022-11-03 12:04:54 +01:00
Mark Qvist 5f7e37187f Fixed local firmware cache location for rnodeconf 2022-11-03 12:03:26 +01:00
Mark Qvist 622fd6cf46 Updated docs 2022-11-03 00:45:53 +01:00
Mark Qvist b9d73518dd Improved rnodeconf firmware install 2022-11-03 00:42:46 +01:00
Mark Qvist 17bdf45ac1 Updated documentation 2022-11-02 22:46:47 +01:00
Mark Qvist 36052e2c61 Updated version 2022-11-02 22:34:52 +01:00
Mark Qvist 06d232f889 Added Bluetooth control interface for RNode interfaces on Android 2022-11-02 22:34:07 +01:00
Mark Qvist f9b3c749e0 Improved cleanup on device disconnect 2022-11-02 20:44:09 +01:00
Mark Qvist 63a59753af Implemented Bluetooth support for RNode interfaces on Android. Added Bluetooth/USB multiplexing and Bluetooth manager to interface. 2022-11-02 20:43:46 +01:00
Mark Qvist 20696e7827 Bluetooth support for RNode interfaces on Linux (via standard rfcomm driver) 2022-11-02 20:42:45 +01:00
Mark Qvist 127c9862da Updated manual 2022-11-02 01:31:32 +01:00
Mark Qvist fee9473cac Improved rnodeconf timings 2022-11-02 01:23:23 +01:00
Mark Qvist 5337b72853 Updated manual 2022-11-01 23:54:28 +01:00
Mark Qvist 9bc5d91106 Added rnodeconf to package 2022-11-01 22:40:09 +01:00
Mark Qvist 45ae66e9bf Updated bluetooth control commands for RNode interface 2022-11-01 20:27:41 +01:00
Mark Qvist f03cf34370 Updated documentation 2022-11-01 20:27:11 +01:00
Mark Qvist 47db2a3bd5 Added log output control options 2022-11-01 20:26:55 +01:00
Mark Qvist 40cd961eab Added better teardown handling on RNodeInterfaces 2022-10-30 23:13:44 +01:00
Mark Qvist 34cdd4bf0f Improved RNode error reporting and teardown 2022-10-29 16:41:47 +02:00
Mark Qvist b0ef58e5ca Added support for writing to display framebuffer of connected RNodes 2022-10-29 14:28:53 +02:00
Mark Qvist b6020b5ea8 Updated version 2022-10-29 14:28:06 +02:00
Mark Qvist ee544fcf31 Updated documentation 2022-10-22 01:43:51 +02:00
Mark Qvist 886b0ac0ca Fixed Android interfaces import 2022-10-22 01:38:38 +02:00
Mark Qvist ed4070a3d1 Removed stray import. Fixes #125. 2022-10-22 01:05:08 +02:00
Mark Qvist 6d6568852a Updated docs and manual 2022-10-20 20:15:31 +02:00
Mark Qvist b479e14ca5 Improved handling of Android interfaces in apps without hardware access 2022-10-20 20:10:50 +02:00
Mark Qvist 8fec5cedbe Updated readme 2022-10-20 14:52:11 +02:00
Mark Qvist 9852a3534b Updated manual and documentation 2022-10-20 14:39:49 +02:00
Mark Qvist 81fc920bdf Fixed AutoInterface peering hashes on WiFi devices that employ MAC address randomisation on reconnects and roaming 2022-10-19 11:57:09 +02:00
Mark Qvist 5b1b18e84a Fixed incorrect behaviour in announce processing for instance-local destinations to roaming- or boundary-mode interfaces 2022-10-18 18:24:29 +02:00
Mark Qvist 9c8c143c62 Added logging to announce processing 2022-10-18 17:44:14 +02:00
Mark Qvist db9858d75f Cleanup 2022-10-16 00:11:40 +02:00
Mark Qvist 874405cbdd Fixed missing announce cap on hotplugged interfaces 2022-10-15 23:14:47 +02:00
Mark Qvist 2a3f2b8bdc Updated version 2022-10-15 14:57:57 +02:00
Mark Qvist 9aae06c694 Added Android-specific KISS interface 2022-10-15 14:57:16 +02:00
Mark Qvist 70ffc38c49 Android-specific import 2022-10-15 14:56:23 +02:00
Mark Qvist 73071b0755 Cleanup 2022-10-15 14:41:12 +02:00
Mark Qvist ab697dc583 Android-specific import 2022-10-15 11:39:23 +02:00
Mark Qvist ecc78fa45f Added Android serial interface 2022-10-15 11:36:18 +02:00
Mark Qvist e5309caf48 Added Android serial interface 2022-10-15 11:33:48 +02:00
Mark Qvist 094d2f2079 Cleanup 2022-10-15 11:31:34 +02:00
Mark Qvist 5a917c9dac Updated readme 2022-10-14 15:41:30 +02:00
Mark Qvist 1df0eea0b7 Updated readme 2022-10-14 15:31:17 +02:00
Mark Qvist 718c3577db Updated readme 2022-10-14 15:28:41 +02:00
Mark Qvist 5111c32854 Fixed help text 2022-10-13 23:10:38 +02:00
Mark Qvist 63d4e9a399 Updated readme 2022-10-13 23:10:15 +02:00
Mark Qvist 60773ceb16 Return public identity for registered destinations in Identity.recall() 2022-10-13 20:43:38 +02:00
Mark Qvist 5d6c3dd891 Cleanup 2022-10-12 18:56:30 +02:00
Mark Qvist a564dd2b2d Cleanup 2022-10-12 18:06:21 +02:00
Mark Qvist 16cf1ab1ba Fix debug output 2022-10-12 16:08:48 +02:00
Mark Qvist 47e326c8a9 Import Android-specific RNode interface on Android 2022-10-12 16:08:29 +02:00
Mark Qvist 9a7585cbef Added platform detect function 2022-10-12 16:07:53 +02:00
Mark Qvist 902f7af64d Added platform check 2022-10-12 15:14:42 +02:00
Mark Qvist 004bf27526 Added Android-specific RNode interface. Contains debug code. Not ready yet. Hang in there. 2022-10-12 15:11:02 +02:00
Mark Qvist 9cad90266e Reverted RNode interface to exclude Android-specific logic 2022-10-12 15:00:21 +02:00
Mark Qvist e9de01e10e Added property default 2022-10-12 14:58:00 +02:00
Mark Qvist 372bedcd85 Added support for RNode interfaces on Android 2022-10-11 14:06:42 +02:00
Mark Qvist 1141a3034d Updated documentation 2022-10-07 01:00:15 +02:00
Mark Qvist 3f3276ca45 Updated documentation 2022-10-06 23:32:19 +02:00
Mark Qvist 6e742f7267 Updated documentation 2022-10-06 23:22:30 +02:00
Mark Qvist d3525943c2 Updated version 2022-10-06 23:16:01 +02:00
Mark Qvist cb55189e5c Truncate name_hash to 80 bits. Take all array slices from Identity.NAME_HASH_LENGTH constant. 2022-10-06 23:14:32 +02:00
Mark Qvist 0b98a9bff4 Updated docs and manual 2022-10-06 19:11:05 +02:00
Mark Qvist a8d6e1780a Merge branch 'master' of github.com:markqvist/Reticulum 2022-10-06 17:42:11 +02:00
Mark Qvist cb9840250a Updated docs and manual 2022-10-06 17:41:07 +02:00
Mark Qvist 16f8725906 Updated docs and manual 2022-10-06 17:35:38 +02:00
markqvist 2656157462
Update README.md 2022-10-04 23:22:46 +02:00
markqvist c9c7469b32
Update README.md 2022-10-04 23:22:05 +02:00
markqvist 0f429e2385
Update README.md 2022-10-04 23:18:44 +02:00
Mark Qvist 89d8342ce5 Improved logging. Reject mismatching keys on hash collision. 2022-10-04 22:42:59 +02:00
Mark Qvist c18997bf5b Cleanup 2022-10-04 22:41:58 +02:00
Mark Qvist 1e4dd9d6f0 Added note 2022-10-04 22:40:43 +02:00
Mark Qvist b296c10541 Added check for app_data 2022-10-04 22:40:03 +02:00
Mark Qvist 9065de5fb4 Updated docs and manual 2022-10-04 09:34:52 +02:00
Mark Qvist 7997fd104e Fix destination hash construction and random blob extraction 2022-10-04 09:11:20 +02:00
Mark Qvist 11667504b2 Updated docs 2022-10-04 09:06:29 +02:00
Mark Qvist 7744c4ffe6 Updated version 2022-10-04 07:00:13 +02:00
Mark Qvist 8a61d2c8d5 Fixed missing validation in announce processing 2022-10-04 06:59:33 +02:00
Mark Qvist 1380016995 Updated tests 2022-10-04 06:55:50 +02:00
markqvist f2aff3fbd5
Update README.md 2022-09-30 22:54:51 +02:00
Mark Qvist b859984ebe Updated manual 2022-09-30 21:54:51 +02:00
Mark Qvist 9593b1c295 Updated readme 2022-09-30 21:23:30 +02:00
Mark Qvist 3d6455fb37 Updated manual 2022-09-30 21:16:22 +02:00
Mark Qvist b085127d6e Fixed config dir path 2022-09-30 20:41:11 +02:00
Mark Qvist 80ffa5ebc3 Updated manual 2022-09-30 20:38:27 +02:00
Mark Qvist 76fb73f46c Updated configuration path defaults 2022-09-30 20:37:46 +02:00
Mark Qvist e51b0077c7 Improved configuration info in docs 2022-09-30 20:37:03 +02:00
Mark Qvist c18806c912 Updated deprecated threading API call and updated docs 2022-09-30 19:02:41 +02:00
Mark Qvist 683881d6cd Updated documentation 2022-09-30 18:50:35 +02:00
Mark Qvist f62d9946ac Updated documentation and manual 2022-09-30 18:43:04 +02:00
Mark Qvist 893a463663 Updated docs and manual 2022-09-30 14:22:33 +02:00
Mark Qvist 39b788867d Updated docs and manual 2022-09-30 13:09:10 +02:00
Mark Qvist 2abd8a1aae Updated docs and manual 2022-09-30 11:26:51 +02:00
Mark Qvist 7940ac0812 Updated docs and manual 2022-09-30 11:15:34 +02:00
Mark Qvist 3f2075da6f Updated manual and documentation 2022-09-30 00:02:15 +02:00
Mark Qvist e90b2866b4 Updated readme and documentation 2022-09-29 23:20:49 +02:00
Mark Qvist 8886ed5794 Fixed missing destination-side ephemeral key generation in link establishment 2022-09-29 22:47:10 +02:00
Mark Qvist 32ee4216fd Changed log levels 2022-09-24 12:23:59 +02:00
Mark Qvist 571ad2c8fb Added initial connection timeout option to TCPClientInterface 2022-09-15 15:35:28 +02:00
Mark Qvist 0c47ff1ccc Updated documentation and manual 2022-09-14 18:39:39 +02:00
Mark Qvist 18f450c58b Periodically try to connect RNodes that were unavailable at startup. Closes #87. 2022-09-14 17:43:07 +02:00
Mark Qvist b3d85b583f Place config in .config dir by default 2022-09-14 16:21:34 +02:00
Mark Qvist 03695565ba Added rnsd warning on start as client 2022-09-14 00:13:20 +02:00
Mark Qvist 3e380a8fc7 Fixed rendering in rnpath utility 2022-09-14 00:07:23 +02:00
Mark Qvist fd35451927 Updated readme 2022-09-14 00:04:00 +02:00
Mark Qvist 921987c999 Added table persist on local client disconnect 2022-09-13 22:32:00 +02:00
Mark Qvist 81e0989070 Updated readme 2022-09-13 22:30:28 +02:00
Mark Qvist 3fa7698438 Updated readme 2022-09-13 21:06:07 +02:00
Mark Qvist 75e32af1c5 Added periodic data persistence for shared and standalone instances 2022-09-13 20:17:25 +02:00
Mark Qvist 9775893840 Improved known destination saving 2022-09-06 19:43:46 +02:00
Mark Qvist e5c0ee4153 Added build variant to makefile 2022-09-06 19:42:50 +02:00
Mark Qvist 4042dd6ef7 Added locking and timeouts to table saving routines 2022-09-06 18:05:02 +02:00
Mark Qvist af538e0489 Improved shutdown handling and table saving 2022-09-06 17:42:13 +02:00
Mark Qvist 8f4cf433ba Updated docs 2022-09-06 16:50:33 +02:00
Mark Qvist c55e1e9628 Version bump 2022-09-06 12:24:46 +02:00
Mark Qvist be02586133 Added detach handler to TCP Server Interface 2022-09-06 12:23:52 +02:00
Mark Qvist 6db742ade7 Updated documentation 2022-08-25 11:00:30 +02:00
Mark Qvist 6a53298aa2 Merge branch 'master' of github.com:markqvist/Reticulum 2022-08-25 10:59:47 +02:00
Mark Qvist f00b6a6fdb Updated roadmap 2022-08-25 10:59:43 +02:00
markqvist dc0a0735db
Merge pull request #90 from huyndao/huy-proofread
Huy proofread
2022-08-12 10:40:13 +02:00
huyndao b230edd21d Fixed additional spelling errors 2022-08-05 17:54:14 -04:00
huyndao 30e75b1bfb Fixed spelling errors 2022-08-05 17:38:23 -04:00
Mark Qvist 7f70ffdc21 Updated documentation 2022-07-09 15:52:24 +02:00
Mark Qvist 6e6b49dcd2 Added extra resource transfer test 2022-07-09 15:50:18 +02:00
Mark Qvist 383f96d82a Updated version 2022-07-09 15:46:42 +02:00
Mark Qvist ebef2da7a8 Fixed incorrect allocation size in resource advertisements after switching to 128-bit address space 2022-07-09 15:46:19 +02:00
Mark Qvist 4946d9f2eb Updated readme 2022-07-08 17:05:02 +02:00
Mark Qvist fcb61e3ebf Updated documentation and manual 2022-07-08 12:14:03 +02:00
Mark Qvist eae788957a Updated version 2022-07-08 12:01:13 +02:00
Mark Qvist 045a9d8451 Fixed a race condition in link establishment flow 2022-07-08 11:14:35 +02:00
Mark Qvist da644d33ea Updated documentation 2022-07-08 00:32:03 +02:00
Mark Qvist e03fc38920 Updated documentation 2022-07-08 00:28:41 +02:00
Mark Qvist c36c0368ef Updated manual 2022-07-08 00:24:00 +02:00
Mark Qvist 3d979e2d65 Added Android compatibility to AES proxy class 2022-07-08 00:22:30 +02:00
Mark Qvist 5158613501 Fixed missing config section check 2022-07-08 00:21:48 +02:00
Mark Qvist b53185779a Updated documentation 2022-07-08 00:21:23 +02:00
Mark Qvist 5b63f84491 Updated readme 2022-07-05 00:45:58 +02:00
163 changed files with 26864 additions and 6095 deletions

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
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

View File

@ -0,0 +1,35 @@
---
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**.
- After reading the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md), delete this section from your bug report.
**Describe the Bug**
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.
**System Information**
- OS and version
- Python version
- Program version
**Additional context**
Add any other context about the problem here.

1
.gitignore vendored
View File

@ -10,5 +10,6 @@ docs/build
rns*.egg-info
profile.data
tests/rnsconfig/storage
tests/rnsconfig/logfile*
*.data
*.result

1167
Changelog.md Normal file

File diff suppressed because it is too large Load Diff

43
Contributing.md Normal file
View File

@ -0,0 +1,43 @@
# 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** - also in the face of disputes and heated disagreements. Speak your mind here, discussions are welcome. Just do so in the spirit of being face-to-face with everyone else. Thank you.
## 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) or on the [Reticulum Matrix channel](https://matrix.to/#/#reticulum:matrix.org) at `#reticulum:matrix.org`
## Providing Feedback & Ideas
Likewise, feedback, ideas and feature requests are a very welcome way to contribute, and should also be posted on the [discussions](https://github.com/markqvist/Reticulum/discussions), or on the [Reticulum Matrix channel](https://matrix.to/#/#reticulum:matrix.org) at `#reticulum:matrix.org`.
Please do not post feature requests or general ideas on the issue tracker, or in direct messages to the primary developers. You are much more likely to get a response and start a constructive discussion by posting your ideas in the public channels created for these purposes.
## Reporting Issues
If you have found a bug or issue in this project, please report it using the [issue tracker](https://github.com/markqvist/Reticulum/issues). If at all possible, be sure to include details on how to reproduce the bug.
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.
By contributing code to this project, you agree that copyright for the code is transferred to the Reticulum maintainers and that the code is irrevocably placed under the [MIT license](./LICENSE).

View File

@ -32,7 +32,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 +53,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
@ -130,10 +130,11 @@ class ExampleAnnounceHandler:
RNS.prettyhexrep(destination_hash)
)
RNS.log(
"The announce contained the following app data: "+
app_data.decode("utf-8")
)
if app_data:
RNS.log(
"The announce contained the following app data: "+
app_data.decode("utf-8")
)
##########################################################
#### Program Startup #####################################

323
Examples/Buffer.py Normal file
View File

@ -0,0 +1,323 @@
##########################################################
# 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")
exit()
# 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")
RNS.Reticulum.exit_handler()
time.sleep(1.5)
os._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("")
exit()

390
Examples/Channel.py Normal file
View File

@ -0,0 +1,390 @@
##########################################################
# 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(
"Link 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")
exit()
# 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")
RNS.Reticulum.exit_handler()
time.sleep(1.5)
os._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("")
exit()

View File

@ -5,6 +5,7 @@
# of the packet. #
##########################################################
import os
import argparse
import RNS
@ -27,8 +28,19 @@ def server(configpath):
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our echo server
server_identity = RNS.Identity()
# Load identity from file if it exist or randomley create
if configpath:
ifilepath = "%s/storage/identitiesy/%s" % (configpath,APP_NAME)
else:
ifilepath = "%s/storage/identities/%s" % (RNS.Reticulum.configdir,APP_NAME)
if os.path.exists(ifilepath):
# Load identity from file
server_identity = RNS.Identity.from_file(ifilepath)
RNS.log("loaded identity from file: "+ifilepath, RNS.LOG_VERBOSE)
else:
# Randomly create a new identity for our echo example
server_identity = RNS.Identity()
RNS.log("created new identity", RNS.LOG_VERBOSE)
# We create a destination that clients can query. We want
# to be able to verify echo replies to our clients, so we
@ -46,7 +58,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)
@ -210,6 +222,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 +340,4 @@ if __name__ == "__main__":
client(args.destination, configarg, timeout=timeoutarg)
except KeyboardInterrupt:
print("")
exit()
exit()

View File

@ -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

View File

@ -28,8 +28,20 @@ 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()
# Load identity from file if it exist or randomley create
if configpath:
ifilepath = "%s/storage/identitiesy/%s" % (configpath,APP_NAME)
else:
ifilepath = "%s/storage/identities/%s" % (RNS.Reticulum.configdir,APP_NAME)
RNS.log("ifilepath: %s" % ifilepath)
if os.path.exists(ifilepath):
# Load identity from file
server_identity = RNS.Identity.from_file(ifilepath)
RNS.log("loaded identity from file: "+ifilepath, RNS.LOG_VERBOSE)
else:
# Randomly create a new identity for our link example
server_identity = RNS.Identity()
RNS.log("created new identity", RNS.LOG_VERBOSE)
# We create a destination that clients can connect to. We
# want clients to create links to this destination, so we
@ -288,4 +300,4 @@ if __name__ == "__main__":
except KeyboardInterrupt:
print("")
exit()
exit()

View File

@ -25,7 +25,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 +36,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

View File

@ -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)]

2
FUNDING.yml Normal file
View File

@ -0,0 +1,2 @@
ko_fi: markqvist
custom: "https://unsigned.io/donate"

View File

@ -1,6 +1,6 @@
MIT License, unless otherwise noted
Copyright (c) 2016-2022 Mark Qvist / unsigned.io
Copyright (c) 2016-2024 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

View File

@ -21,6 +21,7 @@ clean:
@-rm -rf ./tests/__pycache__
@-rm -rf ./tests/rnsconfig/storage
@-rm -rf ./*.egg-info
@make -C docs clean
@echo Done
remove_symlinks:
@ -46,10 +47,12 @@ documentation:
make -C docs html
manual:
make -C docs latexpdf
make -C docs latexpdf epub
release: test remove_symlinks 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
@read VOID

314
README.md
View File

@ -1,32 +1,51 @@
Reticulum Network Stack β
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"/>
==========
<p align="center"><img width="200" src="https://unsigned.io/wp-content/uploads/2022/03/reticulum_logo_512.png"></p>
<p align="center"><img width="200" src="https://raw.githubusercontent.com/markqvist/Reticulum/master/docs/source/graphics/rns_logo_512.png"></p>
Reticulum is the cryptography-based networking stack for wide-area networks built on 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.
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*.
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.
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.
Having no dependencies on traditional networking stacks frees up overhead that
has been used to implement a networking stack built directly on cryptographic
principles, allowing resilience and stable functionality, even in open and
trustless networks.
No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python 3.
No kernel modules or drivers are required. Reticulum runs completely in
userland, and can run on practically any system that runs Python 3.
## Read The Manual
The full documentation for Reticulum is available 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/)
## Notable Features
- Coordination-less globally unique addressing and identification
- Fully self-configuring multi-hop routing
- Complete initiator anonymity, communicate without revealing your identity
- Initiator anonymity, communicate without revealing your identity
- 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
- Reticulum uses the following format for encrypted tokens:
- Keys are ephemeral and derived from an ECDH key exchange on Curve25519
- AES-128 in CBC mode with PKCS7 padding
- HMAC using SHA256 for authentication
@ -40,61 +59,124 @@ For more info, see [unsigned.io/projects/reticulum](https://unsigned.io/projects
- 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 265 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
## Roadmap
While Reticulum is already a fully featured and functional networking stack,
many improvements and additions are actively being worked on, and planned for the future.
To learn more about the direction and future of Reticulum, please see the [Development Roadmap](./Roadmap.md).
## Examples of Reticulum Applications
If you want to quickly get an idea of what Reticulum can do, take a look at the following resources.
If you want to quickly get an idea of what Reticulum can do, take a look at the
following resources.
- [LXMF](https://github.com/markqvist/lxmf) is a distributed, delay and disruption tolerant message transfer protocol built on Reticulum
- You can use the [rnsh](https://github.com/acehoss/rnsh) program to establish remote shell sessions over Reticulum.
- 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://unsigned.io/sideband) has a graphical interface and focuses on ease of use.
- 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
## 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, 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.
Over practically any medium that can support at least a half-duplex channel
with greater throughput than 5 bits per second, and an MTU of 500 bytes. Data radios,
modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes,
WiFi and Ethernet devices, free-space optical links, and similar systems are
all examples of the types of physical devices Reticulum can use.
An open-source LoRa-based interface called [RNode](https://markqvist.github.io/Reticulum/manual/hardware.html#rnode) has been designed specifically for use with Reticulum. It is possible to build yourself, or it can be purchased as a complete transceiver that just needs a USB connection to the host.
An open-source LoRa-based interface called
[RNode](https://markqvist.github.io/Reticulum/manual/hardware.html#rnode) has
been designed specifically for use with Reticulum. It is possible to build
yourself, or it can be purchased as a complete transceiver that just needs a
USB connection to the host.
Reticulum can also be encapsulated over existing IP networks, so there's nothing stopping you from using it over wired ethernet, your local WiFi network or the Internet, where it'll work just as well. In fact, one of the strengths of Reticulum is how easily it allows you to connect different mediums into a self-configuring, resilient and encrypted mesh, using any available mixture of available infrastructure.
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.
As an example, it's possible to set up a Raspberry Pi connected to both a LoRa
radio, a packet radio TNC and a WiFi network. Once the interfaces are
configured, Reticulum will take care of the rest, and any device on the WiFi
network can communicate with nodes on the LoRa and packet radio sides of the
network, and vice versa.
## How do I get started?
The best way to get started with the Reticulum Network Stack depends on what
you want to do. For full details and examples, have a look at the [Getting Started Fast](https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
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
pip3 install rns
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:
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.
```text
[global]
break-system-packages = true
```
For more detailed examples on how to expand communication over many mediums such as packet radio or LoRa, serial ports, or over fast IP links and the Internet using the UDP and TCP interfaces, take a look at the [Supported Interfaces](https://markqvist.github.io/Reticulum/manual/interfaces.html) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
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
reachable. The default config file contains a few examples, and references for
creating a more complex configuration.
If you have an old version of `pip` on your system, you may need to upgrade it first with `pip install pip --upgrade`. If you no not already have `pip` installed, you can install it using the package manager of your system with `sudo apt install python3-pip` or similar.
For more detailed examples on how to expand communication over many mediums such
as packet radio or LoRa, serial ports, or over fast IP links and the Internet using
the UDP and TCP interfaces, take a look at the [Supported Interfaces](https://markqvist.github.io/Reticulum/manual/interfaces.html)
section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
## Included Utilities
Reticulum includes a range of useful utilities for managing your networks, viewing status and information, and other tasks. You can read more about these programs in the [Included Utility Programs](https://markqvist.github.io/Reticulum/manual/using.html#included-utility-programs) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
Reticulum includes a range of useful utilities for managing your networks,
viewing status and information, and other tasks. You can read more about these
programs in the [Included Utility Programs](https://markqvist.github.io/Reticulum/manual/using.html#included-utility-programs)
section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
- The system daemon `rnsd` for running Reticulum as an always-available service
- 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 remote command execution program `rnx` let's you run commands and
programs and retrieve output from remote systems
All tools, including `rnx` and `rncp`, work reliably and well even over very low-bandwidth links like LoRa or Packet Radio.
All tools, including `rnx` and `rncp`, work reliably and well even over very
low-bandwidth links like LoRa or Packet Radio. For full-featured remote shells
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.
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.
Currently, the following interfaces are supported:
- Any ethernet device
- LoRa using [RNode](https://unsigned.io/projects/rnode/)
- Any Ethernet device
- LoRa using [RNode](https://unsigned.io/rnode/)
- Packet Radio TNCs (with or without AX.25)
- KISS-compatible hardware and software modems
- Any device with a serial port
@ -104,89 +186,85 @@ Currently, the following interfaces are supported:
- Custom hardware via stdio or pipes
## Performance
Reticulum targets a *very* wide usable performance envelope, but prioritises functionality and performance over 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.
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 500 bits per second to 20 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.
Currently, the usable performance envelope is approximately 150 bits per second
to 40 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
- Version 0.4.0
- 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
- Support for radio and modem interfaces on Android
- More interface types for even broader compatibility
- Plain ESP32 devices (ESP-Now, WiFi, Bluetooth, etc.)
- More LoRa transceivers
- IR Transceivers
- Planned, but not yet scheduled
- Distributed Destination Naming System
- Network-wide path balancing
- Globally routable multicast
- Bindings for other programming languages
- 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
- AWDL / OWL
- HF Modems
- CAN-bus
- ZeroMQ
- MQTT
- SPI
- i²c
- Tor
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.
## Dependencies
The installation of the default `rns` package requires the 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.
The installation of the default `rns` package requires the 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 install or even compile one or more of the above modules. In such situations, you can use the `rnspure` package instead, which require no external dependencies for installation. Please note that the contents of the `rns` and `rnspure` packages are *identical*. The only difference is that the `rnspure` package lists no dependencies required for installation.
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 `rnspure` package instead, which require no external
dependencies for installation. Please note that the contents of the `rns` and
`rnspure` packages are *identical*. The only difference is that the `rnspure`
package lists no dependencies required for installation.
No matter how Reticulum is installed and started, it will load external dependencies only if they are *needed* and *available*. If for example you want to use Reticulum on a system that cannot support [pyserial](https://github.com/pyserial/pyserial), it is perfectly possible to do so using the `rnspure` package, but Reticulum will not be able to use serial-based interfaces. All other available modules will still be loaded when needed.
No matter how Reticulum is installed and started, it will load external
dependencies only if they are *needed* and *available*. If for example you want
to use Reticulum on a system that cannot support
[pyserial](https://github.com/pyserial/pyserial), it is perfectly possible to
do so using the `rnspure` package, but Reticulum will not be able to use
serial-based interfaces. All other available modules will still be loaded when
needed.
**Please Note!** If you use the `rnspure` package to run Reticulum on systems that do not support [PyCA/cryptography](https://github.com/pyca/cryptography), it is important that you read and understand the [Cryptographic Primitives](#cryptographic-primitives) section of this document.
**Please Note!** If you use the `rnspure` package to run Reticulum on systems
that do not support [PyCA/cryptography](https://github.com/pyca/cryptography),
it is important that you read and understand the [Cryptographic
Primitives](#cryptographic-primitives) section of this document.
## 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.
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:
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:
```
# For connecting over TCP/IP on v0.3.8:
[[RNS Testnet Frankfurt]]
type = TCPClientInterface
interface_enabled = yes
outgoing = True
target_host = frankfurt.rns.unsigned.io
target_port = 4965
# For connecting over I2P on v0.3.8:
[[RNS Testnet I2P Node A]]
type = I2PInterface
interface_enabled = yes
peers = ykzlw5ujbaqc2xkec4cpvgyxj257wcrmmgkuxqmqcur7cq3w3lha.b32.i2p
# For connecting over TCP/IP on v0.3.9+:
[[RNS Testnet Dublin]]
# TCP/IP interface to the RNS Amsterdam Hub
[[RNS Testnet Amsterdam]]
type = TCPClientInterface
enabled = yes
target_host = dublin.connect.reticulum.network
target_host = amsterdam.connect.reticulum.network
target_port = 4965
# TCP/IP interface to the BetweenTheBorders Hub (community-provided)
[[RNS Testnet BetweenTheBorders]]
type = TCPClientInterface
enabled = yes
target_host = betweentheborders.com
target_port = 4242
# For connecting over I2P on v0.3.9+:
[[RNS Testnet I2P Hub A]]
# Interface to Testnet I2P Hub
[[RNS Testnet I2P Hub]]
type = I2PInterface
enabled = yes
peers = uxg5kubabakh3jtnvsipingbr5574dle7bubvip7llfvwx2tgrua.b32.i2p
peers = g3br23bvx3lq5uddcsjii74xgmn6y5q325ovrkq2zw2wbzbqgbuq.b32.i2p
```
The testnet also contains a number of [Nomad Network](https://github.com/markqvist/nomadnet) nodes, and LXMF propagation nodes.
@ -196,24 +274,25 @@ You can help support the continued development of open, free and private communi
- Monero:
```
84FpY1QbxHcgdseePYNmhTHcrgMX4nFf
BYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD1
9b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
```
- Ethereum
```
0x81F7B979fEa6134bA9FD5c701b3501A2e61E897a
0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73
```
- Bitcoin
```
3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq
35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH
```
- 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.
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 primitives, with widely available implementations that can be used both on general-purpose CPUs and on microcontrollers. The necessary primitives are:
Reticulum uses a simple suite of efficient, strong and modern cryptographic
primitives, with widely available implementations that can be used both on
general-purpose CPUs and on microcontrollers. The necessary primitives are:
- Ed25519 for signatures
- X22519 for ECDH key exchanges
@ -225,7 +304,13 @@ Reticulum uses a simple suite of efficient, strong and modern cryptographic prim
- 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/) (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 provided by the following internal implementations:
In the default installation configuration, the `X25519`, `Ed25519` and
`AES-128-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
provided by the following internal implementations:
- [HKDF.py](RNS/Cryptography/HKDF.py)
- [HMAC.py](RNS/Cryptography/HMAC.py)
@ -233,16 +318,33 @@ In the default installation configuration, the `X25519`, `Ed25519` and `AES-128-
- [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 Reticulum is started, Reticulum will instead use the internal pure-python primitives. A trivial consequence of this is performance, with the OpenSSL backend being *much* faster. The most important consequence however, is the potential loss of security by using primitives that has not seen the same amount of scrutiny, testing and review as those from OpenSSL.
Reticulum also includes a complete implementation of all necessary primitives
in pure Python. If OpenSSL & PyCA are not available on the system when
Reticulum is started, Reticulum will instead use the internal pure-python
primitives. A trivial consequence of this is performance, with the OpenSSL
backend being *much* faster. The most important consequence however, is the
potential loss of security by using primitives that has not seen the same
amount of scrutiny, testing and review as those from OpenSSL.
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.
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.
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.
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:
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*
@ -251,8 +353,8 @@ A number of other modules and projects are either part of, or used by Reticulum.
- [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 [Pydron](https://github.com/pydron), *MIT License*
- [Umsgpack.py](https://github.com/vsergeev/u-msgpack-python) by [Ivan A. Sergeev](https://github.com/vsergeev)
- [Python](https://www.python.org)

359
RNS/Buffer.py Normal file
View File

@ -0,0 +1,359 @@
# MIT License
#
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
#
# 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 __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
"""
MAX_DATA_LEN = RNS.Link.MDU - 2 - 6 # 2 for stream data message header, 6 for channel envelope
"""
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:
self.data = bz2.decompress(self.data)
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
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)

694
RNS/Channel.py Normal file
View File

@ -0,0 +1,694 @@
# MIT License
#
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
#
# 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 __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
"""
return self._outlet.mdu - 6 # sizeof(msgtype) + sizeof(length) + sizeof(sequence)
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

View File

@ -21,6 +21,7 @@
# SOFTWARE.
import RNS.Cryptography.Provider as cp
import RNS.vendor.platformutils as pu
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
from .aes import AES
@ -28,6 +29,9 @@ if cp.PROVIDER == cp.PROVIDER_INTERNAL:
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
class AES_128_CBC:
@ -38,7 +42,11 @@ class AES_128_CBC:
return cipher.encrypt(plaintext, iv)
elif cp.PROVIDER == cp.PROVIDER_PYCA:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
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
@ -50,7 +58,11 @@ class AES_128_CBC:
return cipher.decrypt(ciphertext, iv)
elif cp.PROVIDER == cp.PROVIDER_PYCA:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
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

View File

@ -45,10 +45,10 @@ class Fernet():
def __init__(self, key = None):
if key == None:
raise ValueError("Fernet key cannot be None")
raise ValueError("Token key cannot be None")
if len(key) != 32:
raise ValueError("Fernet key must be 32 bytes, not "+str(len(key)))
raise ValueError("Token key must be 32 bytes, not "+str(len(key)))
self._signing_key = key[:16]
self._encryption_key = key[16:]
@ -72,7 +72,7 @@ class Fernet():
current_time = int(time.time())
if not isinstance(data, bytes):
raise TypeError("Fernet token plaintext input must be bytes")
raise TypeError("Token plaintext input must be bytes")
ciphertext = AES_128_CBC.encrypt(
plaintext = PKCS7.pad(data),
@ -87,10 +87,10 @@ class Fernet():
def decrypt(self, token = None):
if not isinstance(token, bytes):
raise TypeError("Fernet token must be bytes")
raise TypeError("Token must be bytes")
if not self.verify_hmac(token):
raise ValueError("Fernet token HMAC was invalid")
raise ValueError("Token HMAC was invalid")
iv = token[:16]
ciphertext = token[16:-32]
@ -107,4 +107,4 @@ class Fernet():
return plaintext
except Exception as e:
raise ValueError("Could not decrypt Fernet token")
raise ValueError("Could not decrypt token")

View File

@ -33,15 +33,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""
@ -54,4 +51,4 @@ def hkdf(length=None, derive_from=None, salt=None, context=None):
block = hmac_sha256(pseudorandom_key, block + context + bytes([i + 1]))
derived += block
return derived[:length]
return derived[:length]

View File

@ -1,6 +1,6 @@
# MIT License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -69,8 +69,10 @@ class Destination:
OUT = 0x12;
directions = [IN, OUT]
PR_TAG_WINDOW = 30
@staticmethod
def full_name(app_name, *aspects):
def expand_name(identity, app_name, *aspects):
"""
:returns: A string containing the full human-readable name of the destination, for an app_name and a number of aspects.
"""
@ -81,20 +83,30 @@ class Destination:
name = app_name
for aspect in aspects:
if "." in aspect: raise ValueError("Dots can't be used in aspects")
name = name + "." + aspect
name += "." + aspect
if identity != None:
name += "." + identity.hexhash
return name
@staticmethod
def hash(app_name, *aspects):
def hash(identity, app_name, *aspects):
"""
:returns: A destination name in adressable hash form, for an app_name and a number of aspects.
"""
name = Destination.full_name(app_name, *aspects)
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:
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")
# Create a digest for the destination
return RNS.Identity.full_hash(name.encode("utf-8"))[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8]
return RNS.Identity.full_hash(addr_hash_material)[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8]
@staticmethod
def app_and_aspects_from_name(full_name):
@ -110,8 +122,8 @@ class Destination:
:returns: A destination name in adressable hash form, for a full name string and Identity instance.
"""
app_name, aspects = Destination.app_and_aspects_from_name(full_name)
aspects.append(identity.hexhash)
return Destination.hash(app_name, *aspects)
return Destination.hash(identity, app_name, *aspects)
def __init__(self, identity, direction, type, app_name, *aspects):
# Check input values and build name string
@ -127,11 +139,9 @@ class Destination:
self.proof_strategy = Destination.PROVE_NONE
self.mtu = 0
self.path_responses = {}
self.links = []
if identity != None and type == Destination.SINGLE:
aspects = aspects+(identity.hexhash,)
if identity == None and direction == Destination.IN and self.type != Destination.PLAIN:
identity = RNS.Identity()
aspects = aspects+(identity.hexhash,)
@ -140,12 +150,14 @@ class Destination:
raise TypeError("Selected destination type PLAIN cannot hold an identity")
self.identity = identity
self.name = Destination.expand_name(identity, app_name, *aspects)
self.name = Destination.full_name(app_name, *aspects)
self.hash = Destination.hash(app_name, *aspects)
# Generate the destination address hash
self.hash = Destination.hash(self.identity, app_name, *aspects)
self.name_hash = RNS.Identity.full_hash(self.expand_name(None, app_name, *aspects).encode("utf-8"))[:(RNS.Identity.NAME_HASH_LENGTH//8)]
self.hexhash = self.hash.hex()
self.default_app_data = None
self.default_app_data = None
self.callback = None
self.proofcallback = None
@ -159,7 +171,7 @@ class Destination:
return "<"+self.name+"/"+self.hexhash+">"
def announce(self, app_data=None, path_response=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.
@ -169,35 +181,68 @@ 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()+random_hash
if app_data != None:
signed_data += app_data
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()+random_hash+signature
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:
destination_hash = self.hash
random_hash = RNS.Identity.get_random_hash()[0:5]+int(time.time()).to_bytes(5, "big")
if app_data != None:
announce_data += app_data
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
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+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
RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context).send()
announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context, attached_interface = attached_interface)
if send:
announce_packet.send()
else:
return announce_packet
def accepts_links(self, accepts = None):
"""
@ -259,7 +304,7 @@ class Destination:
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.
:raises: ``ValueError`` if any of the supplied arguments are invalid.

View File

@ -1,6 +1,6 @@
# MIT License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -58,6 +58,7 @@ class Identity:
HASHLENGTH = 256 # In bits
SIGLENGTH = KEYSIZE # In bits
NAME_HASH_LENGTH = 80
TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH
"""
Constant specifying the truncated hash length (in bits) used by Reticulum
@ -90,6 +91,13 @@ class Identity:
identity.app_data = identity_data[3]
return identity
else:
for registered_destination in RNS.Transport.destinations:
if destination_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
@ -108,7 +116,26 @@ class Identity:
@staticmethod
def save_known_destinations():
# 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
# simply overwrite on exit now that every local client
# disconnect triggers a data persist.
try:
if hasattr(Identity, "saving_known_destinations"):
wait_interval = 0.2
wait_timeout = 5
wait_start = time.time()
while Identity.saving_known_destinations:
time.sleep(wait_interval)
if time.time() > wait_start+wait_timeout:
RNS.log("Could not save known destinations to storage, waiting for previous save operation timed out.", RNS.LOG_ERROR)
return False
Identity.saving_known_destinations = True
save_start = time.time()
storage_known_destinations = {}
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
try:
@ -118,17 +145,31 @@ class Identity:
except:
pass
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]
try:
for destination_hash in storage_known_destinations:
if not destination_hash in Identity.known_destinations:
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
except Exception as e:
RNS.log("Skipped recombining known destinations from disk, since an error occurred: "+str(e), RNS.LOG_WARNING)
RNS.log("Saving known destinations to storage...", RNS.LOG_VERBOSE)
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("Done saving known destinations to storage", RNS.LOG_VERBOSE)
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"
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_DEBUG)
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():
@ -144,7 +185,8 @@ class Identity:
Identity.known_destinations[known_destination] = loaded_known_destinations[known_destination]
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE)
except:
except Exception as e:
RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR)
else:
RNS.log("Destinations file does not exist, no known destinations loaded", RNS.LOG_VERBOSE)
@ -180,38 +222,73 @@ class Identity:
return Identity.truncated_hash(os.urandom(Identity.TRUNCATED_HASHLENGTH//8))
@staticmethod
def validate_announce(packet):
def validate_announce(packet, only_validate_signature=False):
try:
if packet.packet_type == RNS.Packet.ANNOUNCE:
destination_hash = packet.destination_hash
public_key = packet.data[:Identity.KEYSIZE//8]
random_hash = packet.data[Identity.KEYSIZE//8:Identity.KEYSIZE//8+10]
signature = packet.data[Identity.KEYSIZE//8+10:Identity.KEYSIZE//8+10+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+10+Identity.KEYSIZE//8:
app_data = packet.data[Identity.KEYSIZE//8+10+Identity.KEYSIZE//8:]
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+random_hash+app_data
signed_data = destination_hash+public_key+name_hash+random_hash+app_data
if not len(packet.data) > Identity.KEYSIZE//8+10+Identity.KEYSIZE//8:
if not len(packet.data) > Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:
app_data = None
announced_identity = Identity(create_keys=False)
announced_identity.load_public_key(public_key)
if announced_identity.pub != None and announced_identity.validate(signature, signed_data):
RNS.Identity.remember(packet.get_hash(), destination_hash, public_key, app_data)
del announced_identity
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]
if destination_hash == expected_hash:
# Check if we already have a public key for this destination
# and make sure the public key is not different.
if destination_hash in Identity.known_destinations:
if public_key != Identity.known_destinations[destination_hash][2]:
# In reality, this should never occur, but in the odd case
# that someone manages a hash collision, we reject the announce.
RNS.log("Received announce with valid signature and destination hash, but announced public key does not match already known public key.", RNS.LOG_CRITICAL)
RNS.log("This may indicate an attempt to modify network paths, or a random hash collision. The announce was rejected.", RNS.LOG_CRITICAL)
return False
RNS.Identity.remember(packet.get_hash(), destination_hash, public_key, app_data)
del announced_identity
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:
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)
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)
return True
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)
else:
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received on "+str(packet.receiving_interface), RNS.LOG_EXTREME)
return True
RNS.log("Received invalid announce for "+RNS.prettyhexrep(destination_hash)+": Destination mismatch.", RNS.LOG_DEBUG)
return False
else:
RNS.log("Received invalid announce for "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG)
RNS.log("Received invalid announce for "+RNS.prettyhexrep(destination_hash)+": Invalid signature.", RNS.LOG_DEBUG)
del announced_identity
return False
@ -219,9 +296,14 @@ class Identity:
RNS.log("Error occurred while validating announce. The contained exception was: "+str(e), RNS.LOG_ERROR)
return False
@staticmethod
def persist_data():
if not RNS.Transport.owner.is_connected_to_shared_instance:
Identity.save_known_destinations()
@staticmethod
def exit_handler():
Identity.save_known_destinations()
Identity.persist_data()
@staticmethod
@ -379,7 +461,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

View File

@ -77,8 +77,7 @@ 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__()
self.HW_MTU = 564
@ -153,7 +152,7 @@ class AX25KISSInterface(Interface):
# Allow time for interface to initialise before config
sleep(2.0)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True)
thread.daemon = True
thread.start()
self.online = True
RNS.log("Serial port "+self.port+" is now open")
@ -367,5 +366,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+"]"

View File

@ -0,0 +1,409 @@
# MIT License
#
# Copyright (c) 2016-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.
from RNS.Interfaces.Interface import Interface
from time import sleep
import sys
import threading
import time
import RNS
class KISS():
FEND = 0xC0
FESC = 0xDB
TFEND = 0xDC
TFESC = 0xDD
CMD_UNKNOWN = 0xFE
CMD_DATA = 0x00
CMD_TXDELAY = 0x01
CMD_P = 0x02
CMD_SLOTTIME = 0x03
CMD_TXTAIL = 0x04
CMD_FULLDUPLEX = 0x05
CMD_SETHARDWARE = 0x06
CMD_READY = 0x0F
CMD_RETURN = 0xFF
@staticmethod
def escape(data):
data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
return data
class KISSInterface(Interface):
MAX_CHUNK = 32768
BITRATE_GUESS = 1200
owner = None
port = None
speed = None
databits = None
parity = None
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
if RNS.vendor.platformutils.is_android():
self.on_android = True
if importlib.util.find_spec('usbserial4a') != None:
if importlib.util.find_spec('jnius') == None:
RNS.log("Could not load jnius API wrapper for Android, KISS interface cannot be created.", RNS.LOG_CRITICAL)
RNS.log("This probably means you are trying to use an USB-based interface from within Termux or similar.", RNS.LOG_CRITICAL)
RNS.log("This is currently not possible, due to this environment limiting access to the native Android APIs.", RNS.LOG_CRITICAL)
RNS.panic()
from usbserial4a import serial4a as serial
self.parity = "N"
else:
RNS.log("Could not load USB serial module for Android, KISS interface cannot be created.", RNS.LOG_CRITICAL)
RNS.log("You can install this module by issuing: pip install usbserial4a", RNS.LOG_CRITICAL)
RNS.panic()
else:
raise SystemError("Android-specific interface was used on non-Android OS")
super().__init__()
self.HW_MTU = 564
if beacon_data == None:
beacon_data = ""
self.pyserial = serial
self.serial = None
self.owner = owner
self.name = name
self.port = port
self.speed = speed
self.databits = databits
self.parity = "N"
self.stopbits = stopbits
self.timeout = 100
self.online = False
self.beacon_i = beacon_interval
self.beacon_d = beacon_data.encode("utf-8")
self.first_tx = None
self.bitrate = KISSInterface.BITRATE_GUESS
self.packet_queue = []
self.flow_control = flow_control
self.interface_ready = False
self.flow_control_timeout = 5
self.flow_control_locked = time.time()
self.preamble = preamble if preamble != None else 350;
self.txtail = txtail if txtail != None else 20;
self.persistence = persistence if persistence != None else 64;
self.slottime = slottime if slottime != None else 20;
if parity.lower() == "e" or parity.lower() == "even":
self.parity = "E"
if parity.lower() == "o" or parity.lower() == "odd":
self.parity = "O"
try:
self.open_port()
except Exception as e:
RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR)
raise e
if self.serial.is_open:
self.configure_device()
else:
raise IOError("Could not open serial port")
def open_port(self):
RNS.log("Opening serial port "+self.port+"...")
# Get device parameters
from usb4a import usb
device = usb.get_usb_device(self.port)
if device:
vid = device.getVendorId()
pid = device.getProductId()
# Driver overrides for speficic chips
proxy = self.pyserial.get_serial_port
if vid == 0x1A86 and pid == 0x55D4:
# Force CDC driver for Qinheng CH34x
RNS.log(str(self)+" using CDC driver for "+RNS.hexrep(vid)+":"+RNS.hexrep(pid), RNS.LOG_DEBUG)
from usbserial4a.cdcacmserial4a import CdcAcmSerial
proxy = CdcAcmSerial
self.serial = proxy(
self.port,
baudrate = self.speed,
bytesize = self.databits,
parity = self.parity,
stopbits = self.stopbits,
xonxoff = False,
rtscts = False,
timeout = None,
inter_byte_timeout = None,
# write_timeout = wtimeout,
dsrdtr = False,
)
if vid == 0x0403:
# Hardware parameters for FTDI devices @ 115200 baud
self.serial.DEFAULT_READ_BUFFER_SIZE = 16 * 1024
self.serial.USB_READ_TIMEOUT_MILLIS = 100
self.serial.timeout = 0.1
elif vid == 0x10C4:
# Hardware parameters for SiLabs CP210x @ 115200 baud
self.serial.DEFAULT_READ_BUFFER_SIZE = 64
self.serial.USB_READ_TIMEOUT_MILLIS = 12
self.serial.timeout = 0.012
elif vid == 0x1A86 and pid == 0x55D4:
# Hardware parameters for Qinheng CH34x @ 115200 baud
self.serial.DEFAULT_READ_BUFFER_SIZE = 64
self.serial.USB_READ_TIMEOUT_MILLIS = 12
self.serial.timeout = 0.1
else:
# Default values
self.serial.DEFAULT_READ_BUFFER_SIZE = 1 * 1024
self.serial.USB_READ_TIMEOUT_MILLIS = 100
self.serial.timeout = 0.1
RNS.log(str(self)+" USB read buffer size set to "+RNS.prettysize(self.serial.DEFAULT_READ_BUFFER_SIZE), RNS.LOG_DEBUG)
RNS.log(str(self)+" USB read timeout set to "+str(self.serial.USB_READ_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
RNS.log(str(self)+" USB write timeout set to "+str(self.serial.USB_WRITE_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
def configure_device(self):
# Allow time for interface to initialise before config
sleep(2.0)
thread = threading.Thread(target=self.readLoop)
thread.daemon = True
thread.start()
self.online = True
RNS.log("Serial port "+self.port+" is now open")
RNS.log("Configuring KISS interface parameters...")
self.setPreamble(self.preamble)
self.setTxTail(self.txtail)
self.setPersistence(self.persistence)
self.setSlotTime(self.slottime)
self.setFlowControl(self.flow_control)
self.interface_ready = True
RNS.log("KISS interface configured")
def setPreamble(self, preamble):
preamble_ms = preamble
preamble = int(preamble_ms / 10)
if preamble < 0:
preamble = 0
if preamble > 255:
preamble = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not configure KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")")
def setTxTail(self, txtail):
txtail_ms = txtail
txtail = int(txtail_ms / 10)
if txtail < 0:
txtail = 0
if txtail > 255:
txtail = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not configure KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")")
def setPersistence(self, persistence):
if persistence < 0:
persistence = 0
if persistence > 255:
persistence = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not configure KISS interface persistence to "+str(persistence))
def setSlotTime(self, slottime):
slottime_ms = slottime
slottime = int(slottime_ms / 10)
if slottime < 0:
slottime = 0
if slottime > 255:
slottime = 255
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("Could not configure KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")")
def setFlowControl(self, flow_control):
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
if (flow_control):
raise IOError("Could not enable KISS interface flow control")
else:
raise IOError("Could not enable KISS interface flow control")
def processIncoming(self, data):
self.rxb += len(data)
def af():
self.owner.inbound(data, self)
threading.Thread(target=af, daemon=True).start()
def processOutgoing(self,data):
datalen = len(data)
if self.online:
if self.interface_ready:
if self.flow_control:
self.interface_ready = False
self.flow_control_locked = time.time()
data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc]))
frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND])
written = self.serial.write(frame)
self.txb += datalen
if data == self.beacon_d:
self.first_tx = None
else:
if self.first_tx == None:
self.first_tx = time.time()
if written != len(frame):
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
else:
self.queue(data)
def queue(self, data):
self.packet_queue.append(data)
def process_queue(self):
if len(self.packet_queue) > 0:
data = self.packet_queue.pop(0)
self.interface_ready = True
self.processOutgoing(data)
elif len(self.packet_queue) == 0:
self.interface_ready = True
def readLoop(self):
try:
in_frame = False
escape = False
command = KISS.CMD_UNKNOWN
data_buffer = b""
last_read_ms = int(time.time()*1000)
while self.serial.is_open:
serial_bytes = self.serial.read()
got = len(serial_bytes)
for byte in serial_bytes:
last_read_ms = int(time.time()*1000)
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
in_frame = False
self.processIncoming(data_buffer)
elif (byte == KISS.FEND):
in_frame = True
command = KISS.CMD_UNKNOWN
data_buffer = b""
elif (in_frame and len(data_buffer) < self.HW_MTU):
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
# We only support one HDLC port for now, so
# strip off the port nibble
byte = byte & 0x0F
command = byte
elif (command == KISS.CMD_DATA):
if (byte == KISS.FESC):
escape = True
else:
if (escape):
if (byte == KISS.TFEND):
byte = KISS.FEND
if (byte == KISS.TFESC):
byte = KISS.FESC
escape = False
data_buffer = data_buffer+bytes([byte])
elif (command == KISS.CMD_READY):
self.process_queue()
if got == 0:
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
command = KISS.CMD_UNKNOWN
escape = False
sleep(0.05)
if self.flow_control:
if not self.interface_ready:
if time.time() > self.flow_control_locked + self.flow_control_timeout:
RNS.log("Interface "+str(self)+" is unlocking flow control due to time-out. This should not happen. Your hardware might have missed a flow-control READY command, or maybe it does not support flow-control.", RNS.LOG_WARNING)
self.process_queue()
if self.beacon_i != None and self.beacon_d != None:
if self.first_tx != None:
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)
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()
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))
def should_ingress_limit(self):
return False
def __str__(self):
return "KISSInterface["+self.name+"]"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,260 @@
# MIT License
#
# Copyright (c) 2016-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.
from RNS.Interfaces.Interface import Interface
from time import sleep
import sys
import threading
import time
import RNS
class HDLC():
# The Serial 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
class SerialInterface(Interface):
MAX_CHUNK = 32768
owner = None
port = None
speed = None
databits = None
parity = None
stopbits = None
serial = None
def __init__(self, owner, name, port, speed, databits, parity, stopbits):
import importlib
if RNS.vendor.platformutils.is_android():
self.on_android = True
if importlib.util.find_spec('usbserial4a') != None:
if importlib.util.find_spec('jnius') == None:
RNS.log("Could not load jnius API wrapper for Android, Serial interface cannot be created.", RNS.LOG_CRITICAL)
RNS.log("This probably means you are trying to use an USB-based interface from within Termux or similar.", RNS.LOG_CRITICAL)
RNS.log("This is currently not possible, due to this environment limiting access to the native Android APIs.", RNS.LOG_CRITICAL)
RNS.panic()
from usbserial4a import serial4a as serial
self.parity = "N"
else:
RNS.log("Could not load USB serial module for Android, Serial interface cannot be created.", RNS.LOG_CRITICAL)
RNS.log("You can install this module by issuing: pip install usbserial4a", RNS.LOG_CRITICAL)
RNS.panic()
else:
raise SystemError("Android-specific interface was used on non-Android OS")
super().__init__()
self.HW_MTU = 564
self.pyserial = serial
self.serial = None
self.owner = owner
self.name = name
self.port = port
self.speed = speed
self.databits = databits
self.parity = "N"
self.stopbits = stopbits
self.timeout = 100
self.online = False
self.bitrate = self.speed
if parity.lower() == "e" or parity.lower() == "even":
self.parity = "E"
if parity.lower() == "o" or parity.lower() == "odd":
self.parity = "O"
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 self.serial.is_open:
self.configure_device()
else:
raise IOError("Could not open serial port")
def open_port(self):
RNS.log("Opening serial port "+self.port+"...")
# Get device parameters
from usb4a import usb
device = usb.get_usb_device(self.port)
if device:
vid = device.getVendorId()
pid = device.getProductId()
# Driver overrides for speficic chips
proxy = self.pyserial.get_serial_port
if vid == 0x1A86 and pid == 0x55D4:
# Force CDC driver for Qinheng CH34x
RNS.log(str(self)+" using CDC driver for "+RNS.hexrep(vid)+":"+RNS.hexrep(pid), RNS.LOG_DEBUG)
from usbserial4a.cdcacmserial4a import CdcAcmSerial
proxy = CdcAcmSerial
self.serial = proxy(
self.port,
baudrate = self.speed,
bytesize = self.databits,
parity = self.parity,
stopbits = self.stopbits,
xonxoff = False,
rtscts = False,
timeout = None,
inter_byte_timeout = None,
# write_timeout = wtimeout,
dsrdtr = False,
)
if vid == 0x0403:
# Hardware parameters for FTDI devices @ 115200 baud
self.serial.DEFAULT_READ_BUFFER_SIZE = 16 * 1024
self.serial.USB_READ_TIMEOUT_MILLIS = 100
self.serial.timeout = 0.1
elif vid == 0x10C4:
# Hardware parameters for SiLabs CP210x @ 115200 baud
self.serial.DEFAULT_READ_BUFFER_SIZE = 64
self.serial.USB_READ_TIMEOUT_MILLIS = 12
self.serial.timeout = 0.012
elif vid == 0x1A86 and pid == 0x55D4:
# Hardware parameters for Qinheng CH34x @ 115200 baud
self.serial.DEFAULT_READ_BUFFER_SIZE = 64
self.serial.USB_READ_TIMEOUT_MILLIS = 12
self.serial.timeout = 0.1
else:
# Default values
self.serial.DEFAULT_READ_BUFFER_SIZE = 1 * 1024
self.serial.USB_READ_TIMEOUT_MILLIS = 100
self.serial.timeout = 0.1
RNS.log(str(self)+" USB read buffer size set to "+RNS.prettysize(self.serial.DEFAULT_READ_BUFFER_SIZE), RNS.LOG_DEBUG)
RNS.log(str(self)+" USB read timeout set to "+str(self.serial.USB_READ_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
RNS.log(str(self)+" USB write timeout set to "+str(self.serial.USB_WRITE_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
def configure_device(self):
sleep(0.5)
thread = threading.Thread(target=self.readLoop)
thread.daemon = True
thread.start()
self.online = True
RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
def processIncoming(self, data):
self.rxb += len(data)
def af():
self.owner.inbound(data, self)
threading.Thread(target=af, daemon=True).start()
def processOutgoing(self,data):
if self.online:
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
written = self.serial.write(data)
self.txb += len(data)
if written != len(data):
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
def readLoop(self):
try:
in_frame = False
escape = False
data_buffer = b""
last_read_ms = int(time.time()*1000)
while self.serial.is_open:
serial_bytes = self.serial.read()
got = len(serial_bytes)
for byte in serial_bytes:
last_read_ms = int(time.time()*1000)
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 got == 0:
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()
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))
def should_ingress_limit(self):
return False
def __str__(self):
return "SerialInterface["+self.name+"]"

View File

@ -0,0 +1,27 @@
# MIT License
#
# Copyright (c) 2016-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 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')]

View File

@ -21,8 +21,10 @@
# SOFTWARE.
from .Interface import Interface
from collections import deque
import socketserver
import threading
import re
import socket
import struct
import time
@ -41,25 +43,53 @@ class AutoInterface(Interface):
SCOPE_ORGANISATION = "8"
SCOPE_GLOBAL = "e"
MULTICAST_PERMANENT_ADDRESS_TYPE = "0"
MULTICAST_TEMPORARY_ADDRESS_TYPE = "1"
PEERING_TIMEOUT = 7.5
ALL_IGNORE_IFS = ["lo0"]
DARWIN_IGNORE_IFS = ["awdl0", "llw0", "lo0", "en5"]
ANDROID_IGNORE_IFS = ["dummy0", "lo", "tun0"]
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
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, name, group_id=None, discovery_scope=None, discovery_port=None, multicast_address_type=None, data_port=None, allowed_interfaces=None, ignored_interfaces=None, configured_bitrate=None):
from RNS.vendor.ifaddr import niwrapper
super().__init__()
self.netinfo = niwrapper
self.HW_MTU = 1064
@ -70,16 +100,26 @@ class AutoInterface(Interface):
self.peers = {}
self.link_local_addresses = []
self.adopted_interfaces = {}
self.interface_servers = {}
self.multicast_echoes = {}
self.timed_out_interfaces = {}
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
# Increase peering timeout on Android, due to potential
# low-power modes implemented on many chipsets.
if RNS.vendor.platformutils.is_android():
self.peering_timeout *= 3
if allowed_interfaces == None:
self.allowed_interfaces = []
else:
@ -100,6 +140,15 @@ class AutoInterface(Interface):
else:
self.discovery_port = discovery_port
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:
@ -128,106 +177,129 @@ 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:
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)
# 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 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)
# 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)
# Join multicast group
mcast_group = socket.inet_pton(socket.AF_INET6, mcast_addr) + if_struct
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mcast_group)
# Bind socket
addr_info = socket.getaddrinfo(mcast_addr+"%"+ifname, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
discovery_socket.bind(addr_info[0][4])
# Bind socket
if RNS.vendor.platformutils.is_windows():
# Set up thread for discovery packets
def discovery_loop():
self.discovery_handler(discovery_socket, ifname)
# window 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))
thread = threading.Thread(target=discovery_loop)
thread.setDaemon(True)
thread.start()
else:
suitable_interfaces += 1
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 discovery packets
def discovery_loop():
self.discovery_handler(discovery_socket, ifname)
thread = threading.Thread(target=discovery_loop)
thread.daemon = True
thread.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
if configured_bitrate != None:
self.bitrate = configured_bitrate
else:
self.bitrate = AutoInterface.BITRATE_GUESS
peering_wait = self.announce_interval*1.2
RNS.log(str(self)+" discovering peers for "+str(round(peering_wait, 2))+" seconds...", RNS.LOG_VERBOSE)
def 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
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]
self.server = socketserver.UDPServer(address, handlerFactory(self.processIncoming))
udp_server = socketserver.UDPServer(address, self.handler_factory(self.processIncoming))
self.interface_servers[ifname] = udp_server
thread = threading.Thread(target=self.server.serve_forever)
thread.setDaemon(True)
thread = threading.Thread(target=udp_server.serve_forever)
thread.daemon = True
thread.start()
job_thread = threading.Thread(target=self.peer_jobs)
job_thread.setDaemon(True)
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
@ -236,7 +308,7 @@ class AutoInterface(Interface):
self.announce_handler(ifname)
thread = threading.Thread(target=announce_loop)
thread.setDaemon(True)
thread.daemon = True
thread.start()
while True:
@ -266,16 +338,62 @@ class AutoInterface(Interface):
RNS.log(str(self)+" removed peer "+str(peer_addr)+" on "+str(removed_peer[0]), RNS.LOG_DEBUG)
for ifname in self.adopted_interfaces:
# Check that the link-local address has not changed
try:
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"])
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.processIncoming))
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]
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
@ -292,7 +410,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()
@ -326,16 +444,27 @@ class AutoInterface(Interface):
self.peers[addr][1] = time.time()
def processIncoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self)
data_hash = RNS.Identity.full_hash(data)
deque_hit = False
if data_hash in self.mif_deque:
for te in self.mif_deque_times:
if te[0] == data_hash and time.time() < te[1]+AutoInterface.MULTI_IF_DEQUE_TTL:
deque_hit = True
break
if not deque_hit:
self.mif_deque.append(data_hash)
self.mif_deque_times.append([data_hash, time.time()])
self.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self,data):
for peer in self.peers:
try:
if self.outbound_udp_socket == None:
self.outbound_udp_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
peer_addr = str(peer)+"%"+str(self.peers[peer][0])
peer_addr = str(peer)+"%"+str(self.interface_name_to_index(self.peers[peer][0]))
addr_info = socket.getaddrinfo(peer_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
self.outbound_udp_socket.sendto(data, addr_info[0][4])
@ -346,6 +475,11 @@ class AutoInterface(Interface):
self.txb += len(data)
# Until per-device sub-interfacing is implemented,
# ingress limiting should be disabled on AutoInterface
def should_ingress_limit(self):
return False
def __str__(self):
return "AutoInterface["+self.name+"]"

View File

@ -161,8 +161,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 +173,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 +383,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 +413,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,46 +482,40 @@ 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.setDaemon(True)
thread.daemon = True
thread.start()
def wait_job():
while self.awaiting_i2p_tunnel:
time.sleep(0.25)
time.sleep(2)
if not self.kiss_framing:
self.wants_tunnel = True
if not self.connect(initial=True):
thread = threading.Thread(target=self.reconnect)
thread.setDaemon(True)
thread.daemon = True
thread.start()
else:
thread = threading.Thread(target=self.read_loop)
thread.setDaemon(True)
thread.daemon = True
thread.start()
thread = threading.Thread(target=wait_job)
thread.setDaemon(True)
thread.daemon = True
thread.start()
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 +524,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 +590,6 @@ class I2PInterfacePeer(Interface):
return True
def reconnect(self):
if self.initiator:
if not self.reconnecting:
@ -600,7 +618,7 @@ class I2PInterfacePeer(Interface):
self.reconnecting = False
thread = threading.Thread(target=self.read_loop)
thread.setDaemon(True)
thread.daemon = True
thread.start()
if not self.kiss_framing:
RNS.Transport.synthesize_tunnel(self)
@ -632,6 +650,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 +661,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 +723,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
@ -705,6 +776,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()
@ -754,9 +830,8 @@ class I2PInterfacePeer(Interface):
class I2PInterface(Interface):
BITRATE_GUESS = 256*1000
def __init__(self, owner, name, rns_storagepath, peers, connectable = False):
self.rxb = 0
self.txb = 0
def __init__(self, owner, name, rns_storagepath, peers, connectable = False, ifac_size = 16, ifac_netname = None, ifac_netkey = None):
super().__init__()
self.HW_MTU = 1064
@ -780,11 +855,14 @@ 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.online = False
i2p_thread = threading.Thread(target=self.i2p.start)
i2p_thread.setDaemon(True)
i2p_thread.daemon = True
i2p_thread.start()
i2p_notready_warning = False
@ -809,7 +887,7 @@ class I2PInterface(Interface):
self.server = ThreadingI2PServer(self.address, handlerFactory(self.incoming_connection))
thread = threading.Thread(target=self.server.serve_forever)
thread.setDaemon(True)
thread.daemon = True
thread.start()
if self.connectable:
@ -828,7 +906,7 @@ class I2PInterface(Interface):
thread = threading.Thread(target=tunnel_job)
thread.setDaemon(True)
thread.daemon = True
thread.start()
if peers != None:
@ -850,9 +928,27 @@ class I2PInterface(Interface):
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
@ -866,6 +962,12 @@ class I2PInterface(Interface):
def processOutgoing(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 detach(self):
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
self.i2p.stop()

View File

@ -1,6 +1,6 @@
# MIT License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2023 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
@ -23,6 +23,7 @@
import RNS
import time
import threading
from collections import deque
class Interface:
IN = False
@ -39,18 +40,161 @@ 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 = 6
OA_FREQ_SAMPLES = 6
# 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.5
IC_BURST_FREQ = 12
IC_BURST_HOLD = 1*60
IC_BURST_PENALTY = 5*60
IC_HELD_RELEASE_INTERVAL = 30
def __init__(self):
self.rxb = 0
self.txb = 0
self.created = time.time()
self.online = False
self.bitrate = 1e6
self.ingress_control = True
self.ic_max_held_announces = Interface.MAX_HELD_ANNOUNCES
self.ic_burst_hold = Interface.IC_BURST_HOLD
self.ic_burst_active = False
self.ic_burst_activated = 0
self.ic_held_release = 0
self.ic_burst_freq_new = Interface.IC_BURST_FREQ_NEW
self.ic_burst_freq = Interface.IC_BURST_FREQ
self.ic_new_time = Interface.IC_NEW_TIME
self.ic_burst_penalty = Interface.IC_BURST_PENALTY
self.ic_held_release_interval = Interface.IC_HELD_RELEASE_INTERVAL
self.held_announces = {}
self.ia_freq_deque = deque(maxlen=Interface.IA_FREQ_SAMPLES)
self.oa_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:
self.ic_burst_active = False
self.ic_held_release = time.time() + self.ic_burst_penalty
return True
else:
if ia_freq > freq_threshold:
self.ic_burst_active = True
self.ic_burst_activated = time.time()
return True
else:
return False
else:
return False
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 not self.should_ingress_limit() and 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):
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):
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 incoming_announce_frequency(self):
if not len(self.ia_freq_deque) > 1:
return 0
else:
dq_len = len(self.ia_freq_deque)
delta_sum = 0
for i in range(1,dq_len):
delta_sum += self.ia_freq_deque[i]-self.ia_freq_deque[i-1]
delta_sum += time.time() - self.ia_freq_deque[dq_len-1]
if delta_sum == 0:
avg = 0
else:
avg = 1/(delta_sum/(dq_len))
return avg
def outgoing_announce_frequency(self):
if not len(self.oa_freq_deque) > 1:
return 0
else:
dq_len = len(self.oa_freq_deque)
delta_sum = 0
for i in range(1,dq_len):
delta_sum += self.oa_freq_deque[i]-self.oa_freq_deque[i-1]
delta_sum += time.time() - self.oa_freq_deque[dq_len-1]
if delta_sum == 0:
avg = 0
else:
avg = 1/(delta_sum/(dq_len))
return avg
def process_announce_queue(self):
if not hasattr(self, "announce_cap"):
self.announce_cap = RNS.Reticulum.ANNOUNCE_CAP
@ -79,6 +223,7 @@ class Interface:
self.announce_allowed_at = now + wait_time
self.processOutgoing(selected["raw"])
self.sent_announce()
if selected in self.announce_queue:
self.announce_queue.remove(selected)

View File

@ -70,8 +70,7 @@ 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__()
self.HW_MTU = 564
@ -144,7 +143,7 @@ class KISSInterface(Interface):
# Allow time for interface to initialise before config
sleep(2.0)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True)
thread.daemon = True
thread.start()
self.online = True
RNS.log("Serial port "+self.port+" is now open")
@ -349,5 +348,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+"]"

View File

@ -1,6 +1,6 @@
# MIT License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2023 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
@ -28,6 +28,7 @@ import time
import sys
import os
import RNS
from threading import Lock
class HDLC():
FLAG = 0x7E
@ -41,14 +42,19 @@ 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
def __init__(self, owner, name, target_port = None, connected_socket=None):
self.rxb = 0
self.txb = 0
super().__init__()
# TODO: Remove at some point
# self.rxptime = 0
@ -72,6 +78,7 @@ class LocalClientInterface(Interface):
self.target_ip = None
self.target_port = None
self.socket = connected_socket
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.is_connected_to_shared_instance = False
@ -86,17 +93,23 @@ class LocalClientInterface(Interface):
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.setDaemon(True)
thread.daemon = True
thread.start()
def should_ingress_limit(self):
return False
def connect(self):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.socket.connect((self.target_ip, self.target_port))
self.online = True
@ -127,9 +140,12 @@ class LocalClientInterface(Interface):
self.reconnecting = False
thread = threading.Thread(target=self.read_loop)
thread.setDaemon(True)
thread.daemon = True
thread.start()
RNS.Transport.shared_connection_reappeared()
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)
@ -154,6 +170,16 @@ class LocalClientInterface(Interface):
if self.online:
try:
self.writing = True
if self._force_bitrate:
if not hasattr(self, "send_lock"):
self.send_lock = Lock()
with self.send_lock:
s = len(data) / self.bitrate * 8
RNS.log(f"Simulating latency of {RNS.prettytime(s)} for {len(data)} bytes")
time.sleep(s)
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
self.socket.sendall(data)
self.writing = False
@ -246,6 +272,8 @@ class LocalClientInterface(Interface):
RNS.Transport.local_client_interfaces.remove(self)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.clients -= 1
if hasattr(RNS.Transport, "owner") and RNS.Transport.owner != None:
RNS.Transport.owner._should_persist_data()
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)
@ -266,8 +294,7 @@ class LocalClientInterface(Interface):
class LocalServerInterface(Interface):
def __init__(self, owner, bindport=None):
self.rxb = 0
self.txb = 0
super().__init__()
self.online = False
self.clients = 0
@ -291,11 +318,10 @@ class LocalServerInterface(Interface):
address = (self.bind_ip, self.bind_port)
ThreadingTCPServer.allow_reuse_address = True
self.server = ThreadingTCPServer(address, handlerFactory(self.incoming_connection))
thread = threading.Thread(target=self.server.serve_forever)
thread.setDaemon(True)
thread.daemon = True
thread.start()
self.announce_rate_target = None
@ -316,7 +342,9 @@ class LocalServerInterface(Interface):
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)
if hasattr(self, "_force_bitrate"):
spawned_interface._force_bitrate = self._force_bitrate
# RNS.log("Accepting new connection to shared instance: "+str(spawned_interface), RNS.LOG_EXTREME)
RNS.Transport.interfaces.append(spawned_interface)
RNS.Transport.local_client_interfaces.append(spawned_interface)
self.clients += 1
@ -325,6 +353,12 @@ class LocalServerInterface(Interface):
def processOutgoing(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 __str__(self):
return "Shared Instance["+str(self.bind_port)+"]"

View File

@ -54,8 +54,7 @@ class PipeInterface(Interface):
if respawn_delay == None:
respawn_delay = 5
self.rxb = 0
self.txb = 0
super().__init__()
self.HW_MTU = 1064
@ -96,7 +95,7 @@ class PipeInterface(Interface):
def configure_pipe(self):
sleep(0.01)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True)
thread.daemon = True
thread.start()
self.online = True
RNS.log("Subprocess pipe for "+str(self)+" is now connected", RNS.LOG_VERBOSE)

View File

@ -1,6 +1,6 @@
# MIT License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2023 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
@ -43,14 +43,23 @@ class KISS():
CMD_CR = 0x05
CMD_RADIO_STATE = 0x06
CMD_RADIO_LOCK = 0x07
CMD_ST_ALOCK = 0x0B
CMD_LT_ALOCK = 0x0C
CMD_DETECT = 0x08
CMD_LEAVE = 0x0A
CMD_READY = 0x0F
CMD_STAT_RX = 0x21
CMD_STAT_TX = 0x22
CMD_STAT_RSSI = 0x23
CMD_STAT_SNR = 0x24
CMD_STAT_CHTM = 0x25
CMD_STAT_PHYPRM = 0x26
CMD_BLINK = 0x30
CMD_RANDOM = 0x40
CMD_FB_EXT = 0x41
CMD_FB_READ = 0x42
CMD_FB_WRITE = 0x43
CMD_BT_CTRL = 0x46
CMD_PLATFORM = 0x48
CMD_MCU = 0x49
CMD_FW_VERSION = 0x50
@ -82,25 +91,26 @@ class KISS():
class RNodeInterface(Interface):
MAX_CHUNK = 32768
owner = None
port = None
speed = None
databits = None
parity = None
stopbits = None
serial = None
FREQ_MIN = 137000000
FREQ_MAX = 1020000000
FREQ_MAX = 3000000000
RSSI_OFFSET = 157
CALLSIGN_MAX_LEN = 32
REQUIRED_FW_VER_MAJ = 1
REQUIRED_FW_VER_MIN = 26
REQUIRED_FW_VER_MIN = 52
RECONNECT_WAIT = 5
Q_SNR_MIN_BASE = -9
Q_SNR_MAX = 6
Q_SNR_STEP = 2
def __init__(self, owner, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, flow_control = False, id_interval = None, id_callsign = None, st_alock = None, lt_alock = None):
if RNS.vendor.platformutils.is_android():
raise SystemError("Invalid interface type. The Android-specific RNode interface must be used on Android")
def __init__(self, owner, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, flow_control = False, id_interval = None, id_callsign = None):
import importlib
if importlib.util.find_spec('serial') != None:
import serial
@ -109,8 +119,7 @@ class RNodeInterface(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__()
self.HW_MTU = 508
@ -121,10 +130,11 @@ class RNodeInterface(Interface):
self.port = port
self.speed = 115200
self.databits = 8
self.parity = serial.PARITY_NONE
self.stopbits = 1
self.timeout = 100
self.online = False
self.detached = False
self.reconnecting= False
self.frequency = frequency
self.bandwidth = bandwidth
@ -133,7 +143,10 @@ class RNodeInterface(Interface):
self.cr = cr
self.state = KISS.RADIO_STATE_OFF
self.bitrate = 0
self.st_alock = st_alock
self.lt_alock = lt_alock
self.platform = None
self.display = None
self.mcu = None
self.detected = False
self.firmware_ok = False
@ -142,6 +155,7 @@ class RNodeInterface(Interface):
self.last_id = 0
self.first_tx = None
self.reconnect_w = RNodeInterface.RECONNECT_WAIT
self.r_frequency = None
self.r_bandwidth = None
@ -153,26 +167,38 @@ class RNodeInterface(Interface):
self.r_stat_rx = None
self.r_stat_tx = None
self.r_stat_rssi = None
self.r_stat_snr = None
self.r_st_alock = None
self.r_lt_alock = None
self.r_random = None
self.r_airtime_short = 0.0
self.r_airtime_long = 0.0
self.r_channel_load_short = 0.0
self.r_channel_load_long = 0.0
self.r_symbol_time_ms = None
self.r_symbol_rate = None
self.r_preamble_symbols = None
self.r_premable_time_ms = None
self.packet_queue = []
self.flow_control = flow_control
self.interface_ready = False
self.announce_rate_target = None
self.validcfg = True
if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
if (self.txpower < 0 or self.txpower > 17):
if (self.txpower < 0 or self.txpower > 22):
RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
if (self.bandwidth < 7800 or self.bandwidth > 500000):
if (self.bandwidth < 7800 or self.bandwidth > 1625000):
RNS.log("Invalid bandwidth configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
if (self.sf < 7 or self.sf > 12):
if (self.sf < 5 or self.sf > 12):
RNS.log("Invalid spreading factor configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
@ -180,6 +206,14 @@ class RNodeInterface(Interface):
RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
if (self.st_alock and (self.st_alock < 0.0 or self.st_alock > 100.0)):
RNS.log("Invalid short-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
if (self.lt_alock and (self.lt_alock < 0.0 or self.lt_alock > 100.0)):
RNS.log("Invalid long-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
self.validcfg = False
if id_interval != None and id_callsign != None:
if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN):
self.should_id = True
@ -197,14 +231,21 @@ class RNodeInterface(Interface):
try:
self.open_port()
if self.serial.is_open:
self.configure_device()
else:
raise IOError("Could not open serial port")
except Exception as e:
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
raise e
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR)
if not self.detached and not self.reconnecting:
thread = threading.Thread(target=self.reconnect_port)
thread.daemon = True
thread.start()
if self.serial.is_open:
self.configure_device()
else:
raise IOError("Could not open serial port")
def open_port(self):
RNS.log("Opening serial port "+self.port+"...")
@ -212,7 +253,7 @@ class RNodeInterface(Interface):
port = self.port,
baudrate = self.speed,
bytesize = self.databits,
parity = self.parity,
parity = self.pyserial.PARITY_NONE,
stopbits = self.stopbits,
xonxoff = False,
rtscts = False,
@ -224,35 +265,42 @@ class RNodeInterface(Interface):
def configure_device(self):
self.r_frequency = None
self.r_bandwidth = None
self.r_txpower = None
self.r_sf = None
self.r_cr = None
self.r_state = None
self.r_lock = None
sleep(2.0)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True)
thread.daemon = True
thread.start()
self.detect()
sleep(0.1)
sleep(0.2)
if not self.detected:
raise IOError("Could not detect device")
RNS.log("Could not detect device for "+str(self), RNS.LOG_ERROR)
self.serial.close()
else:
if self.platform == KISS.PLATFORM_ESP32:
RNS.log("Resetting ESP32-based device before configuration...", RNS.LOG_VERBOSE)
self.hard_reset()
self.display = True
self.online = True
RNS.log("Serial port "+self.port+" is now open")
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
self.initRadio()
if (self.validateRadioState()):
self.interface_ready = True
RNS.log(str(self)+" is configured and powered up")
sleep(1.0)
sleep(0.3)
self.online = True
else:
RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
self.serial.close()
raise IOError("RNode interface did not pass configuration validation")
def initRadio(self):
@ -261,13 +309,59 @@ class RNodeInterface(Interface):
self.setTXPower()
self.setSpreadingFactor()
self.setCodingRate()
self.setSTALock()
self.setLTALock()
self.setRadioState(KISS.RADIO_STATE_ON)
def detect(self):
kiss_command = bytes([KISS.FEND, KISS.CMD_DETECT, KISS.DETECT_REQ, KISS.FEND, KISS.CMD_FW_VERSION, 0x00, KISS.FEND, KISS.CMD_PLATFORM, 0x00, KISS.FEND, KISS.CMD_MCU, 0x00, KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while detecting hardware for "+self(str))
raise IOError("An IO error occurred while detecting hardware for "+str(self))
def leave(self):
kiss_command = bytes([KISS.FEND, KISS.CMD_LEAVE, 0xFF, KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while sending host left command to device")
def enable_external_framebuffer(self):
if self.display != None:
kiss_command = bytes([KISS.FEND, KISS.CMD_FB_EXT, 0x01, KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while enabling external framebuffer on device")
def disable_external_framebuffer(self):
if self.display != None:
kiss_command = bytes([KISS.FEND, KISS.CMD_FB_EXT, 0x00, KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while disabling external framebuffer on device")
FB_PIXEL_WIDTH = 64
FB_BITS_PER_PIXEL = 1
FB_PIXELS_PER_BYTE = 8//FB_BITS_PER_PIXEL
FB_BYTES_PER_LINE = FB_PIXEL_WIDTH//FB_PIXELS_PER_BYTE
def display_image(self, imagedata):
if self.display != None:
lines = len(imagedata)//8
for line in range(lines):
line_start = line*RNodeInterface.FB_BYTES_PER_LINE
line_end = line_start+RNodeInterface.FB_BYTES_PER_LINE
line_data = bytes(imagedata[line_start:line_end])
self.write_framebuffer(line, line_data)
def write_framebuffer(self, line, line_data):
if self.display != None:
line_byte = line.to_bytes(1, byteorder="big", signed=False)
data = line_byte+line_data
escaped_data = KISS.escape(data)
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FB_WRITE])+escaped_data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while writing framebuffer data device")
def hard_reset(self):
kiss_command = bytes([KISS.FEND, KISS.CMD_RESET, 0xf8, KISS.FEND])
@ -286,7 +380,7 @@ class RNodeInterface(Interface):
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while configuring frequency for "+self(str))
raise IOError("An IO error occurred while configuring frequency for "+str(self))
def setBandwidth(self):
c1 = self.bandwidth >> 24
@ -298,35 +392,59 @@ class RNodeInterface(Interface):
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while configuring bandwidth for "+self(str))
raise IOError("An IO error occurred while configuring bandwidth for "+str(self))
def setTXPower(self):
txp = bytes([self.txpower])
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while configuring TX power for "+self(str))
raise IOError("An IO error occurred while configuring TX power for "+str(self))
def setSpreadingFactor(self):
sf = bytes([self.sf])
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while configuring spreading factor for "+self(str))
raise IOError("An IO error occurred while configuring spreading factor for "+str(self))
def setCodingRate(self):
cr = bytes([self.cr])
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while configuring coding rate for "+self(str))
raise IOError("An IO error occurred while configuring coding rate for "+str(self))
def setSTALock(self):
if self.st_alock != None:
at = int(self.st_alock*100)
c1 = at >> 8 & 0xFF
c2 = at & 0xFF
data = KISS.escape(bytes([c1])+bytes([c2]))
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_ST_ALOCK])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while configuring short-term airtime limit for "+str(self))
def setLTALock(self):
if self.lt_alock != None:
at = int(self.lt_alock*100)
c1 = at >> 8 & 0xFF
c2 = at & 0xFF
data = KISS.escape(bytes([c1])+bytes([c2]))
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_LT_ALOCK])+data+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while configuring long-term airtime limit for "+str(self))
def setRadioState(self, state):
self.state = state
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
written = self.serial.write(kiss_command)
if written != len(kiss_command):
raise IOError("An IO error occurred while configuring radio state for "+self(str))
raise IOError("An IO error occurred while configuring radio state for "+str(self))
def validate_firmware(self):
if (self.maj_version >= RNodeInterface.REQUIRED_FW_VER_MAJ):
@ -338,14 +456,16 @@ class RNodeInterface(Interface):
RNS.log("The firmware version of the connected RNode is "+str(self.maj_version)+"."+str(self.min_version), RNS.LOG_ERROR)
RNS.log("This version of Reticulum requires at least version "+str(RNodeInterface.REQUIRED_FW_VER_MAJ)+"."+str(RNodeInterface.REQUIRED_FW_VER_MIN), RNS.LOG_ERROR)
RNS.log("Please update your RNode firmware with rnodeconf (https://github.com/markqvist/rnodeconfigutil/)")
RNS.log("Please update your RNode firmware with rnodeconf from https://github.com/markqvist/rnodeconfigutil/")
RNS.panic()
def validateRadioState(self):
RNS.log("Wating for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
RNS.log("Waiting for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
sleep(0.25);
if (self.frequency != self.r_frequency):
self.validcfg = True
if (self.r_frequency != None and abs(self.frequency - int(self.r_frequency)) > 100):
RNS.log("Frequency mismatch", RNS.LOG_ERROR)
self.validcfg = False
if (self.bandwidth != self.r_bandwidth):
@ -556,6 +676,96 @@ class RNodeInterface(Interface):
self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET
elif (command == KISS.CMD_STAT_SNR):
self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25
try:
sfs = self.r_sf-7
snr = self.r_stat_snr
q_snr_min = RNodeInterface.Q_SNR_MIN_BASE-sfs*RNodeInterface.Q_SNR_STEP
q_snr_max = RNodeInterface.Q_SNR_MAX
q_snr_span = q_snr_max-q_snr_min
quality = round(((snr-q_snr_min)/(q_snr_span))*100,1)
if quality > 100.0: quality = 100.0
if quality < 0.0: quality = 0.0
self.r_stat_q = quality
except:
pass
elif (command == KISS.CMD_ST_ALOCK):
if (byte == KISS.FESC):
escape = True
else:
if (escape):
if (byte == KISS.TFEND):
byte = KISS.FEND
if (byte == KISS.TFESC):
byte = KISS.FESC
escape = False
command_buffer = command_buffer+bytes([byte])
if (len(command_buffer) == 2):
at = command_buffer[0] << 8 | command_buffer[1]
self.r_st_alock = at/100.0
RNS.log(str(self)+" Radio reporting short-term airtime limit is "+str(self.r_st_alock)+"%", RNS.LOG_DEBUG)
elif (command == KISS.CMD_LT_ALOCK):
if (byte == KISS.FESC):
escape = True
else:
if (escape):
if (byte == KISS.TFEND):
byte = KISS.FEND
if (byte == KISS.TFESC):
byte = KISS.FESC
escape = False
command_buffer = command_buffer+bytes([byte])
if (len(command_buffer) == 2):
at = command_buffer[0] << 8 | command_buffer[1]
self.r_lt_alock = at/100.0
RNS.log(str(self)+" Radio reporting long-term airtime limit is "+str(self.r_lt_alock)+"%", RNS.LOG_DEBUG)
elif (command == KISS.CMD_STAT_CHTM):
if (byte == KISS.FESC):
escape = True
else:
if (escape):
if (byte == KISS.TFEND):
byte = KISS.FEND
if (byte == KISS.TFESC):
byte = KISS.FESC
escape = False
command_buffer = command_buffer+bytes([byte])
if (len(command_buffer) == 8):
ats = command_buffer[0] << 8 | command_buffer[1]
atl = command_buffer[2] << 8 | command_buffer[3]
cus = command_buffer[4] << 8 | command_buffer[5]
cul = command_buffer[6] << 8 | command_buffer[7]
self.r_airtime_short = ats/100.0
self.r_airtime_long = atl/100.0
self.r_channel_load_short = cus/100.0
self.r_channel_load_long = cul/100.0
elif (command == KISS.CMD_STAT_PHYPRM):
if (byte == KISS.FESC):
escape = True
else:
if (escape):
if (byte == KISS.TFEND):
byte = KISS.FEND
if (byte == KISS.TFESC):
byte = KISS.FESC
escape = False
command_buffer = command_buffer+bytes([byte])
if (len(command_buffer) == 10):
lst = (command_buffer[0] << 8 | command_buffer[1])/1000.0
lsr = command_buffer[2] << 8 | command_buffer[3]
prs = command_buffer[4] << 8 | command_buffer[5]
prt = command_buffer[6] << 8 | command_buffer[7]
cst = command_buffer[8] << 8 | command_buffer[9]
if lst != self.r_symbol_time_ms or lsr != self.r_symbol_rate or prs != self.r_preamble_symbols or prt != self.r_premable_time_ms or cst != self.r_csma_slot_time_ms:
self.r_symbol_time_ms = lst
self.r_symbol_rate = lsr
self.r_preamble_symbols = prs
self.r_premable_time_ms = prt
self.r_csma_slot_time_ms = cst
RNS.log(str(self)+" Radio reporting symbol time is "+str(round(self.r_symbol_time_ms,2))+"ms (at "+str(self.r_symbol_rate)+" baud)", RNS.LOG_DEBUG)
RNS.log(str(self)+" Radio reporting preamble is "+str(self.r_preamble_symbols)+" symbols ("+str(self.r_premable_time_ms)+"ms)", RNS.LOG_DEBUG)
RNS.log(str(self)+" Radio reporting CSMA slot time is "+str(self.r_csma_slot_time_ms)+"ms", RNS.LOG_DEBUG)
elif (command == KISS.CMD_RANDOM):
self.r_random = byte
elif (command == KISS.CMD_PLATFORM):
@ -566,7 +776,7 @@ class RNodeInterface(Interface):
if (byte == KISS.ERROR_INITRADIO):
RNS.log(str(self)+" hardware initialisation error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
raise IOError("Radio initialisation failure")
elif (byte == KISS.ERROR_INITRADIO):
elif (byte == KISS.ERROR_TXFAILED):
RNS.log(str(self)+" hardware TX error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
raise IOError("Hardware transmit failure")
else:
@ -589,7 +799,7 @@ class RNodeInterface(Interface):
else:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
RNS.log(str(self)+" serial read timeout", RNS.LOG_DEBUG)
RNS.log(str(self)+" serial read timeout in command "+str(command), RNS.LOG_WARNING)
data_buffer = b""
in_frame = False
command = KISS.CMD_UNKNOWN
@ -614,13 +824,19 @@ class RNodeInterface(Interface):
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
self.online = False
self.serial.close()
self.reconnect_port()
try:
self.serial.close()
except Exception as e:
pass
if not self.detached and not self.reconnecting:
self.reconnect_port()
def reconnect_port(self):
while not self.online:
self.reconnecting = True
while not self.online and not self.detached:
try:
time.sleep(3.5)
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:
@ -628,8 +844,18 @@ class RNodeInterface(Interface):
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))
self.reconnecting = False
if self.online:
RNS.log("Reconnected serial port for "+str(self))
def detach(self):
self.detached = True
self.disable_external_framebuffer()
self.setRadioState(KISS.RADIO_STATE_OFF)
self.leave()
def should_ingress_limit(self):
return False
def __str__(self):
return "RNodeInterface["+str(self.name)+"]"

View File

@ -60,8 +60,7 @@ 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__()
self.HW_MTU = 564
@ -116,7 +115,7 @@ class SerialInterface(Interface):
def configure_device(self):
sleep(0.5)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True)
thread.daemon = True
thread.start()
self.online = True
RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
@ -201,5 +200,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+"]"

View File

@ -70,14 +70,16 @@ class TCPClientInterface(Interface):
TCP_PROBE_INTERVAL = 2
TCP_PROBES = 12
INITIAL_CONNECT_TIMEOUT = 5
SYNCHRONOUS_START = True
I2P_USER_TIMEOUT = 45
I2P_PROBE_AFTER = 10
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):
self.rxb = 0
self.txb = 0
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):
super().__init__()
self.HW_MTU = 1064
@ -114,23 +116,37 @@ 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
self.target_port = target_port
self.initiator = True
if not self.connect(initial=True):
thread = threading.Thread(target=self.reconnect)
thread.setDaemon(True)
thread.start()
else:
thread = threading.Thread(target=self.read_loop)
thread.setDaemon(True)
thread.start()
if not self.kiss_framing:
self.wants_tunnel = True
if connect_timeout != None:
self.connect_timeout = connect_timeout
else:
self.connect_timeout = TCPClientInterface.INITIAL_CONNECT_TIMEOUT
if TCPClientInterface.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:
thread = threading.Thread(target=self.read_loop)
thread.daemon = True
thread.start()
if not self.kiss_framing:
self.wants_tunnel = True
def set_timeouts_linux(self):
if not self.i2p_tunneled:
@ -181,9 +197,18 @@ class TCPClientInterface(Interface):
def connect(self, initial=False):
try:
if initial:
RNS.log("Establishing TCP connection for "+str(self)+"...", RNS.LOG_DEBUG)
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(TCPClientInterface.INITIAL_CONNECT_TIMEOUT)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.socket.connect((self.target_ip, self.target_port))
self.socket.settimeout(None)
self.online = True
if initial:
RNS.log("TCP connection for "+str(self)+" established", RNS.LOG_DEBUG)
except Exception as e:
if initial:
@ -231,7 +256,7 @@ class TCPClientInterface(Interface):
self.reconnecting = False
thread = threading.Thread(target=self.read_loop)
thread.setDaemon(True)
thread.daemon = True
thread.start()
if not self.kiss_framing:
RNS.Transport.synthesize_tunnel(self)
@ -385,29 +410,18 @@ class TCPServerInterface(Interface):
@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()
import RNS.vendor.ifaddr.niwrapper as 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()
import RNS.vendor.ifaddr.niwrapper as netinfo
ifaddr = netinfo.ifaddresses(name)
return ifaddr[netinfo.AF_INET][0]["broadcast"]
def __init__(self, owner, name, device=None, bindip=None, bindport=None, i2p_tunneled=False):
self.rxb = 0
self.txb = 0
super().__init__()
self.HW_MTU = 1064
@ -417,6 +431,7 @@ class TCPServerInterface(Interface):
self.IN = True
self.OUT = False
self.name = name
self.detached = False
self.i2p_tunneled = i2p_tunneled
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
@ -443,7 +458,7 @@ class TCPServerInterface(Interface):
self.bitrate = TCPServerInterface.BITRATE_GUESS
thread = threading.Thread(target=self.server.serve_forever)
thread.setDaemon(True)
thread.daemon = True
thread.start()
self.online = True
@ -459,9 +474,27 @@ class TCPServerInterface(Interface):
spawned_interface.target_port = str(handler.client_address[1])
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
@ -473,12 +506,34 @@ class TCPServerInterface(Interface):
self.clients += 1
spawned_interface.read_loop()
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 processOutgoing(self, data):
pass
def detach(self):
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 = None
except Exception as e:
RNS.log("Error while shutting down server for "+str(self)+": "+str(e))
def __str__(self):
return "TCPServerInterface["+self.name+"/"+self.bind_ip+":"+str(self.bind_port)+"]"
class TCPInterfaceHandler(socketserver.BaseRequestHandler):
def __init__(self, callback, *args, **keys):
self.callback = callback

View File

@ -34,29 +34,18 @@ class UDPInterface(Interface):
@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()
import RNS.vendor.ifaddr.niwrapper as 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()
import RNS.vendor.ifaddr.niwrapper as 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
super().__init__()
self.HW_MTU = 1064
@ -89,7 +78,7 @@ class UDPInterface(Interface):
self.server = socketserver.UDPServer(address, handlerFactory(self.processIncoming))
thread = threading.Thread(target=self.server.serve_forever)
thread.setDaemon(True)
thread.daemon = True
thread.start()
self.online = True

View File

@ -22,6 +22,7 @@
import os
import glob
import RNS.Interfaces.Android
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]

View File

@ -1,6 +1,6 @@
# MIT License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -22,10 +22,12 @@
from RNS.Cryptography import X25519PrivateKey, X25519PublicKey, Ed25519PrivateKey, Ed25519PublicKey
from RNS.Cryptography import Fernet
from RNS.Channel import Channel, LinkChannelOutlet
from time import sleep
from .vendor import umsgpack as umsgpack
import threading
import inspect
import math
import time
import RNS
@ -45,7 +47,7 @@ class Link:
"""
This class is used to establish and manage links to other peers. When a
link instance is created, Reticulum will attempt to establish verified
connectivity with the specified destination.
and encrypted connectivity with the specified destination.
:param destination: A :ref:`RNS.Destination<api-destination>` instance which to establish a link to.
:param established_callback: An optional function or method with the signature *callback(link)* to be called when the link has been established.
@ -110,9 +112,10 @@ class Link:
link = Link(owner = owner, peer_pub_bytes=data[:Link.ECPUBSIZE//2], peer_sig_pub_bytes=data[Link.ECPUBSIZE//2:Link.ECPUBSIZE])
link.set_link_id(packet)
link.destination = packet.destination
link.establishment_timeout = Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, packet.hops)
link.establishment_timeout = Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, packet.hops) + Link.KEEPALIVE
link.establishment_cost += len(packet.raw)
RNS.log("Validating link request "+RNS.prettyhexrep(link.link_id), RNS.LOG_VERBOSE)
RNS.log(f"Establishment timeout is {RNS.prettytime(link.establishment_timeout)} for incoming link request "+RNS.prettyhexrep(link.link_id), RNS.LOG_EXTREME)
link.handshake()
link.attached_interface = packet.receiving_interface
link.prove()
@ -121,7 +124,7 @@ class Link:
link.last_inbound = time.time()
link.start_watchdog()
RNS.log("Incoming link request "+str(link)+" accepted", RNS.LOG_VERBOSE)
RNS.log("Incoming link request "+str(link)+" accepted", RNS.LOG_DEBUG)
return link
except Exception as e:
@ -130,7 +133,7 @@ class Link:
return None
else:
RNS.log("Invalid link request payload size, dropping request", RNS.LOG_VERBOSE)
RNS.log("Invalid link request payload size, dropping request", RNS.LOG_DEBUG)
return None
@ -146,10 +149,15 @@ class Link:
self.pending_requests = []
self.last_inbound = 0
self.last_outbound = 0
self.last_proof = 0
self.last_data = 0
self.tx = 0
self.rx = 0
self.txbytes = 0
self.rxbytes = 0
self.rssi = None
self.snr = None
self.q = None
self.traffic_timeout_factor = Link.TRAFFIC_TIMEOUT_FACTOR
self.keepalive_timeout_factor = Link.KEEPALIVE_TIMEOUT_FACTOR
self.keepalive = Link.KEEPALIVE
@ -160,15 +168,21 @@ class Link:
self.type = RNS.Destination.LINK
self.owner = owner
self.destination = destination
self.expected_hops = None
self.attached_interface = None
self.__remote_identity = None
self.__track_phy_stats = False
self._channel = None
if self.destination == None:
self.initiator = False
self.prv = self.owner.identity.prv
self.prv = X25519PrivateKey.generate()
self.sig_prv = self.owner.identity.sig_prv
else:
self.initiator = True
self.establishment_timeout = Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, RNS.Transport.hops_to(destination.hash))
self.expected_hops = RNS.Transport.hops_to(self.destination.hash)
self.establishment_timeout = RNS.Reticulum.get_instance().get_first_hop_timeout(destination.hash)
self.establishment_timeout += Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, RNS.Transport.hops_to(destination.hash))
self.prv = X25519PrivateKey.generate()
self.sig_prv = Ed25519PrivateKey.generate()
@ -193,21 +207,18 @@ class Link:
self.set_link_closed_callback(closed_callback)
if (self.initiator):
peer_pub_bytes = self.destination.identity.get_public_key()[:Link.ECPUBSIZE//2]
peer_sig_pub_bytes = self.destination.identity.get_public_key()[Link.ECPUBSIZE//2:Link.ECPUBSIZE]
self.request_data = self.pub_bytes+self.sig_pub_bytes
self.packet = RNS.Packet(destination, self.request_data, packet_type=RNS.Packet.LINKREQUEST)
self.packet.pack()
self.establishment_cost += len(self.packet.raw)
self.set_link_id(self.packet)
self.load_peer(peer_pub_bytes, peer_sig_pub_bytes)
self.handshake()
RNS.Transport.register_link(self)
self.request_time = time.time()
self.start_watchdog()
self.packet.send()
self.had_outbound()
RNS.log("Link request "+RNS.prettyhexrep(self.link_id)+" sent to "+str(self.destination), RNS.LOG_DEBUG)
RNS.log(f"Establishment timeout is {RNS.prettytime(self.establishment_timeout)} for link request "+RNS.prettyhexrep(self.link_id), RNS.LOG_EXTREME)
def load_peer(self, peer_pub_bytes, peer_sig_pub_bytes):
@ -225,22 +236,25 @@ class Link:
self.hash = self.link_id
def handshake(self):
self.status = Link.HANDSHAKE
self.shared_key = self.prv.exchange(self.peer_pub)
if self.status == Link.PENDING and self.prv != None:
self.status = Link.HANDSHAKE
self.shared_key = self.prv.exchange(self.peer_pub)
self.derived_key = RNS.Cryptography.hkdf(
length=32,
derive_from=self.shared_key,
salt=self.get_salt(),
context=self.get_context(),
)
self.derived_key = RNS.Cryptography.hkdf(
length=32,
derive_from=self.shared_key,
salt=self.get_salt(),
context=self.get_context(),
)
else:
RNS.log("Handshake attempt on "+str(self)+" with invalid state "+str(self.status), RNS.LOG_ERROR)
def prove(self):
signed_data = self.link_id+self.pub_bytes+self.sig_pub_bytes
signature = self.owner.identity.sign(signed_data)
proof_data = signature
proof_data = signature+self.pub_bytes
proof = RNS.Packet(self, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.LRPROOF)
proof.send()
self.establishment_cost += len(proof.raw)
@ -261,31 +275,50 @@ class Link:
self.had_outbound()
def validate_proof(self, packet):
if self.status == Link.HANDSHAKE:
if self.initiator and len(packet.data) == RNS.Identity.SIGLENGTH//8:
self.establishment_cost += len(packet.raw)
signed_data = self.link_id+self.peer_pub_bytes+self.peer_sig_pub_bytes
signature = packet.data[:RNS.Identity.SIGLENGTH//8]
if self.destination.identity.validate(signature, signed_data):
self.rtt = time.time() - self.request_time
self.attached_interface = packet.receiving_interface
self.__remote_identity = self.destination.identity
RNS.Transport.activate_link(self)
RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+str(round(self.rtt, 3))+"s", RNS.LOG_VERBOSE)
rtt_data = umsgpack.packb(self.rtt)
rtt_packet = RNS.Packet(self, rtt_data, context=RNS.Packet.LRRTT)
rtt_packet.send()
self.had_outbound()
try:
if self.status == Link.PENDING:
if self.initiator and len(packet.data) == RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2:
peer_pub_bytes = packet.data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2]
peer_sig_pub_bytes = self.destination.identity.get_public_key()[Link.ECPUBSIZE//2:Link.ECPUBSIZE]
self.load_peer(peer_pub_bytes, peer_sig_pub_bytes)
self.handshake()
self.status = Link.ACTIVE
self.activated_at = time.time()
if self.callbacks.link_established != None:
thread = threading.Thread(target=self.callbacks.link_established, args=(self,))
thread.setDaemon(True)
thread.start()
else:
RNS.log("Invalid link proof signature received by "+str(self)+". Ignoring.", RNS.LOG_DEBUG)
self.establishment_cost += len(packet.raw)
signed_data = self.link_id+self.peer_pub_bytes+self.peer_sig_pub_bytes
signature = packet.data[:RNS.Identity.SIGLENGTH//8]
if self.destination.identity.validate(signature, signed_data):
if self.status != Link.HANDSHAKE:
raise IOError("Invalid link state for proof validation: "+str(self.status))
self.rtt = time.time() - self.request_time
self.attached_interface = packet.receiving_interface
self.__remote_identity = self.destination.identity
self.status = Link.ACTIVE
self.activated_at = time.time()
self.last_proof = self.activated_at
RNS.Transport.activate_link(self)
RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+str(round(self.rtt, 3))+"s", RNS.LOG_VERBOSE)
if self.rtt != None and self.establishment_cost != None and self.rtt > 0 and self.establishment_cost > 0:
self.establishment_rate = self.establishment_cost/self.rtt
rtt_data = umsgpack.packb(self.rtt)
rtt_packet = RNS.Packet(self, rtt_data, context=RNS.Packet.LRRTT)
rtt_packet.send()
self.had_outbound()
if self.callbacks.link_established != None:
thread = threading.Thread(target=self.callbacks.link_established, args=(self,))
thread.daemon = True
thread.start()
else:
RNS.log("Invalid link proof signature received by "+str(self)+". Ignoring.", RNS.LOG_DEBUG)
except Exception as e:
self.status = Link.CLOSED
RNS.log("An error ocurred while validating link request proof on "+str(self)+".", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
def identify(self, identity):
@ -297,7 +330,7 @@ class Link:
:param identity: An RNS.Identity instance to identify as.
"""
if self.initiator:
if self.initiator and self.status == Link.ACTIVE:
signed_data = self.link_id + identity.get_public_key()
signature = identity.sign(signed_data)
proof_data = identity.get_public_key() + signature
@ -323,7 +356,7 @@ class Link:
packed_request = umsgpack.packb(unpacked_request)
if timeout == None:
timeout = self.rtt * self.traffic_timeout_factor + RNS.Resource.RESPONSE_MAX_GRACE_TIME/4.0
timeout = self.rtt * self.traffic_timeout_factor + RNS.Resource.RESPONSE_MAX_GRACE_TIME*1.125
if len(packed_request) <= Link.MDU:
request_packet = RNS.Packet(self, packed_request, RNS.Packet.DATA, context = RNS.Packet.REQUEST)
@ -361,25 +394,68 @@ class Link:
def rtt_packet(self, packet):
try:
# TODO: This is crude, we should use the delta
# to model a more representative per-bit round
# trip time, and use that to set a sensible RTT
# expectancy for the link. This will have to do
# for now though.
measured_rtt = time.time() - self.request_time
plaintext = self.decrypt(packet.data)
rtt = umsgpack.unpackb(plaintext)
self.rtt = max(measured_rtt, rtt)
self.status = Link.ACTIVE
self.activated_at = time.time()
if plaintext != None:
rtt = umsgpack.unpackb(plaintext)
self.rtt = max(measured_rtt, rtt)
self.status = Link.ACTIVE
self.activated_at = time.time()
if self.rtt != None and self.establishment_cost != None and self.rtt > 0 and self.establishment_cost > 0:
self.establishment_rate = self.establishment_cost/self.rtt
try:
if self.owner.callbacks.link_established != None:
self.owner.callbacks.link_established(self)
except Exception as e:
RNS.log("Error occurred in external link establishment callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
if self.owner.callbacks.link_established != None:
self.owner.callbacks.link_established(self)
except Exception as e:
RNS.log("Error occurred while processing RTT packet, tearing down link. The contained exception was: "+str(e), RNS.LOG_ERROR)
self.teardown()
def track_phy_stats(self, track):
"""
You can enable physical layer statistics on a per-link basis. If this is enabled,
and the link is running over an interface that supports reporting physical layer
statistics, you will be able to retrieve stats such as *RSSI*, *SNR* and physical
*Link Quality* for the link.
:param track: Whether or not to keep track of physical layer statistics. Value must be ``True`` or ``False``.
"""
if track:
self.__track_phy_stats = True
else:
self.__track_phy_stats = False
def get_rssi(self):
"""
:returns: The physical layer *Received Signal Strength Indication* if available, otherwise ``None``. Physical layer statistics must be enabled on the link for this method to return a value.
"""
return self.rssi
def get_snr(self):
"""
:returns: The physical layer *Signal-to-Noise Ratio* if available, otherwise ``None``. Physical layer statistics must be enabled on the link for this method to return a value.
"""
return self.rssi
def get_q(self):
"""
:returns: The physical layer *Link Quality* if available, otherwise ``None``. Physical layer statistics must be enabled on the link for this method to return a value.
"""
return self.rssi
def get_establishment_rate(self):
"""
:returns: The data transfer rate at which the link establishment procedure ocurred, in bits per second.
"""
if self.establishment_rate != None:
return self.establishment_rate*8
else:
return None
def get_salt(self):
return self.link_id
@ -388,7 +464,7 @@ class Link:
def no_inbound_for(self):
"""
:returns: The time in seconds since last inbound packet on the link.
:returns: The time in seconds since last inbound packet on the link. This includes keepalive packets.
"""
activated_at = self.activated_at if self.activated_at != None else 0
last_inbound = max(self.last_inbound, activated_at)
@ -396,24 +472,32 @@ class Link:
def no_outbound_for(self):
"""
:returns: The time in seconds since last outbound packet on the link.
:returns: The time in seconds since last outbound packet on the link. This includes keepalive packets.
"""
return time.time() - self.last_outbound
def no_data_for(self):
"""
:returns: The time in seconds since payload data traversed the link. This excludes keepalive packets.
"""
return time.time() - self.last_data
def inactive_for(self):
"""
:returns: The time in seconds since activity on the link.
:returns: The time in seconds since activity on the link. This includes keepalive packets.
"""
return min(self.no_inbound_for(), self.no_outbound_for())
def get_remote_identity(self):
"""
:returns: The identity of the remote peer, if it is known
:returns: The identity of the remote peer, if it is known. Calling this method will not query the remote initiator to reveal its identity. Returns ``None`` if the link initiator has not already independently called the ``identify(identity)`` method.
"""
return self.__remote_identity
def had_outbound(self):
def had_outbound(self, is_keepalive=False):
self.last_outbound = time.time()
if not is_keepalive:
self.last_data = self.last_outbound
def teardown(self):
"""
@ -440,6 +524,7 @@ class Link:
self.teardown_reason = Link.DESTINATION_CLOSED
else:
self.teardown_reason = Link.INITIATOR_CLOSED
self.__update_phy_stats(packet)
self.link_closed()
except Exception as e:
pass
@ -449,6 +534,8 @@ class Link:
resource.cancel()
for resource in self.outgoing_resources:
resource.cancel()
if self._channel:
self._channel._shutdown()
self.prv = None
self.pub = None
@ -470,13 +557,17 @@ class Link:
def start_watchdog(self):
thread = threading.Thread(target=self.__watchdog_job)
thread.setDaemon(True)
thread.daemon = True
thread.start()
def __watchdog_job(self):
while not self.status == Link.CLOSED:
while (self.watchdog_lock):
sleep(max(self.rtt, 0.025))
rtt_wait = 0.025
if hasattr(self, "rtt") and self.rtt:
rtt_wait = self.rtt
sleep(max(rtt_wait, 0.025))
if not self.status == Link.CLOSED:
# Link was initiated, but no response
@ -495,19 +586,19 @@ class Link:
next_check = self.request_time + self.establishment_timeout
sleep_time = next_check - time.time()
if time.time() >= self.request_time + self.establishment_timeout:
if self.initiator:
RNS.log("Timeout waiting for link request proof", RNS.LOG_DEBUG)
else:
RNS.log("Timeout waiting for RTT packet from link initiator", RNS.LOG_DEBUG)
self.status = Link.CLOSED
self.teardown_reason = Link.TIMEOUT
self.link_closed()
sleep_time = 0.001
if self.initiator:
RNS.log("Timeout waiting for link request proof", RNS.LOG_DEBUG)
else:
RNS.log("Timeout waiting for RTT packet from link initiator", RNS.LOG_DEBUG)
elif self.status == Link.ACTIVE:
activated_at = self.activated_at if self.activated_at != None else 0
last_inbound = max(self.last_inbound, activated_at)
last_inbound = max(max(self.last_inbound, self.last_proof), activated_at)
if time.time() >= last_inbound + self.keepalive:
if self.initiator:
@ -539,10 +630,25 @@ class Link:
sleep(sleep_time)
def __update_phy_stats(self, packet, query_shared = True):
if self.__track_phy_stats:
if query_shared:
reticulum = RNS.Reticulum.get_instance()
if packet.rssi == None: packet.rssi = reticulum.get_packet_rssi(packet.packet_hash)
if packet.snr == None: packet.snr = reticulum.get_packet_snr(packet.packet_hash)
if packet.q == None: packet.q = reticulum.get_packet_q(packet.packet_hash)
if packet.rssi != None:
self.rssi = packet.rssi
if packet.snr != None:
self.snr = packet.snr
if packet.q != None:
self.q = packet.q
def send_keepalive(self):
keepalive_packet = RNS.Packet(self, bytes([0xFF]), context=RNS.Packet.KEEPALIVE)
keepalive_packet.send()
self.had_outbound()
self.had_outbound(is_keepalive = True)
def handle_request(self, request_id, unpacked_request):
if self.status == Link.ACTIVE:
@ -567,7 +673,13 @@ class Link:
if allowed:
RNS.log("Handling request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_DEBUG)
response = response_generator(path, request_data, request_id, self.__remote_identity, requested_at)
if len(inspect.signature(response_generator).parameters) == 5:
response = response_generator(path, request_data, request_id, self.__remote_identity, requested_at)
elif len(inspect.signature(response_generator).parameters) == 6:
response = response_generator(path, request_data, request_id, self.link_id, self.__remote_identity, requested_at)
else:
raise TypeError("Invalid signature for response generator callback")
if response != None:
packed_response = umsgpack.packb([request_id, response])
@ -587,7 +699,9 @@ class Link:
remove = pending_request
try:
pending_request.response_size = response_size
pending_request.response_transfer_size = response_transfer_size
if pending_request.response_transfer_size == None:
pending_request.response_transfer_size = 0
pending_request.response_transfer_size += response_transfer_size
pending_request.response_received(response_data)
except Exception as e:
RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
@ -623,6 +737,16 @@ class Link:
if pending_request.request_id == resource.request_id:
pending_request.request_timed_out(None)
def get_channel(self):
"""
Get the ``Channel`` for this link.
:return: ``Channel`` object
"""
if self._channel is None:
self._channel = Channel(LinkChannelOutlet(self))
return self._channel
def receive(self, packet):
self.watchdog_lock = True
if not self.status == Link.CLOSED and not (self.initiator and packet.context == RNS.Packet.KEEPALIVE and packet.data == bytes([0xFF])):
@ -630,135 +754,167 @@ class Link:
RNS.log("Link-associated packet received on unexpected interface! Someone might be trying to manipulate your communication!", RNS.LOG_ERROR)
else:
self.last_inbound = time.time()
if packet.context != RNS.Packet.KEEPALIVE:
self.last_data = self.last_inbound
self.rx += 1
self.rxbytes += len(packet.data)
if self.status == Link.STALE:
self.status = Link.ACTIVE
if packet.packet_type == RNS.Packet.DATA:
should_query = False
if packet.context == RNS.Packet.NONE:
plaintext = self.decrypt(packet.data)
if self.callbacks.packet != None:
thread = threading.Thread(target=self.callbacks.packet, args=(plaintext, packet))
thread.setDaemon(True)
thread.start()
if self.destination.proof_strategy == RNS.Destination.PROVE_ALL:
packet.prove()
if plaintext != None:
if self.callbacks.packet != None:
thread = threading.Thread(target=self.callbacks.packet, args=(plaintext, packet))
thread.daemon = True
thread.start()
if self.destination.proof_strategy == RNS.Destination.PROVE_ALL:
packet.prove()
should_query = True
elif self.destination.proof_strategy == RNS.Destination.PROVE_APP:
if self.destination.callbacks.proof_requested:
try:
self.destination.callbacks.proof_requested(packet)
except Exception as e:
RNS.log("Error while executing proof request callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
elif self.destination.proof_strategy == RNS.Destination.PROVE_APP:
if self.destination.callbacks.proof_requested:
try:
if self.destination.callbacks.proof_requested(packet):
packet.prove()
should_query = True
except Exception as e:
RNS.log("Error while executing proof request callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
self.__update_phy_stats(packet, query_shared=should_query)
elif packet.context == RNS.Packet.LINKIDENTIFY:
plaintext = self.decrypt(packet.data)
if plaintext != None:
if not self.initiator and len(plaintext) == RNS.Identity.KEYSIZE//8 + RNS.Identity.SIGLENGTH//8:
public_key = plaintext[:RNS.Identity.KEYSIZE//8]
signed_data = self.link_id+public_key
signature = plaintext[RNS.Identity.KEYSIZE//8:RNS.Identity.KEYSIZE//8+RNS.Identity.SIGLENGTH//8]
identity = RNS.Identity(create_keys=False)
identity.load_public_key(public_key)
if not self.initiator and len(plaintext) == RNS.Identity.KEYSIZE//8 + RNS.Identity.SIGLENGTH//8:
public_key = plaintext[:RNS.Identity.KEYSIZE//8]
signed_data = self.link_id+public_key
signature = plaintext[RNS.Identity.KEYSIZE//8:RNS.Identity.KEYSIZE//8+RNS.Identity.SIGLENGTH//8]
identity = RNS.Identity(create_keys=False)
identity.load_public_key(public_key)
if identity.validate(signature, signed_data):
self.__remote_identity = identity
if self.callbacks.remote_identified != None:
try:
self.callbacks.remote_identified(self, self.__remote_identity)
except Exception as e:
RNS.log("Error while executing remote identified callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
if identity.validate(signature, signed_data):
self.__remote_identity = identity
if self.callbacks.remote_identified != None:
try:
self.callbacks.remote_identified(self, self.__remote_identity)
except Exception as e:
RNS.log("Error while executing remote identified callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
self.__update_phy_stats(packet, query_shared=True)
elif packet.context == RNS.Packet.REQUEST:
try:
request_id = packet.getTruncatedHash()
packed_request = self.decrypt(packet.data)
unpacked_request = umsgpack.unpackb(packed_request)
self.handle_request(request_id, unpacked_request)
if packed_request != None:
unpacked_request = umsgpack.unpackb(packed_request)
self.handle_request(request_id, unpacked_request)
self.__update_phy_stats(packet, query_shared=True)
except Exception as e:
RNS.log("Error occurred while handling request. The contained exception was: "+str(e), RNS.LOG_ERROR)
elif packet.context == RNS.Packet.RESPONSE:
try:
packed_response = self.decrypt(packet.data)
unpacked_response = umsgpack.unpackb(packed_response)
request_id = unpacked_response[0]
response_data = unpacked_response[1]
transfer_size = len(umsgpack.packb(response_data))-2
self.handle_response(request_id, response_data, transfer_size, transfer_size)
if packed_response != None:
unpacked_response = umsgpack.unpackb(packed_response)
request_id = unpacked_response[0]
response_data = unpacked_response[1]
transfer_size = len(umsgpack.packb(response_data))-2
self.handle_response(request_id, response_data, transfer_size, transfer_size)
self.__update_phy_stats(packet, query_shared=True)
except Exception as e:
RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
elif packet.context == RNS.Packet.LRRTT:
if not self.initiator:
self.rtt_packet(packet)
self.__update_phy_stats(packet, query_shared=True)
elif packet.context == RNS.Packet.LINKCLOSE:
self.teardown_packet(packet)
self.__update_phy_stats(packet, query_shared=True)
elif packet.context == RNS.Packet.RESOURCE_ADV:
packet.plaintext = self.decrypt(packet.data)
if packet.plaintext != None:
self.__update_phy_stats(packet, query_shared=True)
if RNS.ResourceAdvertisement.is_request(packet):
RNS.Resource.accept(packet, callback=self.request_resource_concluded)
elif RNS.ResourceAdvertisement.is_response(packet):
request_id = RNS.ResourceAdvertisement.read_request_id(packet)
for pending_request in self.pending_requests:
if pending_request.request_id == request_id:
RNS.Resource.accept(packet, callback=self.response_resource_concluded, progress_callback=pending_request.response_resource_progress, request_id = request_id)
pending_request.response_size = RNS.ResourceAdvertisement.read_size(packet)
pending_request.response_transfer_size = RNS.ResourceAdvertisement.read_transfer_size(packet)
pending_request.started_at = time.time()
elif self.resource_strategy == Link.ACCEPT_NONE:
pass
elif self.resource_strategy == Link.ACCEPT_APP:
if self.callbacks.resource != None:
try:
resource_advertisement = RNS.ResourceAdvertisement.unpack(packet.plaintext)
resource_advertisement.link = self
if self.callbacks.resource(resource_advertisement):
RNS.Resource.accept(packet, self.callbacks.resource_concluded)
except Exception as e:
RNS.log("Error while executing resource accept callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
elif self.resource_strategy == Link.ACCEPT_ALL:
RNS.Resource.accept(packet, self.callbacks.resource_concluded)
if RNS.ResourceAdvertisement.is_request(packet):
RNS.Resource.accept(packet, callback=self.request_resource_concluded)
elif RNS.ResourceAdvertisement.is_response(packet):
request_id = RNS.ResourceAdvertisement.read_request_id(packet)
for pending_request in self.pending_requests:
if pending_request.request_id == request_id:
response_resource = RNS.Resource.accept(packet, callback=self.response_resource_concluded, progress_callback=pending_request.response_resource_progress, request_id = request_id)
if response_resource != None:
if pending_request.response_size == None:
pending_request.response_size = RNS.ResourceAdvertisement.read_size(packet)
if pending_request.response_transfer_size == None:
pending_request.response_transfer_size = 0
pending_request.response_transfer_size += RNS.ResourceAdvertisement.read_transfer_size(packet)
if pending_request.started_at == None:
pending_request.started_at = time.time()
pending_request.response_resource_progress(response_resource)
elif self.resource_strategy == Link.ACCEPT_NONE:
pass
elif self.resource_strategy == Link.ACCEPT_APP:
if self.callbacks.resource != None:
try:
resource_advertisement = RNS.ResourceAdvertisement.unpack(packet.plaintext)
resource_advertisement.link = self
if self.callbacks.resource(resource_advertisement):
RNS.Resource.accept(packet, self.callbacks.resource_concluded)
except Exception as e:
RNS.log("Error while executing resource accept callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
elif self.resource_strategy == Link.ACCEPT_ALL:
RNS.Resource.accept(packet, self.callbacks.resource_concluded)
elif packet.context == RNS.Packet.RESOURCE_REQ:
plaintext = self.decrypt(packet.data)
if ord(plaintext[:1]) == RNS.Resource.HASHMAP_IS_EXHAUSTED:
resource_hash = plaintext[1+RNS.Resource.MAPHASH_LEN:RNS.Identity.HASHLENGTH//8+1+RNS.Resource.MAPHASH_LEN]
else:
resource_hash = plaintext[1:RNS.Identity.HASHLENGTH//8+1]
if plaintext != None:
self.__update_phy_stats(packet, query_shared=True)
if ord(plaintext[:1]) == RNS.Resource.HASHMAP_IS_EXHAUSTED:
resource_hash = plaintext[1+RNS.Resource.MAPHASH_LEN:RNS.Identity.HASHLENGTH//8+1+RNS.Resource.MAPHASH_LEN]
else:
resource_hash = plaintext[1:RNS.Identity.HASHLENGTH//8+1]
for resource in self.outgoing_resources:
if resource.hash == resource_hash:
# We need to check that this request has not been
# received before in order to avoid sequencing errors.
if not packet.packet_hash in resource.req_hashlist:
resource.req_hashlist.append(packet.packet_hash)
resource.request(plaintext)
for resource in self.outgoing_resources:
if resource.hash == resource_hash:
# We need to check that this request has not been
# received before in order to avoid sequencing errors.
if not packet.packet_hash in resource.req_hashlist:
resource.req_hashlist.append(packet.packet_hash)
resource.request(plaintext)
elif packet.context == RNS.Packet.RESOURCE_HMU:
plaintext = self.decrypt(packet.data)
resource_hash = plaintext[:RNS.Identity.HASHLENGTH//8]
for resource in self.incoming_resources:
if resource_hash == resource.hash:
resource.hashmap_update_packet(plaintext)
if plaintext != None:
self.__update_phy_stats(packet, query_shared=True)
resource_hash = plaintext[:RNS.Identity.HASHLENGTH//8]
for resource in self.incoming_resources:
if resource_hash == resource.hash:
resource.hashmap_update_packet(plaintext)
elif packet.context == RNS.Packet.RESOURCE_ICL:
plaintext = self.decrypt(packet.data)
resource_hash = plaintext[:RNS.Identity.HASHLENGTH//8]
for resource in self.incoming_resources:
if resource_hash == resource.hash:
resource.cancel()
if plaintext != None:
self.__update_phy_stats(packet)
resource_hash = plaintext[:RNS.Identity.HASHLENGTH//8]
for resource in self.incoming_resources:
if resource_hash == resource.hash:
resource.cancel()
elif packet.context == RNS.Packet.KEEPALIVE:
if not self.initiator and packet.data == bytes([0xFF]):
keepalive_packet = RNS.Packet(self, bytes([0xFE]), context=RNS.Packet.KEEPALIVE)
keepalive_packet.send()
self.had_outbound()
self.had_outbound(is_keepalive = True)
# TODO: find the most efficient way to allow multiple
@ -768,6 +924,17 @@ class Link:
elif packet.context == RNS.Packet.RESOURCE:
for resource in self.incoming_resources:
resource.receive_part(packet)
self.__update_phy_stats(packet)
elif packet.context == RNS.Packet.CHANNEL:
if not self._channel:
RNS.log(f"Channel data received without open channel", RNS.LOG_DEBUG)
else:
packet.prove()
plaintext = self.decrypt(packet.data)
if plaintext != None:
self.__update_phy_stats(packet)
self._channel._receive(plaintext)
elif packet.packet_type == RNS.Packet.PROOF:
if packet.context == RNS.Packet.RESOURCE_PRF:
@ -775,6 +942,7 @@ class Link:
for resource in self.outgoing_resources:
if resource_hash == resource.hash:
resource.validate_proof(packet.data)
self.__update_phy_stats(packet, query_shared=True)
self.watchdog_lock = False
@ -785,7 +953,7 @@ class Link:
try:
self.fernet = Fernet(self.derived_key)
except Exception as e:
RNS.log("Could not "+str(self)+" instantiate Fernet while performin encryption on link. The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("Could not instantiate Fernet while performin encryption on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
raise e
return self.fernet.encrypt(plaintext)
@ -804,6 +972,7 @@ class Link:
except Exception as e:
RNS.log("Decryption failed on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
return None
def sign(self, message):
@ -899,6 +1068,13 @@ class Link:
def register_incoming_resource(self, resource):
self.incoming_resources.append(resource)
def has_incoming_resource(self, resource):
for incoming_resource in self.incoming_resources:
if incoming_resource.hash == resource.hash:
return True
return False
def cancel_outgoing_resource(self, resource):
if resource in self.outgoing_resources:
self.outgoing_resources.remove(resource)
@ -977,11 +1153,12 @@ class RequestReceipt():
def request_resource_concluded(self, resource):
if resource.status == RNS.Resource.COMPLETE:
RNS.log("Request "+RNS.prettyhexrep(self.request_id)+" successfully sent as resource.", RNS.LOG_DEBUG)
self.started_at = time.time()
if self.started_at == None:
self.started_at = time.time()
self.status = RequestReceipt.DELIVERED
self.__resource_response_timeout = time.time()+self.timeout
response_timeout_thread = threading.Thread(target=self.__response_timeout_job)
response_timeout_thread.setDaemon(True)
response_timeout_thread.daemon = True
response_timeout_thread.start()
else:
RNS.log("Sending request "+RNS.prettyhexrep(self.request_id)+" as resource failed with status: "+RNS.hexrep([resource.status]), RNS.LOG_DEBUG)
@ -1018,24 +1195,26 @@ class RequestReceipt():
def response_resource_progress(self, resource):
if not self.status == RequestReceipt.FAILED:
self.status = RequestReceipt.RECEIVING
if self.packet_receipt != None:
self.packet_receipt.status = RNS.PacketReceipt.DELIVERED
self.packet_receipt.proved = True
self.packet_receipt.concluded_at = time.time()
if self.packet_receipt.callbacks.delivery != None:
self.packet_receipt.callbacks.delivery(self.packet_receipt)
if resource != None:
if not self.status == RequestReceipt.FAILED:
self.status = RequestReceipt.RECEIVING
if self.packet_receipt != None:
if self.packet_receipt.status != RNS.PacketReceipt.DELIVERED:
self.packet_receipt.status = RNS.PacketReceipt.DELIVERED
self.packet_receipt.proved = True
self.packet_receipt.concluded_at = time.time()
if self.packet_receipt.callbacks.delivery != None:
self.packet_receipt.callbacks.delivery(self.packet_receipt)
self.progress = resource.get_progress()
if self.callbacks.progress != None:
try:
self.callbacks.progress(self)
except Exception as e:
RNS.log("Error while executing response progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
resource.cancel()
self.progress = resource.get_progress()
if self.callbacks.progress != None:
try:
self.callbacks.progress(self)
except Exception as e:
RNS.log("Error while executing response progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
resource.cancel()
def response_received(self, response):

View File

@ -1,6 +1,6 @@
# MIT License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -29,15 +29,19 @@ import RNS
class Packet:
"""
The Packet class is used to create packet instances that can be sent
over a Reticulum network. Packets to will automatically be encrypted if
they are adressed to a ``RNS.Destination.SINGLE`` destination,
over a Reticulum network. Packets will automatically be encrypted if
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.
pre-shared key configured for the destination. All packets to group
destinations are encrypted with the same AES-128 key.
For ``RNS.Destination.SINGLE`` destinations and :ref:`RNS.Link<api-link>`
destinations, reticulum will use ephemeral keys, and offers **Forward Secrecy**.
For ``RNS.Destination.SINGLE`` destinations, Reticulum will use a newly
derived ephemeral AES-128 key for every packet.
For :ref:`RNS.Link<api-link>` destinations, Reticulum will use per-link
ephemeral keys, and offers **Forward Secrecy**.
:param destination: A :ref:`RNS.Destination<api-destination>` instance to which the packet will be sent.
:param data: The data payload to be included in the packet as *bytes*.
@ -54,9 +58,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
@ -73,6 +75,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
@ -135,10 +138,11 @@ class Packet:
self.receiving_interface = None
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.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
return packed_flags
@ -169,8 +173,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
@ -211,7 +215,7 @@ class Packet:
self.flags = self.raw[0]
self.hops = self.raw[1]
self.header_type = (self.flags & 0b11000000) >> 6
self.header_type = (self.flags & 0b01000000) >> 6
self.transport_type = (self.flags & 0b00110000) >> 4
self.destination_type = (self.flags & 0b00001100) >> 2
self.packet_type = (self.flags & 0b00000011)
@ -273,6 +277,10 @@ 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:
@ -363,8 +371,8 @@ class PacketReceipt:
if packet.destination.type == RNS.Destination.LINK:
self.timeout = packet.destination.rtt * packet.destination.traffic_timeout_factor
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):
"""
@ -393,9 +401,15 @@ 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)
return True
else:
return False
@ -490,7 +504,7 @@ class PacketReceipt:
if self.callbacks.timeout:
thread = threading.Thread(target=self.callbacks.timeout, args=(self,))
thread.setDaemon(True)
thread.daemon = True
thread.start()

27
RNS/Resolver.py Normal file
View File

@ -0,0 +1,27 @@
# MIT License
#
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
#
# 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.
class Resolver:
@staticmethod
def resolve_identity(full_name):
pass

View File

@ -1,6 +1,6 @@
# MIT License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -25,7 +25,9 @@ import os
import bz2
import math
import time
import tempfile
import threading
from threading import Lock
from .vendor import umsgpack as umsgpack
from time import sleep
@ -47,13 +49,13 @@ class Resource:
WINDOW = 4
# Absolute minimum window size during transfer
WINDOW_MIN = 1
WINDOW_MIN = 2
# The maximum window size for transfers on slow links
WINDOW_MAX_SLOW = 10
# The maximum window size for transfers on fast links
WINDOW_MAX_FAST = 76
WINDOW_MAX_FAST = 75
# For calculating maps and guard segments, this
# must be set to the global maximum window.
@ -103,9 +105,10 @@ class Resource:
PART_TIMEOUT_FACTOR = 4
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
MAX_RETRIES = 8
MAX_RETRIES = 16
MAX_ADV_RETRIES = 4
SENDER_GRACE_TIME = 10
SENDER_GRACE_TIME = 10.0
PROCESSING_GRACE = 1.0
RETRY_GRACE_TIME = 0.25
PER_RETRY_DELAY = 0.5
@ -149,7 +152,7 @@ class Resource:
resource.total_parts = int(math.ceil(resource.size/float(Resource.SDU)))
resource.received_count = 0
resource.outstanding_parts = 0
resource.parts = [None] * resource.total_parts
resource.parts = [None] * resource.total_parts
resource.window = Resource.WINDOW
resource.window_max = Resource.WINDOW_MAX_SLOW
resource.window_min = Resource.WINDOW_MIN
@ -167,25 +170,29 @@ class Resource:
resource.hashmap = [None] * resource.total_parts
resource.hashmap_height = 0
resource.waiting_for_hmu = False
resource.receiving_part = False
resource.consecutive_completed_height = 0
resource.consecutive_completed_height = -1
resource.link.register_incoming_resource(resource)
if not resource.link.has_incoming_resource(resource):
resource.link.register_incoming_resource(resource)
RNS.log("Accepting resource advertisement for "+RNS.prettyhexrep(resource.hash), RNS.LOG_DEBUG)
if resource.link.callbacks.resource_started != None:
try:
resource.link.callbacks.resource_started(resource)
except Exception as e:
RNS.log("Error while executing resource started callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log(f"Accepting resource advertisement for {RNS.prettyhexrep(resource.hash)}. Transfer size is {RNS.prettysize(resource.size)} in {resource.total_parts} parts.", RNS.LOG_DEBUG)
if resource.link.callbacks.resource_started != None:
try:
resource.link.callbacks.resource_started(resource)
except Exception as e:
RNS.log("Error while executing resource started callback from "+str(resource)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
resource.hashmap_update(0, resource.hashmap_raw)
resource.hashmap_update(0, resource.hashmap_raw)
resource.watchdog_job()
resource.watchdog_job()
return resource
else:
RNS.log("Ignoring resource advertisement for "+RNS.prettyhexrep(resource.hash)+", resource already transferring", RNS.LOG_DEBUG)
return None
return resource
except Exception as e:
RNS.log("Could not decode resource advertisement, dropping resource", RNS.LOG_DEBUG)
return None
@ -198,9 +205,19 @@ class Resource:
resource_data = None
self.assembly_lock = False
if data != None:
if not hasattr(data, "read") and len(data) > Resource.MAX_EFFICIENT_SIZE:
original_data = data
data_size = len(original_data)
data = tempfile.TemporaryFile()
data.write(original_data)
del original_data
if hasattr(data, "read"):
data_size = os.stat(data.name).st_size
self.total_size = data_size
if data_size == None:
data_size = os.stat(data.name).st_size
self.total_size = data_size
self.grand_total_parts = math.ceil(data_size/Resource.SDU)
if data_size <= Resource.MAX_EFFICIENT_SIZE:
@ -273,7 +290,7 @@ class Resource:
self.uncompressed_data = data
compression_began = time.time()
if (auto_compress and len(self.uncompressed_data) < Resource.AUTO_COMPRESS_MAX_SIZE):
if (auto_compress and len(self.uncompressed_data) <= Resource.AUTO_COMPRESS_MAX_SIZE):
RNS.log("Compressing resource data...", RNS.LOG_DEBUG)
self.compressed_data = bz2.compress(self.uncompressed_data)
RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_DEBUG)
@ -360,7 +377,8 @@ class Resource:
if advertise:
self.advertise()
else:
pass
self.receive_lock = Lock()
def hashmap_update_packet(self, plaintext):
if not self.status == Resource.FAILED:
@ -393,12 +411,11 @@ class Resource:
the resource advertisement it will begin transferring.
"""
thread = threading.Thread(target=self.__advertise_job)
thread.setDaemon(True)
thread.daemon = True
thread.start()
def __advertise_job(self):
data = ResourceAdvertisement(self).pack()
self.advertisement_packet = RNS.Packet(self.link, data, context=RNS.Packet.RESOURCE_ADV)
self.advertisement_packet = RNS.Packet(self.link, ResourceAdvertisement(self).pack(), context=RNS.Packet.RESOURCE_ADV)
while not self.link.ready_for_new_resource():
self.status = Resource.QUEUED
sleep(0.25)
@ -421,7 +438,7 @@ class Resource:
def watchdog_job(self):
thread = threading.Thread(target=self.__watchdog_job)
thread.setDaemon(True)
thread.daemon = True
thread.start()
def __watchdog_job(self):
@ -435,7 +452,7 @@ class Resource:
sleep_time = None
if self.status == Resource.ADVERTISED:
sleep_time = (self.adv_sent+self.timeout)-time.time()
sleep_time = (self.adv_sent+self.timeout+Resource.PROCESSING_GRACE)-time.time()
if sleep_time < 0:
if self.retries_left <= 0:
RNS.log("Resource transfer timeout after sending advertisement", RNS.LOG_DEBUG)
@ -445,12 +462,13 @@ class Resource:
try:
RNS.log("No part requests received, retrying resource advertisement...", RNS.LOG_DEBUG)
self.retries_left -= 1
self.advertisement_packet.resend()
self.advertisement_packet = RNS.Packet(self.link, ResourceAdvertisement(self).pack(), context=RNS.Packet.RESOURCE_ADV)
self.advertisement_packet.send()
self.last_activity = time.time()
self.adv_sent = self.last_activity
sleep_time = 0.001
except Exception as e:
RNS.log("Could not resend advertisement packet, cancelling resource", RNS.LOG_VERBOSE)
RNS.log("Could not resend advertisement packet, cancelling resource. The contained exception was: "+str(e), RNS.LOG_VERBOSE)
self.cancel()
@ -470,7 +488,8 @@ class Resource:
if sleep_time < 0:
if self.retries_left > 0:
RNS.log("Timed out waiting for parts, requesting retry", RNS.LOG_DEBUG)
ms = "" if self.outstanding_parts == 1 else "s"
RNS.log("Timed out waiting for "+str(self.outstanding_parts)+" part"+ms+", requesting retry", RNS.LOG_DEBUG)
if self.window > self.window_min:
self.window -= 1
if self.window_max > self.window_min:
@ -605,10 +624,26 @@ class Resource:
self.callback(self)
except Exception as e:
RNS.log("Error while executing resource concluded callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
finally:
try:
if hasattr(self, "input_file"):
if hasattr(self.input_file, "close") and callable(self.input_file.close):
self.input_file.close()
except Exception as e:
RNS.log("Error while closing resource input file: "+str(e), RNS.LOG_ERROR)
else:
# Otherwise we'll recursively create the
# next segment of the resource
Resource(self.input_file, self.link, callback = self.callback, segment_index = self.segment_index+1, original_hash=self.original_hash, progress_callback = self.__progress_callback)
Resource(
self.input_file, self.link,
callback = self.callback,
segment_index = self.segment_index+1,
original_hash=self.original_hash,
progress_callback = self.__progress_callback,
request_id = self.request_id,
is_response = self.is_response,
)
else:
pass
else:
@ -616,98 +651,99 @@ class Resource:
def receive_part(self, packet):
while self.receiving_part:
sleep(0.001)
with self.receive_lock:
self.receiving_part = True
self.last_activity = time.time()
self.retries_left = self.max_retries
self.receiving_part = True
self.last_activity = time.time()
self.retries_left = self.max_retries
if self.req_resp == None:
self.req_resp = self.last_activity
rtt = self.req_resp-self.req_sent
self.part_timeout_factor = Resource.PART_TIMEOUT_FACTOR_AFTER_RTT
if self.rtt == None:
self.rtt = self.link.rtt
self.watchdog_job()
elif rtt < self.rtt:
self.rtt = max(self.rtt - self.rtt*0.05, rtt)
elif rtt > self.rtt:
self.rtt = min(self.rtt + self.rtt*0.05, rtt)
if self.req_resp == None:
self.req_resp = self.last_activity
rtt = self.req_resp-self.req_sent
self.part_timeout_factor = Resource.PART_TIMEOUT_FACTOR_AFTER_RTT
if self.rtt == None:
self.rtt = self.link.rtt
self.watchdog_job()
elif rtt < self.rtt:
self.rtt = max(self.rtt - self.rtt*0.05, rtt)
elif rtt > self.rtt:
self.rtt = min(self.rtt + self.rtt*0.05, rtt)
if rtt > 0:
req_resp_cost = len(packet.raw)+self.req_sent_bytes
self.req_resp_rtt_rate = req_resp_cost / rtt
if rtt > 0:
req_resp_cost = len(packet.raw)+self.req_sent_bytes
self.req_resp_rtt_rate = req_resp_cost / rtt
if self.req_resp_rtt_rate > Resource.RATE_FAST and self.fast_rate_rounds < Resource.FAST_RATE_THRESHOLD:
self.fast_rate_rounds += 1
if self.req_resp_rtt_rate > Resource.RATE_FAST and self.fast_rate_rounds < Resource.FAST_RATE_THRESHOLD:
self.fast_rate_rounds += 1
if self.fast_rate_rounds == Resource.FAST_RATE_THRESHOLD:
self.window_max = Resource.WINDOW_MAX_FAST
if self.fast_rate_rounds == Resource.FAST_RATE_THRESHOLD:
self.window_max = Resource.WINDOW_MAX_FAST
if not self.status == Resource.FAILED:
self.status = Resource.TRANSFERRING
part_data = packet.data
part_hash = self.get_map_hash(part_data)
if not self.status == Resource.FAILED:
self.status = Resource.TRANSFERRING
part_data = packet.data
part_hash = self.get_map_hash(part_data)
i = self.consecutive_completed_height
for map_hash in self.hashmap[self.consecutive_completed_height:self.consecutive_completed_height+self.window]:
if map_hash == part_hash:
if self.parts[i] == None:
# Insert data into parts list
self.parts[i] = part_data
self.rtt_rxd_bytes += len(part_data)
self.received_count += 1
self.outstanding_parts -= 1
consecutive_index = self.consecutive_completed_height if self.consecutive_completed_height >= 0 else 0
i = consecutive_index
for map_hash in self.hashmap[consecutive_index:consecutive_index+self.window]:
if map_hash == part_hash:
if self.parts[i] == None:
# Update consecutive completed pointer
if i == self.consecutive_completed_height + 1:
self.consecutive_completed_height = i
cp = self.consecutive_completed_height + 1
while cp < len(self.parts) and self.parts[cp] != None:
self.consecutive_completed_height = cp
cp += 1
# Insert data into parts list
self.parts[i] = part_data
self.rtt_rxd_bytes += len(part_data)
self.received_count += 1
self.outstanding_parts -= 1
if self.__progress_callback != None:
try:
self.__progress_callback(self)
except Exception as e:
RNS.log("Error while executing progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
# Update consecutive completed pointer
if i == self.consecutive_completed_height + 1:
self.consecutive_completed_height = i
cp = self.consecutive_completed_height + 1
while cp < len(self.parts) and self.parts[cp] != None:
self.consecutive_completed_height = cp
cp += 1
i += 1
if self.__progress_callback != None:
try:
self.__progress_callback(self)
except Exception as e:
RNS.log("Error while executing progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
self.receiving_part = False
i += 1
if self.received_count == self.total_parts and not self.assembly_lock:
self.assembly_lock = True
self.assemble()
elif self.outstanding_parts == 0:
# TODO: Figure out if there is a mathematically
# optimal way to adjust windows
if self.window < self.window_max:
self.window += 1
if (self.window - self.window_min) > (self.window_flexibility-1):
self.window_min += 1
self.receiving_part = False
if self.req_sent != 0:
rtt = time.time()-self.req_sent
req_transferred = self.rtt_rxd_bytes - self.rtt_rxd_bytes_at_part_req
if self.received_count == self.total_parts and not self.assembly_lock:
self.assembly_lock = True
self.assemble()
elif self.outstanding_parts == 0:
# TODO: Figure out if there is a mathematically
# optimal way to adjust windows
if self.window < self.window_max:
self.window += 1
if (self.window - self.window_min) > (self.window_flexibility-1):
self.window_min += 1
if rtt != 0:
self.req_data_rtt_rate = req_transferred/rtt
self.rtt_rxd_bytes_at_part_req = self.rtt_rxd_bytes
if self.req_sent != 0:
rtt = time.time()-self.req_sent
req_transferred = self.rtt_rxd_bytes - self.rtt_rxd_bytes_at_part_req
if self.req_data_rtt_rate > Resource.RATE_FAST and self.fast_rate_rounds < Resource.FAST_RATE_THRESHOLD:
self.fast_rate_rounds += 1
if rtt != 0:
self.req_data_rtt_rate = req_transferred/rtt
self.rtt_rxd_bytes_at_part_req = self.rtt_rxd_bytes
if self.fast_rate_rounds == Resource.FAST_RATE_THRESHOLD:
self.window_max = Resource.WINDOW_MAX_FAST
if self.req_data_rtt_rate > Resource.RATE_FAST and self.fast_rate_rounds < Resource.FAST_RATE_THRESHOLD:
self.fast_rate_rounds += 1
self.request_next()
else:
self.receiving_part = False
if self.fast_rate_rounds == Resource.FAST_RATE_THRESHOLD:
self.window_max = Resource.WINDOW_MAX_FAST
self.request_next()
else:
self.receiving_part = False
# Called on incoming resource to send a request for more data
def request_next(self):
@ -720,11 +756,11 @@ class Resource:
hashmap_exhausted = Resource.HASHMAP_IS_NOT_EXHAUSTED
requested_hashes = b""
offset = (1 if self.consecutive_completed_height > 0 else 0)
i = 0; pn = self.consecutive_completed_height+offset
i = 0; pn = self.consecutive_completed_height+1
search_start = pn
for part in self.parts[search_start:search_start+self.window]:
search_size = self.window
for part in self.parts[search_start:search_start+search_size]:
if part == None:
part_hash = self.hashmap[pn]
if part_hash != None:
@ -744,7 +780,6 @@ class Resource:
hmu_part += last_map_hash
self.waiting_for_hmu = True
requested_data = b""
request_data = hmu_part + self.hash + requested_hashes
request_packet = RNS.Packet(self.link, request_data, context = RNS.Packet.RESOURCE_REQ)
@ -754,6 +789,7 @@ class Resource:
self.req_sent = self.last_activity
self.req_sent_bytes = len(request_packet.raw)
self.req_resp = None
except Exception as e:
RNS.log("Could not send resource request packet, cancelling resource", RNS.LOG_DEBUG)
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
@ -899,8 +935,7 @@ class Resource:
else:
self.progress_total_parts = float(self.total_parts)
progress = self.processed_parts / self.progress_total_parts
progress = min(1.0, self.processed_parts / self.progress_total_parts)
return progress
def get_transfer_size(self):
@ -944,7 +979,7 @@ class Resource:
class ResourceAdvertisement:
OVERHEAD = 128
OVERHEAD = 134
HASHMAP_MAX_LEN = math.floor((RNS.Link.MDU-OVERHEAD)/Resource.MAPHASH_LEN)
COLLISION_GUARD_SIZE = 2*Resource.WINDOW_MAX+HASHMAP_MAX_LEN
@ -988,6 +1023,7 @@ class ResourceAdvertisement:
def __init__(self, resource=None, request_id=None, is_response=False):
self.link = None
if resource != None:
self.t = resource.size # Transfer size
self.d = resource.total_size # Total uncompressed data size
@ -1034,6 +1070,9 @@ class ResourceAdvertisement:
def is_compressed(self):
return self.c
def get_link(self):
return self.link
def pack(self, segment=0):
hashmap_start = segment*ResourceAdvertisement.HASHMAP_MAX_LEN
hashmap_end = min((segment+1)*(ResourceAdvertisement.HASHMAP_MAX_LEN), self.n)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@
import RNS
import argparse
import threading
import time
import sys
import os
@ -34,9 +35,12 @@ APP_NAME = "rncp"
allow_all = False
allowed_identity_hashes = []
def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_identity = False, limit = None, disable_auth = None, disable_announce = False):
def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identity = False, limit = None, disable_auth = None, announce = False):
global allow_all, allowed_identity_hashes
from tempfile import TemporaryFile
identity = None
if announce < 0:
announce = False
targetloglevel = 3+verbosity-quietness
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
@ -54,16 +58,48 @@ def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_ident
if display_identity:
print("Identity : "+str(identity))
print("Receiving on : "+RNS.prettyhexrep(destination.hash))
print("Listening on : "+RNS.prettyhexrep(destination.hash))
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:
@ -78,16 +114,60 @@ def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_ident
if len(allowed_identity_hashes) < 1 and not disable_auth:
print("Warning: No allowed identities configured, rncp will not accept any files!")
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):
target_link = None
for link in RNS.Transport.active_links:
if link.link_id == link_id:
target_link = link
if not disable_announce:
destination.announce()
file_path = os.path.expanduser(data)
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)
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)
fetch_resource = RNS.Resource(temp_file, target_link)
return True
else:
return None
destination.set_link_established_callback(client_link_established)
destination.register_request_handler("fetch_file", response_generator=fetch_request, allow=RNS.Destination.ALLOW_LIST, allowed_list=allowed_identity_hashes)
print("rncp listening on "+RNS.prettyhexrep(destination.hash))
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)
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)
@ -181,7 +261,221 @@ def sender_progress(resource):
resource_done = True
link = None
def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT):
def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False):
global current_resource, resource_done, link, speed
targetloglevel = 3+verbosity-quietness
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))
exit(1)
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
if os.path.isfile(identity_path):
identity = RNS.Identity.from_file(identity_path)
if identity == None:
RNS.log("Could not load identity for rncp. The identity file at \""+str(identity_path)+"\" may be corrupt or unreadable.", RNS.LOG_ERROR)
exit(2)
else:
identity = None
if identity == None:
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
identity = RNS.Identity()
identity.to_file(identity_path)
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=" ")
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("\r \rPath not found")
exit(1)
else:
if silent:
print("Establishing link with "+RNS.prettyhexrep(destination_hash))
else:
print("\r \rEstablishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=" ")
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("\r \rCould not establish link with "+RNS.prettyhexrep(destination_hash))
exit(1)
else:
if silent:
print("Requesting file from remote...")
else:
print("\r \rRequesting file from remote ", end=" ")
link.identify(identity)
request_resolved = False
request_status = "unknown"
resource_resolved = False
resource_status = "unrequested"
current_resource = 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"
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_resource = resource
current_resource.progress_callback(sender_progress)
resource_status = "started"
def fetch_resource_concluded(resource):
nonlocal resource_resolved, resource_status
if resource.status == RNS.Resource.COMPLETE:
if resource.total_size > 4:
filename_len = int.from_bytes(resource.data.read(2), "big")
filename = resource.data.read(filename_len).decode("utf-8")
counter = 0
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()
resource_status = "completed"
else:
print("Invalid data received, ignoring resource")
resource_status = "invalid_data"
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 == "not_found":
if not silent: print("\r \r", end="")
print("Fetch request failed, the file "+str(file)+" was not found on the remote")
link.teardown()
time.sleep(1)
exit(0)
elif request_status == "remote_error":
if not silent: print("\r \r", end="")
print("Fetch request failed due to an error on the remote system")
link.teardown()
time.sleep(1)
exit(0)
elif request_status == "unknown":
if not silent: print("\r \r", end="")
print("Fetch request failed due to an unknown error (probably not authorised)")
link.teardown()
time.sleep(1)
exit(0)
elif request_status == "found":
if not silent: print("\r \r", 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)
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=" ")
else:
print("\r \rWaiting for transfer to start "+syms[i]+" ", end=" ")
sys.stdout.flush()
i = (i+1)%len(syms)
if current_resource.status != RNS.Resource.COMPLETE:
if silent:
print("The transfer failed")
else:
print("\r \rThe transfer failed")
exit(1)
else:
if silent:
print(str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
else:
print("\r \r"+str(file)+" fetched from "+RNS.prettyhexrep(destination_hash))
link.teardown()
time.sleep(0.25)
exit(0)
link.teardown()
exit(0)
def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False):
global current_resource, resource_done, link, speed
from tempfile import TemporaryFile
targetloglevel = 3+verbosity-quietness
@ -226,7 +520,12 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
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("Could not load identity for rncp. The identity file at \""+str(identity_path)+"\" may be corrupt or unreadable.", RNS.LOG_ERROR)
exit(2)
else:
identity = None
if identity == None:
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
@ -235,23 +534,33 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
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=" ")
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")
if silent:
print("Path not found")
else:
print("\r \rPath not found")
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("\r \rEstablishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=" ")
receiver_identity = RNS.Identity.recall(destination_hash)
receiver_destination = RNS.Destination(
@ -264,48 +573,75 @@ 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))
if time.time() > estab_timeout:
if silent:
print("Link establishment with "+RNS.prettyhexrep(destination_hash)+" timed out")
else:
print("\r \rLink establishment with "+RNS.prettyhexrep(destination_hash)+" timed out")
exit(1)
elif not RNS.Transport.has_path(destination_hash):
if silent:
print("No path found to "+RNS.prettyhexrep(destination_hash))
else:
print("\r \rNo path found to "+RNS.prettyhexrep(destination_hash))
exit(1)
else:
print("\r \rAdvertising file resource ", end=" ")
if silent:
print("Advertising file resource...")
else:
print("\r \rAdvertising file resource ", end=" ")
link.identify(identity)
resource = RNS.Resource(temp_file, link, callback = sender_progress, progress_callback = sender_progress)
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)
if resource.status > RNS.Resource.COMPLETE:
print("\r \rFile was not accepted by "+RNS.prettyhexrep(destination_hash))
if silent:
print("File was not accepted by "+RNS.prettyhexrep(destination_hash))
else:
print("\r \rFile was not accepted by "+RNS.prettyhexrep(destination_hash))
exit(1)
else:
print("\r \rTransferring file ", end=" ")
if silent:
print("Transferring file...")
else:
print("\r \rTransferring file ", end=" ")
while not resource_done:
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=" ")
sys.stdout.flush()
i = (i+1)%len(syms)
if not silent:
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=" ")
sys.stdout.flush()
i = (i+1)%len(syms)
if current_resource.status != RNS.Resource.COMPLETE:
print("\r \rThe transfer failed")
if silent:
print("The transfer failed")
else:
print("\r \rThe transfer failed")
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("\r \r"+str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
link.teardown()
time.sleep(0.25)
real_file.close()
@ -320,19 +656,21 @@ 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('-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("-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("-f", '--fetch', action='store_true', default=False, help="fetch file from remote listener instead of sending")
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="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('-p', '--print-identity', action='store_true', default=False, help="print identity and destination info and exit")
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("--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,
verbosity=args.verbose,
quietness=args.quiet,
@ -340,9 +678,25 @@ def main():
display_identity=args.print_identity,
# limit=args.limit,
disable_auth=args.no_auth,
disable_announce=args.no_announce,
announce=args.b,
)
elif args.fetch:
if args.destination != None and args.file != None:
fetch(
configdir = args.config,
verbosity = args.verbose,
quietness = args.quiet,
destination = args.destination,
file = args.file,
timeout = args.w,
silent = args.silent,
)
else:
print("")
parser.print_help()
print("")
elif args.destination != None and args.file != None:
send(
configdir = args.config,
@ -351,6 +705,7 @@ def main():
destination = args.destination,
file = args.file,
timeout = args.w,
silent = args.silent,
)
else:

600
RNS/Utilities/rnid.py Normal file
View File

@ -0,0 +1,600 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2023 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 RNS
import argparse
import time
import sys
import os
import base64
from RNS._version import __version__
APP_NAME = "rnid"
SIG_EXT = "rsg"
ENCRYPT_EXT = "rfe"
CHUNK_SIZE = 16*1024*1024
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
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum Identity & Encryption Utility")
# parser.add_argument("file", nargs="?", default=None, help="input file path", type=str)
parser.add_argument("--config", metavar="path", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("-i", "--identity", metavar="identity", action="store", default=None, help="hexadecimal Reticulum Destination hash or path to Identity file", type=str)
parser.add_argument("-g", "--generate", metavar="file", action="store", default=None, help="generate a new Identity")
parser.add_argument("-m", "--import", dest="import_str", metavar="identity_data", action="store", default=None, help="import Reticulum identity in hex, base32 or base64 format", type=str)
parser.add_argument("-x", "--export", action="store_true", default=None, help="export identity to hex, base32 or base64 format")
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("-a", "--announce", metavar="aspects", action="store", 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("-e", "--encrypt", metavar="file", action="store", default=None, help="encrypt file")
parser.add_argument("-d", "--decrypt", metavar="file", action="store", default=None, help="decrypt file")
parser.add_argument("-s", "--sign", metavar="path", action="store", default=None, help="sign file")
parser.add_argument("-V", "--validate", metavar="path", action="store", default=None, help="validate signature")
parser.add_argument("-r", "--read", metavar="file", action="store", default=None, help="input file path", type=str)
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) # "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",
parser.add_argument("-R", "--request", action="store_true", default=False, help="request unknown Identities from the network")
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")
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("--version", action="version", version="rnid {version}".format(version=__version__))
args = parser.parse_args()
ops = 0;
for t in [args.encrypt, args.decrypt, args.validate, args.sign]:
if t:
ops += 1
if ops > 1:
RNS.log("This utility currently only supports one of the encrypt, decrypt, sign or verify operations per invocation", RNS.LOG_ERROR)
exit(1)
if not args.read:
if args.encrypt:
args.read = args.encrypt
if args.decrypt:
args.read = args.decrypt
if args.sign:
args.read = args.sign
identity_str = args.identity
if args.import_str:
identity_bytes = None
try:
if args.base64:
identity_bytes = base64.urlsafe_b64decode(args.import_str)
elif args.base32:
identity_bytes = base64.b32decode(args.import_str)
else:
identity_bytes = bytes.fromhex(args.import_str)
except Exception as e:
print("Invalid identity data specified for import: "+str(e))
exit(41)
try:
identity = RNS.Identity.from_bytes(identity_bytes)
except Exception as e:
print("Could not create Reticulum identity from specified data: "+str(e))
exit(42)
RNS.log("Identity imported")
if args.base64:
RNS.log("Public Key : "+base64.urlsafe_b64encode(identity.get_public_key()).decode("utf-8"))
elif args.base32:
RNS.log("Public Key : "+base64.b32encode(identity.get_public_key()).decode("utf-8"))
else:
RNS.log("Public Key : "+RNS.hexrep(identity.get_public_key(), delimit=False))
if identity.prv:
if args.print_private:
if args.base64:
RNS.log("Private Key : "+base64.urlsafe_b64encode(identity.get_private_key()).decode("utf-8"))
elif args.base32:
RNS.log("Private Key : "+base64.b32encode(identity.get_private_key()).decode("utf-8"))
else:
RNS.log("Private Key : "+RNS.hexrep(identity.get_private_key(), delimit=False))
else:
RNS.log("Private Key : Hidden")
if args.write:
try:
wp = os.path.expanduser(args.write)
if not os.path.isfile(wp) or args.force:
identity.to_file(wp)
RNS.log("Wrote imported identity to "+str(args.write))
else:
print("File "+str(wp)+" already exists, not overwriting")
exit(43)
except Exception as e:
print("Error while writing imported identity to file: "+str(e))
exit(44)
exit(0)
if not args.generate and not identity_str:
print("\nNo identity provided, cannot continue\n")
parser.print_help()
print("")
exit(2)
else:
targetloglevel = 4
verbosity = args.verbose
quietness = args.quiet
if verbosity != 0 or quietness != 0:
targetloglevel = targetloglevel+verbosity-quietness
# Start Reticulum
reticulum = RNS.Reticulum(configdir=args.config, loglevel=targetloglevel)
RNS.compact_log_fmt = True
if args.stdout:
RNS.loglevel = -1
if args.generate:
identity = RNS.Identity()
if not args.force and os.path.isfile(args.generate):
RNS.log("Identity file "+str(args.generate)+" already exists. Not overwriting.", RNS.LOG_ERROR)
exit(3)
else:
try:
identity.to_file(args.generate)
RNS.log("New identity written to "+str(args.generate))
exit(0)
except Exception as e:
RNS.log("An error ocurred while saving the generated Identity.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
exit(4)
identity = None
if len(identity_str) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2 and not os.path.isfile(identity_str):
# Try recalling Identity from hex-encoded hash
try:
destination_hash = bytes.fromhex(identity_str)
identity = RNS.Identity.recall(destination_hash)
if identity == None:
if not args.request:
RNS.log("Could not recall Identity for "+RNS.prettyhexrep(destination_hash)+".", RNS.LOG_ERROR)
RNS.log("You can query the network for unknown Identities with the -R option.", RNS.LOG_ERROR)
exit(5)
else:
RNS.Transport.request_path(destination_hash)
def spincheck():
return RNS.Identity.recall(destination_hash) != None
spin(spincheck, "Requesting unknown Identity for "+RNS.prettyhexrep(destination_hash), args.t)
if not spincheck():
RNS.log("Identity request timed out", RNS.LOG_ERROR)
exit(6)
else:
identity = RNS.Identity.recall(destination_hash)
RNS.log("Received Identity "+str(identity)+" for destination "+RNS.prettyhexrep(destination_hash)+" from the network")
else:
RNS.log("Recalled Identity "+str(identity)+" for destination "+RNS.prettyhexrep(destination_hash))
except Exception as e:
RNS.log("Invalid hexadecimal hash provided", RNS.LOG_ERROR)
exit(7)
else:
# Try loading Identity from file
if not os.path.isfile(identity_str):
RNS.log("Specified Identity file not found")
exit(8)
else:
try:
identity = RNS.Identity.from_file(identity_str)
RNS.log("Loaded Identity "+str(identity)+" from "+str(identity_str))
except Exception as e:
RNS.log("Could not decode Identity from specified file")
exit(9)
if identity != None:
if args.hash:
try:
aspects = args.hash.split(".")
if not len(aspects) > 0:
RNS.log("Invalid destination aspects specified", RNS.LOG_ERROR)
exit(32)
else:
app_name = aspects[0]
aspects = aspects[1:]
if identity.pub != None:
destination = RNS.Destination(identity, RNS.Destination.OUT, RNS.Destination.SINGLE, app_name, *aspects)
RNS.log("The "+str(args.hash)+" destination for this Identity is "+RNS.prettyhexrep(destination.hash))
RNS.log("The full destination specifier is "+str(destination))
time.sleep(0.25)
exit(0)
else:
raise KeyError("No public key known")
except Exception as e:
RNS.log("An error ocurred while attempting to send the announce.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
exit(0)
if args.announce:
try:
aspects = args.announce.split(".")
if not len(aspects) > 1:
RNS.log("Invalid destination aspects specified", RNS.LOG_ERROR)
exit(32)
else:
app_name = aspects[0]
aspects = aspects[1:]
if identity.prv != None:
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, app_name, *aspects)
RNS.log("Created destination "+str(destination))
RNS.log("Announcing destination "+RNS.prettyhexrep(destination.hash))
destination.announce()
time.sleep(0.25)
exit(0)
else:
destination = RNS.Destination(identity, RNS.Destination.OUT, RNS.Destination.SINGLE, app_name, *aspects)
RNS.log("The "+str(args.announce)+" destination for this Identity is "+RNS.prettyhexrep(destination.hash))
RNS.log("The full destination specifier is "+str(destination))
RNS.log("Cannot announce this destination, since the private key is not held")
time.sleep(0.25)
exit(33)
except Exception as e:
RNS.log("An error ocurred while attempting to send the announce.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
exit(0)
if args.print_identity:
if args.base64:
RNS.log("Public Key : "+base64.urlsafe_b64encode(identity.get_public_key()).decode("utf-8"))
elif args.base32:
RNS.log("Public Key : "+base64.b32encode(identity.get_public_key()).decode("utf-8"))
else:
RNS.log("Public Key : "+RNS.hexrep(identity.get_public_key(), delimit=False))
if identity.prv:
if args.print_private:
if args.base64:
RNS.log("Private Key : "+base64.urlsafe_b64encode(identity.get_private_key()).decode("utf-8"))
elif args.base32:
RNS.log("Private Key : "+base64.b32encode(identity.get_private_key()).decode("utf-8"))
else:
RNS.log("Private Key : "+RNS.hexrep(identity.get_private_key(), delimit=False))
else:
RNS.log("Private Key : Hidden")
exit(0)
if args.export:
if identity.prv:
if args.base64:
RNS.log("Exported Identity : "+base64.urlsafe_b64encode(identity.get_private_key()).decode("utf-8"))
elif args.base32:
RNS.log("Exported Identity : "+base64.b32encode(identity.get_private_key()).decode("utf-8"))
else:
RNS.log("Exported Identity : "+RNS.hexrep(identity.get_private_key(), delimit=False))
else:
RNS.log("Identity doesn't hold a private key, cannot export")
exit(50)
exit(0)
if args.validate:
if not args.read and args.validate.lower().endswith("."+SIG_EXT):
args.read = str(args.validate).replace("."+SIG_EXT, "")
if not os.path.isfile(args.validate):
RNS.log("Signature file "+str(args.read)+" not found", RNS.LOG_ERROR)
exit(10)
if not os.path.isfile(args.read):
RNS.log("Input file "+str(args.read)+" not found", RNS.LOG_ERROR)
exit(11)
data_input = None
if args.read:
if not os.path.isfile(args.read):
RNS.log("Input file "+str(args.read)+" not found", RNS.LOG_ERROR)
exit(12)
else:
try:
data_input = open(args.read, "rb")
except Exception as e:
RNS.log("Could not open input file for reading", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
exit(13)
# TODO: Actually expand this to a good solution
# probably need to create a wrapper that takes
# into account not closing stdin when done
# elif args.stdin:
# data_input = sys.stdin
data_output = None
if args.encrypt and not args.write and not args.stdout and args.read:
args.write = str(args.read)+"."+ENCRYPT_EXT
if args.decrypt and not args.write and not args.stdout and args.read and args.read.lower().endswith("."+ENCRYPT_EXT):
args.write = str(args.read).replace("."+ENCRYPT_EXT, "")
if args.sign and identity.prv == None:
RNS.log("Specified Identity does not hold a private key. Cannot sign.", RNS.LOG_ERROR)
exit(14)
if args.sign and not args.write and not args.stdout and args.read:
args.write = str(args.read)+"."+SIG_EXT
if args.write:
if not args.force and os.path.isfile(args.write):
RNS.log("Output file "+str(args.write)+" already exists. Not overwriting.", RNS.LOG_ERROR)
exit(15)
else:
try:
data_output = open(args.write, "wb")
except Exception as e:
RNS.log("Could not open output file for writing", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
exit(15)
# TODO: Actually expand this to a good solution
# probably need to create a wrapper that takes
# into account not closing stdout when done
# elif args.stdout:
# data_output = sys.stdout
if args.sign:
if identity.prv == None:
RNS.log("Specified Identity does not hold a private key. Cannot sign.", RNS.LOG_ERROR)
exit(16)
if not data_input:
if not args.stdout:
RNS.log("Signing requested, but no input data specified", RNS.LOG_ERROR)
exit(17)
else:
if not data_output:
if not args.stdout:
RNS.log("Signing requested, but no output specified", RNS.LOG_ERROR)
exit(18)
if not args.stdout:
RNS.log("Signing "+str(args.read))
try:
data_output.write(identity.sign(data_input.read()))
data_output.close()
data_input.close()
if not args.stdout:
if args.read:
RNS.log("File "+str(args.read)+" signed with "+str(identity)+" to "+str(args.write))
exit(0)
except Exception as e:
if not args.stdout:
RNS.log("An error ocurred while encrypting data.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
try:
data_output.close()
except:
pass
try:
data_input.close()
except:
pass
exit(19)
if args.validate:
if not data_input:
if not args.stdout:
RNS.log("Signature verification requested, but no input data specified", RNS.LOG_ERROR)
exit(20)
else:
# if not args.stdout:
# RNS.log("Verifying "+str(args.validate)+" for "+str(args.read))
try:
try:
sig_input = open(args.validate, "rb")
except Exception as e:
RNS.log("An error ocurred while opening "+str(args.validate)+".", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
exit(21)
validated = identity.validate(sig_input.read(), data_input.read())
sig_input.close()
data_input.close()
if not validated:
if not args.stdout:
RNS.log("Signature "+str(args.validate)+" for file "+str(args.read)+" is invalid", RNS.LOG_ERROR)
exit(22)
else:
if not args.stdout:
RNS.log("Signature "+str(args.validate)+" for file "+str(args.read)+" made by Identity "+str(identity)+" is valid")
exit(0)
except Exception as e:
if not args.stdout:
RNS.log("An error ocurred while validating signature.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
try:
data_output.close()
except:
pass
try:
data_input.close()
except:
pass
exit(23)
if args.encrypt:
if not data_input:
if not args.stdout:
RNS.log("Encryption requested, but no input data specified", RNS.LOG_ERROR)
exit(24)
else:
if not data_output:
if not args.stdout:
RNS.log("Encryption requested, but no output specified", RNS.LOG_ERROR)
exit(25)
if not args.stdout:
RNS.log("Encrypting "+str(args.read))
try:
more_data = True
while more_data:
chunk = data_input.read(CHUNK_SIZE)
if chunk:
data_output.write(identity.encrypt(chunk))
else:
more_data = False
data_output.close()
data_input.close()
if not args.stdout:
if args.read:
RNS.log("File "+str(args.read)+" encrypted for "+str(identity)+" to "+str(args.write))
exit(0)
except Exception as e:
if not args.stdout:
RNS.log("An error ocurred while encrypting data.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
try:
data_output.close()
except:
pass
try:
data_input.close()
except:
pass
exit(26)
if args.decrypt:
if identity.prv == None:
RNS.log("Specified Identity does not hold a private key. Cannot decrypt.", RNS.LOG_ERROR)
exit(27)
if not data_input:
if not args.stdout:
RNS.log("Decryption requested, but no input data specified", RNS.LOG_ERROR)
exit(28)
else:
if not data_output:
if not args.stdout:
RNS.log("Decryption requested, but no output specified", RNS.LOG_ERROR)
exit(29)
if not args.stdout:
RNS.log("Decrypting "+str(args.read)+"...")
try:
more_data = True
while more_data:
chunk = data_input.read(CHUNK_SIZE)
if chunk:
plaintext = identity.decrypt(chunk)
if plaintext == None:
if not args.stdout:
RNS.log("Data could not be decrypted with the specified Identity")
exit(30)
else:
data_output.write(plaintext)
else:
more_data = False
data_output.close()
data_input.close()
if not args.stdout:
if args.read:
RNS.log("File "+str(args.read)+" decrypted with "+str(identity)+" to "+str(args.write))
exit(0)
except Exception as e:
if not args.stdout:
RNS.log("An error ocurred while decrypting data.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
try:
data_output.close()
except:
pass
try:
data_input.close()
except:
pass
exit(31)
if True:
pass
elif False:
pass
else:
print("")
parser.print_help()
print("")
except KeyboardInterrupt:
print("")
exit(255)
if __name__ == "__main__":
main()

74
RNS/Utilities/rnir.py Normal file
View File

@ -0,0 +1,74 @@
#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2023 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 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="ir {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()
__example_rns_config__ = '''# This is an example Identity Resolver file.
'''
if __name__ == "__main__":
main()

3690
RNS/Utilities/rnodeconf.py Executable file

File diff suppressed because one or more lines are too long

View File

@ -30,7 +30,7 @@ import argparse
from RNS._version import __version__
def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity, timeout, drop_queues):
def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity, timeout, drop_queues, drop_via):
if table:
destination_hash = None
if destination_hexhash != None:
@ -155,6 +155,29 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
sys.exit(1)
elif drop_via:
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 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:
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
@ -187,17 +210,22 @@ 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")
print("\r \rPath not found")
sys.exit(1)
@ -251,6 +279,13 @@ def main():
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",
@ -277,7 +312,7 @@ def main():
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:
print("")
parser.print_help()
print("")
@ -291,6 +326,7 @@ def main():
verbosity = args.verbose,
timeout = args.w,
drop_queues = args.drop_announces,
drop_via = args.drop_via,
)
sys.exit(0)

View File

@ -31,8 +31,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 +73,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 +96,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 +228,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:

View File

@ -30,15 +30,20 @@ from RNS._version import __version__
def program_setup(configdir, verbosity = 0, quietness = 0, service = False):
targetloglevel = 3+verbosity-quietness
targetverbosity = verbosity-quietness
if service:
RNS.logdest = RNS.LOG_FILE
RNS.logfile = RNS.Reticulum.configdir+"/logfile"
targetloglevel = None
targetlogdest = RNS.LOG_FILE
targetverbosity = None
else:
targetlogdest = RNS.LOG_STDOUT
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:
RNS.log("Started rnsd version {version}".format(version=__version__), RNS.LOG_NOTICE)
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
RNS.log("Started rnsd version {version}".format(version=__version__), RNS.LOG_NOTICE)
while True:
time.sleep(1)
@ -82,7 +87,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
@ -106,6 +111,18 @@ share_instance = Yes
shared_instance_port = 37428
instance_control_port = 37429
# 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
# You can configure Reticulum to panic and forcibly close
# if an unrecoverable interface error occurs, such as the
# hardware device for an interface disappearing. This is
@ -115,6 +132,17 @@ instance_control_port = 37429
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
[logging]
# Valid log levels are 0 through 7:
# 0: Log only critical information

View File

@ -46,7 +46,7 @@ def size_str(num, suffix='B'):
return "%.2f%s%s" % (num, last_unit, suffix)
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None):
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, sorting=None, sort_reverse=False):
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
stats = None
@ -56,7 +56,44 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None):
pass
if stats != None:
for ifstat in stats["interfaces"]:
if json:
import json
for s in stats:
if isinstance(stats[s], bytes):
stats[s] = RNS.hexrep(stats[s], delimit=False)
if isinstance(stats[s], dict):
for i in stats[s]:
if isinstance(i, dict):
for k in i:
if isinstance(i[k], bytes):
i[k] = RNS.hexrep(i[k], delimit=False)
print(json.dumps(stats))
exit()
interfaces = stats["interfaces"]
if sorting != None and isinstance(sorting, str):
sorting = sorting.lower()
if sorting == "rate" or sorting == "bitrate":
interfaces.sort(key=lambda i: i["bitrate"], reverse=not sort_reverse)
if sorting == "rx":
interfaces.sort(key=lambda i: i["rxb"], reverse=not sort_reverse)
if sorting == "tx":
interfaces.sort(key=lambda i: i["txb"], reverse=not sort_reverse)
if sorting == "traffic":
interfaces.sort(key=lambda i: i["rxb"]+i["txb"], reverse=not sort_reverse)
if sorting == "announces" or sorting == "announce":
interfaces.sort(key=lambda i: i["incoming_announce_frequency"]+i["outgoing_announce_frequency"], reverse=not sort_reverse)
if sorting == "arx":
interfaces.sort(key=lambda i: i["incoming_announce_frequency"], reverse=not sort_reverse)
if sorting == "atx":
interfaces.sort(key=lambda i: i["outgoing_announce_frequency"], reverse=not sort_reverse)
if sorting == "held":
interfaces.sort(key=lambda i: i["held_announces"], reverse=not sort_reverse)
for ifstat in interfaces:
name = ifstat["name"]
if dispall or not (
@ -98,7 +135,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None):
else:
spec_str = " programs"
clients_string = "Serving : "+str(cnum)+spec_str
clients_string = "Serving : "+str(cnum)+spec_str
elif name.startswith("I2PInterface["):
if "i2p_connectable" in ifstat and ifstat["i2p_connectable"] == True:
cnum = clients
@ -107,11 +144,11 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None):
else:
spec_str = " connected I2P endpoints"
clients_string = "Peers : "+str(cnum)+spec_str
clients_string = "Peers : "+str(cnum)+spec_str
else:
clients_string = ""
else:
clients_string = "Clients : "+str(clients)
clients_string = "Clients : "+str(clients)
else:
clients = None
@ -119,40 +156,63 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None):
print(" {n}".format(n=ifstat["name"]))
if "ifac_netname" in ifstat and ifstat["ifac_netname"] != None:
print(" Network : {nn}".format(nn=ifstat["ifac_netname"]))
print(" Network : {nn}".format(nn=ifstat["ifac_netname"]))
print(" Status : {ss}".format(ss=ss))
print(" Status : {ss}".format(ss=ss))
if clients != None and clients_string != "":
print(" "+clients_string)
if not (name.startswith("Shared Instance[") or name.startswith("TCPInterface[Client") or name.startswith("LocalInterface[")):
print(" Mode : {mode}".format(mode=modestr))
print(" Mode : {mode}".format(mode=modestr))
if "bitrate" in ifstat and ifstat["bitrate"] != None:
print(" Rate : {ss}".format(ss=speed_str(ifstat["bitrate"])))
print(" Rate : {ss}".format(ss=speed_str(ifstat["bitrate"])))
if "airtime_short" in ifstat and "airtime_long" in ifstat:
print(" Airtime : {ats}% (15s), {atl}% (1h)".format(ats=str(ifstat["airtime_short"]),atl=str(ifstat["airtime_long"])))
if "channel_load_short" in ifstat and "channel_load_long" in ifstat:
print(" Ch.Load : {ats}% (15s), {atl}% (1h)".format(ats=str(ifstat["channel_load_short"]),atl=str(ifstat["channel_load_long"])))
if "peers" in ifstat and ifstat["peers"] != None:
print(" Peers : {np} reachable".format(np=ifstat["peers"]))
print(" Peers : {np} reachable".format(np=ifstat["peers"]))
if "tunnelstate" in ifstat and ifstat["tunnelstate"] != None:
print(" I2P : {ts}".format(ts=ifstat["tunnelstate"]))
if "ifac_signature" in ifstat and ifstat["ifac_signature"] != None:
sigstr = "<…"+RNS.hexrep(ifstat["ifac_signature"][-5:], delimit=False)+">"
print(" Access : {nb}-bit IFAC by {sig}".format(nb=ifstat["ifac_size"]*8, sig=sigstr))
print(" Access : {nb}-bit IFAC by {sig}".format(nb=ifstat["ifac_size"]*8, sig=sigstr))
if "i2p_b32" in ifstat and ifstat["i2p_b32"] != None:
print(" I2P B32 : {ep}".format(ep=str(ifstat["i2p_b32"])))
print(" I2P B32 : {ep}".format(ep=str(ifstat["i2p_b32"])))
if "announce_queue" in ifstat and ifstat["announce_queue"] != None and ifstat["announce_queue"] > 0:
if astats and "announce_queue" in ifstat and ifstat["announce_queue"] != None and ifstat["announce_queue"] > 0:
aqn = ifstat["announce_queue"]
if aqn == 1:
print(" Queued : {np} announce".format(np=aqn))
print(" Queued : {np} announce".format(np=aqn))
else:
print(" Queued : {np} announces".format(np=aqn))
print(" Queued : {np} announces".format(np=aqn))
print(" Traffic : {txb}\n {rxb}".format(rxb=size_str(ifstat["rxb"]), txb=size_str(ifstat["txb"])))
if astats and "held_announces" in ifstat and ifstat["held_announces"] != None and ifstat["held_announces"] > 0:
aqn = ifstat["held_announces"]
if aqn == 1:
print(" Held : {np} announce".format(np=aqn))
else:
print(" Held : {np} announces".format(np=aqn))
if astats and "incoming_announce_frequency" in ifstat and ifstat["incoming_announce_frequency"] != None:
print(" Announces : {iaf}".format(iaf=RNS.prettyfrequency(ifstat["outgoing_announce_frequency"])))
print(" {iaf}".format(iaf=RNS.prettyfrequency(ifstat["incoming_announce_frequency"])))
print(" Traffic : {txb}\n {rxb}".format(rxb=size_str(ifstat["rxb"]), txb=size_str(ifstat["txb"])))
if "transport_id" in stats and stats["transport_id"] != None:
print("\n Reticulum Transport Instance "+RNS.prettyhexrep(stats["transport_id"])+" is running")
print("\n Transport Instance "+RNS.prettyhexrep(stats["transport_id"])+" running")
if "probe_responder" in stats and stats["probe_responder"] != None:
print(" Probe responder at "+RNS.prettyhexrep(stats["probe_responder"])+ " active")
print(" Uptime is "+RNS.prettytime(stats["transport_uptime"]))
print("")
@ -172,6 +232,39 @@ def main():
help="show all interfaces",
default=False
)
parser.add_argument(
"-A",
"--announce-stats",
action="store_true",
help="show announce stats",
default=False
)
parser.add_argument(
"-s",
"--sort",
action="store",
help="sort interfaces by [rate, traffic, rx, tx, announces, arx, atx, held]",
default=None,
type=str
)
parser.add_argument(
"-r",
"--reverse",
action="store_true",
help="reverse sorting",
default=False,
)
parser.add_argument(
"-j",
"--json",
action="store_true",
help="output in JSON format",
default=False
)
parser.add_argument('-v', '--verbose', action='count', default=0)
@ -184,7 +277,16 @@ def main():
else:
configarg = None
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter)
program_setup(
configdir = configarg,
dispall = args.all,
verbosity=args.verbose,
name_filter=args.filter,
json=args.json,
astats=args.announce_stats,
sorting=args.sort,
sort_reverse=args.reverse,
)
except KeyboardInterrupt:
print("")

View File

@ -538,7 +538,7 @@ def main():
parser.add_argument("-x", '--interactive', action='store_true', default=False, help="enter interactive mode")
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', '--noauth', action='store_true', default=False, help="accept files from anyone")
parser.add_argument('-n', '--noauth', action='store_true', default=False, help="accept commands from anyone")
parser.add_argument('-N', '--noid', action='store_true', default=False, help="don't identify to listener")
parser.add_argument("-d", '--detailed', action='store_true', default=False, help="show detailed result output")
parser.add_argument("-m", action='store_true', dest="mirror", default=False, help="mirror exit code of remote command")

View File

@ -1,6 +1,6 @@
# MIT License
#
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -32,10 +32,13 @@ from ._version import __version__
from .Reticulum import Reticulum
from .Identity import Identity
from .Link import Link, RequestReceipt
from .Channel import MessageBase
from .Buffer import Buffer, RawChannelReader, RawChannelWriter
from .Transport import Transport
from .Destination import Destination
from .Packet import Packet
from .Packet import PacketReceipt
from .Resolver import Resolver
from .Resource import Resource, ResourceAdvertisement
from .Cryptography import HKDF
from .Cryptography import Hashes
@ -57,10 +60,11 @@ LOG_FILE = 0x92
LOG_MAXSIZE = 5*1024*1024
loglevel = LOG_NOTICE
logfile = None
logdest = LOG_STDOUT
logtimefmt = "%Y-%m-%d %H:%M:%S"
loglevel = LOG_NOTICE
logfile = None
logdest = LOG_STDOUT
logtimefmt = "%Y-%m-%d %H:%M:%S"
compact_log_fmt = False
instance_random = random.Random()
instance_random.seed(os.urandom(10))
@ -101,10 +105,14 @@ def timestamp_str(time_s):
return time.strftime(logtimefmt, timestamp)
def log(msg, level=3, _override_destination = False):
global _always_override_destination
global _always_override_destination, compact_log_fmt
msg = str(msg)
if loglevel >= level:
logstring = "["+timestamp_str(time.time())+"] ["+loglevelname(level)+"] "+msg
if not compact_log_fmt:
logstring = "["+timestamp_str(time.time())+"] ["+loglevelname(level)+"] "+msg
else:
logstring = "["+timestamp_str(time.time())+"] "+msg
logging_lock.acquire()
if (logdest == LOG_STDOUT or _always_override_destination or _override_destination):
@ -136,6 +144,12 @@ def rand():
result = instance_random.random()
return result
def trace_exception(e):
import traceback
exception_info = "".join(traceback.TracebackException.from_exception(e).format())
log(f"An unhandled {str(type(e))} exception occurred: {str(e)}", LOG_ERROR)
log(exception_info, LOG_ERROR)
def hexrep(data, delimit=True):
try:
iter(data)
@ -153,6 +167,9 @@ def prettyhexrep(data):
hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">"
return hexrep
def prettyspeed(num, suffix="b"):
return prettysize(num/8, suffix=suffix)+"ps"
def prettysize(num, suffix='B'):
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
@ -172,32 +189,68 @@ def prettysize(num, suffix='B'):
return "%.2f%s%s" % (num, last_unit, suffix)
def prettytime(time, verbose=False):
def prettyfrequency(hz, suffix="Hz"):
num = hz*1e6
units = ["µ", "m", "", "K","M","G","T","P","E","Z"]
last_unit = "Y"
for unit in units:
if abs(num) < 1000.0:
return "%.2f %s%s" % (num, unit, suffix)
num /= 1000.0
return "%.2f%s%s" % (num, last_unit, suffix)
def prettydistance(m, suffix="m"):
num = m*1e6
units = ["µ", "m", "c", ""]
last_unit = "K"
for unit in units:
divisor = 1000.0
if unit == "m": divisor = 10
if unit == "c": divisor = 100
if abs(num) < divisor:
return "%.2f %s%s" % (num, unit, suffix)
num /= divisor
return "%.2f %s%s" % (num, last_unit, suffix)
def prettytime(time, verbose=False, compact=False):
days = int(time // (24 * 3600))
time = time % (24 * 3600)
hours = int(time // 3600)
time %= 3600
minutes = int(time // 60)
time %= 60
seconds = round(time, 2)
if compact:
seconds = int(time)
else:
seconds = round(time, 2)
ss = "" if seconds == 1 else "s"
sm = "" if minutes == 1 else "s"
sh = "" if hours == 1 else "s"
sd = "" if days == 1 else "s"
displayed = 0
components = []
if days > 0:
if days > 0 and ((not compact) or displayed < 2):
components.append(str(days)+" day"+sd if verbose else str(days)+"d")
displayed += 1
if hours > 0:
if hours > 0 and ((not compact) or displayed < 2):
components.append(str(hours)+" hour"+sh if verbose else str(hours)+"h")
displayed += 1
if minutes > 0:
if minutes > 0 and ((not compact) or displayed < 2):
components.append(str(minutes)+" minute"+sm if verbose else str(minutes)+"m")
displayed += 1
if seconds > 0:
if seconds > 0 and ((not compact) or displayed < 2):
components.append(str(seconds)+" second"+ss if verbose else str(seconds)+"s")
displayed += 1
i = 0
tstr = ""
@ -212,14 +265,17 @@ def prettytime(time, verbose=False):
tstr += c
return tstr
if tstr == "":
return "0s"
else:
return tstr
def phyparams():
print("Required Physical Layer MTU : "+str(Reticulum.MTU)+" bytes")
print("Plaintext Packet MDU : "+str(Packet.PLAIN_MDU)+" bytes")
print("Encrypted Packet MDU : "+str(Packet.ENCRYPTED_MDU)+" bytes")
print("Link Curve : "+str(Link.CURVE))
print("Link Packet MDU : "+str(Packet.ENCRYPTED_MDU)+" bytes")
print("Link Packet MDU : "+str(Link.MDU)+" bytes")
print("Link Public Key Size : "+str(Link.ECPUBSIZE*8)+" bits")
print("Link Private Key Size : "+str(Link.KEYSIZE*8)+" bits")
@ -228,4 +284,4 @@ def panic():
def exit():
print("")
sys.exit(0)
sys.exit(0)

View File

@ -1 +1 @@
__version__ = "0.3.9"
__version__ = "0.7.5"

33
RNS/vendor/ifaddr/__init__.py vendored Normal file
View File

@ -0,0 +1,33 @@
# Copyright (c) 2014 Stefan C. Mueller
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import os
from RNS.vendor.ifaddr._shared import Adapter, IP
if os.name == "nt":
from RNS.vendor.ifaddr._win32 import get_adapters
elif os.name == "posix":
from RNS.vendor.ifaddr._posix import get_adapters
else:
raise RuntimeError("Unsupported Operating System: %s" % os.name)
__all__ = ['Adapter', 'IP', 'get_adapters']

93
RNS/vendor/ifaddr/_posix.py vendored Normal file
View File

@ -0,0 +1,93 @@
# Copyright (c) 2014 Stefan C. Mueller
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import os
import ctypes.util
import ipaddress
import collections
import socket
from typing import Iterable, Optional
import RNS.vendor.ifaddr._shared as shared
class ifaddrs(ctypes.Structure):
pass
ifaddrs._fields_ = [
('ifa_next', ctypes.POINTER(ifaddrs)),
('ifa_name', ctypes.c_char_p),
('ifa_flags', ctypes.c_uint),
('ifa_addr', ctypes.POINTER(shared.sockaddr)),
('ifa_netmask', ctypes.POINTER(shared.sockaddr)),
]
libc = ctypes.CDLL(ctypes.util.find_library("socket" if os.uname()[0] == "SunOS" else "c"), use_errno=True) # type: ignore
def get_adapters(include_unconfigured: bool = False) -> Iterable[shared.Adapter]:
addr0 = addr = ctypes.POINTER(ifaddrs)()
retval = libc.getifaddrs(ctypes.byref(addr))
if retval != 0:
eno = ctypes.get_errno()
raise OSError(eno, os.strerror(eno))
ips = collections.OrderedDict()
def add_ip(adapter_name: str, ip: Optional[shared.IP]) -> None:
if adapter_name not in ips:
index = None # type: Optional[int]
try:
# Mypy errors on this when the Windows CI runs:
# error: Module has no attribute "if_nametoindex"
index = socket.if_nametoindex(adapter_name) # type: ignore
except (OSError, AttributeError):
pass
ips[adapter_name] = shared.Adapter(adapter_name, adapter_name, [], index=index)
if ip is not None:
ips[adapter_name].ips.append(ip)
while addr:
name = addr[0].ifa_name.decode(encoding='UTF-8')
ip_addr = shared.sockaddr_to_ip(addr[0].ifa_addr)
if ip_addr:
if addr[0].ifa_netmask and not addr[0].ifa_netmask[0].sa_familiy:
addr[0].ifa_netmask[0].sa_familiy = addr[0].ifa_addr[0].sa_familiy
netmask = shared.sockaddr_to_ip(addr[0].ifa_netmask)
if isinstance(netmask, tuple):
netmaskStr = str(netmask[0])
prefixlen = shared.ipv6_prefixlength(ipaddress.IPv6Address(netmaskStr))
else:
assert netmask is not None, f'sockaddr_to_ip({addr[0].ifa_netmask}) returned None'
netmaskStr = str('0.0.0.0/' + netmask)
prefixlen = ipaddress.IPv4Network(netmaskStr).prefixlen
ip = shared.IP(ip_addr, prefixlen, name)
add_ip(name, ip)
else:
if include_unconfigured:
add_ip(name, None)
addr = addr[0].ifa_next
libc.freeifaddrs(addr0)
return ips.values()

198
RNS/vendor/ifaddr/_shared.py vendored Normal file
View File

@ -0,0 +1,198 @@
# Copyright (c) 2014 Stefan C. Mueller
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import ctypes
import socket
import ipaddress
import platform
from typing import List, Optional, Tuple, Union
class Adapter(object):
"""
Represents a network interface device controller (NIC), such as a
network card. An adapter can have multiple IPs.
On Linux aliasing (multiple IPs per physical NIC) is implemented
by creating 'virtual' adapters, each represented by an instance
of this class. Each of those 'virtual' adapters can have both
a IPv4 and an IPv6 IP address.
"""
def __init__(self, name: str, nice_name: str, ips: List['IP'], index: Optional[int] = None) -> None:
#: Unique name that identifies the adapter in the system.
#: On Linux this is of the form of `eth0` or `eth0:1`, on
#: Windows it is a UUID in string representation, such as
#: `{846EE342-7039-11DE-9D20-806E6F6E6963}`.
self.name = name
#: Human readable name of the adpater. On Linux this
#: is currently the same as :attr:`name`. On Windows
#: this is the name of the device.
self.nice_name = nice_name
#: List of :class:`ifaddr.IP` instances in the order they were
#: reported by the system.
self.ips = ips
#: Adapter index as used by some API (e.g. IPv6 multicast group join).
self.index = index
def __repr__(self) -> str:
return "Adapter(name={name}, nice_name={nice_name}, ips={ips}, index={index})".format(
name=repr(self.name), nice_name=repr(self.nice_name), ips=repr(self.ips), index=repr(self.index)
)
# Type of an IPv4 address (a string in "xxx.xxx.xxx.xxx" format)
_IPv4Address = str
# Type of an IPv6 address (a three-tuple `(ip, flowinfo, scope_id)`)
_IPv6Address = Tuple[str, int, int]
class IP(object):
"""
Represents an IP address of an adapter.
"""
def __init__(self, ip: Union[_IPv4Address, _IPv6Address], network_prefix: int, nice_name: str) -> None:
#: IP address. For IPv4 addresses this is a string in
#: "xxx.xxx.xxx.xxx" format. For IPv6 addresses this
#: is a three-tuple `(ip, flowinfo, scope_id)`, where
#: `ip` is a string in the usual collon separated
#: hex format.
self.ip = ip
#: Number of bits of the IP that represent the
#: network. For a `255.255.255.0` netmask, this
#: number would be `24`.
self.network_prefix = network_prefix
#: Human readable name for this IP.
#: On Linux is this currently the same as the adapter name.
#: On Windows this is the name of the network connection
#: as configured in the system control panel.
self.nice_name = nice_name
@property
def is_IPv4(self) -> bool:
"""
Returns `True` if this IP is an IPv4 address and `False`
if it is an IPv6 address.
"""
return not isinstance(self.ip, tuple)
@property
def is_IPv6(self) -> bool:
"""
Returns `True` if this IP is an IPv6 address and `False`
if it is an IPv4 address.
"""
return isinstance(self.ip, tuple)
def __repr__(self) -> str:
return "IP(ip={ip}, network_prefix={network_prefix}, nice_name={nice_name})".format(
ip=repr(self.ip), network_prefix=repr(self.network_prefix), nice_name=repr(self.nice_name)
)
if platform.system() == "Darwin" or "BSD" in platform.system():
# BSD derived systems use marginally different structures
# than either Linux or Windows.
# I still keep it in `shared` since we can use
# both structures equally.
class sockaddr(ctypes.Structure):
_fields_ = [
('sa_len', ctypes.c_uint8),
('sa_familiy', ctypes.c_uint8),
('sa_data', ctypes.c_uint8 * 14),
]
class sockaddr_in(ctypes.Structure):
_fields_ = [
('sa_len', ctypes.c_uint8),
('sa_familiy', ctypes.c_uint8),
('sin_port', ctypes.c_uint16),
('sin_addr', ctypes.c_uint8 * 4),
('sin_zero', ctypes.c_uint8 * 8),
]
class sockaddr_in6(ctypes.Structure):
_fields_ = [
('sa_len', ctypes.c_uint8),
('sa_familiy', ctypes.c_uint8),
('sin6_port', ctypes.c_uint16),
('sin6_flowinfo', ctypes.c_uint32),
('sin6_addr', ctypes.c_uint8 * 16),
('sin6_scope_id', ctypes.c_uint32),
]
else:
class sockaddr(ctypes.Structure): # type: ignore
_fields_ = [('sa_familiy', ctypes.c_uint16), ('sa_data', ctypes.c_uint8 * 14)]
class sockaddr_in(ctypes.Structure): # type: ignore
_fields_ = [
('sin_familiy', ctypes.c_uint16),
('sin_port', ctypes.c_uint16),
('sin_addr', ctypes.c_uint8 * 4),
('sin_zero', ctypes.c_uint8 * 8),
]
class sockaddr_in6(ctypes.Structure): # type: ignore
_fields_ = [
('sin6_familiy', ctypes.c_uint16),
('sin6_port', ctypes.c_uint16),
('sin6_flowinfo', ctypes.c_uint32),
('sin6_addr', ctypes.c_uint8 * 16),
('sin6_scope_id', ctypes.c_uint32),
]
def sockaddr_to_ip(sockaddr_ptr: 'ctypes.pointer[sockaddr]') -> Optional[Union[_IPv4Address, _IPv6Address]]:
if sockaddr_ptr:
if sockaddr_ptr[0].sa_familiy == socket.AF_INET:
ipv4 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in))
ippacked = bytes(bytearray(ipv4[0].sin_addr))
ip = str(ipaddress.ip_address(ippacked))
return ip
elif sockaddr_ptr[0].sa_familiy == socket.AF_INET6:
ipv6 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in6))
flowinfo = ipv6[0].sin6_flowinfo
ippacked = bytes(bytearray(ipv6[0].sin6_addr))
ip = str(ipaddress.ip_address(ippacked))
scope_id = ipv6[0].sin6_scope_id
return (ip, flowinfo, scope_id)
return None
def ipv6_prefixlength(address: ipaddress.IPv6Address) -> int:
prefix_length = 0
for i in range(address.max_prefixlen):
if int(address) >> i & 1:
prefix_length = prefix_length + 1
return prefix_length

145
RNS/vendor/ifaddr/_win32.py vendored Normal file
View File

@ -0,0 +1,145 @@
# Copyright (c) 2014 Stefan C. Mueller
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import ctypes
from ctypes import wintypes
from typing import Iterable, List
import RNS.vendor.ifaddr._shared as shared
NO_ERROR = 0
ERROR_BUFFER_OVERFLOW = 111
MAX_ADAPTER_NAME_LENGTH = 256
MAX_ADAPTER_DESCRIPTION_LENGTH = 128
MAX_ADAPTER_ADDRESS_LENGTH = 8
AF_UNSPEC = 0
class SOCKET_ADDRESS(ctypes.Structure):
_fields_ = [('lpSockaddr', ctypes.POINTER(shared.sockaddr)), ('iSockaddrLength', wintypes.INT)]
class IP_ADAPTER_UNICAST_ADDRESS(ctypes.Structure):
pass
IP_ADAPTER_UNICAST_ADDRESS._fields_ = [
('Length', wintypes.ULONG),
('Flags', wintypes.DWORD),
('Next', ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)),
('Address', SOCKET_ADDRESS),
('PrefixOrigin', ctypes.c_uint),
('SuffixOrigin', ctypes.c_uint),
('DadState', ctypes.c_uint),
('ValidLifetime', wintypes.ULONG),
('PreferredLifetime', wintypes.ULONG),
('LeaseLifetime', wintypes.ULONG),
('OnLinkPrefixLength', ctypes.c_uint8),
]
class IP_ADAPTER_ADDRESSES(ctypes.Structure):
pass
IP_ADAPTER_ADDRESSES._fields_ = [
('Length', wintypes.ULONG),
('IfIndex', wintypes.DWORD),
('Next', ctypes.POINTER(IP_ADAPTER_ADDRESSES)),
('AdapterName', ctypes.c_char_p),
('FirstUnicastAddress', ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)),
('FirstAnycastAddress', ctypes.c_void_p),
('FirstMulticastAddress', ctypes.c_void_p),
('FirstDnsServerAddress', ctypes.c_void_p),
('DnsSuffix', ctypes.c_wchar_p),
('Description', ctypes.c_wchar_p),
('FriendlyName', ctypes.c_wchar_p),
]
iphlpapi = ctypes.windll.LoadLibrary("Iphlpapi") # type: ignore
def enumerate_interfaces_of_adapter(
nice_name: str, address: IP_ADAPTER_UNICAST_ADDRESS
) -> Iterable[shared.IP]:
# Iterate through linked list and fill list
addresses = [] # type: List[IP_ADAPTER_UNICAST_ADDRESS]
while True:
addresses.append(address)
if not address.Next:
break
address = address.Next[0]
for address in addresses:
ip = shared.sockaddr_to_ip(address.Address.lpSockaddr)
assert ip is not None, f'sockaddr_to_ip({address.Address.lpSockaddr}) returned None'
network_prefix = address.OnLinkPrefixLength
yield shared.IP(ip, network_prefix, nice_name)
def get_adapters(include_unconfigured: bool = False) -> Iterable[shared.Adapter]:
# Call GetAdaptersAddresses() with error and buffer size handling
addressbuffersize = wintypes.ULONG(15 * 1024)
retval = ERROR_BUFFER_OVERFLOW
while retval == ERROR_BUFFER_OVERFLOW:
addressbuffer = ctypes.create_string_buffer(addressbuffersize.value)
retval = iphlpapi.GetAdaptersAddresses(
wintypes.ULONG(AF_UNSPEC),
wintypes.ULONG(0),
None,
ctypes.byref(addressbuffer),
ctypes.byref(addressbuffersize),
)
if retval != NO_ERROR:
raise ctypes.WinError() # type: ignore
# Iterate through adapters fill array
address_infos = [] # type: List[IP_ADAPTER_ADDRESSES]
address_info = IP_ADAPTER_ADDRESSES.from_buffer(addressbuffer)
while True:
address_infos.append(address_info)
if not address_info.Next:
break
address_info = address_info.Next[0]
# Iterate through unicast addresses
result = [] # type: List[shared.Adapter]
for adapter_info in address_infos:
# We don't expect non-ascii characters here, so encoding shouldn't matter
name = adapter_info.AdapterName.decode()
nice_name = adapter_info.Description
index = adapter_info.IfIndex
if adapter_info.FirstUnicastAddress:
ips = enumerate_interfaces_of_adapter(
adapter_info.FriendlyName, adapter_info.FirstUnicastAddress[0]
)
ips = list(ips)
result.append(shared.Adapter(name, nice_name, ips, index=index))
elif include_unconfigured:
result.append(shared.Adapter(name, nice_name, [], index=index))
return result

57
RNS/vendor/ifaddr/niwrapper.py vendored Normal file
View File

@ -0,0 +1,57 @@
import ipaddress
import RNS.vendor.ifaddr
import socket
from typing import List
AF_INET6 = socket.AF_INET6.value
AF_INET = socket.AF_INET.value
def interfaces() -> List[str]:
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
return [a.name for a in adapters]
def interface_names_to_indexes() -> dict:
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
results = {}
for adapter in adapters:
results[adapter.name] = adapter.index
return results
def interface_name_to_nice_name(ifname) -> str:
try:
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
for adapter in adapters:
if adapter.name == ifname:
if hasattr(adapter, "nice_name"):
return adapter.nice_name
except:
return None
return None
def ifaddresses(ifname) -> dict:
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
ifa = {}
for a in adapters:
if a.name == ifname:
ipv4s = []
ipv6s = []
for ip in a.ips:
t = {}
if ip.is_IPv4:
net = ipaddress.ip_network(str(ip.ip)+"/"+str(ip.network_prefix), strict=False)
t["addr"] = ip.ip
t["prefix"] = ip.network_prefix
t["broadcast"] = str(net.broadcast_address)
ipv4s.append(t)
if ip.is_IPv6:
t["addr"] = ip.ip[0]
ipv6s.append(t)
if len(ipv4s) > 0:
ifa[AF_INET] = ipv4s
if len(ipv6s) > 0:
ifa[AF_INET6] = ipv6s
return ifa

0
RNS/vendor/ifaddr/py.typed vendored Normal file
View File

View File

@ -8,6 +8,12 @@ def get_platform():
import sys
return sys.platform
def is_linux():
if get_platform() == "linux":
return True
else:
return False
def is_darwin():
if get_platform() == "darwin":
return True
@ -42,4 +48,4 @@ def cryptography_old_api():
if cryptography.__version__ == "2.8":
return True
else:
return False
return False

112
Roadmap.md Normal file
View File

@ -0,0 +1,112 @@
# Reticulum Development Roadmap
This document outlines the currently established development roadmap for Reticulum.
1. [Currently Active Work Areas](#currently-active-work-areas)
2. [Primary Efforts](#primary-efforts)
- [Comprehensibility](#comprehensibility)
- [Universality](#universality)
- [Functionality](#functionality)
- [Usability & Utility](#usability--utility)
- [Interfaceability](#interfaceability)
3. [Auxillary Efforts](#auxillary-efforts)
4. [Release History](#release-history)
## Currently Active Work Areas
For each release cycle of Reticulum, improvements and additions from the five [Primary Efforts](#primary-efforts) are selected as active work areas, and can be expected to be included in the upcoming releases within that cycle. While not entirely set in stone for each release cycle, they serve as a pointer of what to expect in the near future.
- The current `0.7.x` release cycle aims at completing
- [ ] Overhauling and updating the documentation
- [ ] Distributed Destination Naming System
- [ ] Create a standalone RNS Daemon app for Android
- [ ] Network-wide path balancing
- [ ] Add automatic retries to all use cases of the `Request` API
- [ ] Performance and memory optimisations of the Python reference implementation
- [ ] Fixing bugs discovered while operating Reticulum systems and applications
## Primary Efforts
The development path for Reticulum is currently laid out in five distinct areas: *Comprehensibility*, *Universality*, *Functionality*, *Usability & Utility* and *Interfaceability*. Conceptualising the development of Reticulum into these areas serves to advance the implementation and work towards the Foundational Goals & Values of Reticulum.
### Comprehensibility
These efforts are aimed at improving the ease of which Reticulum is understood, and lowering the barrier to entry for people who wish to start building systems on Reticulum.
- Improving [the manual](https://markqvist.github.io/Reticulum/manual/) with tutorials specifically for beginners
- Updating the documentation to reflect recent changes and improvements
- Update descriptions of protocol mechanics
- Update announce description
- Add in-depth explanation of the IFAC system
- Software
- Update Sideband screenshots
- Update Sideband description
- Update NomadNet screenshots
- Update Sideband screenshots
- Installation
- [x] Add a *Reticulum On Raspberry Pi* section
- [x] Update *Reticulum On Android* section if necessary
- [x] Update Android install documentation.
- Communications hardware section
- Add information about RNode external displays.
- [x] Packet radio modems.
- Possibly add other relevant types here as well.
- Setup *Best Practices For...* / *Installation Examples* section.
- Home or office (example)
- Vehicles (example)
- No-grid/solar/remote sites (example)
### Universality
These efforts seek to broaden the universality of the Reticulum software and hardware ecosystem by continously diversifying platform support, and by improving the overall availability and ease of deployment of the Reticulum stack.
- OpenWRT support
- Create a standalone RNS Daemon app for Android
- A lightweight and portable C implementation for microcontrollers, µRNS
- A portable, high-performance Reticulum implementation in C/C++, see [#21](https://github.com/markqvist/Reticulum/discussions/21)
- Performance and memory optimisations of the Python implementation
- Bindings for other programming languages
### Functionality
These efforts aim to expand and improve the core functionality and reliability of Reticulum.
- Add automatic retries to all use cases of the `Request` API
- Network-wide path balancing
- Distributed Destination Naming System
- Globally routable multicast
- Destination proxying
- [Metric-based path selection and multiple paths](https://github.com/markqvist/Reticulum/discussions/86)
### Usability & Utility
These effors seek to make Reticulum easier to use and operate, and to expand the utility of the stack on deployed systems.
- Easy way to share interface configurations, see [#19](https://github.com/markqvist/Reticulum/discussions/19)
- Transit traffic display in rnstatus
- rnsconfig utility
### Interfaceability
These efforts aim to expand the types of physical and virtual interfaces that Reticulum can natively use to transport data.
- Filesystem interface
- Plain ESP32 devices (ESP-Now, WiFi, Bluetooth, etc.)
- More LoRa transceivers
- AT-compatible modems
- Direct SDR Support
- Optical mediums
- IR Transceivers
- AWDL / OWL
- HF Modems
- GNU Radio
- CAN-bus
- Raw SPI
- Raw i²c
- MQTT
- XBee
- Tor
## Auxillary Efforts
The Reticulum ecosystem is enriched by several other software and hardware projects, and the support and improvement of these, in symbiosis with the core Reticulum project helps expand the reach and utility of Reticulum itself.
This section lists, in no particular order, various important efforts that would be beneficial to the goals of Reticulum.
- The [RNode](https://unsigned.io/rnode/) project
- [ ] Create a WebUSB-based bootstrapping utility, and integrate this directly into the [RNode Bootstrap Console](#), both on-device, and on an Internet-reachable copy. This will make it much easier to create new RNodes for average users.
## Release History
Please see the [Changelog](./Changelog.md) for a complete release history and changelog of Reticulum.

View File

@ -30,3 +30,8 @@ help:
cp -r build/latex/reticulumnetworkstack.pdf ./Reticulum\ Manual.pdf; \
echo "PDF Manual Generated"; \
fi
@if [ $@ = "epub" ]; then \
cp -r build/epub/ReticulumNetworkStack.epub ./Reticulum\ Manual.epub; \
echo "EPUB Manual Generated"; \
fi

BIN
docs/Reticulum Manual.epub Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
config: d4963122ff51d6bca74c9598f37a0cc1
config: e6cf914f5d96347d4f64d2e8bcefb841
tags: 645f666f9bcd5a90fca523b33c5a78b7

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -92,6 +92,28 @@ The *Request* example explores sendig requests and receiving responses.
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Request.py>`_.
.. _example-channel:
Channel
=======
The *Channel* example explores using a ``Channel`` to send structured
data between peers of a ``Link``.
.. literalinclude:: ../../Examples/Channel.py
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Channel.py>`_.
Buffer
======
The *Buffer* example explores using buffered readers and writers to send
binary data between peers of a ``Link``.
.. literalinclude:: ../../Examples/Buffer.py
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Buffer.py>`_.
.. _example-filetransfer:
Filetransfer

View File

@ -0,0 +1,4 @@
********************************************
An Explanation of Reticulum for Human Beings
********************************************

View File

@ -7,22 +7,72 @@ you want to do. This guide will outline sensible starting paths for different
scenarios.
Standalone Reticulum Installation
=============================================
If you simply want to install Reticulum and related utilities on a system,
the easiest way is via the ``pip`` package manager:
.. code::
pip install rns
If you do not already have pip installed, you can install it using the package manager
of your system with a command like ``sudo apt install python3-pip``,
``sudo pamac install python-pip`` or similar.
You can also dowload the Reticulum release wheels from GitHub, or other release channels,
and install them offline using ``pip``:
.. code::
pip install ./rns-0.5.1-py3-none-any.whl
Resolving Dependency & Installation Issues
=============================================
On some platforms, there may not be binary packages available for all dependencies, and
``pip`` installation may fail with an error message. In these cases, the issue can usually
be resolved by installing the development essentials packages for your platform:
.. code::
# Debian / Ubuntu / Derivatives
sudo apt install build-essential
# Arch / Manjaro / Derivatives
sudo pamac install base-devel
# Fedora
sudo dnf groupinstall "Development Tools" "Development Libraries"
With the base development packages installed, ``pip`` should be able to compile any missing
dependencies from source, and complete installation even on platforms that don't have pre-
compiled packages available.
Try Using a Reticulum-based Program
=============================================
If you simply want to try using a program built with Reticulum, a few different
programs exist that allow basic communication and a range of other useful functions
over even extremely low-bandwidth Reticulum networks.
programs exist that allow basic communication and a range of other useful functions,
even over extremely low-bandwidth Reticulum networks.
These programs will let you get a feel for how Reticulum works. They have been designed
to run well over networks based on LoRa or packet radio, but can also be used completely
over local WiFi, wired ethernet, the Internet, or any combination.
to run well over networks based on LoRa or packet radio, but can also be used over fast
links, such as local WiFi, wired Ethernet, the Internet, or any combination.
As such, it is easy to get started experimenting, without having to set up any radio
transceivers or infrastructure just to try it out. Launching the programs on separate
devices connected to the same WiFi network is enough to get started, and physical
radio interfaces can then be added later.
Remote Shell
^^^^^^^^^^^^
The `rnsh <https://github.com/acehoss/rnsh>`_ program lets you establish fully interactive
remote shell sessions over Reticulum. It also allows you to pipe any program to or from a
remote system, and is similar to how ``ssh`` works. The ``rnsh`` is very efficient, and
can facilitate fully interactive shell sessions, even over extremely low-bandwidth links.
Nomad Network
^^^^^^^^^^^^^
@ -44,7 +94,7 @@ You can install Nomad Network via pip:
.. code::
# Install ...
pip3 install nomadnet
pip install nomadnet
# ... and run
nomadnet
@ -61,13 +111,22 @@ If you would rather use a program with a graphical user interface, you can take
a look at `Sideband <https://unsigned.io/sideband>`_, which is available for Android,
Linux and macOS.
.. image:: screenshots/sideband_1.png
:width: 400px
:align: center
:target: _images/sideband_1.png
.. only:: html
Sideband is currently in the early stages of development, but already provides basic
communication features, and interoperates with Nomad Network, or any other LXMF client.
.. image:: screenshots/sideband_devices.webp
:align: center
:target: _images/sideband_devices.webp
.. only:: latexpdf
.. image:: screenshots/sideband_devices.png
:align: center
:target: _images/sideband_devices.png
Sideband allows you to communicate with other people or LXMF-compatible
systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted QR
Paper Messages, or anything else Reticulum supports. It also interoperates with
the Nomad Network program.
Using the Included Utilities
=============================================
@ -87,12 +146,12 @@ Creating a Network With Reticulum
=============================================
To create a network, you will need to specify one or more *interfaces* for
Reticulum to use. This is done in the Reticulum configuration file, which by
default is located at ``~/.reticulum/config``. You can edit this file by hand,
or use the interactive ``rnsconfig`` utility.
default is located at ``~/.reticulum/config``. You can get an example
configuration file with all options via ``rnsd --exampleconfig``.
When Reticulum is started for the first time, it will create a default
configuration file, with one active interface. This default interface uses
your existing ethernet and WiFi networks (if any), and only allows you to
your existing Ethernet and WiFi networks (if any), and only allows you to
communicate with other Reticulum peers within your local broadcast domains.
To communicate further, you will have to add one or more interfaces. The default
@ -107,7 +166,7 @@ Once Reticulum knows which interfaces it should use, it will automatically
discover topography and configure transport of data to any destinations it
knows about.
In situations where you already have an established WiFi or ethernet network, and
In situations where you already have an established WiFi or Ethernet network, and
many devices that want to utilise the same external Reticulum network paths (for example over
LoRa), it will often be sufficient to let one system act as a Reticulum gateway, by
adding any external interfaces to the configuration of this system, and then enabling transport on it. Any
@ -121,34 +180,34 @@ and :ref:`Interfaces<interfaces-main>` chapters of this manual.
Connecting Reticulum Instances Over the Internet
================================================
Reticulum currently offers two interfaces suitable for connecting instances over the Internet: :ref:`TCP<interfaces-tcps>`
and :ref:`I2P<interfaces-i2p>`. Each interface offers a different set of features, and Reticulum
users should carefully choose the interface which best suites their needs.
and :ref:`I2P<interfaces-i2p>`. Each interface offers a different set of features, and Reticulum
users should carefully choose the interface which best suites their needs.
The ``TCPServerInterface`` allows users to host an instance accessible over TCP/IP. This
method is generally faster, lower latency, and more energy efficient than using ``I2PInterface``,
however it also leaks more data about the server host.
TCP connections reveal the IP address of both your instance and the server to anyone who can
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
inspecting your packets may be able to record packet metadata like time of transmission and packet size.
Even though Reticulum encrypts traffic, TCP does not, so an adversary may be able to use
packet inspection to learn that a system is running Reticulum, and what other IP adresses connect to it.
packet inspection to learn that a system is running Reticulum, and what other IP addresses connect to it.
Hosting a publicly reachable instance over TCP also requires a publicly reachable IP address,
which most Internet connections don't offer anymore.
The ``I2PInterface`` routes messages through the `Invisible Internet Protocol
The ``I2PInterface`` routes messages through the `Invisible Internet Protocol
(I2P) <https://geti2p.net/en/>`_. To use this interface, users must also run an I2P daemon in
parallel to ``rnsd``. For always-on I2P nodes it is recommended to use `i2pd <https://i2pd.website/>`_.
parallel to ``rnsd``. For always-on I2P nodes it is recommended to use `i2pd <https://i2pd.website/>`_.
By default, I2P will encrypt and mix all traffic sent over the Internet, and
hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node
By default, I2P will encrypt and mix all traffic sent over the Internet, and
hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node
will also relay other I2P user's encrypted packets, which will use extra
bandwidth and compute power, but also makes timing attacks and other forms of
bandwidth and compute power, but also makes timing attacks and other forms of
deep-packet-inspection much more difficult.
I2P also allows users to host globally available Reticulum instances from non-public IPs and behind firewalls and NAT.
I2P also allows users to host globally available Reticulum instances from non-public IP's and behind firewalls and NAT.
In general it is recommended to use an I2P node if you want to host a publically accessible
In general it is recommended to use an I2P node if you want to host a publicly accessible
instance, while preserving anonymity. If you care more about performance, and a slightly
easier setup, use TCP.
@ -161,25 +220,25 @@ by adding one of the following interfaces to your ``.reticulum/config`` file:
.. code::
# TCP/IP interface to the Dublin hub
[[RNS Testnet Dublin]]
# TCP/IP interface to the RNS Amsterdam Hub
[[RNS Testnet Amsterdam]]
type = TCPClientInterface
enabled = yes
target_host = dublin.connect.reticulum.network
target_host = amsterdam.connect.reticulum.network
target_port = 4965
# TCP/IP interface to the Frankfurt hub
[[RNS Testnet Dublin]]
# TCP/IP interface to the BetweenTheBorders Hub (community-provided)
[[RNS Testnet BetweenTheBorders]]
type = TCPClientInterface
enabled = yes
target_host = frankfurt.connect.reticulum.network
target_port = 5377
target_host = betweentheborders.com
target_port = 4242
# Interface to I2P hub A
[[RNS Testnet I2P Hub A]]
# Interface to Testnet I2P Hub
[[RNS Testnet I2P Hub]]
type = I2PInterface
enabled = yes
peers = uxg5kubabakh3jtnvsipingbr5574dle7bubvip7llfvwx2tgrua.b32.i2p
peers = g3br23bvx3lq5uddcsjii74xgmn6y5q325ovrkq2zw2wbzbqgbuq.b32.i2p
Many other Reticulum instances are connecting to this testnet, and you can also join it
via other entry points if you know them. There is absolutely no control over the network
@ -206,7 +265,7 @@ chapter for a guide. If you prefer purchasing a ready-made unit, you can refer t
refer to these additional external resources:
* `How To Make Your Own RNodes <https://unsigned.io/how-to-make-your-own-rnodes/>`_
* `Installing RNode Firmware on Compatible LoRa Devices <https://unsigned.io/installing-rnode-firmware-on-t-beam-and-lora32-devices/>`_
* `Installing RNode Firmware on Compatible LoRa Devices <https://unsigned.io/installing-rnode-firmware-on-supported-devices/>`_
* `Private, Secure and Uncensorable Messaging Over a LoRa Mesh <https://unsigned.io/private-messaging-over-lora/>`_
* `RNode Firmware <https://github.com/markqvist/RNode_Firmware/>`_
@ -223,7 +282,7 @@ started is to install the latest release of Reticulum via pip:
.. code::
pip3 install rns
pip install rns
The above command will install Reticulum and dependencies, and you will be
ready to import and use RNS in your own programs. The next step will most
@ -233,7 +292,7 @@ For extended functionality, you can install optional dependencies:
.. code::
pip3 install pyserial netifaces
pip install pyserial
Further information can be found in the :ref:`API Reference<api-main>`.
@ -248,7 +307,7 @@ don't use pip, but try this recipe:
.. code::
# Install dependencies
pip3 install cryptography pyserial netifaces
pip install cryptography pyserial
# Clone repository
git clone https://github.com/markqvist/Reticulum.git
@ -258,34 +317,111 @@ don't use pip, but try this recipe:
ln -s ../RNS ./Examples/
# Run an example
python3 Examples/Echo.py -s
python Examples/Echo.py -s
# Unless you've manually created a config file, Reticulum will do so now,
# and immediately exit. Make any necessary changes to the file:
nano ~/.reticulum/config
# ... and launch the example again.
python3 Examples/Echo.py -s
python Examples/Echo.py -s
# You can now repeat the process on another computer,
# and run the same example with -h to get command line options.
python3 Examples/Echo.py -h
python Examples/Echo.py -h
# Run the example in client mode to "ping" the server.
# Replace the hash below with the actual destination hash of your server.
python3 Examples/Echo.py 3e12fc71692f8ec47bc5
python Examples/Echo.py 174a64852a75682259ad8b921b8bf416
# Have a look at another example
python3 Examples/Filetransfer.py -h
python Examples/Filetransfer.py -h
When you have experimented with the basic examples, it's time to go read the
:ref:`Understanding Reticulum<understanding-main>` chapter.
:ref:`Understanding Reticulum<understanding-main>` chapter. Before submitting
your first pull request, it is probably a good idea to introduce yourself on
the `disucssion forum on GitHub <https://github.com/markqvist/Reticulum/discussions>`_,
or ask one of the developers or maintainers for a good place to start.
Reticulum on ARM64
Platform-Specific Install Notes
==============================================
Some platforms require a slightly different installation procedure, or have
various quirks that are worth being aware of. These are listed here.
Android
^^^^^^^^^^^^^^^^^^^^^^^^
Reticulum can be used on Android in different ways. The easiest way to get
started is using an app like `Sideband <https://unsigned.io/sideband>`_.
For more control and features, you can use Reticulum and related programs via
the `Termux app <https://termux.com/>`_, at the time of writing available on
`F-droid <https://f-droid.org>`_.
Termux is a terminal emulator and Linux environment for Android based devices,
which includes the ability to use many different programs and libraries,
including Reticulum.
To use Reticulum within the Termux environment, you will need to install
``python`` and the ``python-cryptography`` library using ``pkg``, the package-manager
build into Termux. After that, you can use ``pip`` to install Reticulum.
From within Termux, execute the following:
.. code::
# First, make sure indexes and packages are up to date.
pkg update
pkg upgrade
# Then install python and the cryptography library.
pkg install python python-cryptography
# Make sure pip is up to date, and install the wheel module.
pip install wheel pip --upgrade
# Install Reticulum
pip install rns
If for some reason the ``python-cryptography`` package is not available for
your platform via the Termux package manager, you can attempt to build it
locally on your device using the following command:
.. code::
# First, make sure indexes and packages are up to date.
pkg update
pkg upgrade
# Then install dependencies for the cryptography library.
pkg install python build-essential openssl libffi rust
# Make sure pip is up to date, and install the wheel module.
pip install wheel pip --upgrade
# To allow the installer to build the cryptography module,
# we need to let it know what platform we are compiling for:
export CARGO_BUILD_TARGET="aarch64-linux-android"
# Start the install process for the cryptography module.
# Depending on your device, this can take several minutes,
# since the module must be compiled locally on your device.
pip install cryptography
# If the above installation succeeds, you can now install
# Reticulum and any related software
pip install rns
It is also possible to include Reticulum in apps compiled and distributed as
Android APKs. A detailed tutorial and example source code will be included
here at a later point. Until then you can use the `Sideband source code <https://github.com/markqvist/sideband>`_ as an example and starting point.
ARM64
^^^^^^^^^^^^^^^^^^^^^^^^
On some architectures, including ARM64, not all dependencies have precompiled
binaries. On such systems, you will need to install ``python3-dev`` before
binaries. On such systems, you may need to install ``python3-dev`` before
installing Reticulum or programs that depend on Reticulum.
.. code::
@ -298,62 +434,90 @@ installing Reticulum or programs that depend on Reticulum.
python3 -m pip install rns
Reticulum on Android
==============================================
Reticulum can be used on Android in different ways. The easiest way to get
started is using an app like `Sideband <https://unsigned.io/sideband>`_.
Raspberry Pi
^^^^^^^^^^^^^^^^^^^^^^^^^
It is currently recommended to use a 64-bit version of the Raspberry Pi OS
if you want to run Reticulum on Raspberry Pi computers, since 32-bit versions
don't always have packages available for some dependencies.
For more control and features, you can use Reticulum and related programs via
the `Termux app <https://termux.com/>`_, at the time of writing available on
`F-droid <https://f-droid.org>`_.
While it is possible to install and run Reticulum on 32-bit Rasperry Pi OSes,
it will require manually configuring and installing some packages, and is not
detailed in this manual.
Termux is a terminal emulator and Linux environment for Android based devices,
which includes the ability to use many different programs and libraries,
including Reticulum.
Since the Python cryptography.io module does not offer pre-built wheels for
Android, the standard one-line install of Reticulum does not work on Android,
and a few extra commands are required.
From within Termux, execute the following:
Debian Bookworm
^^^^^^^^^^^^^^^^^^^^^^^^
On versions of Debian released after April 2023, it is no longer possible by default
to use ``pip`` to install packages onto your system. Unfortunately, you will need to
use the replacement ``pipx`` command instead, which places installed packages in an
isolated environment. This should not negatively affect Reticulum, but will not work
for including and using Reticulum in your own scripts and programs.
.. code::
# First, make sure indexes and packages are up to date.
pkg update
pkg upgrade
# Install pipx
sudo apt install pipx
# Then install dependencies for the cryptography library.
pkg install python build-essential openssl libffi rust
# Make installed programs available on the command line
pipx ensurepath
# Make sure pip is up to date, and install the wheel module.
pip3 install wheel pip --upgrade
# Install Reticulum
pipx install rns
# To allow the installer to build the cryptography module,
# we need to let it know what platform we are compiling for:
export CARGO_BUILD_TARGET="aarch64-linux-android"
Alternatively, you can restore normal behaviour to ``pip`` by creating or editing
the configuration file located at ``~/.config/pip/pip.conf``, and adding the
following section:
# Start the install process for the cryptography module.
# Depending on your device, this can take several minutes,
# since the module must be compiled locally on your device.
pip3 install cryptography
.. code:: text
# If the above installation succeeds, you can now install
# Reticulum and any related software
pip3 install rns
[global]
break-system-packages = true
It is also possible to include Reticulum in apps compiled and distributed as
Android APKs. A detailed tutorial and example source code will be included
here at a later point.
Please note that the "break-system-packages" directive is a somewhat misleading choice
of words. Setting it will of course not break any system packages, but will simply
allow installing ``pip`` packages user- and system-wide. While this *could* in rare
cases lead to version conflicts, it does not generally pose any problems.
Ubuntu Lunar
^^^^^^^^^^^^^^^^^^^^^^^^
On versions of Ubuntu released after April 2023, it is no longer possible by default
to use ``pip`` to install packages onto your system. Unfortunately, you will need to
use the replacement ``pipx`` command instead, which places installed packages in an
isolated environment. This should not negatively affect Reticulum, but will not work
for including and using Reticulum in your own scripts and programs.
.. code::
# Install pipx
sudo apt install pipx
# Make installed programs available on the command line
pipx ensurepath
# Install Reticulum
pipx install rns
Alternatively, you can restore normal behaviour to ``pip`` by creating or editing
the configuration file located at ``~/.config/pip/pip.conf``, and adding the
following section:
.. code:: text
[global]
break-system-packages = true
Please note that the "break-system-packages" directive is a somewhat misleading choice
of words. Setting it will of course not break any system packages, but will simply
allow installing ``pip`` packages user- and system-wide. While this _could_ in rare
cases lead to version conflicts, it does not generally pose any problems.
Pure-Python Reticulum
==============================================
In some rare cases, and on more obscure system types, it is not possible to
install one or more dependencies
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 ``rnspure`` package instead of the ``rns`` package. The ``rnspure``
install one or more dependencies. In such situations,
you can use the ``rnspure`` package instead of the ``rns`` package, or use ``pip``
with the ``--no-dependencies`` command-line option. The ``rnspure``
package requires no external dependencies for installation. Please note that the
actual contents of the ``rns`` and ``rnspure`` packages are *completely identical*.
The only difference is that the ``rnspure`` package lists no dependencies required
@ -368,4 +532,4 @@ All other available modules will still be loaded when needed.
**Please Note!** If you use the `rnspure` package to run Reticulum on systems that
do not support `PyCA/cryptography <https://github.com/pyca/cryptography>`_, it is
important that you read and understand the :ref:`Cryptographic Primitives <understanding-primitives>`
section of this manual.
section of this manual.

View File

@ -24,11 +24,20 @@ starting from scratch.
This chapter will outline a few different sensible starting paths to get
real-world functional wireless communications up and running with minimal cost
and effort. Two fundamental devices categories will be covered, *RNodes* and
*WiFi-based radios*.
*WiFi-based radios*. Additionally, other common options will be briefly described.
Knowing how to employ just a few different types of hardware will make it possible
to build a wide range of useful networks with little effort.
Combining Hardware Types
========================
It is useful to combine different link and hardware types when designing and
building a network. One useful design pattern is to employ high-capacity point-to-point
links based on WiFi or millimeter-wave radios (with high-gain directional antennas)
for the network backbone, and using LoRa-based RNodes for covering large areas with
connectivity for client devices.
While there are many other device categories that are useful in building Reticulum
networks, knowing how to employ just these two will make it possible to build
a wide range of useful networks with little effort.
.. _rnode-main:
@ -52,7 +61,7 @@ the discussion to RNodes using *LoRa* modulation in common ISM bands.
**Avoid Confusion!** RNodes can use LoRa as a *physical-layer modulation*, but it
does not use, and has nothing to do with the *LoRaWAN* protocol and standard, commonly
used for centrally controlled IoT devices. RNodes use *raw LoRa modulation*, without
any additional protocol overhead. All high-level protocol funcionality is handled
any additional protocol overhead. All high-level protocol functionality is handled
directly by Reticulum.
.. _rnode-creating:
@ -160,12 +169,13 @@ Installation
Once you have obtained compatible boards, you can install the `RNode Firmware <https://github.com/markqvist/RNode_Firmware>`_
using the `RNode Configuration Utility <https://github.com/markqvist/rnodeconfigutil>`_.
Make sure that ``Python3`` and ``pip`` is installed on your system, and then install
the config utility with ``pip``:
If you have installed Reticulum on your system, the ``rnodeconf`` program will already be
available. If not, make sure that ``Python3`` and ``pip`` is installed on your system, and
then install Reticulum with with ``pip``:
.. code::
pip3 install rnodeconf
pip install rns
Once installation has completed, it is time to start installing the firmware on your
devices. Run ``rnodeconf`` in auto-install mode like so:
@ -176,12 +186,7 @@ devices. Run ``rnodeconf`` in auto-install mode like so:
The utility will guide you through the installation process by asking a series of
questions about your hardware. Simply follow the guide, and the utility will
auto-install and configure your devices
**Important Note!** It is currently recommended to use the v1.x line of the RNode firmware,
even though the v2.x line is available for early testing. The v2.x line should still be
considered an experimental pre-release. Only use the v2.x firmware line if you want to test
out the absolutely newest version, and don't care about stability.
auto-install and configure your devices.
.. _rnode-usage:
@ -194,19 +199,12 @@ such as serial port and on-air parameters. For v2.x firmwares, you just need to
the Connection ID of the RNode, and Reticulum will automatically locate and connect to the
RNode, using the parameters stored in the RNode itself.
.. _rnode-suppliers:
Suppliers
^^^^^^^^^
Get in touch if you want to have your RNode supplier listed here, or if you want help to
get started with producing RNodes.
WiFi-based Hardware
===================
It is possible to use all kinds of both short- and long-range Wifi-based hardware
with Reticulum. Any kind of hardware that fully supports bridged ethernet over the
It is possible to use all kinds of both short- and long-range WiFi-based hardware
with Reticulum. Any kind of hardware that fully supports bridged Ethernet over the
WiFi interface will work with the :ref:`AutoInterface<interfaces-auto>` in Reticulum.
Most devices will behave like this by default, or allow it via configuration options.
@ -235,11 +233,31 @@ that is relatively cheap while providing long range and high capacity for Reticu
networks. As in all other cases, it is also possible for Reticulum to co-exist with IP
networks running concurrently on such devices.
Combining Hardware Types
========================
Ethernet-based Hardware
=======================
It is useful to combine different link and hardware types when designing and
building a network. One useful design pattern is to employ high-capacity point-to-point
links based on WiFi or millimeter-wave radios (with high-gain directional antennas)
for the network backbone, and using LoRa-based RNodes for covering large areas with
connectivity for client devices.
Reticulum can run over any kind of hardware that can provide a switched Ethernet-based
medium. This means that anything from a plain Ethernet switch, to fiber-optic systems,
to data radios with Ethernet interfaces can be used by Reticulum.
The Ethernet medium does not need to have any IP infrastructure such as DHCP servers
or routing set up, but in case such infrastructure does exist, Reticulum will simply
co-exist with.
To use Reticulum over Ethernet-based mediums, it is generally enough to use the included
:ref:`AutoInterface<interfaces-auto>`. This interface also works over any kind of
virtual networking adapter, such as ``tun`` and ``tap`` devices in Linux.
Serial Lines & Devices
======================
Using Reticulum over any kind of raw serial line is also possible with the
:ref:`SerialInterface<interfaces-serial>`. This interface type is also useful for
using Reticulum over communications hardware that provides a serial port interface.
Packet Radio Modems
===================
Any packet radio modem that provides a standard KISS interface over USB, serial or TCP
can be used with Reticulum. This includes virtual software modems such as
`FreeDV TNC <https://github.com/xssfox/freedv-tnc>`_ and `Dire Wolf <https://github.com/wb2osz/direwolf>`_.

View File

@ -1,10 +1,20 @@
******************************
Reticulum Network Stack Manual
******************************
This manual aims to provide you with all the information you need to
understand Reticulum, build networks or develop programs using it, or
to participate in the development of Reticulum itself.
.. only:: builder_html
This manual is also available in `PDF <https://github.com/markqvist/Reticulum/releases/latest/download/Reticulum.Manual.pdf>`_ and `EPUB <https://github.com/markqvist/Reticulum/releases/latest/download/Reticulum.Manual.epub>`_ formats.
.. only:: builder_html
Table Of Contents
=================
.. toctree::
:maxdepth: 3
@ -15,10 +25,14 @@ to participate in the development of Reticulum itself.
hardware
interfaces
networks
reference
examples
support
.. toctree::
:maxdepth: 2
reference
.. only:: html

View File

@ -1,9 +1,9 @@
.. _interfaces-main:
********************
Supported Interfaces
********************
**********************
Configuring Interfaces
**********************
Reticulum supports using many kinds of devices as networking interfaces, and
allows you to mix and match them in any way you choose. The number of distinct
@ -33,9 +33,20 @@ system, which should be enabled by default in almost all OSes.
.. code::
# This example demonstrates a TCP server interface.
# It will listen for incoming connections on the
# specified IP address and port number.
# This example demonstrates a bare-minimum setup
# of an Auto Interface. It will allow communica-
# tion with all other reachable devices on all
# usable physical ethernet-based devices that
# are available on the system.
[[Default Interface]]
type = AutoInterface
interface_enabled = True
# This example demonstrates an more specifically
# configured Auto Interface, that only uses spe-
# cific physical interfaces, and has a number of
# other configuration options set.
[[Default Interface]]
type = AutoInterface
@ -47,6 +58,12 @@ system, which should be enabled by default in almost all OSes.
group_id = reticulum
# You can also choose the multicast address type:
# temporary (default, Temporary Multicast Address)
# or permanent (Permanent Multicast Address)
multicast_address_type = permanent
# You can also select specifically which
# kernel networking devices to use.
@ -98,13 +115,13 @@ and persistent I2P address that your Reticulum instance can be reached
at.
To use the I2P interface, you must have an I2P router running
on your system. The easiest way to acheive this is to download and
on your system. The easiest way to achieve this is to download and
install the `latest release <https://github.com/PurpleI2P/i2pd/releases/latest>`_
of the ``i2pd`` package. For more details about I2P, see the
`geti2p.net website <https://geti2p.net/en/about/intro>`_.
When an I2P router is running on your system, you can simply add
an I2P interface to reticulum:
an I2P interface to Reticulum:
.. code::
@ -270,7 +287,7 @@ with all other peers on a local area network.
*Please Note!* Using broadcast UDP traffic has performance implications,
especially on WiFi. If your goal is simply to enable easy communication
with all peers in your local ethernet broadcast domain, the
with all peers in your local Ethernet broadcast domain, the
:ref:`Auto Interface<interfaces-auto>` performs better, and is even
easier to use.
@ -365,6 +382,7 @@ can be used, and offers full control over LoRa parameters.
# out identification on the channel with
# a set interval by configuring the
# following two parameters.
# id_callsign = MYCALL-0
# id_interval = 600
@ -372,7 +390,21 @@ can be used, and offers full control over LoRa parameters.
# with low amounts of RAM, using packet
# flow control can be useful. By default
# it is disabled.
flow_control = False
# flow_control = False
# It is possible to limit the airtime
# utilisation of an RNode by using the
# following two configuration options.
# The short-term limit is applied in a
# window of approximately 15 seconds,
# and the long-term limit is enforced
# over a rolling 60 minute window. Both
# options are specified in percent.
# airtime_limit_long = 1.5
# airtime_limit_short = 33
.. _interfaces-serial:
@ -404,7 +436,7 @@ directly over a wire-pair, or for using devices such as data radios and lasers.
Pipe Interface
==============
Using this interface, reticulum can use any program as an interface via `stdin` and
Using this interface, Reticulum can use any program as an interface via `stdin` and
`stdout`. This can be used to easily create virtual interfaces, or to interface with
custom hardware or other systems.
@ -421,7 +453,7 @@ custom hardware or other systems.
respawn_delay = 5
Reticulum will write all packets to `stdin` of the ``command`` option, and will
continously read and scan its `stdout` for Reticulum packets. If ``EOF`` is reached,
continuously read and scan its `stdout` for Reticulum packets. If ``EOF`` is reached,
Reticulum will try to respawn the program after waiting for ``respawn_interval`` seconds.
.. _interfaces-kiss:
@ -746,3 +778,74 @@ conserve bandwidth, while very fast networks can support applications that
need very frequent announces. Reticulum implements these mechanisms to ensure
that a large span of network types can seamlessly *co-exist* and interconnect.
.. _interfaces-ingress-control:
New Destination Rate Limiting
=============================
On public interfaces, where anyone may connect and announce new destinations,
it can be useful to control the rate at which announces for *new* destinations are
processed.
If a large influx of announces for newly created or previously unknown destinations
occur within a short amount of time, Reticulum will place these announces on hold,
so that announce traffic for known and previously established destinations can
continue to be processed without interruptions.
After the burst subsides, and an additional waiting period has passed, the held
announces will be released at a slow rate, until the hold queue is cleared. This
also means, that should a node decide to connect to a public interface, announce
a large amount of bogus destinations, and then disconnect, these destination will
never make it into path tables and waste network bandwidth on retransmitted
announces.
**It's important to note** that the ingress control works at the level of *individual
sub-interfaces*. As an example, this means that one client on a :ref:`TCP Server Interface<interfaces-tcps>`
cannot disrupt processing of incoming announces for other connected clients on the same
:ref:`TCP Server Interface<interfaces-tcps>`. All other clients on the same interface will still have new announces
processed without interruption.
By default, Reticulum will handle this automatically, and ingress announce
control will be enabled on interface where it is sensible to do so. It should
generally not be neccessary to modify the ingress control configuration,
but all the parameters are exposed for configuration if needed.
* | The ``ingress_control`` option tells Reticulum whether or not
to enable announce ingress control on the interface. Defaults to
``True``.
* | The ``ic_new_time`` option configures how long (in seconds) an
interface is considered newly spawned. Defaults to ``2*60*60`` seconds. This
option is useful on publicly accessible interfaces that spawn new
sub-interfaces when a new client connects.
* | The ``ic_burst_freq_new`` option sets the maximum announce ingress
frequency for newly spawned interfaces. Defaults to ``3.5``
announces per second.
* | The ``ic_burst_freq`` option sets the maximum announce ingress
frequency for other interfaces. Defaults to ``12`` announces
per second.
*If an interface exceeds its burst frequency, incoming announces
for unknown destinations will be temporarily held in a queue, and
not processed until later.*
* | The ``ic_max_held_announces`` option sets the maximum amount of
unique announces that will be held in the queue. Any additional
unique announces will be dropped. Defaults to ``256`` announces.
* | The ``ic_burst_hold`` option sets how much time (in seconds) must
pass after the burst frequency drops below its threshold, for the
announce burst to be considered cleared. Defaults to ``60``
seconds.
* | The ``ic_burst_penalty`` option sets how much time (in seconds) must
pass after the burst is considered cleared, before held announces can
start being released from the queue. Defaults to ``5*60``
seconds.
* | The ``ic_held_release_interval`` option sets how much time (in seconds)
must pass between releasing each held announce from the queue. Defaults
to ``30`` seconds.

View File

@ -9,7 +9,7 @@ Reticulum, which can often be easier than using traditional stacks, since you
don't have to worry about coordinating addresses, subnets and routing for an
entire network that you might not know how will evolve in the future. With
Reticulum, you can simply add more segments to your network when it becomes
necesarry, and Reticulum will handle the convergence of the entire network
necessary, and Reticulum will handle the convergence of the entire network
automatically.
Concepts & Overview
@ -18,13 +18,13 @@ Concepts & Overview
There are important points that need to be kept in mind when building networks
with Reticulum:
* | In a Reticulum network, any node can autonomously generate as many adresses
* | In a Reticulum network, any node can autonomously generate as many addresses
(called *destinations* in Reticulum terminology) as it needs, which become
globally reachable to the rest of the network. There is no central point of
control over the adress space.
control over the address space.
* | Reticulum was designed to handle both very small, and very large networks.
While the adress space can support billions of endpoints, Reticulum is
While the address space can support billions of endpoints, Reticulum is
also very useful when just a few devices needs to communicate.
* | Low-bandwidth networks, like LoRa and packet radio, can interoperate and
@ -113,8 +113,8 @@ WiFi based radios for interconnecting the sites.
At each site, a Raspberry Pi is installed to function as a gateway. A LoRa radio
is connected to the Pi with a USB cable, and the WiFi radio is connected to the
ethernet port of the Pi. At site B, two WiFi radios are needed to be able to reach
both site A and site C, so an extra ethernet adapter is connected to the Pi in
Ethernet port of the Pi. At site B, two WiFi radios are needed to be able to reach
both site A and site C, so an extra Ethernet adapter is connected to the Pi in
this location.
Once the hardware has been installed, Reticulum is installed on all the Pis, and at

View File

@ -1,18 +1,23 @@
:tocdepth: 4
.. _api-main:
*************
API Reference
*************
This reference guide lists and explains all classes exposed by the RNS API.
Classes
=========================
Communication over a Reticulum network is achieved using a set of classes exposed by RNS.
Communication over Reticulum networks is achieved by using a simple set of classes exposed by the RNS API.
This chapter lists and explains all classes exposed by the Reticulum Network Stack API, along with their method signatures and usage. It can be used as a reference while writing applications that utilise Reticulum, or it can be read in entirity to gain an understanding of the complete functionality of RNS from a developers perspective.
.. _api-reticulum:
Reticulum
---------
.. only:: html
|start-h3| Reticulum |end-h3|
.. only:: latex
Reticulum
---------
.. autoclass:: RNS.Reticulum
:members:
@ -20,64 +25,190 @@ Reticulum
.. _api-identity:
Identity
--------
.. only:: html
|start-h3| Identity |end-h3|
.. only:: latex
Identity
--------
.. autoclass:: RNS.Identity
:members:
.. _api-destination:
Destination
-----------
.. only:: html
|start-h3| Destination |end-h3|
.. only:: latex
Destination
-----------
.. autoclass:: RNS.Destination
:members:
.. _api-packet:
Packet
------
.. only:: html
|start-h3| Packet |end-h3|
.. only:: latex
Packet
------
.. autoclass:: RNS.Packet(destination, data, create_receipt = True)
:members:
.. _api-packetreceipt:
Packet Receipt
--------------
.. only:: html
|start-h3| Packet Receipt |end-h3|
.. only:: latex
Packet Receipt
--------------
.. autoclass:: RNS.PacketReceipt()
:members:
.. _api-link:
Link
----
.. only:: html
|start-h3| Link |end-h3|
.. only:: latex
Link
----
.. autoclass:: RNS.Link(destination, established_callback=None, closed_callback = None)
:members:
.. _api-requestreceipt:
Request Receipt
---------------
.. only:: html
|start-h3| Request Receipt |end-h3|
.. only:: latex
Request Receipt
---------------
.. autoclass:: RNS.RequestReceipt()
:members:
.. _api-resource:
Resource
--------
.. only:: html
|start-h3| Resource |end-h3|
.. only:: latex
Resource
--------
.. autoclass:: RNS.Resource(data, link, advertise=True, auto_compress=True, callback=None, progress_callback=None, timeout=None)
:members:
.. _api-channel:
.. only:: html
|start-h3| Channel |end-h3|
.. only:: latex
Channel
-------
.. autoclass:: RNS.Channel.Channel()
:members:
.. _api-messsagebase:
.. only:: html
|start-h3| MessageBase |end-h3|
.. only:: latex
MessageBase
-----------
.. autoclass:: RNS.MessageBase()
:members:
.. _api-buffer:
.. only:: html
|start-h3| Buffer |end-h3|
.. only:: latex
Buffer
------
.. autoclass:: RNS.Buffer
:members:
.. _api-rawchannelreader:
.. only:: html
|start-h3| RawChannelReader |end-h3|
.. only:: latex
RawChannelReader
----------------
.. autoclass:: RNS.RawChannelReader
:members: __init__, add_ready_callback, remove_ready_callback
.. _api-rawchannelwriter:
.. only:: html
|start-h3| RawChannelWriter |end-h3|
.. only:: latex
RawChannelWriter
----------------
.. autoclass:: RNS.RawChannelWriter
:members: __init__
.. _api-transport:
Transport
---------
.. only:: html
|start-h3| Transport |end-h3|
.. only:: latex
Transport
---------
.. autoclass:: RNS.Transport
:members:
:members:
.. |start-h3| raw:: html
<h3>
.. |end-h3| raw:: html
</h3>

View File

@ -32,11 +32,11 @@ Provide Feedback
================
All feedback on the usage, functioning and potential dysfunctioning of any and
all components of the system is very valuable to the continued development and
improvement of Reticulum. Absolutely no automated analytics, telemetly, error
improvement of Reticulum. Absolutely no automated analytics, telemetry, error
reporting or statistics is collected and reported by Reticulum under any
circumstances, so we rely on old-fashioned human feedback.
Contribute Code
===============
Join us on `the GitHub repository <https://github.com/markqvist/reticulum>`_ to
report issues, suggest functionality and contribute code to Reticulum.
report issues, suggest functionality and contribute code to Reticulum.

View File

@ -70,12 +70,12 @@ guide the design of Reticulum:
* **Hardware layer agnosticism**
Reticulum must be fully hardware agnostic, and shall be useable over a wide range of
physical networking layers, such as data radios, serial lines, modems, handheld transceivers,
wired ethernet, wifi, or anything else that can carry a digital data stream. Hardware made for
wired Ethernet, WiFi, or anything else that can carry a digital data stream. Hardware made for
dedicated Reticulum use shall be as cheap as possible and use off-the-shelf components, so
it can be easily modified and replicated by anyone interested in doing so.
* **Very low bandwidth requirements**
Reticulum should be able to function reliably over links with a transmission capacity as low
as *500 bits per second*.
as *5 bits per second*.
* **Encryption by default**
Reticulum must use strong encryption by default for all communication.
* **Initiator Anonymity**
@ -107,13 +107,13 @@ guide the design of Reticulum:
Introduction & Basic Functionality
==================================
Reticulum is a networking stack suited for high-latency, low-bandwidth links. Reticulum is at its
Reticulum is a networking stack suited for high-latency, low-bandwidth links. Reticulum is at its
core a *message oriented* system. It is suited for both local point-to-point or point-to-multipoint
scenarios where alle nodes are within range of each other, as well as scenarios where packets need
scenarios where all nodes are within range of each other, as well as scenarios where packets need
to be transported over multiple hops in a complex network to reach the recipient.
Reticulum does away with the idea of addresses and ports known from IP, TCP and UDP. Instead
Reticulum uses the singular concept of *destinations*. Any application using Reticulum as its
Reticulum uses the singular concept of *destinations*. Any application using Reticulum as its
networking stack will need to create one or more destinations to receive data, and know the
destinations it needs to send data to.
@ -121,22 +121,23 @@ All destinations in Reticulum are _represented_ as a 16 byte hash. This hash is
SHA-256 hash of identifying characteristics of the destination. To users, the destination addresses
will be displayed as 16 hexadecimal bytes, like this example: ``<13425ec15b621c1d928589718000d814>``.
The truncation size of 16 bytes (128 bits) for destinations has been choosen as a reasonable tradeoff
The truncation size of 16 bytes (128 bits) for destinations has been chosen as a reasonable trade-off
between address space
and packet overhead. The address space accomodated by this size can support many billions of
and packet overhead. The address space accommodated by this size can support many billions of
simultaneously active devices on the same network, while keeping packet overhead low, which is
essential on low-bandwidth networks. In the very unlikely case that this address space nears
congestion, a one-line code change can upgrade the Reticulum address space all the way up to 256
bits, ensuring the Reticulum address space could potentially support galactic-scale networks.
This is obviusly complete and ridiculous over-allocation, and as such, the current 128 bits should
This is obviously complete and ridiculous over-allocation, and as such, the current 128 bits should
be sufficient, even far into the future.
By default Reticulum encrypts all data using elliptic curve cryptography. Any packet sent to a
destination is encrypted with a derived ephemeral key. Reticulum can also set up an encrypted
By default Reticulum encrypts all data using elliptic curve cryptography and AES. Any packet sent to a
destination is encrypted with a per-packet derived key. Reticulum can also set up an encrypted
channel to a destination, called a *Link*. Both data sent over Links and single packets offer
*Forward Secrecy* and *Initiator Anonymity*, by using an Elliptic Curve Diffie Hellman key exchange
on Curve25519 to derive ephemeral keys. The multi-hop transport, coordination, verification
and reliability layers are fully autonomous and also based on elliptic curve cryptography.
*Initiator Anonymity*, and links additionally offer *Forward Secrecy* by using an Elliptic Curve
Diffie Hellman key exchange on Curve25519 to derive per-link ephemeral keys. The multi-hop transport,
coordination, verification and reliability layers are fully autonomous and also based on elliptic
curve cryptography.
Reticulum also offers symmetric key encryption for group-oriented communications, as well as
unencrypted packets for local broadcast purposes.
@ -163,7 +164,7 @@ destinations. Reticulum uses three different basic destination types, and one sp
A *plain* destination type is unencrypted, and suited for traffic that should be broadcast to a
number of users, or should be readable by anyone. Traffic to a *plain* destination is not encrypted.
Generally, *plain* destinations can be used for broadcast information intended to be public.
Plain destinations are only reachable directly, and packets adressed to plain destinations are
Plain destinations are only reachable directly, and packets addressed to plain destinations are
never transported over multiple hops in the network. To be transportable over multiple hops in Reticulum, information
*must* be encrypted, since Reticulum uses the per-packet encryption to verify routing paths and
keep them alive.
@ -219,10 +220,10 @@ packet.
In actual use of *single* destination naming, it is advisable not to use any uniquely identifying
features in aspect naming. Aspect names should be general terms describing what kind of destination
is represented. The uniquely identifying aspect is always acheived by the appending the public key,
which expands the destination into a uniquely identifyable one. Reticulum does this automatically.
is represented. The uniquely identifying aspect is always achieved by appending the public key,
which expands the destination into a uniquely identifiable one. Reticulum does this automatically.
Any destination on a Reticulum network can be addressed and reached simply by knowning its
Any destination on a Reticulum network can be addressed and reached simply by knowing its
destination hash (and public key, but if the public key is not known, it can be requested from the
network simply by knowing the destination hash). The use of app names and aspects makes it easy to
structure Reticulum programs and makes it possible to filter what information and data your program
@ -238,7 +239,7 @@ To recap, the different destination types should be used in the following situat
* **Plain**
When plain-text communication is desirable, for example when broadcasting information, or for local discovery purposes.
To communicate with a *single* destination, you need to know its public key. Any method for
To communicate with a *single* destination, you need to know its public key. Any method for
obtaining the public key is valid, but Reticulum includes a simple mechanism for making other
nodes aware of your destinations public key, called the *announce*. It is also possible to request
an unknown public key from the network, as all transport instances serve as a distributed ledger
@ -286,7 +287,7 @@ In Reticulum, destinations are allowed to move around the network at will. This
protocols such as IP, where an address is always expected to stay within the network segment it was assigned in.
This limitation does not exist in Reticulum, and any destination is *completely portable* over the entire topography
of the network, and *can even be moved to other Reticulum networks* than the one it was created in, and
still become reachable. To update it's reachability, a destination simply needs to send an announce on any
still become reachable. To update its reachability, a destination simply needs to send an announce on any
networks it is part of. After a short while, it will be globally reachable in the network.
Seeing how *single* destinations are always tied to a private/public key pair leads us to the next topic.
@ -349,7 +350,7 @@ Node Types
----------
Currently, Reticulum distinguishes between two types of network nodes. All nodes on a Reticulum network
are *Reticulum Instances*, and some are alo *Transport Nodes*. If a system running Reticulum is fixed in
are *Reticulum Instances*, and some are also *Transport Nodes*. If a system running Reticulum is fixed in
one place, and is intended to be kept available most of the time, it is a good contender to be a *Transport Node*.
Any Reticulum Instance can become a Transport Node by enabling it in the configuration.
@ -367,7 +368,7 @@ If it is a *Transport Node*, it should be given the configuration directive ``en
The Announce Mechanism in Detail
--------------------------------
When an *announce* for a destination is transmitted by from a Reticulum instance, it will be forwarded by
When an *announce* for a destination is transmitted by a Reticulum instance, it will be forwarded by
any transport node receiving it, but according to some specific rules:
@ -384,7 +385,7 @@ any transport node receiving it, but according to some specific rules:
announces is set at 2%, but can be configured on a per-interface basis.
* | If any given interface does not have enough bandwidth available for retransmitting the announce,
the announce will be assigned a priority inversely proportional to it's hop count, and be inserted
the announce will be assigned a priority inversely proportional to its hop count, and be inserted
into a queue managed by the interface.
* | When the interface has bandwidth available for processing an announce, it will prioritise announces
@ -430,7 +431,7 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
* | A packet is always created with an associated destination and some payload data. When the packet is sent
to a *single* destination type, Reticulum will automatically create an ephemeral encryption key, perform
an ECDH key exchange with the destinations public key, and encrypt the information.
an ECDH key exchange with the destination's public key, and encrypt the information.
* | It is important to note that this key exchange does not require any network traffic. The sender already
knows the public key of the destination from an earlier received *announce*, and can thus perform the ECDH
@ -442,18 +443,17 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
* | When the destination receives the packet, it can itself perform an ECDH key exchange and decrypt the
packet.
* | A new ephemeral key is used for every packet sent in this way, and forward secrecy is guaranteed on a
per packet level.
* | A new ephemeral key is used for every packet sent in this way.
* | Once the packet has been received and decrypted by the addressed destination, that destination can opt
to *prove* its receipt of the packet. It does this by calculating the SHA-256 hash of the received packet,
and signing this hash with it's Ed25519 signing key. Transport nodes in the network can then direct this
*proof* back to the packets origin, where the signature can be verified against the destinations known
and signing this hash with its Ed25519 signing key. Transport nodes in the network can then direct this
*proof* back to the packets origin, where the signature can be verified against the destination's known
public signing key.
* | In case the packet is addressed to a *group* destination type, the packet will be encrypted with the
pre-shared AES-128 key associated with the destination. In case the packet is addressed to a *plain*
destination type, the payload data will not be encrypted. Neither of these two destination types offer
destination type, the payload data will not be encrypted. Neither of these two destination types can offer
forward secrecy. In general, it is recommended to always use the *single* destination type, unless it is
strictly necessary to use one of the others.
@ -465,7 +465,7 @@ For exchanges of larger amounts of data, or when longer sessions of bidirectiona
forward the packet will take note of this *link request*.
* | Second, if the destination accepts the *link request* , it will send back a packet that proves the
authenticity of its identity (and the receipt of the link request) to the initiating node. All
authenticity of its identity (and the receipt of the link request) to the initiating node. All
nodes that initially forwarded the packet will also be able to verify this proof, and thus
accept the validity of the *link* throughout the network.
@ -485,15 +485,20 @@ For exchanges of larger amounts of data, or when longer sessions of bidirectiona
the destination using a Reticulum Identity. This authentication is happening inside the encrypted
link, and is only revealed to the verified destination, and no intermediaries.
In a moment, we will discuss the details of how this methodology is implemented, but lets first
recap what purposes this methodology serves. We first ensure that the node answering our request
is actually the one we want to communicate with, and not a malicious actor pretending to be so.
At the same time we establish an efficient encrypted channel. The setup of this is relatively cheap in
terms of bandwidth, so it can be used just for a short exchange, and then recreated as needed, which will
also rotate encryption keys. The link can also be kept alive for longer periods of time, if this is
more suitable to the application. The procedure also inserts the *link id* , a hash calculated from the link request packet, into the memory of forwarding nodes, which means that the communicating nodes can thereafter reach each other simply by referring to this *link id*.
In a moment, we will discuss the details of how this methodology is
implemented, but lets first recap what purposes this methodology serves. We
first ensure that the node answering our request is actually the one we want to
communicate with, and not a malicious actor pretending to be so. At the same
time we establish an efficient encrypted channel. The setup of this is
relatively cheap in terms of bandwidth, so it can be used just for a short
exchange, and then recreated as needed, which will also rotate encryption keys.
The link can also be kept alive for longer periods of time, if this is more
suitable to the application. The procedure also inserts the *link id* , a hash
calculated from the link request packet, into the memory of forwarding nodes,
which means that the communicating nodes can thereafter reach each other simply
by referring to this *link id*.
The combined bandwidth cost of setting up a link is 3 packets totalling 265 bytes (more info in the
The combined bandwidth cost of setting up a link is 3 packets totalling 297 bytes (more info in the
:ref:`Binary Packet Format<understanding-packetformat>` section). The amount of bandwidth used on keeping
a link open is practically negligible, at 0.45 bits per second. Even on a slow 1200 bits per second packet
radio channel, 100 concurrent links will still leave 96% channel capacity for actual data.
@ -538,14 +543,16 @@ an arbitrary number of hops, where information will be exchanged between two nod
* | A *link proof* packet is now constructed and transmitted over the network. This packet is
addressed to the *link id* of the *link*. It contains the following data: The newly generated X25519
public key *LKr* and an Ed25519 signature of the *link id* and *LKr* made by the signing key of
public key *LKr* and an Ed25519 signature of the *link id* and *LKr* made by the *original signing key* of
the addressed destination.
* | By verifying this *link proof* packet, all nodes that originally transported the *link request*
packet to the destination from the originator can now verify that the intended destination received
the request and accepted it, and that the path they chose for forwarding the request was valid.
In sucessfully carrying out this verification, the transporting nodes marks the link as active.
In successfully carrying out this verification, the transporting nodes marks the link as active.
An abstract bi-directional communication channel has now been established along a path in the network.
Packets can now be exchanged bi-directionally from either end of the link simply by adressing the
packets to the *link id* of the link.
* | When the source receives the *proof* , it will know unequivocally that a verified path has been
established to the destination. It can now also use the X25519 public key contained in the
@ -588,7 +595,7 @@ or less any medium that allows you to send and receive data, which satisfies som
minimum requirements.
The communication channel must support at least half-duplex operation, and provide an average
throughput of around 500 bits per second, and supports a physical layer MTU of 500 bytes. The
throughput of 5 bits per second or greater, and supports a physical layer MTU of 500 bytes. The
Reticulum stack should be able to run on more or less any hardware that can provide a Python 3.x
runtime environment.
@ -764,7 +771,7 @@ Wire Format
| | | | | | | |
00000000 00000111 [HASH1, 16 bytes] [CONTEXT, 1 byte] [DATA]
|| | | | |
|| | | | +-- Hops = 0
|| | | | +-- Hops = 7
|| | | +------- Packet Type = DATA
|| | +--------- Destination Type = SINGLE
|| +----------- Propagation Type = BROADCAST
@ -779,7 +786,7 @@ Wire Format
| | | | | | | | | |
10000000 00000111 [IFAC, N bytes] [HASH1, 16 bytes] [CONTEXT, 1 byte] [DATA]
|| | | | |
|| | | | +-- Hops = 0
|| | | | +-- Hops = 7
|| | | +------- Packet Type = DATA
|| | +--------- Destination Type = SINGLE
|| +----------- Propagation Type = BROADCAST
@ -796,9 +803,9 @@ Wire Format
but excluding any interface access codes.
- Path Request : 51 bytes
- Announce : 157 bytes
- Announce : 167 bytes
- Link Request : 83 bytes
- Link Proof : 83 bytes
- Link Proof : 115 bytes
- Link RTT packet : 99 bytes
- Link keepalive : 20 bytes
@ -851,7 +858,7 @@ both on general-purpose CPUs and on microcontrollers. The necessary primitives a
* Ed25519 for signatures
* X22519 for ECDH key exchanges
* X25519 for ECDH key exchanges
* HKDF for key derivation
@ -887,4 +894,4 @@ testing and review as those from OpenSSL.
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.
those risks are acceptable to you.

View File

@ -5,28 +5,43 @@ Using Reticulum on Your System
******************************
Reticulum is not installed as a driver or kernel module, as one might expect
of a networking stack. Instead, Reticulum is distributed as a Python module.
of a networking stack. Instead, Reticulum is distributed as a Python module,
containing the networking core, and a set of utility and daemon programs.
This means that no special privileges are required to install or use it. It
is also very light-weight, and easy to transfer to and install on new systems.
Any program or application that uses Reticulum will automatically load and
initialise Reticulum when it starts.
is also very light-weight, and easy to transfer to, and install on new systems.
When you have Reticulum installed, any program or application that uses Reticulum
will automatically load and initialise Reticulum when it starts, if it is not
already running.
In many cases, this approach is sufficient. When any program needs to use
Reticulum, it is loaded, initialised, interfaces are brought up, and the
program can now communicate over any Reticulum networks available. If another
program starts up and also wants access to the same Reticulum network, the
instance is simply shared. This works for any number of programs running
program starts up and also wants access to the same Reticulum network, the already
running instance is simply shared. This works for any number of programs running
concurrently, and is very easy to use, but depending on your use case, there
are other options.
Configuration & Data
--------------------
A Reticulum stores all information that it needs to function in a single file-
system directory. By default, this directory is ``~/.reticulum``, but you can
use any directory you wish. You can also run multiple separate Reticulum
instances on the same physical system, in complete isolation from each other,
or connected together.
Reticulum stores all information that it needs to function in a single file-system
directory. When Reticulum is started, it will look for a valid configuration
directory in the following places:
- ``/etc/reticulum``
- ``~/.config/reticulum``
- ``~/.reticulum``
If no existing configuration directory is found, the directory ``~/.reticulum``
is created, and the default configuration will be automatically created here.
You can move it to one of the other locations if you wish.
It is also possible to use completely arbitrary configuration directories by
specifying the relevant command-line parameters when running Reticulum-based
programs. You can also run multiple separate Reticulum instances on the same
physical system, either in isolation from each other, or connected together.
In most cases, a single physical system will only need to run one Reticulum
instance. This can either be launched at boot, as a system service, or simply
@ -35,7 +50,7 @@ running on the same system will automatically share the same Reticulum instance,
if the configuration allows for it, which it does by default.
The entire configuration of Reticulum is found in the ``~/.reticulum/config``
file. When Reticulum is first started on a new system, a basic, functional
file. When Reticulum is first started on a new system, a basic, but fully functional
configuration file is created. The default configuration looks like this:
.. code::
@ -84,6 +99,17 @@ configuration file is created. The default configuration looks like this:
instance_control_port = 37429
# 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
# You can configure Reticulum to panic and forcibly close
# if an unrecoverable interface error occurs, such as the
# hardware device for an interface disappearing. This is
@ -93,6 +119,17 @@ configuration file is created. The default configuration looks like this:
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
[logging]
# Valid log levels are 0 through 7:
# 0: Log only critical information
@ -130,10 +167,19 @@ configuration file is created. The default configuration looks like this:
If Reticulum infrastructure already exists locally, you probably don't need to
change anything, and you may already be connected to a wider network. If not,
you will probably need to add relevant *interfaces* to the configuration, in
order to communicate with other systems. It is a good idea to read the comments
and explanations in the above default config. It will teach you the basic
concepts you need to understand to configure your network. Once you have done that,
take a look at the :ref:`Interfaces<interfaces-main>` chapter of this manual.
order to communicate with other systems.
You can generate a much more verbose configuration example by running the command:
``rnsd --exampleconfig``
The output includes examples for most interface types supported
by Reticulum, along with additional options and configuration parameters.
It is a good idea to read the comments and explanations in the above default config.
It will teach you the basic concepts you need to understand to configure your network.
Once you have done that, take a look at the :ref:`Interfaces<interfaces-main>` chapter
of this manual.
Included Utility Programs
-------------------------
@ -155,28 +201,47 @@ When ``rnsd`` is running, it will keep all configured interfaces open, handle tr
it is enabled, and allow any other programs to immediately utilise the
Reticulum network it is configured for.
You can even run multiple instances of rnsd with different configurations on
You can even run multiple instances of ``rnsd`` with different configurations on
the same system.
.. code:: text
**Usage Examples**
# Install Reticulum
pip3 install rns
# Run rnsd
rnsd
Run ``rnsd``:
.. code:: text
usage: rnsd [-h] [--config CONFIG] [-v] [-q] [--version]
$ rnsd
[2023-08-18 17:59:56] [Notice] Started rnsd version 0.5.8
Run ``rnsd`` in service mode, ensuring all logging output is sent directly to file:
.. code:: text
$ rnsd -s
Generate a verbose and detailed configuration example, with explanations of all the
various configuration options, and interface configuration examples:
.. code:: text
$ rnsd --exampleconfig
**All Command-Line Options**
.. code:: text
usage: rnsd.py [-h] [--config CONFIG] [-v] [-q] [-s] [--exampleconfig] [--version]
Reticulum Network Stack Daemon
optional arguments:
options:
-h, --help show this help message and exit
--config CONFIG path to alternative Reticulum config directory
-v, --verbose
-q, --quiet
-s, --service rnsd is running as a service and should log to file
--exampleconfig print verbose configuration example to stdout and exit
--version show program's version number and exit
You can easily add ``rnsd`` as an always-on service by :ref:`configuring a service<using-systemd>`.
@ -187,12 +252,14 @@ The rnstatus Utility
Using the ``rnstatus`` utility, you can view the status of configured Reticulum
interfaces, similar to the ``ifconfig`` program.
**Usage Examples**
Run ``rnstatus``:
.. code:: text
# Run rnstatus
rnstatus
$ rnstatus
# Example output
Shared Instance[37428]
Status : Up
Serving : 1 program
@ -208,7 +275,7 @@ interfaces, similar to the ``ifconfig`` program.
Traffic : 63.23 KB↑
80.17 KB↓
TCPInterface[RNS Testnet Frankfurt/frankfurt.rns.unsigned.io:4965]
TCPInterface[RNS Testnet Dublin/dublin.connect.reticulum.network:4965]
Status : Up
Mode : Full
Rate : 10.00 Mbps
@ -225,44 +292,175 @@ interfaces, similar to the ``ifconfig`` program.
Reticulum Transport Instance <5245a8efe1788c6a1cd36144a270e13b> running
Filter output to only show some interfaces:
.. code:: text
usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-v]
$ rnstatus rnode
RNodeInterface[RNode UHF]
Status : Up
Mode : Access Point
Rate : 1.30 kbps
Access : 64-bit IFAC by <…e702c42ba8>
Traffic : 8.49 KB↑
9.23 KB↓
Reticulum Transport Instance <5245a8efe1788c6a1cd36144a270e13b> running
**All Command-Line Options**
.. code:: text
usage: rnstatus.py [-h] [--config CONFIG] [--version] [-a] [-A] [-s SORT]
[-r] [-j] [-v] [filter]
Reticulum Network Stack Status
optional arguments:
-h, --help show this help message and exit
--config CONFIG path to alternative Reticulum config directory
--version show program's version number and exit
-a, --all show all interfaces
positional arguments:
filter only display interfaces with names including filter
options:
-h, --help show this help message and exit
--config CONFIG path to alternative Reticulum config directory
--version show program's version number and exit
-a, --all show all interfaces
-A, --announce-stats show announce stats
-s SORT, --sort SORT sort interfaces by [rate, traffic, rx, tx, announces, arx, atx, held]
-r, --reverse reverse sorting
-j, --json output in JSON format
-v, --verbose
The rnid Utility
====================
With the ``rnid`` utility, you can generate, manage and view Reticulum Identities.
The program can also calculate Destination hashes, and perform encryption and
decryption of files.
Using ``rnid``, it is possible to asymmetrically encrypt files and information for
any Reticulum destination hash, and also to create and verify cryptographic signatures.
**Usage Examples**
Generate a new Identity:
.. code:: text
$ rnid -g ./new_identity
Display Identity key information:
.. code:: text
$ rnid -i ./new_identity -p
Loaded Identity <984b74a3f768bef236af4371e6f248cd> from new_id
Public Key : 0f4259fef4521ab75a3409e353fe9073eb10783b4912a6a9937c57bf44a62c1e
Private Key : Hidden
Encrypt a file for an LXMF user:
.. code:: text
$ rnid -i 8dd57a738226809646089335a6b03695 -e my_file.txt
Recalled Identity <bc7291552be7a58f361522990465165c> for destination <8dd57a738226809646089335a6b03695>
Encrypting my_file.txt
File my_file.txt encrypted for <bc7291552be7a58f361522990465165c> to my_file.txt.rfe
If the Identity for the destination is not already known, you can fetch it from the network by using the ``-R`` command-line option:
.. code:: text
$ rnid -R -i 30602def3b3506a28ed33db6f60cc6c9 -e my_file.txt
Requesting unknown Identity for <30602def3b3506a28ed33db6f60cc6c9>...
Received Identity <2b489d06eaf7c543808c76a5332a447d> for destination <30602def3b3506a28ed33db6f60cc6c9> from the network
Encrypting my_file.txt
File my_file.txt encrypted for <2b489d06eaf7c543808c76a5332a447d> to my_file.txt.rfe
Decrypt a file using the Reticulum Identity it was encrypted for:
.. code:: text
$ rnid -i ./my_identity -d my_file.txt.rfe
Loaded Identity <2225fdeecaf6e2db4556c3c2d7637294> from ./my_identity
Decrypting ./my_file.txt.rfe...
File ./my_file.txt.rfe decrypted with <2225fdeecaf6e2db4556c3c2d7637294> to ./my_file.txt
**All Command-Line Options**
.. code:: text
usage: rnid.py [-h] [--config path] [-i identity] [-g path] [-v] [-q] [-a aspects]
[-H aspects] [-e path] [-d path] [-s path] [-V path] [-r path] [-w path]
[-f] [-R] [-t seconds] [-p] [-P] [--version]
Reticulum Identity & Encryption Utility
options:
-h, --help show this help message and exit
--config path path to alternative Reticulum config directory
-i identity, --identity identity
hexadecimal Reticulum Destination hash or path to Identity file
-g path, --generate path
generate a new Identity
-v, --verbose increase verbosity
-q, --quiet decrease verbosity
-a aspects, --announce aspects
announce a destination based on this Identity
-H aspects, --hash aspects
show destination hashes for other aspects for this Identity
-e path, --encrypt path
encrypt file
-d path, --decrypt path
decrypt file
-s path, --sign path sign file
-V path, --validate path
validate signature
-r path, --read path input file path
-w path, --write path
output file path
-f, --force write output even if it overwrites existing files
-R, --request request unknown Identities from the network
-t seconds identity request timeout before giving up
-p, --print-identity print identity info and exit
-P, --print-private allow displaying private keys
--version show program's version number and exit
The rnpath Utility
====================
With the ``rnpath`` utility, you can look up and view paths for
destinations on the Reticulum network.
.. code:: text
**Usage Examples**
# Run rnpath
rnpath c89b4da064bf66d280f0e4d8abfd9806
# Example output
Path found, destination <c89b4da064bf66d280f0e4d8abfd9806> is 4 hops away via <f53a1c4278e0726bb73fcc623d6ce763> on TCPInterface[Testnet/frankfurt.connect.reticulu.network:4965]
Resolve path to a destination:
.. code:: text
usage: rnpath [-h] [--config CONFIG] [--version] [-t] [-r] [-d] [-D] [-w seconds] [-v] [destination]
$ rnpath c89b4da064bf66d280f0e4d8abfd9806
Path found, destination <c89b4da064bf66d280f0e4d8abfd9806> is 4 hops away via <f53a1c4278e0726bb73fcc623d6ce763> on TCPInterface[Testnet/dublin.connect.reticulum.network:4965]
**All Command-Line Options**
.. code:: text
usage: rnpath.py [-h] [--config CONFIG] [--version] [-t] [-r] [-d] [-D]
[-x] [-w seconds] [-v] [destination]
Reticulum Path Discovery Utility
positional arguments:
destination hexadecimal hash of the destination
optional arguments:
options:
-h, --help show this help message and exit
--config CONFIG path to alternative Reticulum config directory
--version show program's version number and exit
@ -270,6 +468,7 @@ destinations on the Reticulum network.
-r, --rates show announce rate info
-d, --drop remove the path to a destination
-D, --drop-announces drop all queued announces
-x, --drop-via drop all paths via specified transport instance
-w seconds timeout before giving up
-v, --verbose
@ -280,21 +479,53 @@ The rnprobe Utility
The ``rnprobe`` utility lets you probe a destination for connectivity, similar
to the ``ping`` program. Please note that probes will only be answered if the
specified destination is configured to send proofs for received packets. Many
destinations will not have this option enabled, and will not be probable.
destinations will not have this option enabled, so most destinations will not
be probable.
You can enable a probe-reply destination on Reticulum Transport Instances by
setting the ``respond_to_probes`` configuration directive. Reticulum will then
print the probe destination to the log on Transport Instance startup.
**Usage Examples**
Probe a destination:
.. code:: text
# Run rnprobe
rnprobe example_utilities.echo.request 2d03725b327348980d570f739a3a5708
$ rnprobe rnstransport.probe 2d03725b327348980d570f739a3a5708
# Example output
Sent 16 byte probe to <2d03725b327348980d570f739a3a5708>
Valid reply received from <2d03725b327348980d570f739a3a5708>
Round-trip time is 38.469 milliseconds over 2 hops
Send a larger probe:
.. code:: text
usage: rnprobe [-h] [--config CONFIG] [--version] [-v] [full_name] [destination_hash]
$ rnprobe rnstransport.probe 2d03725b327348980d570f739a3a5708 -s 256
Sent 16 byte probe to <2d03725b327348980d570f739a3a5708>
Valid reply received from <2d03725b327348980d570f739a3a5708>
Round-trip time is 38.781 milliseconds over 2 hops
If the interface that receives the probe replies supports reporting radio
parameters such as **RSSI** and **SNR**, the ``rnprobe`` utility will print
these as part of the result as well.
.. code:: text
$ rnprobe rnstransport.probe e7536ee90bd4a440e130490b87a25124
Sent 16 byte probe to <e7536ee90bd4a440e130490b87a25124>
Valid reply received from <e7536ee90bd4a440e130490b87a25124>
Round-trip time is 1.809 seconds over 1 hop [RSSI -73 dBm] [SNR 12.0 dB]
**All Command-Line Options**
.. code:: text
usage: rnprobe [-h] [--config CONFIG] [--version] [-v] [-s SIZE]
[full_name] [destination_hash]
Reticulum Probe Utility
@ -305,6 +536,7 @@ destinations will not have this option enabled, and will not be probable.
optional arguments:
-h, --help show this help message and exit
--config CONFIG path to alternative Reticulum config directory
-s SIZE, --size SIZE size of probe packet payload in bytes
--version show program's version number and exit
-v, --verbose
@ -315,20 +547,39 @@ The rncp Utility
The ``rncp`` utility is a simple file transfer tool. Using it, you can transfer
files through Reticulum.
**Usage Examples**
Run rncp on the receiving system, specifying which identities are allowed to send files:
.. code:: text
# Run rncp on the receiving system, specifying which identities
# are allowed to send files
rncp --receive -a 1726dbad538775b5bf9b0ea25a4079c8 -a c50cc4e4f7838b6c31f60ab9032cbc62
$ rncp --listen -a 1726dbad538775b5bf9b0ea25a4079c8 -a c50cc4e4f7838b6c31f60ab9032cbc62
# From another system, copy a file to the receiving system
rncp ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e
You can specify as many allowed senders as needed, or complete disable authentication.
You can also specify allowed identity hashes (one per line) in the file ~/.rncp/allowed_identities
and simply running the program in listener mode:
.. code:: text
usage: rncp [-h] [--config path] [-v] [-q] [-p] [-r] [-b] [-a allowed_hash] [-n] [-w seconds] [--version] [file] [destination]
$ rncp --listen
From another system, copy a file to the receiving system:
.. code:: text
$ rncp ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e
Or fetch a file from the remote system:
.. code:: text
$ rncp --fetch ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e
**All Command-Line Options**
.. code:: text
usage: rncp.py [-h] [--config path] [-v] [-q] [-S] [-l] [-f] [-b seconds]
[-a allowed_hash] [-n] [-p] [-w seconds] [--version] [file] [destination]
Reticulum File Transfer Utility
@ -336,19 +587,20 @@ You can specify as many allowed senders as needed, or complete disable authentic
file file to be transferred
destination hexadecimal hash of the receiver
optional arguments:
options:
-h, --help show this help message and exit
--config path path to alternative Reticulum config directory
-v, --verbose increase verbosity
-q, --quiet decrease verbosity
-p, --print-identity print identity and destination info and exit
-r, --receive wait for incoming files
-b, --no-announce don't announce at program start
-S, --silent disable transfer progress output
-l, --listen listen for incoming transfer requests
-f, --fetch fetch file from remote listener instead of sending
-b seconds announce interval, 0 to only announce at startup
-a allowed_hash accept from this identity
-n, --no-auth accept files from anyone
-n, --no-auth accept files and fetches from anyone
-p, --print-identity print identity and destination info and exit
-w seconds sender timeout before giving up
--version show program's version number and exit
-v, --verbose
The rnx Utility
@ -356,32 +608,43 @@ The rnx Utility
The ``rnx`` utility is a basic remote command execution program. It allows you to
execute commands on remote systems over Reticulum, and to view returned command
output.
output. For a fully interactive remote shell solution, be sure to also take a look
at the `rnsh <https://github.com/acehoss/rnsh>`_ program.
**Usage Examples**
Run rnx on the listening system, specifying which identities are allowed to execute commands:
.. code:: text
# Run rnx on the listening system, specifying which identities
# are allowed to execute commands
rncp --listen -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
$ rnx --listen -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
# From another system, run a command
rnx 7a55144adf826958a9529a3bcf08b149 "cat /proc/cpuinfo"
# Or enter the interactive mode pseudo-shell
rnx 7a55144adf826958a9529a3bcf08b149 -x
# The default identity file is stored in
# ~/.reticulum/identities/rnx, but you can use
# another one, which will be created if it does
# not already exist
rnx 7a55144adf826958a9529a3bcf08b149 -i /path/to/identity -x
You can specify as many allowed senders as needed, or completely disable authentication.
From another system, run a command on the remote:
.. code:: text
usage: rnx [-h] [--config path] [-v] [-q] [-p] [-l] [-i identity] [-x] [-b] [-a allowed_hash] [-n] [-N] [-d] [-m] [-w seconds] [-W seconds] [--stdin STDIN] [--stdout STDOUT] [--stderr STDERR] [--version]
[destination] [command]
$ rnx 7a55144adf826958a9529a3bcf08b149 "cat /proc/cpuinfo"
Or enter the interactive mode pseudo-shell:
.. code:: text
$ rnx 7a55144adf826958a9529a3bcf08b149 -x
The default identity file is stored in ``~/.reticulum/identities/rnx``, but you can use
another one, which will be created if it does not already exist
.. code:: text
$ rnx 7a55144adf826958a9529a3bcf08b149 -i /path/to/identity -x
**All Command-Line Options**
.. code:: text
usage: rnx [-h] [--config path] [-v] [-q] [-p] [-l] [-i identity] [-x] [-b] [-n] [-N]
[-d] [-m] [-a allowed_hash] [-w seconds] [-W seconds] [--stdin STDIN]
[--stdout STDOUT] [--stderr STDERR] [--version] [destination] [command]
Reticulum Remote Execution Utility
@ -412,6 +675,63 @@ You can specify as many allowed senders as needed, or completely disable authent
--version show program's version number and exit
The rnodeconf Utility
=====================
The ``rnodeconf`` utility allows you to inspect and configure existing :ref:`RNodes<rnode-main>`, and
to create and provision new :ref:`RNodes<rnode-main>` from any supported hardware devices.
**All Command-Line Options**
.. code:: text
usage: rnodeconf.py [-h] [-i] [-a] [-u] [-U] [--fw-version version] [--nocheck] [-e]
[-E] [-C] [--baud-flash baud_flash] [-N] [-T] [-b] [-B] [-p] [-D i]
[--freq Hz] [--bw Hz] [--txp dBm] [--sf factor] [--cr rate]
[--eeprom-backup] [--eeprom-dump] [--eeprom-wipe] [-P]
[--trust-key hexbytes] [--version] [port]
RNode Configuration and firmware utility. This program allows you to change various
settings and startup modes of RNode. It can also install, flash and update the firmware
on supported devices.
positional arguments:
port serial port where RNode is attached
options:
-h, --help show this help message and exit
-i, --info Show device info
-a, --autoinstall Automatic installation on various supported devices
-u, --update Update firmware to the latest version
-U, --force-update Update to specified firmware even if version matches or is older than installed version
--fw-version version Use a specific firmware version for update or autoinstall
--nocheck Don't check for firmware updates online
-e, --extract Extract firmware from connected RNode for later use
-E, --use-extracted Use the extracted firmware for autoinstallation or update
-C, --clear-cache Clear locally cached firmware files
--baud-flash baud_flash
Set specific baud rate when flashing device. Default is 921600
-N, --normal Switch device to normal mode
-T, --tnc Switch device to TNC mode
-b, --bluetooth-on Turn device bluetooth on
-B, --bluetooth-off Turn device bluetooth off
-p, --bluetooth-pair Put device into bluetooth pairing mode
-D i, --display i Set display intensity (0-255)
--freq Hz Frequency in Hz for TNC mode
--bw Hz Bandwidth in Hz for TNC mode
--txp dBm TX power in dBm for TNC mode
--sf factor Spreading factor for TNC mode (7 - 12)
--cr rate Coding rate for TNC mode (5 - 8)
--eeprom-backup Backup EEPROM to file
--eeprom-dump Dump EEPROM to console
--eeprom-wipe Unlock and wipe EEPROM
-P, --public Display public part of signing key
--trust-key hexbytes Public key to trust for device verification
--version Print program version and exit
For more information on how to create your own RNodes, please read the :ref:`Creating RNodes<rnode-creating>`
section of this manual.
Improving System Configuration
------------------------------
@ -460,6 +780,9 @@ Reticulum as a System Service
Instead of starting Reticulum manually, you can install ``rnsd`` as a system
service and have it start automatically at boot.
Systemwide Service
^^^^^^^^^^^^^^^^^^
If you installed Reticulum with ``pip``, the ``rnsd`` program will most likely
be located in a user-local installation path only, which means ``systemd`` will not
be able to execute it. In this case, you can simply symlink the ``rnsd`` program
@ -506,4 +829,49 @@ If you want to automatically start ``rnsd`` at boot, run:
.. code:: text
sudo systemctl enable rnsd
sudo systemctl enable rnsd
Userspace Service
^^^^^^^^^^^^^^^^^
Alternatively you can use a user systemd service instead of a system wide one. This way the whole setup can be done as a regular user.
Create a user systemd service files ``~/.config/systemd/user/rnsd.service`` with the following content:
.. code:: text
[Unit]
Description=Reticulum Network Stack Daemon
After=default.target
[Service]
# If you run Reticulum on WiFi devices,
# or other devices that need some extra
# time to initialise, you might want to
# add a short delay before Reticulum is
# started by systemd:
# ExecStartPre=/bin/sleep 10
Type=simple
Restart=always
RestartSec=3
ExecStart=RNS_BIN_DIR/rnsd --service
[Install]
WantedBy=default.target
Replace ``RNS_BIN_DIR`` with the path to your Reticulum binary directory (eg. /home/USERNAMEHERE/rns/bin).
Start user service:
.. code:: text
systemctl --user daemon-reload
systemctl --user start rnsd.service
If you want to automatically start ``rnsd`` without having to log in as the USERNAMEHERE, do:
.. code:: text
sudo loginctl enable-linger USERNAMEHERE
systemctl --user enable rnsd.service

View File

@ -2,23 +2,48 @@
What is Reticulum?
******************
Reticulum is a cryptography-based networking stack for building wide-area networks with readily available hardware, that can continue to operate even with extremely low bandwidth and very high latency.
Reticulum is a cryptography-based networking stack for building both local and
wide-area networks with readily available hardware, that can continue to operate
under adverse conditions, such as extremely low bandwidth and very high latency.
Reticulum allows you to build wide-area networks with off-the-shelf tools, and offers end-to-end encryption, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, unforgeable packet acknowledgements and more.
Reticulum allows you to build wide-area networks with off-the-shelf tools, and
offers end-to-end encryption, forward secrecy, autoconfiguring cryptographically
backed multi-hop transport, efficient addressing, unforgeable packet
acknowledgements and more.
Reticulum is a complete networking stack, and does not need IP or higher layers, although it is easy to utilise IP (with TCP or UDP) as the underlying carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the Internet or private IP networks. Reticulum is built directly on cryptographic principles, allowing resilience and stable functionality in open and trustless networks.
From a users perspective, Reticulum allows the creation of applications that
respect and empower the autonomy and sovereignty of communities and individuals.
Reticulum enables secure digital communication that cannot be subjected to
outside control, manipulation or censorship.
No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python 3. Reticulum runs well even on small single-board computers like the Pi Zero.
Reticulum enables the construction of both small and potentially planetary-scale
networks, without any need for hierarchical or beaureucratic structures to control
or manage them, while ensuring individuals and communities full sovereignty
over their own network segments.
Reticulum is a **complete networking stack**, and does not need IP or higher
layers, although it is easy to utilise IP (with TCP or UDP) as the underlying
carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the
Internet or private IP networks. Reticulum is built directly on cryptographic
principles, allowing resilience and stable functionality in open and trustless
networks.
No kernel modules or drivers are required. Reticulum can run completely in
userland, and will run on practically any system that runs Python 3. Reticulum
runs well even on small single-board computers like the Pi Zero.
Current Status
==============
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 stable at the moment, but could change if absolutely warranted.
**Please know!** 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 complete and stable at the moment, but could change if absolutely warranted.
What does Reticulum Offer?
==========================
* Coordination-less globally unique adressing and identification
* Coordination-less globally unique addressing and identification
* Fully self-configuring multi-hop routing
@ -26,7 +51,7 @@ What does Reticulum Offer?
* Asymmetric encryption based on X25519, and Ed25519 signatures as a basis for all communication
* Forward Secrecy by using ephemereal Elliptic Curve Diffie-Hellman keys on Curve25519
* Forward Secrecy by using 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
@ -46,7 +71,7 @@ What does Reticulum Offer?
* Efficient link establishment
* Total bandwidth cost of setting up a link is only 3 packets, totalling 265 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
@ -66,7 +91,7 @@ What does Reticulum Offer?
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,
ad-hoc WiFi, free-space optical links and similar systems are all examples
of the types of interfaces Reticulum was designed for.
@ -77,7 +102,7 @@ Reticulum. It is possible to build it yourself, to transform a common LoRa
development board into one, or it can be purchased as a complete transceiver.
Reticulum can also be encapsulated over existing IP networks, so there's
nothing stopping you from using it over wired ethernet or your local WiFi
nothing stopping you from using it over wired Ethernet or your local WiFi
network, 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.
@ -92,15 +117,15 @@ Interface Types and Devices
===========================
Reticulum implements a range of generalised interface types that covers the communications hardware that Reticulum can run over. If your hardware is not supported, it's relatively simple to implement an interface class. Currently, Reticulum can use the following devices and communication mediums:
* Any ethernet device
* Any Ethernet device
* WiFi devices
* Wired ethernet devices
* Wired Ethernet devices
* Fibre-optic transceivers
* Data radios with ethernet ports
* Data radios with Ethernet ports
* LoRa using `RNode <https://unsigned.io/rnode>`_
@ -135,4 +160,9 @@ For a full list and more details, see the :ref:`Supported Interfaces<interfaces-
Caveat Emptor
==============
Reticulum is an experimental networking stack, and should be considered as such. While it has been built with cryptography best-practices very foremost in mind, it has not been externally security audited, and there could very well be privacy-breaking bugs. To be considered secure, Reticulum needs a thourough security review by independt cryptographers and security researchers. If you want to help out, or help sponsor an audit, please do get in touch.
Reticulum is an experimental networking stack, and should be considered as
such. While it has been built with cryptography best-practices very foremost in
mind, it has not yet been externally security audited, and there could very well be
privacy-breaking bugs. To be considered secure, Reticulum needs a thorough
security review by independent cryptographers and security researchers. If you
want to help out with this, or can help sponsor an audit, please do get in touch.

View File

@ -0,0 +1,134 @@
/*
* _sphinx_javascript_frameworks_compat.js
* ~~~~~~~~~~
*
* Compatability shim for jQuery and underscores.js.
*
* WILL BE REMOVED IN Sphinx 6.0
* xref RemovedInSphinx60Warning
*
*/
/**
* select a different prefix for underscore
*/
$u = _.noConflict();
/**
* small helper function to urldecode strings
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
*/
jQuery.urldecode = function(x) {
if (!x) {
return x
}
return decodeURIComponent(x.replace(/\+/g, ' '));
};
/**
* small helper function to urlencode strings
*/
jQuery.urlencode = encodeURIComponent;
/**
* This function returns the parsed url parameters of the
* current request. Multiple values per key are supported,
* it will always return arrays of strings for the value parts.
*/
jQuery.getQueryParameters = function(s) {
if (typeof s === 'undefined')
s = document.location.search;
var parts = s.substr(s.indexOf('?') + 1).split('&');
var result = {};
for (var i = 0; i < parts.length; i++) {
var tmp = parts[i].split('=', 2);
var key = jQuery.urldecode(tmp[0]);
var value = jQuery.urldecode(tmp[1]);
if (key in result)
result[key].push(value);
else
result[key] = [value];
}
return result;
};
/**
* highlight a given string on a jquery object by wrapping it in
* span elements with the given class name.
*/
jQuery.fn.highlightText = function(text, className) {
function highlight(node, addItems) {
if (node.nodeType === 3) {
var val = node.nodeValue;
var pos = val.toLowerCase().indexOf(text);
if (pos >= 0 &&
!jQuery(node.parentNode).hasClass(className) &&
!jQuery(node.parentNode).hasClass("nohighlight")) {
var span;
var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
if (isInSVG) {
span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
} else {
span = document.createElement("span");
span.className = className;
}
span.appendChild(document.createTextNode(val.substr(pos, text.length)));
node.parentNode.insertBefore(span, node.parentNode.insertBefore(
document.createTextNode(val.substr(pos + text.length)),
node.nextSibling));
node.nodeValue = val.substr(0, pos);
if (isInSVG) {
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
var bbox = node.parentElement.getBBox();
rect.x.baseVal.value = bbox.x;
rect.y.baseVal.value = bbox.y;
rect.width.baseVal.value = bbox.width;
rect.height.baseVal.value = bbox.height;
rect.setAttribute('class', className);
addItems.push({
"parent": node.parentNode,
"target": rect});
}
}
}
else if (!jQuery(node).is("button, select, textarea")) {
jQuery.each(node.childNodes, function() {
highlight(this, addItems);
});
}
}
var addItems = [];
var result = this.each(function() {
highlight(this, addItems);
});
for (var i = 0; i < addItems.length; ++i) {
jQuery(addItems[i].parent).before(addItems[i].target);
}
return result;
};
/*
* backward compatibility for jQuery.browser
* This will be supported until firefox bug is fixed.
*/
if (!jQuery.browser) {
jQuery.uaMatch = function(ua) {
ua = ua.toLowerCase();
var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
/(msie) ([\w.]+)/.exec(ua) ||
ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
[];
return {
browser: match[ 1 ] || "",
version: match[ 2 ] || "0"
};
};
jQuery.browser = {};
jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
}

View File

@ -4,7 +4,7 @@
*
* Sphinx stylesheet -- basic theme.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@ -222,7 +222,7 @@ table.modindextable td {
/* -- general body styles --------------------------------------------------- */
div.body {
min-width: 450px;
min-width: 360px;
max-width: 800px;
}
@ -237,16 +237,6 @@ a.headerlink {
visibility: hidden;
}
a.brackets:before,
span.brackets > a:before{
content: "[";
}
a.brackets:after,
span.brackets > a:after {
content: "]";
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
@ -334,13 +324,15 @@ aside.sidebar {
p.sidebar-title {
font-weight: bold;
}
nav.contents,
aside.topic,
div.admonition, div.topic, blockquote {
clear: left;
}
/* -- topics ---------------------------------------------------------------- */
nav.contents,
aside.topic,
div.topic {
border: 1px solid #ccc;
padding: 7px;
@ -379,6 +371,8 @@ div.body p.centered {
div.sidebar > :last-child,
aside.sidebar > :last-child,
nav.contents > :last-child,
aside.topic > :last-child,
div.topic > :last-child,
div.admonition > :last-child {
margin-bottom: 0;
@ -386,6 +380,8 @@ div.admonition > :last-child {
div.sidebar::after,
aside.sidebar::after,
nav.contents::after,
aside.topic::after,
div.topic::after,
div.admonition::after,
blockquote::after {
@ -428,10 +424,6 @@ table.docutils td, table.docutils th {
border-bottom: 1px solid #aaa;
}
table.footnote td, table.footnote th {
border: 0 !important;
}
th {
text-align: left;
padding-right: 5px;
@ -614,20 +606,26 @@ ol.simple p,
ul.simple p {
margin-bottom: 0;
}
dl.footnote > dt,
dl.citation > dt {
aside.footnote > span,
div.citation > span {
float: left;
margin-right: 0.5em;
}
dl.footnote > dd,
dl.citation > dd {
aside.footnote > span:last-of-type,
div.citation > span:last-of-type {
padding-right: 0.5em;
}
aside.footnote > p {
margin-left: 2em;
}
div.citation > p {
margin-left: 4em;
}
aside.footnote > p:last-of-type,
div.citation > p:last-of-type {
margin-bottom: 0em;
}
dl.footnote > dd:after,
dl.citation > dd:after {
aside.footnote > p:last-of-type:after,
div.citation > p:last-of-type:after {
content: "";
clear: both;
}
@ -644,10 +642,6 @@ dl.field-list > dt {
padding-right: 5px;
}
dl.field-list > dt:after {
content: ":";
}
dl.field-list > dd {
padding-left: 0.5em;
margin-top: 0em;
@ -731,8 +725,9 @@ dl.glossary dt {
.classifier:before {
font-style: normal;
margin: 0.5em;
margin: 0 0.5em;
content: ":";
display: inline-block;
}
abbr, acronym {
@ -756,6 +751,7 @@ span.pre {
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none;
white-space: nowrap;
}
div[class*="highlight-"] {
@ -819,7 +815,7 @@ div.code-block-caption code {
table.highlighttable td.linenos,
span.linenos,
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
-webkit-user-select: text; /* Safari fallback only */
-webkit-user-select: none; /* Chrome/Safari */

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#22863a" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M5 12l5 5l10 -10" />
</svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@ -1,266 +0,0 @@
/*
* classic.css_t
* ~~~~~~~~~~~~~
*
* Sphinx stylesheet -- classic theme.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
html {
/* CSS hack for macOS's scrollbar (see #1125) */
background-color: #FFFFFF;
}
body {
font-family: sans-serif;
font-size: 100%;
background-color: #11303d;
color: #000;
margin: 0;
padding: 0;
}
div.document {
background-color: #1c4e63;
}
div.documentwrapper {
float: left;
width: 100%;
}
div.bodywrapper {
margin: 0 0 0 230px;
}
div.body {
background-color: #ffffff;
color: #000000;
padding: 0 20px 30px 20px;
}
div.footer {
color: #ffffff;
width: 100%;
padding: 9px 0 9px 0;
text-align: center;
font-size: 75%;
}
div.footer a {
color: #ffffff;
text-decoration: underline;
}
div.related {
background-color: #133f52;
line-height: 30px;
color: #ffffff;
}
div.related a {
color: #ffffff;
}
div.sphinxsidebar {
}
div.sphinxsidebar h3 {
font-family: 'Trebuchet MS', sans-serif;
color: #ffffff;
font-size: 1.4em;
font-weight: normal;
margin: 0;
padding: 0;
}
div.sphinxsidebar h3 a {
color: #ffffff;
}
div.sphinxsidebar h4 {
font-family: 'Trebuchet MS', sans-serif;
color: #ffffff;
font-size: 1.3em;
font-weight: normal;
margin: 5px 0 0 0;
padding: 0;
}
div.sphinxsidebar p {
color: #ffffff;
}
div.sphinxsidebar p.topless {
margin: 5px 10px 10px 10px;
}
div.sphinxsidebar ul {
margin: 10px;
padding: 0;
color: #ffffff;
}
div.sphinxsidebar a {
color: #98dbcc;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
/* -- hyperlink styles ------------------------------------------------------ */
a {
color: #355f7c;
text-decoration: none;
}
a:visited {
color: #355f7c;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* -- body styles ----------------------------------------------------------- */
div.body h1,
div.body h2,
div.body h3,
div.body h4,
div.body h5,
div.body h6 {
font-family: 'Trebuchet MS', sans-serif;
background-color: #f2f2f2;
font-weight: normal;
color: #20435c;
border-bottom: 1px solid #ccc;
margin: 20px -20px 10px -20px;
padding: 3px 0 3px 10px;
}
div.body h1 { margin-top: 0; font-size: 200%; }
div.body h2 { font-size: 160%; }
div.body h3 { font-size: 140%; }
div.body h4 { font-size: 120%; }
div.body h5 { font-size: 110%; }
div.body h6 { font-size: 100%; }
a.headerlink {
color: #c60f0f;
font-size: 0.8em;
padding: 0 4px 0 4px;
text-decoration: none;
}
a.headerlink:hover {
background-color: #c60f0f;
color: white;
}
div.body p, div.body dd, div.body li, div.body blockquote {
text-align: justify;
line-height: 130%;
}
div.admonition p.admonition-title + p {
display: inline;
}
div.admonition p {
margin-bottom: 5px;
}
div.admonition pre {
margin-bottom: 5px;
}
div.admonition ul, div.admonition ol {
margin-bottom: 5px;
}
div.note {
background-color: #eee;
border: 1px solid #ccc;
}
div.seealso {
background-color: #ffc;
border: 1px solid #ff6;
}
div.topic {
background-color: #eee;
}
div.warning {
background-color: #ffe4e4;
border: 1px solid #f66;
}
p.admonition-title {
display: inline;
}
p.admonition-title:after {
content: ":";
}
pre {
padding: 5px;
background-color: unset;
color: unset;
line-height: 120%;
border: 1px solid #ac9;
border-left: none;
border-right: none;
}
code {
background-color: #ecf0f3;
padding: 0 1px 0 1px;
font-size: 0.95em;
}
th, dl.field-list > dt {
background-color: #ede;
}
.warning code {
background: #efc2c2;
}
.note code {
background: #d6d6d6;
}
.viewcode-back {
font-family: sans-serif;
}
div.viewcode-block:target {
background-color: #f4debf;
border-top: 1px solid #ac9;
border-bottom: 1px solid #ac9;
}
div.code-block-caption {
color: #efefef;
background-color: #1c4e63;
}

7
docs/manual/_static/clipboard.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-copy" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<rect x="8" y="8" width="12" height="12" rx="2" />
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" />
</svg>

After

Width:  |  Height:  |  Size: 411 B

View File

@ -0,0 +1,94 @@
/* Copy buttons */
button.copybtn {
position: absolute;
display: flex;
top: .3em;
right: .3em;
width: 1.7em;
height: 1.7em;
opacity: 0;
transition: opacity 0.3s, border .3s, background-color .3s;
user-select: none;
padding: 0;
border: none;
outline: none;
border-radius: 0.4em;
/* The colors that GitHub uses */
border: #1b1f2426 1px solid;
background-color: #f6f8fa;
color: #57606a;
}
button.copybtn.success {
border-color: #22863a;
color: #22863a;
}
button.copybtn svg {
stroke: currentColor;
width: 1.5em;
height: 1.5em;
padding: 0.1em;
}
div.highlight {
position: relative;
}
/* Show the copybutton */
.highlight:hover button.copybtn, button.copybtn.success {
opacity: 1;
}
.highlight button.copybtn:hover {
background-color: rgb(235, 235, 235);
}
.highlight button.copybtn:active {
background-color: rgb(187, 187, 187);
}
/**
* A minimal CSS-only tooltip copied from:
* https://codepen.io/mildrenben/pen/rVBrpK
*
* To use, write HTML like the following:
*
* <p class="o-tooltip--left" data-tooltip="Hey">Short</p>
*/
.o-tooltip--left {
position: relative;
}
.o-tooltip--left:after {
opacity: 0;
visibility: hidden;
position: absolute;
content: attr(data-tooltip);
padding: .2em;
font-size: .8em;
left: -.2em;
background: grey;
color: white;
white-space: nowrap;
z-index: 2;
border-radius: 2px;
transform: translateX(-102%) translateY(0);
transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1);
}
.o-tooltip--left:hover:after {
display: block;
opacity: 1;
visibility: visible;
transform: translateX(-100%) translateY(0);
transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1);
transition-delay: .5s;
}
/* By default the copy button shouldn't show up when printing a page */
@media print {
button.copybtn {
display: none;
}
}

View File

@ -0,0 +1,248 @@
// Localization support
const messages = {
'en': {
'copy': 'Copy',
'copy_to_clipboard': 'Copy to clipboard',
'copy_success': 'Copied!',
'copy_failure': 'Failed to copy',
},
'es' : {
'copy': 'Copiar',
'copy_to_clipboard': 'Copiar al portapapeles',
'copy_success': '¡Copiado!',
'copy_failure': 'Error al copiar',
},
'de' : {
'copy': 'Kopieren',
'copy_to_clipboard': 'In die Zwischenablage kopieren',
'copy_success': 'Kopiert!',
'copy_failure': 'Fehler beim Kopieren',
},
'fr' : {
'copy': 'Copier',
'copy_to_clipboard': 'Copier dans le presse-papier',
'copy_success': 'Copié !',
'copy_failure': 'Échec de la copie',
},
'ru': {
'copy': 'Скопировать',
'copy_to_clipboard': 'Скопировать в буфер',
'copy_success': 'Скопировано!',
'copy_failure': 'Не удалось скопировать',
},
'zh-CN': {
'copy': '复制',
'copy_to_clipboard': '复制到剪贴板',
'copy_success': '复制成功!',
'copy_failure': '复制失败',
},
'it' : {
'copy': 'Copiare',
'copy_to_clipboard': 'Copiato negli appunti',
'copy_success': 'Copiato!',
'copy_failure': 'Errore durante la copia',
}
}
let locale = 'en'
if( document.documentElement.lang !== undefined
&& messages[document.documentElement.lang] !== undefined ) {
locale = document.documentElement.lang
}
let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT;
if (doc_url_root == '#') {
doc_url_root = '';
}
/**
* SVG files for our copy buttons
*/
let iconCheck = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#22863a" fill="none" stroke-linecap="round" stroke-linejoin="round">
<title>${messages[locale]['copy_success']}</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M5 12l5 5l10 -10" />
</svg>`
// If the user specified their own SVG use that, otherwise use the default
let iconCopy = ``;
if (!iconCopy) {
iconCopy = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-copy" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
<title>${messages[locale]['copy_to_clipboard']}</title>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<rect x="8" y="8" width="12" height="12" rx="2" />
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" />
</svg>`
}
/**
* Set up copy/paste for code blocks
*/
const runWhenDOMLoaded = cb => {
if (document.readyState != 'loading') {
cb()
} else if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', cb)
} else {
document.attachEvent('onreadystatechange', function() {
if (document.readyState == 'complete') cb()
})
}
}
const codeCellId = index => `codecell${index}`
// Clears selected text since ClipboardJS will select the text when copying
const clearSelection = () => {
if (window.getSelection) {
window.getSelection().removeAllRanges()
} else if (document.selection) {
document.selection.empty()
}
}
// Changes tooltip text for a moment, then changes it back
// We want the timeout of our `success` class to be a bit shorter than the
// tooltip and icon change, so that we can hide the icon before changing back.
var timeoutIcon = 2000;
var timeoutSuccessClass = 1500;
const temporarilyChangeTooltip = (el, oldText, newText) => {
el.setAttribute('data-tooltip', newText)
el.classList.add('success')
// Remove success a little bit sooner than we change the tooltip
// So that we can use CSS to hide the copybutton first
setTimeout(() => el.classList.remove('success'), timeoutSuccessClass)
setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon)
}
// Changes the copy button icon for two seconds, then changes it back
const temporarilyChangeIcon = (el) => {
el.innerHTML = iconCheck;
setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon)
}
const addCopyButtonToCodeCells = () => {
// If ClipboardJS hasn't loaded, wait a bit and try again. This
// happens because we load ClipboardJS asynchronously.
if (window.ClipboardJS === undefined) {
setTimeout(addCopyButtonToCodeCells, 250)
return
}
// Add copybuttons to all of our code cells
const COPYBUTTON_SELECTOR = 'div.highlight pre';
const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR)
codeCells.forEach((codeCell, index) => {
const id = codeCellId(index)
codeCell.setAttribute('id', id)
const clipboardButton = id =>
`<button class="copybtn o-tooltip--left" data-tooltip="${messages[locale]['copy']}" data-clipboard-target="#${id}">
${iconCopy}
</button>`
codeCell.insertAdjacentHTML('afterend', clipboardButton(id))
})
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/**
* Removes excluded text from a Node.
*
* @param {Node} target Node to filter.
* @param {string} exclude CSS selector of nodes to exclude.
* @returns {DOMString} Text from `target` with text removed.
*/
function filterText(target, exclude) {
const clone = target.cloneNode(true); // clone as to not modify the live DOM
if (exclude) {
// remove excluded nodes
clone.querySelectorAll(exclude).forEach(node => node.remove());
}
return clone.innerText;
}
// Callback when a copy button is clicked. Will be passed the node that was clicked
// should then grab the text and replace pieces of text that shouldn't be used in output
function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
var regexp;
var match;
// Do we check for line continuation characters and "HERE-documents"?
var useLineCont = !!lineContinuationChar
var useHereDoc = !!hereDocDelim
// create regexp to capture prompt and remaining line
if (isRegexp) {
regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)')
} else {
regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)')
}
const outputLines = [];
var promptFound = false;
var gotLineCont = false;
var gotHereDoc = false;
const lineGotPrompt = [];
for (const line of textContent.split('\n')) {
match = line.match(regexp)
if (match || gotLineCont || gotHereDoc) {
promptFound = regexp.test(line)
lineGotPrompt.push(promptFound)
if (removePrompts && promptFound) {
outputLines.push(match[2])
} else {
outputLines.push(line)
}
gotLineCont = line.endsWith(lineContinuationChar) & useLineCont
if (line.includes(hereDocDelim) & useHereDoc)
gotHereDoc = !gotHereDoc
} else if (!onlyCopyPromptLines) {
outputLines.push(line)
} else if (copyEmptyLines && line.trim() === '') {
outputLines.push(line)
}
}
// If no lines with the prompt were found then just use original lines
if (lineGotPrompt.some(v => v === true)) {
textContent = outputLines.join('\n');
}
// Remove a trailing newline to avoid auto-running when pasting
if (textContent.endsWith("\n")) {
textContent = textContent.slice(0, -1)
}
return textContent
}
var copyTargetText = (trigger) => {
var target = document.querySelector(trigger.attributes['data-clipboard-target'].value);
// get filtered text
let exclude = '.linenos';
let text = filterText(target, exclude);
return formatCopyText(text, '', false, true, true, true, '', '')
}
// Initialize with a callback so we can modify the text before copy
const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText})
// Update UI with error/success messages
clipboard.on('success', event => {
clearSelection()
temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success'])
temporarilyChangeIcon(event.trigger)
})
clipboard.on('error', event => {
temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure'])
})
}
runWhenDOMLoaded(addCopyButtonToCodeCells)

View File

@ -0,0 +1,73 @@
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/**
* Removes excluded text from a Node.
*
* @param {Node} target Node to filter.
* @param {string} exclude CSS selector of nodes to exclude.
* @returns {DOMString} Text from `target` with text removed.
*/
export function filterText(target, exclude) {
const clone = target.cloneNode(true); // clone as to not modify the live DOM
if (exclude) {
// remove excluded nodes
clone.querySelectorAll(exclude).forEach(node => node.remove());
}
return clone.innerText;
}
// Callback when a copy button is clicked. Will be passed the node that was clicked
// should then grab the text and replace pieces of text that shouldn't be used in output
export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
var regexp;
var match;
// Do we check for line continuation characters and "HERE-documents"?
var useLineCont = !!lineContinuationChar
var useHereDoc = !!hereDocDelim
// create regexp to capture prompt and remaining line
if (isRegexp) {
regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)')
} else {
regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)')
}
const outputLines = [];
var promptFound = false;
var gotLineCont = false;
var gotHereDoc = false;
const lineGotPrompt = [];
for (const line of textContent.split('\n')) {
match = line.match(regexp)
if (match || gotLineCont || gotHereDoc) {
promptFound = regexp.test(line)
lineGotPrompt.push(promptFound)
if (removePrompts && promptFound) {
outputLines.push(match[2])
} else {
outputLines.push(line)
}
gotLineCont = line.endsWith(lineContinuationChar) & useLineCont
if (line.includes(hereDocDelim) & useHereDoc)
gotHereDoc = !gotHereDoc
} else if (!onlyCopyPromptLines) {
outputLines.push(line)
} else if (copyEmptyLines && line.trim() === '') {
outputLines.push(line)
}
}
// If no lines with the prompt were found then just use original lines
if (lineGotPrompt.some(v => v === true)) {
textContent = outputLines.join('\n');
}
// Remove a trailing newline to avoid auto-running when pasting
if (textContent.endsWith("\n")) {
textContent = textContent.slice(0, -1)
}
return textContent
}

View File

@ -0,0 +1,20 @@
h3 {
margin-top: 1.75rem;
margin-bottom: 0.5rem;
}
code.literal {
padding-left: 0.25rem !important;
padding-right: 0.25rem !important;
padding-top: 0.25rem !important;
padding-bottom: 0.15rem !important;
}
img[src*="if_mode_graph_b.png"] {
background-color: rgb(169, 177, 186);
}
dt.sig {
margin-bottom: 0.75rem;
margin-top: 1.75rem;
}

View File

@ -0,0 +1,69 @@
/*
This CSS file should be overridden by the theme authors. It's
meant for debugging and developing the skeleton that this theme provides.
*/
body {
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji";
background: lavender;
}
.sb-announcement {
background: rgb(131, 131, 131);
}
.sb-announcement__inner {
background: black;
color: white;
}
.sb-header {
background: lightskyblue;
}
.sb-header__inner {
background: royalblue;
color: white;
}
.sb-header-secondary {
background: lightcyan;
}
.sb-header-secondary__inner {
background: cornflowerblue;
color: white;
}
.sb-sidebar-primary {
background: lightgreen;
}
.sb-main {
background: blanchedalmond;
}
.sb-main__inner {
background: antiquewhite;
}
.sb-header-article {
background: lightsteelblue;
}
.sb-article-container {
background: snow;
}
.sb-article-main {
background: white;
}
.sb-footer-article {
background: lightpink;
}
.sb-sidebar-secondary {
background: lightgoldenrodyellow;
}
.sb-footer-content {
background: plum;
}
.sb-footer-content__inner {
background: palevioletred;
}
.sb-footer {
background: pink;
}
.sb-footer__inner {
background: salmon;
}
.sb-article {
background: white;
}

View File

@ -2,320 +2,155 @@
* doctools.js
* ~~~~~~~~~~~
*
* Sphinx JavaScript utilities for all documentation.
* Base JavaScript utilities for all Sphinx HTML documentation.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
"use strict";
/**
* select a different prefix for underscore
*/
$u = _.noConflict();
const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([
"TEXTAREA",
"INPUT",
"SELECT",
"BUTTON",
]);
/**
* make the code below compatible with browsers without
* an installed firebug like debugger
if (!window.console || !console.firebug) {
var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
"dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
"profile", "profileEnd"];
window.console = {};
for (var i = 0; i < names.length; ++i)
window.console[names[i]] = function() {};
}
*/
/**
* small helper function to urldecode strings
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
*/
jQuery.urldecode = function(x) {
if (!x) {
return x
const _ready = (callback) => {
if (document.readyState !== "loading") {
callback();
} else {
document.addEventListener("DOMContentLoaded", callback);
}
return decodeURIComponent(x.replace(/\+/g, ' '));
};
/**
* small helper function to urlencode strings
*/
jQuery.urlencode = encodeURIComponent;
/**
* This function returns the parsed url parameters of the
* current request. Multiple values per key are supported,
* it will always return arrays of strings for the value parts.
*/
jQuery.getQueryParameters = function(s) {
if (typeof s === 'undefined')
s = document.location.search;
var parts = s.substr(s.indexOf('?') + 1).split('&');
var result = {};
for (var i = 0; i < parts.length; i++) {
var tmp = parts[i].split('=', 2);
var key = jQuery.urldecode(tmp[0]);
var value = jQuery.urldecode(tmp[1]);
if (key in result)
result[key].push(value);
else
result[key] = [value];
}
return result;
};
/**
* highlight a given string on a jquery object by wrapping it in
* span elements with the given class name.
*/
jQuery.fn.highlightText = function(text, className) {
function highlight(node, addItems) {
if (node.nodeType === 3) {
var val = node.nodeValue;
var pos = val.toLowerCase().indexOf(text);
if (pos >= 0 &&
!jQuery(node.parentNode).hasClass(className) &&
!jQuery(node.parentNode).hasClass("nohighlight")) {
var span;
var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
if (isInSVG) {
span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
} else {
span = document.createElement("span");
span.className = className;
}
span.appendChild(document.createTextNode(val.substr(pos, text.length)));
node.parentNode.insertBefore(span, node.parentNode.insertBefore(
document.createTextNode(val.substr(pos + text.length)),
node.nextSibling));
node.nodeValue = val.substr(0, pos);
if (isInSVG) {
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
var bbox = node.parentElement.getBBox();
rect.x.baseVal.value = bbox.x;
rect.y.baseVal.value = bbox.y;
rect.width.baseVal.value = bbox.width;
rect.height.baseVal.value = bbox.height;
rect.setAttribute('class', className);
addItems.push({
"parent": node.parentNode,
"target": rect});
}
}
}
else if (!jQuery(node).is("button, select, textarea")) {
jQuery.each(node.childNodes, function() {
highlight(this, addItems);
});
}
}
var addItems = [];
var result = this.each(function() {
highlight(this, addItems);
});
for (var i = 0; i < addItems.length; ++i) {
jQuery(addItems[i].parent).before(addItems[i].target);
}
return result;
};
/*
* backward compatibility for jQuery.browser
* This will be supported until firefox bug is fixed.
*/
if (!jQuery.browser) {
jQuery.uaMatch = function(ua) {
ua = ua.toLowerCase();
var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
/(msie) ([\w.]+)/.exec(ua) ||
ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
[];
return {
browser: match[ 1 ] || "",
version: match[ 2 ] || "0"
};
};
jQuery.browser = {};
jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
}
/**
* Small JavaScript module for the documentation.
*/
var Documentation = {
init : function() {
this.fixFirefoxAnchorBug();
this.highlightSearchWords();
this.initIndexTable();
if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {
this.initOnKeyListeners();
}
const Documentation = {
init: () => {
Documentation.initDomainIndexTable();
Documentation.initOnKeyListeners();
},
/**
* i18n support
*/
TRANSLATIONS : {},
PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
LOCALE : 'unknown',
TRANSLATIONS: {},
PLURAL_EXPR: (n) => (n === 1 ? 0 : 1),
LOCALE: "unknown",
// gettext and ngettext don't access this so that the functions
// can safely bound to a different name (_ = Documentation.gettext)
gettext : function(string) {
var translated = Documentation.TRANSLATIONS[string];
if (typeof translated === 'undefined')
return string;
return (typeof translated === 'string') ? translated : translated[0];
gettext: (string) => {
const translated = Documentation.TRANSLATIONS[string];
switch (typeof translated) {
case "undefined":
return string; // no translation
case "string":
return translated; // translation exists
default:
return translated[0]; // (singular, plural) translation tuple exists
}
},
ngettext : function(singular, plural, n) {
var translated = Documentation.TRANSLATIONS[singular];
if (typeof translated === 'undefined')
return (n == 1) ? singular : plural;
return translated[Documentation.PLURALEXPR(n)];
ngettext: (singular, plural, n) => {
const translated = Documentation.TRANSLATIONS[singular];
if (typeof translated !== "undefined")
return translated[Documentation.PLURAL_EXPR(n)];
return n === 1 ? singular : plural;
},
addTranslations : function(catalog) {
for (var key in catalog.messages)
this.TRANSLATIONS[key] = catalog.messages[key];
this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
this.LOCALE = catalog.locale;
addTranslations: (catalog) => {
Object.assign(Documentation.TRANSLATIONS, catalog.messages);
Documentation.PLURAL_EXPR = new Function(
"n",
`return (${catalog.plural_expr})`
);
Documentation.LOCALE = catalog.locale;
},
/**
* add context elements like header anchor links
* helper function to focus on search bar
*/
addContextElements : function() {
$('div[id] > :header:first').each(function() {
$('<a class="headerlink">\u00B6</a>').
attr('href', '#' + this.id).
attr('title', _('Permalink to this headline')).
appendTo(this);
});
$('dt[id]').each(function() {
$('<a class="headerlink">\u00B6</a>').
attr('href', '#' + this.id).
attr('title', _('Permalink to this definition')).
appendTo(this);
});
focusSearchBar: () => {
document.querySelectorAll("input[name=q]")[0]?.focus();
},
/**
* workaround a firefox stupidity
* see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
* Initialise the domain index toggle buttons
*/
fixFirefoxAnchorBug : function() {
if (document.location.hash && $.browser.mozilla)
window.setTimeout(function() {
document.location.href += '';
}, 10);
},
/**
* highlight the search words provided in the url in the text
*/
highlightSearchWords : function() {
var params = $.getQueryParameters();
var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
if (terms.length) {
var body = $('div.body');
if (!body.length) {
body = $('body');
initDomainIndexTable: () => {
const toggler = (el) => {
const idNumber = el.id.substr(7);
const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`);
if (el.src.substr(-9) === "minus.png") {
el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`;
toggledRows.forEach((el) => (el.style.display = "none"));
} else {
el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`;
toggledRows.forEach((el) => (el.style.display = ""));
}
window.setTimeout(function() {
$.each(terms, function() {
body.highlightText(this.toLowerCase(), 'highlighted');
});
}, 10);
$('<p class="highlight-link"><a href="javascript:Documentation.' +
'hideSearchWords()">' + _('Hide Search Matches') + '</a></p>')
.appendTo($('#searchbox'));
}
};
const togglerElements = document.querySelectorAll("img.toggler");
togglerElements.forEach((el) =>
el.addEventListener("click", (event) => toggler(event.currentTarget))
);
togglerElements.forEach((el) => (el.style.display = ""));
if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler);
},
/**
* init the domain index toggle buttons
*/
initIndexTable : function() {
var togglers = $('img.toggler').click(function() {
var src = $(this).attr('src');
var idnum = $(this).attr('id').substr(7);
$('tr.cg-' + idnum).toggle();
if (src.substr(-9) === 'minus.png')
$(this).attr('src', src.substr(0, src.length-9) + 'plus.png');
else
$(this).attr('src', src.substr(0, src.length-8) + 'minus.png');
}).css('display', '');
if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) {
togglers.click();
}
},
initOnKeyListeners: () => {
// only install a listener if it is really needed
if (
!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS &&
!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS
)
return;
/**
* helper function to hide the search marks again
*/
hideSearchWords : function() {
$('#searchbox .highlight-link').fadeOut(300);
$('span.highlighted').removeClass('highlighted');
},
document.addEventListener("keydown", (event) => {
// bail for input elements
if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return;
// bail with special keys
if (event.altKey || event.ctrlKey || event.metaKey) return;
/**
* make the url absolute
*/
makeURL : function(relativeURL) {
return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
},
if (!event.shiftKey) {
switch (event.key) {
case "ArrowLeft":
if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
/**
* get the current relative url
*/
getCurrentURL : function() {
var path = document.location.pathname;
var parts = path.split(/\//);
$.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() {
if (this === '..')
parts.pop();
});
var url = parts.join('/');
return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
},
initOnKeyListeners: function() {
$(document).keydown(function(event) {
var activeElementType = document.activeElement.tagName;
// don't navigate when in search box, textarea, dropdown or button
if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT'
&& activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey
&& !event.shiftKey) {
switch (event.keyCode) {
case 37: // left
var prevHref = $('link[rel="prev"]').prop('href');
if (prevHref) {
window.location.href = prevHref;
return false;
const prevLink = document.querySelector('link[rel="prev"]');
if (prevLink && prevLink.href) {
window.location.href = prevLink.href;
event.preventDefault();
}
case 39: // right
var nextHref = $('link[rel="next"]').prop('href');
if (nextHref) {
window.location.href = nextHref;
return false;
break;
case "ArrowRight":
if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
const nextLink = document.querySelector('link[rel="next"]');
if (nextLink && nextLink.href) {
window.location.href = nextLink.href;
event.preventDefault();
}
break;
}
}
// some keyboard layouts may need Shift to get /
switch (event.key) {
case "/":
if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break;
Documentation.focusSearchBar();
event.preventDefault();
}
});
}
},
};
// quick alias for translations
_ = Documentation.gettext;
const _ = Documentation.gettext;
$(document).ready(function() {
Documentation.init();
});
_ready(Documentation.init);

View File

@ -1,12 +1,14 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '0.3.9 beta',
LANGUAGE: 'None',
VERSION: '0.7.5 beta',
LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',
FILE_SUFFIX: '.html',
LINK_SUFFIX: '.html',
HAS_SOURCE: true,
SOURCELINK_SUFFIX: '.txt',
NAVIGATION_WITH_KEYS: false
NAVIGATION_WITH_KEYS: false,
SHOW_SEARCH_SUMMARY: true,
ENABLE_SEARCH_SHORTCUTS: true,
};

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