Compare commits

..

508 Commits

Author SHA1 Message Date
Mark Qvist c6df6293b2 Added hardware MTU parameter to interfaces 2022-05-29 15:43:50 +02:00
Mark Qvist d99d31097b Updated manual 2022-05-29 10:14:31 +02:00
Mark Qvist 54488cfeb5 Updated documentation 2022-05-29 10:13:25 +02:00
Mark Qvist d7e38d646e Updated readme 2022-05-29 09:48:53 +02:00
Mark Qvist b9057bee5f Updated readme 2022-05-29 09:48:31 +02:00
Mark Qvist 9bd64834ec Updated readme 2022-05-29 09:47:26 +02:00
Mark Qvist 9e20ba2dac Implemented I2PInterface recovery on I2P router restart 2022-05-28 02:24:01 +02:00
Mark Qvist 49ed335e19 Cleanup 2022-05-26 16:52:28 +02:00
Mark Qvist 85c71b0b7b Updated docs 2022-05-26 16:50:35 +02:00
Mark Qvist 33fac728f8 Improved link stale process and timeout calculations 2022-05-26 16:49:02 +02:00
Mark Qvist 49616a36cf Fixed I2P controller startup when event loop is not immediately ready 2022-05-26 09:54:56 +02:00
Mark Qvist 1e77f85cd4 Fixed rnx version output 2022-05-26 00:03:37 +02:00
Mark Qvist 9e316ab989 Fixed deprecated options in asyncio API for Python 3.10. Fixes #58. 2022-05-25 23:11:01 +02:00
Mark Qvist 94749e0dde Updated default configs 2022-05-25 23:10:05 +02:00
Mark Qvist a6dbc53209 Improved status display for I2P interfaces 2022-05-25 21:44:49 +02:00
Mark Qvist 3af5a8f3ed Improved I2P server tunnel error handling. Fixes #13. 2022-05-25 21:23:52 +02:00
Mark Qvist fb5172ff10 Improved I2P client tunnel error handling 2022-05-25 20:18:06 +02:00
Mark Qvist 24d6de8490 Updated docs 2022-05-25 15:51:20 +02:00
Mark Qvist d3ab0878e0 Improved I2P interface display in rnstatus 2022-05-25 15:50:54 +02:00
Mark Qvist 7848b7e396 Fixed invalid reference in rnx 2022-05-25 15:08:45 +02:00
Mark Qvist fc80dd2614 Improved rnstatus output 2022-05-25 14:21:04 +02:00
Mark Qvist e00a758b2a Updated readme 2022-05-24 21:00:46 +02:00
Mark Qvist d44ec745df Updated readme 2022-05-24 21:00:25 +02:00
Mark Qvist 7573ac1970 Updated readme 2022-05-24 20:58:58 +02:00
Mark Qvist 88390f0cbc Updated readme 2022-05-24 20:57:56 +02:00
Mark Qvist 3b8490ae9c Added rnx util to documentation 2022-05-24 20:47:45 +02:00
Mark Qvist 417ac9f8da Added rnx remote command utility 2022-05-24 20:14:43 +02:00
Mark Qvist fe5e74bc2b Improved rncp arguments 2022-05-24 20:13:54 +02:00
Mark Qvist 30f71857ae Added docstrings. Added request size to receipts. Fixed link stale time calculation on newly created links with no actual activity. 2022-05-24 20:13:11 +02:00
Mark Qvist c24233845e Implemented bandwidth cap for recursive path requests 2022-05-23 19:49:48 +02:00
Mark Qvist c0fbde5ad1 Added recursive path request loop avoidance 2022-05-23 18:14:45 +02:00
Mark Qvist 5da66402dd Fixed rncp output 2022-05-23 09:23:37 +02:00
Mark Qvist 3bf5694238 Fixed naming conflict in resource advertisements 2022-05-23 08:54:07 +02:00
Mark Qvist 9e6a5d5d91 Fix announce rate targets on I2PInterface peers 2022-05-23 00:28:06 +02:00
Mark Qvist cf3e47f469 Fixed interface mode inheritance 2022-05-23 00:06:26 +02:00
Mark Qvist f8db5a545d Fixed interface mode check 2022-05-23 00:00:14 +02:00
Mark Qvist a79f6e7efa Added rncp utility 2022-05-22 23:44:32 +02:00
Mark Qvist ac4606bcf7 Updated docs 2022-05-22 23:44:12 +02:00
Mark Qvist d1cb07356c Fixed missing recursive progress callback allocation in segmented resource transfer 2022-05-22 21:05:07 +02:00
Mark Qvist e811d54d0f Fixed bug in conditional resource acceptance callback 2022-05-22 19:09:44 +02:00
Mark Qvist 49c8ada478 Added standard identity storage folder 2022-05-22 19:09:16 +02:00
Mark Qvist 6ea7d78b31 Updated API reference 2022-05-22 19:08:32 +02:00
Mark Qvist 0ace84367b Improved link authentication callback 2022-05-22 19:08:03 +02:00
Mark Qvist e63e6821e0 Updated Destination docstrings 2022-05-22 17:11:30 +02:00
Mark Qvist 109132e09d Fixed expired AP and roaming paths not being removed at correct time. 2022-05-22 15:43:46 +02:00
Mark Qvist efd24ec134 Updated documentation 2022-05-22 15:18:09 +02:00
Mark Qvist eefa37f808 Updated documentation 2022-05-22 15:17:54 +02:00
Mark Qvist e4871f7667 Updated documentation 2022-05-22 15:17:25 +02:00
Mark Qvist 44ba5624bc Added gateway mode to rnstatus 2022-05-22 15:16:58 +02:00
Mark Qvist e9c5e3c189 Version bump 2022-05-22 14:29:29 +02:00
Mark Qvist f3ff71d9b8 Implemented unknown path discovery 2022-05-22 14:18:58 +02:00
Mark Qvist 81b92ffdc1 Added gateway interface mode 2022-05-22 11:14:33 +02:00
Mark Qvist 02bb9068cc Updated readme 2022-05-22 11:13:54 +02:00
Mark Qvist ecc9e84bc2 Fixed typo 2022-05-18 00:47:29 +02:00
Mark Qvist 2b43436f56 Updated manual and documentation 2022-05-17 22:12:21 +02:00
Mark Qvist b2d61843d0 Improved log output 2022-05-17 13:25:42 +02:00
Mark Qvist ff74b5a0af Updated documentation 2022-05-14 22:21:59 +02:00
Mark Qvist d66c31b4e9 Added announce rate information to rnpath utility, added exit codes and improved table lookup. 2022-05-14 22:14:38 +02:00
Mark Qvist e825b0b8ff Added Pipe Interface 2022-05-14 20:19:46 +02:00
Mark Qvist b35f86643a Updated documentation 2022-05-14 20:19:15 +02:00
Mark Qvist 3871d8615e Added per-interface announce rate control 2022-05-14 18:09:38 +02:00
Mark Qvist f2c0dac217 Documentation updates 2022-05-14 16:45:16 +02:00
Mark Qvist 8636259886 Added roaming and boundary interface modes 2022-05-13 21:03:51 +02:00
Mark Qvist 4b38a776a3 Added interface modes to documentation 2022-05-13 20:47:26 +02:00
Mark Qvist 7a331a8b60 Added interface modes to documentation 2022-05-13 20:19:54 +02:00
Mark Qvist af1a05ff6a Added announce queue dropping to rnpath utility 2022-05-13 16:18:13 +02:00
Mark Qvist 1b50f5267a Improved announce queue processing 2022-05-13 15:45:09 +02:00
Mark Qvist e95e9e6a89 Updated readme 2022-04-28 15:46:53 +02:00
Mark Qvist e8024e560f Updated manual 2022-04-28 15:31:38 +02:00
Mark Qvist 8cbbcb0fe9 Updated documentation 2022-04-28 15:12:37 +02:00
Mark Qvist 8e4bfbbd94 Updated documentation 2022-04-28 14:56:52 +02:00
Mark Qvist 600bd0e64d Updated documentation 2022-04-28 14:17:12 +02:00
Mark Qvist 123fd1de92 Configure IFAC size in bits 2022-04-28 11:51:04 +02:00
Mark Qvist 29df5950c8 Updated documentation 2022-04-28 11:50:46 +02:00
Mark Qvist b8ca89c2b6 Cleanup 2022-04-28 10:58:26 +02:00
Mark Qvist 79725a1637 Cleanup 2022-04-28 10:56:19 +02:00
Mark Qvist 1a2da0d7c7 Drop IFAC packets on non-IFAC interfaces 2022-04-28 10:38:34 +02:00
Mark Qvist fe065f8bdd Updated documentation 2022-04-28 10:19:43 +02:00
Mark Qvist 5d90ea565a Implemented interface authentication and virtual network segmentation 2022-04-27 19:00:09 +02:00
Mark Qvist b701cdd07f Generalised transport transmit handler 2022-04-27 13:50:56 +02:00
Mark Qvist 8e5b3b4e83 Cleanup 2022-04-27 13:31:44 +02:00
Mark Qvist 24b7cb777f Cleanup 2022-04-27 13:31:07 +02:00
Mark Qvist cf1ca01a3b Configuration support for interface access codes 2022-04-27 13:21:53 +02:00
Mark Qvist 7c70f9d865 Set IFAC parameters on spawned interfaces 2022-04-27 13:20:46 +02:00
Mark Qvist 6cf9288b11 Improved AutoInterface peering timeout 2022-04-27 13:19:48 +02:00
Mark Qvist 00816b55bb Updated docs 2022-04-27 13:19:03 +02:00
Mark Qvist 3856747e31 Updated docs and manual 2022-04-20 21:25:54 +02:00
Mark Qvist 50799bd2a6 Updated docs and manual 2022-04-20 21:24:01 +02:00
Mark Qvist ecffa1a7eb Cleanup 2022-04-20 20:28:14 +02:00
Mark Qvist 9fef53d083 Updated propagation timing 2022-04-20 20:06:44 +02:00
Mark Qvist 0db64610b1 Added reverse path detection 2022-04-20 19:29:25 +02:00
Mark Qvist 4af14a712c Added timeout to rnpath utility 2022-04-20 13:40:07 +02:00
Mark Qvist 402b5fc461 Random hash length from truncated hash length 2022-04-20 13:08:21 +02:00
Mark Qvist 38aeb1ab3b Improved logging 2022-04-20 12:58:00 +02:00
Mark Qvist b0a21b3aa9 Improved logging 2022-04-20 12:56:43 +02:00
Mark Qvist 5e6a1add6b Improved logging and rnpath output 2022-04-20 11:22:52 +02:00
Mark Qvist 104b186047 Added drop path ability to rnpath utility 2022-04-20 11:12:21 +02:00
Mark Qvist 6d23da360d Added path table output to rnpath utility 2022-04-20 10:40:51 +02:00
Mark Qvist 1be00a5c41 Improved announce logging 2022-04-20 10:08:55 +02:00
Mark Qvist 71e5eef8c1 Improved announce logging 2022-04-20 10:08:30 +02:00
Mark Qvist b3a439993d Improved rnstatus output 2022-04-20 09:59:58 +02:00
Mark Qvist 5606b64317 Improved transport logging 2022-04-20 09:24:48 +02:00
Mark Qvist 3d38ef27d4 Improved announce logging 2022-04-20 09:04:12 +02:00
Mark Qvist 93fa8e7240 Updated readme 2022-04-18 19:27:59 +02:00
Mark Qvist d53e8cf037 Bitrate guess on TCP and UDP interfaces 2022-04-18 18:09:31 +02:00
Mark Qvist be820b1965 Updated roadmap 2022-04-18 17:17:43 +02:00
Mark Qvist 425cf66cf7 Updated roadmap 2022-04-18 17:08:06 +02:00
Mark Qvist 8d294df3bb Updated readme formatting 2022-04-18 17:01:41 +02:00
Mark Qvist da297aeb64 Changed log level 2022-04-18 16:54:35 +02:00
Mark Qvist 282239fc57 Added interface announce queue stats to rnstatus 2022-04-18 16:41:38 +02:00
Mark Qvist 222437d851 Version bump 2022-04-18 16:26:08 +02:00
Mark Qvist c9de260e00 Cleanup 2022-04-18 16:25:11 +02:00
Mark Qvist 31104c6e9c Implemented bandwidth-based announce propagation calculation 2022-04-18 16:23:24 +02:00
Mark Qvist 64593e27be Added announce cap setting 2022-04-17 20:14:20 +02:00
Mark Qvist bac33d4e8b Updated documentation 2022-04-17 20:12:23 +02:00
Mark Qvist 124ec006b4 Auto-set sensible interface rate defaults 2022-04-17 19:35:31 +02:00
Mark Qvist dd55899775 Improved I2P interface display in rnstatus 2022-04-17 19:35:05 +02:00
Mark Qvist cc0c01661d Fixed a possible race condition in Transport startup when a local shared instance was restarted and client apps reconnect 2022-04-17 19:34:12 +02:00
Mark Qvist 5f36c8601f Improved rnstatus utility display 2022-04-17 19:08:48 +02:00
Mark Qvist 2f71296816 Basic interface rate estimation 2022-04-17 19:07:32 +02:00
Mark Qvist 7923322d92 Linearized announce propagation delays for supporting up to 128 hops 2022-04-17 18:27:35 +02:00
Mark Qvist fef5ed6bad Cleanup 2022-04-17 17:37:27 +02:00
Mark Qvist 059b0743ef Improved rnstatus util display 2022-04-17 17:30:39 +02:00
Mark Qvist 4d4d39651f Improved rnstatus util display 2022-04-16 23:26:57 +02:00
Mark Qvist 6a1e6417bb Updated logging 2022-04-16 16:44:10 +02:00
Mark Qvist ed20b27e9d Updated interface documentation 2022-04-16 16:43:41 +02:00
Mark Qvist 39f1258d0e Added interface mode config shorthands 2022-04-16 16:42:59 +02:00
Mark Qvist 03d3478b5e Added interface mode shorthand 2022-04-15 22:13:59 +02:00
Mark Qvist b35122a872 Fixed typo 2022-04-15 22:13:41 +02:00
Mark Qvist ae240f4697 Updated manual 2022-04-07 20:22:48 +02:00
Mark Qvist 4e1cdc638f Updated docs 2022-04-07 20:21:15 +02:00
Mark Qvist fc83c5b082 Updated documentation 2022-04-07 20:15:35 +02:00
Mark Qvist ee90605b30 Tweaked decay constant 2022-04-07 19:00:07 +02:00
Mark Qvist 3684fe502f Updated documentation 2022-04-07 18:59:42 +02:00
Mark Qvist d4aeb85191 Merge branch 'master' of github.com:markqvist/Reticulum 2022-04-06 15:51:31 +02:00
Mark Qvist 04540f6e48 Changed config description 2022-04-06 15:51:27 +02:00
markqvist 0db7eb1408 Update README.md 2022-04-06 15:49:39 +02:00
markqvist 5fe55243c6 Update README.md 2022-04-05 11:38:04 +02:00
Mark Qvist b56830b36e Moved docs section contributed by @4c3e 2022-04-02 10:05:46 +02:00
markqvist e3ea61c944 Merge pull request #33 from 4c3e/patch-1
[WIP] First draft of internet bridge section
2022-04-02 09:32:26 +02:00
4c3e 02f9c32da7 First draft of internet bridge section
Open to any and all criticism here, writing documentation isn't my strong suit. But here is a decent start at explaining the differences between the transports. Also I believe there should be a way to "compile" this markdown to html, not exactly sure how to accomplish that, will try to look into this further in the weekend.
2022-04-01 23:15:24 -04:00
Mark Qvist a4a9a1dd53 Updated readme 2022-04-01 18:22:17 +02:00
Mark Qvist d7f9b30638 Updated readme 2022-04-01 18:03:00 +02:00
Mark Qvist 02676d3b25 Updated readme 2022-04-01 18:01:32 +02:00
Mark Qvist 089612bfc1 Updated readme 2022-04-01 17:55:48 +02:00
Mark Qvist ca345b20ff Updated readme 2022-04-01 17:52:56 +02:00
Mark Qvist 3b5973085f Updated readme 2022-04-01 17:20:24 +02:00
Mark Qvist dc6877927e Updated license headers 2022-04-01 17:18:18 +02:00
Mark Qvist f01d838e17 Updated readme 2022-04-01 17:13:47 +02:00
Mark Qvist 9da6d39f64 Updated readme 2022-04-01 17:11:52 +02:00
Mark Qvist d17fbf1f34 Merge branch 'master' of github.com:markqvist/Reticulum 2022-03-28 15:20:35 +02:00
Mark Qvist 7398e312fc Updated version 2022-03-28 15:20:14 +02:00
markqvist 82fc8720ad Update README.md 2022-03-26 22:45:46 +01:00
markqvist 4b9686c31a Update README.md 2022-03-26 22:44:32 +01:00
Mark Qvist 86a5b3302a Updated readme 2022-03-25 20:15:11 +01:00
Mark Qvist c990aae648 Updated license 2022-03-25 20:07:09 +01:00
Mark Qvist 3051b6897d Updated filtering rules. Fixes #18. 2022-03-15 14:55:47 +01:00
Mark Qvist 550dfd44cb Improved cryptography API compatibility 2022-03-08 00:38:51 +01:00
Mark Qvist 95d3346da6 Fixed I2P interface missing attribute 2022-02-26 21:37:50 +01:00
Mark Qvist d4aabc8b89 Added I2P base32 address output to rnstatus utility 2022-02-26 21:04:54 +01:00
Mark Qvist d487609dcf Updated docs and manual 2022-02-26 20:46:14 +01:00
Mark Qvist c96c82f1d1 Updated manual 2022-02-26 19:35:37 +01:00
Mark Qvist cb023cde40 Fixed potential race condition in resource assembly 2022-02-26 18:27:11 +01:00
Mark Qvist 17be289f37 Updated documentation and manual 2022-02-25 22:34:41 +01:00
Mark Qvist b8105e23ff Fixed TCP interface mode reference 2022-02-25 22:10:55 +01:00
Mark Qvist f378d09cbe Updated documentation and manual 2022-02-25 21:50:03 +01:00
Mark Qvist 4dfa62833c Restructured default config 2022-02-25 21:48:25 +01:00
Mark Qvist 2ec6d3ba6c Updated I2P interface documentation 2022-02-25 21:41:43 +01:00
Mark Qvist 15d027e11e Restructured default config and added config example to rnsd 2022-02-25 21:41:24 +01:00
Mark Qvist 87a274d177 Added I2P interface documentation 2022-02-25 21:26:34 +01:00
Mark Qvist f8272793b4 Tuned AutoInterface timeouts 2022-02-25 20:29:47 +01:00
Mark Qvist 3a215be859 Interface mode defaults 2022-02-25 18:56:09 +01:00
Mark Qvist 0e1279d012 Added Access Point interface mode 2022-02-25 18:47:55 +01:00
Mark Qvist 8ec356a28e Interface outbound option enabled by default 2022-02-25 13:31:52 +01:00
Mark Qvist 49d7808835 i2plib license 2022-02-24 01:45:42 +01:00
Mark Qvist 48184134e4 Improved I2P Interface 2022-02-24 01:30:10 +01:00
Mark Qvist 987ff0658b Version bump 2022-02-23 22:53:16 +01:00
Mark Qvist 27dea7c524 Implemented I2PInterface 2022-02-23 22:43:08 +01:00
Mark Qvist 9c6fd132d4 Work on I2P Interface 2022-02-23 22:15:06 +01:00
Mark Qvist 8d58bb62ab Work on I2P Interface 2022-02-23 21:47:30 +01:00
Mark Qvist c357f7a94e Work on I2P Interface 2022-02-23 21:39:29 +01:00
Mark Qvist 4b3ead3db2 Work on I2P Interface 2022-02-23 21:29:18 +01:00
Mark Qvist b62e9af5d4 Work on I2P Interface 2022-02-23 21:19:43 +01:00
Mark Qvist fa82989a2e Preliminary I2P Interface support 2022-02-23 17:40:31 +01:00
Mark Qvist 07a65609b4 Updated documentation 2022-02-22 21:39:16 +01:00
Mark Qvist 257bd95da8 AutoInterface carrier loss detection 2022-02-22 20:16:02 +01:00
Mark Qvist 1ccfa9079c Work on AutoInterface recovery on WiFi carrier loss 2022-02-22 14:49:43 +01:00
Mark Qvist 57226201ff Fixed I2P tunneled config keyword 2022-02-22 14:45:36 +01:00
Mark Qvist d9419cd895 Merge branch 'master' of github.com:markqvist/Reticulum 2022-02-22 14:43:19 +01:00
Mark Qvist aae10ede72 Work on AutoInterface recovery on WiFi carrier loss 2022-02-22 14:43:14 +01:00
Mark Qvist 291b3056cd Updated docs 2022-02-01 23:07:18 +01:00
Mark Qvist 3f53c89d32 Added I2P-tunneled mode to TCP interfaces 2022-01-31 23:31:29 +01:00
Mark Qvist 05288d7c97 Updated documentation for release 2022-01-27 23:49:24 +01:00
Mark Qvist b403441074 Version updated 2022-01-27 23:30:44 +01:00
Mark Qvist d3a23e3b00 RNodeInterface firmware version check 2022-01-22 22:46:47 +01:00
markqvist 329d83587e Merge pull request #10 from 4c3e/4c3e-osx-nameerror-fix
OSX NameError fix
2022-01-22 21:39:01 +01:00
Mark Qvist 0a4dd64434 Improved support for ESP32-based RNodes 2022-01-22 21:36:49 +01:00
4c3e b96cbf1014 OSX NameError fix
Had the following error when trying to run Reticulum on OSX High Sierra:
line 115, in set_timeouts_osx
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
NameError: name 'sock' is not defined
This fix resolved the problem for me.
2022-01-21 01:34:55 +00:00
Mark Qvist 485558cd6b Updated documentation and manual 2022-01-14 22:21:13 +01:00
Mark Qvist 8d93867a22 Updated manual 2022-01-14 22:16:02 +01:00
markqvist 6b20a98adc Update README.md 2022-01-12 12:53:18 +01:00
Mark Qvist f3d04ba90f Improved AutoInterface handling on Android 2022-01-12 12:12:04 +01:00
Mark Qvist 1d2564cedb Interface import on Android 2022-01-12 12:02:00 +01:00
Mark Qvist bec8473695 Better Android detection 2022-01-12 11:50:03 +01:00
Mark Qvist 25620415a0 Updated platform utils 2022-01-12 11:18:24 +01:00
Mark Qvist b6df952995 Platform version check for Windows 2022-01-12 10:16:59 +01:00
Mark Qvist a72aaf12ca Platform version check for Windows 2022-01-12 10:07:44 +01:00
Mark Qvist b978a993b2 Version update 2022-01-11 03:07:34 +01:00
Mark Qvist 5ae00264e8 Preliminaly ESP32 support for RNodeInterface 2022-01-11 03:07:03 +01:00
Mark Qvist 5396b80e80 Updated example 2022-01-11 03:06:35 +01:00
Mark Qvist fdaa58a6fa Improved malformed packet detection 2022-01-11 03:06:16 +01:00
Mark Qvist 4253175627 Cleanup 2021-12-11 20:10:31 +01:00
Mark Qvist 81158c27e4 Cleanup 2021-12-11 18:41:28 +01:00
Mark Qvist eeb424ecee Link request debug 2021-12-11 18:33:09 +01:00
Mark Qvist 0273328b23 Link proof debug 2021-12-11 18:19:51 +01:00
Mark Qvist 20dfbcf0cc Link activation time 2021-12-11 17:26:45 +01:00
Mark Qvist c96e067839 Added proper requester interface detection for path requests for destinations behind local clients. 2021-12-11 16:50:03 +01:00
Mark Qvist 9ff37543f3 Adjusted request timeout 2021-12-11 16:42:57 +01:00
Mark Qvist 974ca48cb4 Adjusted peering timing 2021-12-11 16:42:15 +01:00
Mark Qvist 167d48c8ce Updated peering timeouts 2021-12-11 15:41:34 +01:00
Mark Qvist f253b08774 Updated documentation and manual 2021-12-10 20:10:11 +01:00
Mark Qvist 1c768e9219 Removed log statement 2021-12-10 18:55:17 +01:00
Mark Qvist df39cff520 Added recovery to local shared interfaces if master RNS instance is restarted 2021-12-10 18:32:24 +01:00
Mark Qvist e1e31692d7 UDP socket contructor and doc update 2021-12-10 16:23:35 +01:00
Mark Qvist 293a834c35 Log output and cleanup 2021-12-10 14:48:30 +01:00
Mark Qvist 1bbdd9b3f5 Ignore interfaces on Darwin 2021-12-10 11:10:09 +01:00
Mark Qvist d4b6b6ee59 Ignore interfaces on Darwin 2021-12-10 11:00:48 +01:00
Mark Qvist fca03bbdce Ignore AWDL interfaces on Darwin 2021-12-10 10:58:28 +01:00
Mark Qvist 29aa4f9315 Updated AutoInterface IPv6 bind address 2021-12-10 10:35:25 +01:00
Mark Qvist d5cac30a85 Log cleanup 2021-12-10 09:48:57 +01:00
Mark Qvist 6500bc7390 Updated docs 2021-12-09 18:53:28 +01:00
Mark Qvist 81fed10855 Updated readme 2021-12-09 18:35:38 +01:00
Mark Qvist a39876106b Updated readme 2021-12-09 18:35:01 +01:00
Mark Qvist 90b39774d1 Updated readme 2021-12-09 18:34:07 +01:00
Mark Qvist 006c70cd09 Updated documentation 2021-12-09 18:12:18 +01:00
Mark Qvist 02945f960d Updated timing 2021-12-09 17:44:19 +01:00
Mark Qvist e401ec870d Updated readme 2021-12-09 17:04:20 +01:00
Mark Qvist 90174fcc28 Cleanup 2021-12-09 17:02:13 +01:00
Mark Qvist c18ebed419 Added auto interface 2021-12-09 16:07:36 +01:00
Mark Qvist 1d180a96f6 Updated dependencies 2021-12-08 20:47:14 +01:00
Mark Qvist 4241990690 Implemented AutoInterface outbound traffic and multicast discovery listeners 2021-12-08 20:46:53 +01:00
Mark Qvist 3d49076602 Compatibility with IPv6 based interfaces 2021-12-08 20:45:41 +01:00
Mark Qvist 2e0dd278b6 Updated announce example 2021-12-08 20:45:03 +01:00
Mark Qvist b432a7c7de Updated documentation 2021-12-08 20:42:48 +01:00
Mark Qvist c0383fa2b0 Updated docs 2021-12-06 19:38:03 +01:00
Mark Qvist 98d66e2ba5 Updated documentation 2021-12-06 14:10:22 +01:00
Mark Qvist 2e4fcc659c Added KISS framing option to TCP client interface 2021-12-06 13:07:12 +01:00
Mark Qvist 8fe7c19c59 Updated documentation 2021-12-05 23:31:01 +01:00
Mark Qvist 27b46c9e89 Updated documentation 2021-12-05 23:28:15 +01:00
Mark Qvist 70a3637a98 Updated documentation 2021-12-05 23:26:52 +01:00
Mark Qvist 2e0476e6b9 Updated documentation 2021-12-05 23:24:30 +01:00
Mark Qvist 39911190aa Updated documentation 2021-12-05 16:07:53 +01:00
Mark Qvist 9e9606b8cf Systemd service support and documentation update 2021-12-05 16:05:43 +01:00
Mark Qvist 8be1acee0a Added auto reconnection for disconnected serial-based devices 2021-12-05 14:35:25 +01:00
Mark Qvist ba39a69175 Timeout default structure updated 2021-12-05 11:45:13 +01:00
Mark Qvist a692d29c90 Reconnect on serial port errors for KISS interface 2021-12-05 11:44:30 +01:00
Mark Qvist 7092589388 Updated documentation 2021-12-02 18:33:00 +01:00
Mark Qvist 2d3969aa3d Added makefile 2021-12-01 19:23:19 +01:00
Mark Qvist 1443f4c104 Updated umsgpack to 2.7.1 2021-12-01 19:20:24 +01:00
Mark Qvist d2232f19ba Removed pyserial dependency 2021-12-01 14:05:33 +01:00
Mark Qvist c44c6f9086 Conditional imports for serial-based interfaces 2021-12-01 13:57:40 +01:00
Mark Qvist 259c2aa397 Conditional imports for serial-based interfaces 2021-12-01 13:39:51 +01:00
Mark Qvist 10854bfdbc Added conditional import of netifaces 2021-12-01 11:46:19 +01:00
Mark Qvist f5236878b0 Added Android platform detection 2021-12-01 11:40:44 +01:00
Mark Qvist daf72f4237 Version updated 2021-12-01 11:40:31 +01:00
Mark Qvist 652b884d72 Added conditional import of netifaces 2021-12-01 11:39:40 +01:00
Mark Qvist ea3716f48e Added Android platform detection 2021-12-01 11:39:06 +01:00
Mark Qvist 165e620043 Improved shutdown handling on interrupt. Updated gitignore. 2021-11-04 17:15:58 +01:00
Mark Qvist 58f43b163e Updated docs 2021-10-15 19:26:53 +02:00
Mark Qvist 448ea8ceb5 Added try statements for various callbacks 2021-10-15 14:36:50 +02:00
Mark Qvist f7e8fc4719 Updated docs 2021-10-14 21:06:16 +02:00
Mark Qvist 1d6c877b4c Added RSSI and SNR to echo example 2021-10-12 18:31:46 +02:00
Mark Qvist c3dcd9366d Added RSSI and SNR to echo example 2021-10-12 18:09:02 +02:00
Mark Qvist 8d01586a5a Added RSSI and SNR to echo example 2021-10-12 18:04:55 +02:00
Mark Qvist 3e5f613f66 Fixed typo 2021-10-12 16:36:29 +02:00
Mark Qvist 614a139cd4 Merge branch 'master' of github.com:markqvist/Reticulum 2021-10-12 16:34:25 +02:00
Mark Qvist 1cf6570c2d Added RSSI and SNR reporting to packets on supported interfaces 2021-10-12 16:34:17 +02:00
Mark Qvist d207cbcd9c Update README.md 2021-10-11 15:26:21 +02:00
Mark Qvist 18b20f2d8d Update README.md 2021-10-11 15:26:07 +02:00
Mark Qvist c37533d2c7 Updated docs 2021-10-10 00:27:04 +02:00
Mark Qvist fd13e20165 Updated version 2021-10-09 23:23:44 +02:00
Mark Qvist 66ce58f0f4 Implemented path updating for moving nodes 2021-10-09 22:13:27 +02:00
Mark Qvist e8ee26f78d Emission timestamp in announce. 2021-10-09 21:36:01 +02:00
Mark Qvist c0fb419fe1 Fixed Resource string representation. Added emission timestamp in announce. 2021-10-09 21:30:34 +02:00
Mark Qvist 4ef369cdd8 Added logfile rotation 2021-10-08 19:23:10 +02:00
Mark Qvist a2f18b1daf Updated docs 2021-10-08 18:50:38 +02:00
Mark Qvist 2e411fa1de Updated docs 2021-10-08 18:49:13 +02:00
Mark Qvist 549dc40be6 Updated docs 2021-10-08 18:48:06 +02:00
Mark Qvist 1a99597f4d Updated documentation 2021-10-08 18:31:43 +02:00
Mark Qvist b21e0bee20 Updated documentation 2021-10-08 18:30:17 +02:00
Mark Qvist be8389a906 Updated readme 2021-10-08 17:54:17 +02:00
Mark Qvist 4ca00c6973 Added path expiry check to tunnel restoration 2021-10-08 17:09:31 +02:00
Mark Qvist 95f81cab7f Added path expiry check to tunnel restoration 2021-10-08 17:09:11 +02:00
Mark Qvist 60917f0eea Fixed interface detachment on TCP initiator interfaces 2021-10-08 17:06:00 +02:00
Mark Qvist de800f0ea7 Updated log output 2021-10-08 11:57:23 +02:00
Mark Qvist 5dad76879c Improved known destination saving on shared instances 2021-10-08 08:52:50 +02:00
Mark Qvist 75c3180933 Improved shared instance and local client handling 2021-10-03 15:23:12 +02:00
Mark Qvist 4c6ba97dca Updated readme 2021-09-26 12:34:19 +02:00
Mark Qvist cd6427cc9d Fixed rnstatus output 2021-09-25 23:56:56 +02:00
Mark Qvist 1749393732 Fixed rnstatus output 2021-09-25 23:44:59 +02:00
Mark Qvist dcde5035b9 Updated docs 2021-09-25 23:22:33 +02:00
Mark Qvist c14f6aa14a Updated documentation 2021-09-25 21:39:31 +02:00
Mark Qvist 77fe621cba Updated readme 2021-09-25 17:37:35 +02:00
Mark Qvist 129b1d0713 Updated readme 2021-09-25 17:35:51 +02:00
Mark Qvist 161eeca509 Updated logging 2021-09-25 15:39:42 +02:00
Mark Qvist f25906d44e Improved path utility output 2021-09-25 11:27:43 +02:00
Mark Qvist dd5133751e Updated utilities 2021-09-25 11:03:43 +02:00
Mark Qvist 5f8a55b702 Updated readme 2021-09-24 20:32:48 +02:00
Mark Qvist 7991db5c74 Added rnstatus utility 2021-09-24 20:10:04 +02:00
Mark Qvist f5510f9777 Added verbosity options to rnsd 2021-09-24 20:05:24 +02:00
Mark Qvist 05e0b17fbf Improved rnprobe utility output. 2021-09-24 16:49:07 +02:00
Mark Qvist 7e9d608530 Improved shutdown handling for local shared instances 2021-09-24 16:42:31 +02:00
Mark Qvist 3d4ac0126b Added signal handler and interface detachment oon exit. 2021-09-24 16:09:07 +02:00
Mark Qvist 81cdb0b7e6 Updated version 2021-09-24 15:34:33 +02:00
Mark Qvist c71660a9c3 Added verbosity level to utilities 2021-09-24 15:34:03 +02:00
Mark Qvist 9c1ac46989 Added loglevel override 2021-09-24 15:18:06 +02:00
Mark Qvist c5b792f64a Added rnprobe utility 2021-09-24 14:20:12 +02:00
Mark Qvist 76d75e9a3e Updated rnpath utility 2021-09-24 14:16:25 +02:00
Mark Qvist 9edb641058 Updated utility name 2021-09-24 14:15:15 +02:00
Mark Qvist 1bc2d4015e Fixed bug in reverse table culling 2021-09-24 14:14:34 +02:00
Mark Qvist ab4f3ad8ae Updated logging and default config 2021-09-24 14:13:31 +02:00
Mark Qvist 16dae81844 Fixed regression in TCPInterface client spawner. 2021-09-24 14:11:04 +02:00
Mark Qvist e9e2ffbe0d Improved log output from local interfaces 2021-09-24 14:10:18 +02:00
Mark Qvist dc36644a1e Added rnpath utility 2021-09-24 12:42:24 +02:00
Mark Qvist 8436bc5ba3 Update rnsd utility description 2021-09-24 11:26:29 +02:00
Mark Qvist 858d54f90d Added utility entry points 2021-09-24 11:21:08 +02:00
Mark Qvist 9323fd22ee Improved TCP client interface recovery 2021-09-24 11:20:10 +02:00
Mark Qvist 544e15afdf Added rnsd utility 2021-09-24 11:17:23 +02:00
Mark Qvist acae9e34c2 Improved link status detection and recovery of TCP interfaces over unreliable IP links. 2021-09-23 16:07:57 +02:00
Mark Qvist aaf0ace027 Updated version 2021-09-18 23:32:08 +02:00
Mark Qvist d8b76b4bc5 Improved config parsing 2021-09-18 23:24:12 +02:00
Mark Qvist d29ff38a05 Updated documentation 2021-09-18 23:13:36 +02:00
Mark Qvist 65e8487b39 Added TCP client reconnection on TCP socket drop 2021-09-18 22:49:04 +02:00
Mark Qvist 6362e04567 Cleanup 2021-09-18 21:52:28 +02:00
Mark Qvist 711b754dcc Implemented tunnel saving/restoring. 2021-09-18 21:47:37 +02:00
Mark Qvist 1351316f17 Implemented endpoint tunneling and path restoration on intermittent link layer connections. 2021-09-18 20:38:23 +02:00
Mark Qvist 7af14cec84 Work on tunnels 2021-09-18 20:33:42 +02:00
Mark Qvist 0687ee2231 Work on tunnels 2021-09-18 20:31:43 +02:00
Mark Qvist 872075a31e Work on tunnels 2021-09-18 20:13:51 +02:00
Mark Qvist d8f0380aa9 Work on tunnels 2021-09-18 20:10:39 +02:00
Mark Qvist 569f9bd2b1 Work on tunnels 2021-09-18 19:46:28 +02:00
Mark Qvist 450b88d0f0 Work on tunnels 2021-09-18 19:14:30 +02:00
Mark Qvist 1cb9df109a Work on tunnels 2021-09-18 19:12:09 +02:00
Mark Qvist 80455c9614 Work on tunnels 2021-09-18 19:08:45 +02:00
Mark Qvist c1e280d896 Work on tunnels 2021-09-18 18:54:57 +02:00
Mark Qvist 4a2925cdea Work on tunnels 2021-09-18 18:54:01 +02:00
Mark Qvist 7f38c32e90 Work on tunnels 2021-09-18 18:40:49 +02:00
Mark Qvist 8646be0dcf Work on tunnels 2021-09-18 18:40:27 +02:00
Mark Qvist 6b3cc07740 Work on tunnels 2021-09-18 18:35:10 +02:00
Mark Qvist 3b57b0013b Work on tunnels 2021-09-18 18:34:00 +02:00
Mark Qvist 24d8f39dd1 Work on tunnels 2021-09-18 18:33:28 +02:00
Mark Qvist 58e4bf3c80 Work on tunnels 2021-09-18 18:32:12 +02:00
Mark Qvist 1da8a0c8f1 Work on tunnels 2021-09-18 18:26:50 +02:00
Mark Qvist 8b8d4410ef Work on tunnels 2021-09-18 18:21:32 +02:00
Mark Qvist 7d804daa8f Work on tunnels 2021-09-18 18:19:42 +02:00
Mark Qvist ce00822cb0 Work on tunnels 2021-09-18 18:11:23 +02:00
Mark Qvist 6d6c91edaf Updated docs 2021-09-18 18:10:58 +02:00
Mark Qvist 8432cf40c2 Updated documentation 2021-09-18 16:29:47 +02:00
Mark Qvist 5e21bdd233 Improved link teardown handling. 2021-09-16 20:40:37 +02:00
Mark Qvist c7e5f4612a Updated documentation. 2021-09-11 16:11:44 +02:00
Mark Qvist f80e09cb5c Included six internally. 2021-09-11 16:03:35 +02:00
Mark Qvist 91d94f2f6f Fixed incorrect transfer size indications on single-packet request responses with msgpacked dictionaries as payloads. 2021-09-10 21:35:30 +02:00
Mark Qvist 53ca86ebfc Merge branch 'master' of github.com:markqvist/Reticulum 2021-09-05 20:06:21 +02:00
Mark Qvist 534bb28900 Fixed removal of non-existing receipts. 2021-09-05 20:05:12 +02:00
Mark Qvist 0de5ec73ad Merge pull request #8 from xtoddx/master
Record dependency on six
2021-09-05 15:02:49 +02:00
xtoddx c0f627b50b Record dependency on six 2021-09-04 22:58:42 -04:00
Mark Qvist 5629a062a5 Added resource window timeout recalculations during transfer. 2021-09-03 22:53:25 +02:00
Mark Qvist 83232f0446 Work on resource timing. 2021-09-03 22:20:16 +02:00
Mark Qvist aa794514b3 Work on resource timing. 2021-09-03 22:01:58 +02:00
Mark Qvist 07cf180ea8 Added continous resource timeout adjustment. Fixes missing response timeout check. 2021-09-03 21:08:20 +02:00
Mark Qvist 42a3d23e99 Optimised resource transfer timings. Improved request/response timeout handling. 2021-09-03 18:53:37 +02:00
Mark Qvist d28c888d1c Improved link request/response handling. 2021-09-03 16:24:47 +02:00
Mark Qvist 58d48c18f4 Improved link request/response handling. 2021-09-03 16:23:31 +02:00
Mark Qvist ecf0c55fd6 Improved link establishment. 2021-09-03 16:14:08 +02:00
Mark Qvist 32e4c262ef Improved link timeout handling. 2021-09-03 15:47:42 +02:00
Mark Qvist f87a6a57df Added link error handling. 2021-09-03 15:08:38 +02:00
Mark Qvist 6373f159f8 Added link error handling. 2021-09-03 14:42:59 +02:00
Mark Qvist ad9f548eeb Improved request timeout calculation and handling. 2021-09-03 14:22:53 +02:00
Mark Qvist 425f0153d0 Added flow control timeouts to AX.25 interface and optimised timeouts. 2021-09-03 10:56:49 +02:00
Mark Qvist cd9daaefee Removed option to allow unencrypted links. 2021-09-03 10:13:48 +02:00
Mark Qvist 0fe76d50f6 Improved documentation. 2021-09-02 20:35:42 +02:00
Mark Qvist 9562803bb3 Optimised sent Fernet token data. 2021-09-02 18:42:17 +02:00
Mark Qvist e9c89209c7 Optimised sent Fernet token data. 2021-09-02 18:34:58 +02:00
Mark Qvist cd8de64201 Implemented ability to change MTU. 2021-09-02 18:00:03 +02:00
Mark Qvist 40f7a6d359 Added resource HMU/part request hash filter. Fixes #7. 2021-09-02 14:44:53 +02:00
Mark Qvist 0c96508cca Updated default config. 2021-08-29 13:48:12 +02:00
Mark Qvist 1fd59f1a02 Fixed resource sequencing fail handling. 2021-08-29 13:46:31 +02:00
Mark Qvist 0a0d0af821 Updated docs. 2021-08-29 13:13:51 +02:00
Mark Qvist b694cbdc91 Improved announce handling for local clients. 2021-08-29 12:43:54 +02:00
Mark Qvist 71c3333e10 Improved announce handling for local clients. 2021-08-29 12:28:30 +02:00
Mark Qvist 972fcdee22 Fixed identity saving. 2021-08-29 01:24:21 +02:00
Mark Qvist 17dbfe6401 Updated speed test example. 2021-08-29 00:13:50 +02:00
Mark Qvist 781cb4712d Fixed request packet receipts timing out in spite of delivery. 2021-08-28 23:53:51 +02:00
Mark Qvist cdb08325cc Fixed timeout calculation condition. 2021-08-28 20:34:41 +02:00
Mark Qvist 62d954d7bf Fixed timeout calculation condition. 2021-08-28 20:21:50 +02:00
Mark Qvist 4bbf1ae57d Updated docs 2021-08-28 20:10:00 +02:00
Mark Qvist 2678aeb6a1 Improved timeout calculation and handling. 2021-08-28 20:01:01 +02:00
Mark Qvist 6d441dac02 Better resource advertisement timeout. 2021-08-28 14:41:25 +02:00
Mark Qvist 66b2be87f4 Added speedtest example. 2021-08-27 22:59:34 +02:00
Mark Qvist 2e7126ef39 Cleaned up log statements 2021-08-27 22:51:16 +02:00
Mark Qvist c0f909850b Updated docs. 2021-08-27 19:54:57 +02:00
Mark Qvist a199e4c929 Improved link and resource callbacks and resource handling. 2021-08-27 19:52:48 +02:00
Mark Qvist da13ee9cb9 Updated manual 2021-08-21 21:27:56 +02:00
Mark Qvist f719d44db5 Transport optimisations 2021-08-21 20:23:36 +02:00
Mark Qvist af890d91d2 Fixed race condition in outbound handling packet filter 2021-08-21 19:42:01 +02:00
Mark Qvist 242941fec4 Updated readme 2021-08-21 15:33:14 +02:00
Mark Qvist 0f79197945 Updated docs 2021-08-21 15:19:24 +02:00
Mark Qvist 212518a345 Implemented requests and responses of arbitrary sizes using resources. 2021-08-21 14:52:31 +02:00
Mark Qvist 1dc6655017 Implemented request and response API 2021-08-20 23:29:06 +02:00
Mark Qvist 69930e5652 Updated default config 2021-08-20 11:23:35 +02:00
Mark Qvist 2b8b95da2b Added config options for TCP server interface binding to network interface instead of IP. 2021-08-19 20:13:53 +02:00
Mark Qvist 6382409194 Added config options for UDP interface binding to network interface instead of IP. 2021-08-19 19:56:35 +02:00
Mark Qvist 4fd3d26714 Fixed UDP broadcast echo packets not being filtered. 2021-08-19 17:05:07 +02:00
Mark Qvist 8b6870fad8 Updated docs 2021-08-19 14:11:22 +02:00
Mark Qvist 384a7db974 Implemented link peer identification 2021-08-19 14:10:37 +02:00
Mark Qvist 772ae44ab8 Updated readme and docs 2021-07-25 23:48:18 +02:00
Mark Qvist d326df6c5a Cleanup 2021-05-20 23:31:26 +02:00
Mark Qvist 4269c48293 Updated readme 2021-05-20 23:16:19 +02:00
Mark Qvist 719764fd81 Updated documentation 2021-05-20 22:35:10 +02:00
Mark Qvist 5ccbc825fd Updated examples 2021-05-20 22:31:09 +02:00
Mark Qvist ad67c553d7 Added exception when trying to remember an invalid public key 2021-05-20 22:30:54 +02:00
Mark Qvist d68cfaa8f7 Optimised link establishment 2021-05-20 20:32:08 +02:00
Mark Qvist cf9934810b Updated documentation 2021-05-20 18:37:59 +02:00
Mark Qvist e8ca88377a Updated documentation 2021-05-20 18:37:12 +02:00
Mark Qvist bf410e006f Updated docs 2021-05-20 17:18:38 +02:00
Mark Qvist db527b6759 Optimised announces to 151 bytes 2021-05-20 16:56:08 +02:00
Mark Qvist 9c995b33dd Updated documentation 2021-05-20 16:06:12 +02:00
Mark Qvist f18fb35aba Updated documentation 2021-05-20 15:31:58 +02:00
Mark Qvist ce405b9252 Migrated all asymmetric crypto operations to ECIES on Curve25519. 2021-05-20 15:31:38 +02:00
Mark Qvist 7f5625a526 Cleanup 2021-05-20 13:38:57 +02:00
Mark Qvist e8fb435f00 Updated link example 2021-05-20 13:37:48 +02:00
Mark Qvist f880edbeb8 Store GROUP destination symmetric key as bytes instead of base64 2021-05-20 12:44:12 +02:00
Mark Qvist 2b97c89566 Updated docs 2021-05-20 10:28:58 +02:00
Mark Qvist e746a80dca Fixed beacon data in KISSInterface 2021-05-17 22:01:56 +02:00
Mark Qvist 7a7fd1151f Updated readme 2021-05-17 20:06:58 +02:00
Mark Qvist 57fc2b090b Updated documentation 2021-05-17 20:01:53 +02:00
Mark Qvist 94813d95b1 Separate Ed25519 signature keys on links 2021-05-17 19:11:10 +02:00
Mark Qvist 11fe8b64f8 Updated formatting of examples for better inclusion in documentation. 2021-05-17 19:10:11 +02:00
Mark Qvist eb0c40295f Updated documentation 2021-05-17 17:04:01 +02:00
Mark Qvist 7176fdb71d Updated documentation 2021-05-17 16:10:52 +02:00
Mark Qvist 87a2c6b1fc Updated docs 2021-05-17 16:06:25 +02:00
Mark Qvist aa93e475a4 Updated docs 2021-05-17 15:57:41 +02:00
Mark Qvist 0a0b8c1bf8 Updated theoretical description of link establishment. Finally. 2021-05-17 14:57:31 +02:00
Mark Qvist fc2ec6ad08 Updated docs 2021-05-17 14:10:47 +02:00
Mark Qvist 110e1116e4 Updated docs 2021-05-17 14:09:03 +02:00
Mark Qvist 8478782c18 Moved Reticulum to Curve25519 for ECDH exchanges and signatures 2021-05-17 14:05:13 +02:00
Mark Qvist 4109cbc33d Work on documentation 2021-05-17 11:32:00 +02:00
Mark Qvist 27736ee3f7 Updated readme 2021-05-17 00:03:56 +02:00
Mark Qvist d46a58dbec Updated docs 2021-05-17 00:00:05 +02:00
Mark Qvist c2361bcd34 Updated docs 2021-05-16 23:57:13 +02:00
Mark Qvist 11820b4932 Updated docs 2021-05-16 23:52:13 +02:00
Mark Qvist d736571535 Updated documentation 2021-05-16 23:50:49 +02:00
Mark Qvist 84a55f89b4 Added HTML documentation 2021-05-16 23:40:49 +02:00
Mark Qvist b7e8128e06 Updated docs 2021-05-16 23:39:26 +02:00
Mark Qvist 15db2199a1 Cleanup 2021-05-16 23:30:07 +02:00
Mark Qvist 08dc980282 Added docstrings to Resource 2021-05-16 23:29:25 +02:00
Mark Qvist dfb5af5dd1 Added docstrings, refactored method names. 2021-05-16 23:14:49 +02:00
Mark Qvist 3f1e2bc682 Changed method order 2021-05-16 23:14:19 +02:00
Mark Qvist cd0e177080 Updated docs 2021-05-16 23:13:56 +02:00
Mark Qvist 522204d8a5 Added Identity docstrings. Renamed Identity method. 2021-05-16 21:58:50 +02:00
Mark Qvist 59f83ee1a5 Updated documentation 2021-05-16 21:58:11 +02:00
Mark Qvist e7f7d91276 Updated Destination docstring 2021-05-16 21:57:49 +02:00
Mark Qvist eecfbed3e4 Work on documentation 2021-05-16 19:18:21 +02:00
Mark Qvist 90881e0d47 Moved old documentation 2021-05-16 19:17:57 +02:00
Mark Qvist f698e32ecc Added basic documentation structure 2021-05-16 17:36:50 +02:00
Mark Qvist e8f7e4a5b7 Fixed excessive resource hashmap size 2021-05-16 17:33:55 +02:00
Mark Qvist b0369585e0 Method names updated in examples 2021-05-16 17:33:39 +02:00
Mark Qvist 235b1cea4c Refactored Transport method names 2021-05-16 16:48:54 +02:00
Mark Qvist 8496ee19d9 Refactored Resource method names 2021-05-16 16:43:34 +02:00
Mark Qvist 55c0f44e58 Refactored Packet method names 2021-05-16 16:42:07 +02:00
Mark Qvist cd2f49272d Refactored Link method names 2021-05-16 16:37:12 +02:00
Mark Qvist d03b7d7a52 Refactored Identity method names 2021-05-16 16:15:57 +02:00
Mark Qvist fe773c32e2 Implemented callback as default_app_data. Added docstrings to Destination. 2021-05-16 15:58:06 +02:00
Mark Qvist 27dbde1981 Updated docstrings 2021-05-16 15:52:45 +02:00
Mark Qvist aa02931364 Refactored and documented Reticulum class 2021-05-16 13:02:46 +02:00
Mark Qvist b2eebd90ea Refactored and documented Reticulum class 2021-05-16 12:55:50 +02:00
Mark Qvist 813ddf81d9 Updated readme 2021-05-15 14:40:44 +02:00
Mark Qvist bdf1d289b0 Added default app data to destinations 2021-05-15 13:06:50 +02:00
Mark Qvist 8800a6ab4e Added announce callback handling. Added announce callback example. 2021-05-15 10:58:44 +02:00
Mark Qvist 43de693f01 Fixed typo 2021-05-15 10:57:54 +02:00
Mark Qvist a60e4fc5f1 Renamed has_path method 2021-05-14 21:36:44 +02:00
Mark Qvist 0c76d6a15c Link inbound/outbound inactivity timers 2021-05-14 12:58:02 +02:00
Mark Qvist f242abcf75 Version bump 2021-05-13 16:42:36 +02:00
Mark Qvist 51ab2d3488 Implemented app_data recall from announces, better destination registration handling and link inactivity querying. 2021-05-13 16:41:23 +02:00
Mark Qvist 54206d9101 Added thread locking to log output. Various housekeeping. 2021-05-03 20:24:44 +02:00
Mark Qvist 178c69e361 Updated readme 2020-08-13 15:41:54 +02:00
Mark Qvist f275065b40 Implemented ID beaconing on RNode and KISS interfaces 2020-08-13 15:06:39 +02:00
Mark Qvist 88a956b4f5 Updated readme, version bump 2020-08-13 12:56:39 +02:00
Mark Qvist a43d485630 Renamed UDPInterface 2020-08-13 12:37:54 +02:00
Mark Qvist b9301a2a8a Fixed public exponent 2020-08-13 12:25:56 +02:00
Mark Qvist bd098c338a Indentation and formatting cleanup 2020-08-13 12:15:56 +02:00
Mark Qvist e4dfd052e6 Implemented recursive resource segmentation for large transfers 2020-08-12 21:49:59 +02:00
Mark Qvist 73a3516db8 Indentation rework 2020-08-12 20:59:13 +02:00
Mark Qvist 81804b5d19 Resource work 2020-08-12 20:58:32 +02:00
Mark Qvist bf0e22d461 Indentation fix 2020-08-12 20:51:33 +02:00
Mark Qvist 6b2b66aa25 Moving large transfers to recursive resource segmentation 2020-08-12 20:48:16 +02:00
Mark Qvist 4a3ee622ec Work on bundles 2020-08-12 14:06:29 +02:00
Mark Qvist 90f2a84243 Work on bundles 2020-08-11 20:15:23 +02:00
Mark Qvist 19257b5975 Bundle class setup 2020-06-14 20:18:46 +02:00
Mark Qvist fda6ea741e Updated filetransfer example 2020-06-14 19:06:31 +02:00
Mark Qvist e2122be006 Started bundle class 2020-06-14 18:33:01 +02:00
Mark Qvist 4ffe4482d3 Updated readme and fixed typos 2020-06-14 11:26:11 +02:00
Mark Qvist 843c1a77b7 Updated example description 2020-06-10 11:17:55 +02:00
Mark Qvist 459f6b792f Optimised resource transfers, fixed resource transfer regression, removed txdelay from UDPInterface. 2020-06-10 10:58:13 +02:00
Mark Qvist b61fa6ce8d Dependency version adjustment 2020-06-09 15:01:10 +02:00
Mark Qvist 11c741dacb Version bump to 0.1.4 2020-05-29 15:16:32 +02:00
Mark Qvist 24abb4cfa4 Fixed coding rate reference in RNodeInterface 2020-05-28 22:15:46 +02:00
Mark Qvist 0d069bf1d8 Fixed missing cr statement for RNodeInterface 2020-05-27 16:47:12 +02:00
126 changed files with 46194 additions and 7069 deletions
+2
View File
@@ -3,6 +3,8 @@
testutils
TODO
Examples/RNS
RNS/Utilities/RNS
build
dist
docs/build
rns*.egg-info
-492
View File
@@ -1,492 +0,0 @@
# Reticulum Overview
This paper will briefly describe the overall purpose and operating principles of Reticulum, a
networking stack designed for reliable and secure communication over high-latency, low-bandwidth
links. It should give you an overview of how the stack works, and an understanding of how to
develop networked applications using Reticulum.
This document is not an exhaustive source of information on Reticulum, at least not yet. Currently,
the best place to go for such information is the Python reference implementation of Reticulum. Both
the reference implementation and this document may (and will) change rapidly in the current phase
of development, but historical versions will always be available in the Git repositories.
After reading this document, you should be well-equipped to understand how a Reticulum network
operates, what it can achieve, and how you can use it yourself. If you want to help out with the
development, this is also the place to start, since it will also provide a pretty clear overview of the
sentiments and the philosophy behind Reticulum.
## Motivation
The primary motivation for designing and implementing Reticulum has been the current lack of
reliable, functional and secure minimal-infrastructure modes of digital communication. It is my
belief that it is highly desirable to create a cheap and reliable way to set up a wide-range digital
communication network that can securely allow exchange of information between people and
machines, with no central point of authority, control, censorship or barrier to entry.
Almost all of the various networking stacks in wide use today share a common limitation, namely
that they require large amounts of coordination to work. You cant just plug in a bunch of ethernet
cables to the same switch, or turn on a number of WiFi radios, and expect such a setup to provide a
reliable platform for communication.
The designers of the Internet Protocol had the foresight to create a protocol that powers the modern
Internet, and works brilliantly in world very different from when it was conceived. But networks
using the traditional IP stack needs large amounts of coordination from the people involved, and
without central actors in ultimate control of network segments, it is very easy for a single person to
render the platform unusable for everyone else. These limitations are inherent to the design
principles of IP, and during the design of IP, this was a very reasonable tradeoff indeed.
Reticulum aims to require as little coordination and trust as possible. In fact, the only
“coordination” required is to know how to get connected to a Reticulum network. Since Reticulum
is medium agnostic, this could be whatever is best suited to the situation. In some cases, this might
be 1200 baud packet radio links over VHF frequencies, in other cases it might be a microwave
network using off-the-shelf radios. At the time of release of this document, the recommended setup
is using cheap LoRa radio modules with an open source firmware (see the chapter _Reference System
Setup_ ), connected to a small computer like a Raspberry Pi. As an example, the default reference
setup provides a channel capacity of 5.4 Kbps, and a usable direct node-to-node range of around 15
kilometers (indefinitely extendable by using multiple hops).
## Goals
To be as widely usable and easy to implement as possible, the following goals have been used to
guide the design of Reticulum:
- **Fully useable as open source software stack**
Reticulum must be implemented, and be able to run using only open source software. This is
critical to ensuring availability, security and transparency of the system.
- **Hardware layer agnosticism**
Reticulum shall be fully hardware agnostic, and should be useable over a wide range
physical networking layers, such as data radios, serial lines, modems, handheld transceivers,
wired ethernet, wifi, or anything else that can carry a digital data stream. Hardware made for
dedicated Reticulum use shall be as cheap as possible and use off-the-shelf components, so
it can be easily replicated.
- **Very low bandwidth requirements**
Reticulum should be able to function reliably over links with a data capacity as low as _1,_
_bps_.
- **Encryption by default**
Reticulum must use encryption by default where possible and applicable.
- **Unlicensed use**
Reticulum shall be functional over physical communication mediums that do not require any
form of license to use. Reticulum must be designed in a way, so it is usable over ISM radio
frequency bands, and can provide functional long distance links in such conditions.
- **Supplied software**
Apart from the core networking stack and API, that allows any developer to build
applications with Reticulum, a basic communication suite using Reticulum must be
implemented and released at the same time as Reticulum itself. This shall serve both as a
functional communication suite, and as an example and learning resource to others wishing
to build applications with Reticulum.
- **Ease of use**
The reference implementation of Reticulum is written in Python, to make it very easy to use
and understand. Any programmer with only basic experience should be able to use
Reticulum in their own applications.
- **Low cost**
It shall be as cheap as possible to deploy a communication system based on Reticulum. This
should be achieved by using cheap off-the-shelf hardware that potential users might already
own. The cost of setting up a functioning node should be less than $100 even if all parts
needs to be purchased.
# Introduction & Basic Functionality
Reticulum is a networking stack suited for high-latency, low-bandwidth links. Reticulum is at its
core _message oriented_ , but can provide connection oriented sessions. 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 to be transported over multiple hops to reach the recipient.
Reticulum does away with the idea of addresses and ports known from IP, TCP and UDP. Instead
Reticulum uses the singular concept of _destinations_. Any application using Reticulum as its
networking stack will need to create one or more destinations to receive data, and know the
destinations it needs to send data to.
Reticulum encrypts all data by default using public-key cryptography. Any message sent to a
destination is encrypted with that destinations public key. Reticulum also offers symmetric key
encryption for group-oriented communications, as well as unencrypted packets for broadcast
purposes, or situations where you need the communication to be in plain text. The multi-hop
transport, coordination, verification and reliability layers are fully autonomous and based on public
key cryptography.
Reticulum can connect to a variety of interfaces such as radio modems, data radios and serial ports,
and offers the possibility to easily tunnel Reticulum traffic over IP links such as the Internet or
private IP networks.
## Destinations
To receive and send data with the Reticulum stack, an application needs to create one or more
destinations. Reticulum uses three different basic destination types, and one special:
- **Single**
The _single_ destination type defines a public-key encrypted destination. Any data sent to this
destination will be encrypted with the destinations public key, and will only be readable by
the creator of the destination.
- **Group**
The _group_ destination type defines a symmetrically encrypted destination. Data sent to this
destination will be encrypted with a symmetric key, and will be readable by anyone in
possession of the key. The _group_ destination can be used just as well by only two peers, as it
can by many.
- **Plain**
A _plain_ destination type is unencrypted, and suited for traffic that should be broadcast to a
number of users, or should be readable by anyone.
- **Link**
A _link_ is a special destination type, that serves as an abstract channel between two _single_
destinations, directly connected or over multiple hops. The _link_ also offers reliability and
more efficient encryption, and as such is useful even when nodes are directly connected.
## Destination Naming
Destinations are created and named in an easy to understand dotted notation of _aspects_ , and
represented on the network as a hash of this value. The hash is a SHA-256 truncated to 80 bits. The
top level aspect should always be the a unique identifier for the application using the destination.
The next levels of aspects can be defined in any way by the creator of the application. For example,
a destination for a messaging application could be made up of the application name and a username,
and look like this:
```
name: simplemessenger.someuser hash: 2a7ddfab5213f916dea
```
For the _single_ destination, Reticulum will automatically append the associated public key as a
destination aspect before hashing. This is done to ensure only the correct destination is reached,
since anyone can listen to any destination name. Appending the public key ensures that a given
packet is only directed at the destination that holds the corresponding private key to decrypt the
packet. It is important to understand that anyone can use the destination name
_simplemessenger.myusername_ , but each person that does so will still have a different destination
hash, because their public keys will differ. In actual use of _single_ destination naming, it is advisable
not to use any uniquely identifying features in aspect naming, though. In the simple messenger
example, when using _single_ destinations, we would instead use a destination naming scheme such
as _simplemessenger.user_ where appending the public key expands the destination into a uniquely
identifying one.
To recap, the destination types should be used in the following situations:
- **Single**
When private communication between two endpoints is needed. Supports routing.
- **Group**
When private communication between two or more endpoints is needed. More efficient in
data usage than _single_ destinations. Supports routing indirectly, but must first be established
through a _single_ destination.
- **Plain**
When plain-text communication is desirable, for example when broadcasting information.
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_.
Note that this information could be shared and verified in many other ways, and that it is therefore
not required to use the announce functionality, although it is by far the easiest, and should probably
be used if you are not confident in how to verify public keys and signatures manually.
## Public key announcements
An _announce_ will send a special packet over any configured interfaces, containing all needed
information about the destination hash and public key, and can also contain some additional,
application specific data. The entire packet is signed by the sender to ensure authenticity. It is not
required to use the announce functionality, but in many cases it will be the simplest way to share
public keys on the network. As an example, an announce in a simple messenger application might
contain the following information:
- The announcers destination hash
- The announcers public key
- Application specific data, in this case the users nickname and availability status
- A random blob, making each new announce unique
- A signature of the above information, verifying authenticity
With this information, any Reticulum node that receives it will be able to reconstruct an outgoing
destination to securely communicate with that destination. You might have noticed that there is one
piece of information lacking to reconstruct full knowledge of the announced destination, and that is
the aspect names of the destination. These are intentionally left out to save bandwidth, since they
will be implicit in almost all cases. If a destination name is not entirely implicit, information can be
included in the application specific data part that will allow the receiver to infer the naming.
It is important to note that announcements will be forwarded throughout the network according to a
certain pattern. This will be detailed later. Seeing how _single_ destinations are always tied to a
private/public key pair leads us to the next topic.
## Identities
In Reticulum, an _identity_ does not necessarily represent a personal identity, but is an abstraction that
can represent any kind of _verified entity_. This could very well be a person, but it could also be the
control interface of a machine, a program, robot, computer, sensor or something else entirely. In
general, any kind of agent that can act, or be acted upon, or store or manipulate information, can be
represented as an identity.
As we have seen, a _single_ destination will always have an _identity_ tied to it, but not _plain_ or _group_
destinations. Destinations and identities share a multilateral connection. You can create a
destination, and if it is not connected to an identity upon creation, it will just create a new one to use
automatically. This may be desirable in some situations, but often you will probably want to create
the identity first, and then link it to created destinations.
Building upon the simple messenger example, we could use an identity to represent the user of the
application. Destinations created will then be linked to this identity to allow communication to
reach the user. In such a case it is of great importance to store the users identity securely and
privately.
## Getting Further
The above functions and principles form the core of Reticulum, and would suffice to create
functional networked applications in local clusters, for example over radio links where all interested
nodes can hear each other. But to be truly useful, we need a way to go further. In the next chapter,
two concepts that allow this will be introduced, _paths_ and _resources_.
# Transport
I have purposefully avoided the term routing until now, and will continue to do so, because the
current methods of routing used in IP based networks are fundamentally incompatible for the link
types that Reticulum was designed to handle. These routing methodologies assume trust at the
physical layer. Since Reticulum is designed to run over open radio spectrum, no such trust exists.
Furthermore, existing routing protocols like BGP or OSPF carry too much overhead to be
practically useable over bandwidth-limited, high-latency links.
To overcome such challenges, Reticulums _Transport_ system uses public-key cryptography to
implement the concept of _paths_ that allow discovery of how to get information to a certain
destination, and _resources_ that help alleviate congestion and make reliable communication more
efficient and less bandwidth-hungry.
## Threading a Path
In networks with changing topology and trustless connectivity, nodes need a way to establish
_verified connectivity_ with each other. To do this, the following process is employed:
- First, the node that wishes to establish connectivity will send out a special packet, that
traverses the network and locates the desired destination. Along the way, the nodes that
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
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.
- When the validity of the _link_ has been accepted by forwarding nodes, these nodes will
remember the _link_ , and it can subsequently be used by referring to a hash representing it.
- As a part of the _link request_ , a Diffie-Hellman key exchange takes place, that sets up an
efficient symmetrically encrypted tunnel between the two nodes, using elliptic curve
cryptography. As such, this mode of communication is preferred, even for situations when
nodes can directly communicate, when the amount of data to be exchanged numbers in the
tens of packets.
- When a _link_ has been set up, it automatically provides message receipt functionality, so the
sending node can obtain verified confirmation that the information reached the intended
recipient.
In a moment, we will discuss the specifics of how this methodology is implemented, but lets first
recap what purposes this 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 (keys can also be rotated over an existing path), but the link can also be kept
alive for longer periods of time, if this is more suitable to the application. The amount of bandwidth
used on keeping a link open is practically negligible. 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_.
**Step 1, pathfinding**
The pathfinding method builds on the _announce_ functionality discussed earlier. When an announce
is sent out by a node, it will be forwarded by any node receiving it, but according to some specific
rules:
- If this announce has already been received before, ignore it.
- Record into a table which node the announce was received from, and how many times in
total it has been retransmitted to get here.
- If the announce has been retransmitted _m+1_ times, it will not be forwarded. By default, _m_ is
set to 18.
- The announce will be assigned a delay _d_ = _ch_ seconds, where _c_ is a decay constant, by
default 2, and _h_ is the amount of times this packet has already been forwarded.
- The packet will be given a priority _p = 1/d_.
- If at least _d_ seconds has passed since the announce was received, and no other packets with a
priority higher than _p_ are waiting in the queue (see Packet Prioritisation), and the channel is
not utilized by other traffic, the announce will be forwarded.
- If no other nodes are heard retransmitting the announce with a greater hop count than when
it left this node, transmitting it will be retried _r_ times. By default, _r_ is set to 2. Retries follow
same rules as above, with the exception that it must wait for at least _d = ch+1 + t_ seconds, ie.,
the amount of time it would take the next node to retransmit the packet. By default, _t_ is set to
10.
- If a newer announce from the same destination arrives, while an identical one is already in
the queue, the newest announce is discarded. If the newest announce contains different
application specific data, it will replace the old announce, but will use _d_ and _p_ of the old
announce.
Once an announce has reached a node in the network, any other node in direct contact with that
node will be able to reach the destination the announce originated from, simply by sending a packet
addressed to that destination. Any node with knowledge of the announce will be able to direct the
packet towards the destination by looking up the next node with the shortest amount of hops to the
destination. The specifics of this process is detailed in _Path Calculation_.
According to these rules and default constants, an announce will propagate throughout the network
in a predictable way. In an example network utilising the default constants, and with an average link
distance of _Lavg =_ 15 kilometers, an announce will be able to propagate outwards to a radius of 180
kilometers in 34 minutes, and a _maximum announce radius_ of 270 kilometers in approximately 3
days. Methods for overcoming the distance limitation of _m * Lavg_ will be introduced later in this
chapter.
**Step 2, link establishment**
After seeing how the conditions for finding a path through the network are created, we will now
explore how two nodes can establish reliable communications over multiple hops. The _link_ in
Reticulum terminology should not be viewed as a direct node-to-node link on the physical layer, but
as an abstract channel, that can be open for any amount of time, and can span an arbitrary number
of hops, where information will be exchanged between two nodes.
- When a node in the network wants to establish verified connectivity with another node, it
will create a _link request_ packet, and broadcast it.
- The _link request_ packet contains the destination hash _Hd_ , and an asymmetrically encrypted
part containing the following data: The source hash _Hs_ , a symmetric key _Lk_ , a truncated
hash of a random number _Hr_ , and a signature _S_ of the plaintext values of _Hd_ , _Hs_ , _Lk_ and _Hr_.
- The broadcasted packet will be directed through the network according to the rules laid out
previously.
- Any node that forwards the link request will store a _link id_ in its _link table_ , along with the
amount of hops the packet had taken when received. The link id is a hash of the entire link
request packet. If the path is not _proven_ within some set amount of time, the entry will be
dropped from the table again.
- When the destination receives the link request packet, it will decide whether to accept the
request. If it is accepted, it will create a special packet called a _proof_. A _proof_ is a simple
construct, consisting of a truncated hash of the message that needs to be proven, and a
signature (made by the destinations private key) of this hash. This _proof_ effectively verifies
that the intended recipient got the packet, and also serves to verify the discovered path
through the network. Since the _proof_ hash matches the _path id_ in the intermediary nodes
_path tables_ , the intermediary nodes can forward the proof all the way back to the source.
- When the source receives the _proof_ , it will know unequivocally that a verified path has been
established to the destination, and that information can now be exchanged reliably and
securely.
Its important to note that this methodology ensures that the source of the request does not need to
reveal any identifying information. Only the intended destination will know “who called”, so to
speak. This is a huge improvement to protocols like IP, where by design, you have to reveal your
own address to communicate with anyone, unless you jump through a lot of hoops to hide it.
Reticulum offers initiator anonymity by design.
When using _links_ , Reticulum will automatically verify anything sent over the link, and also
automates retransmissions if parts of a message was lost along the way. Due to the caching features
of Reticulum, such a retransmission does not need to travel the entire length of an established path.
If a packet is lost on the 8th hop of a 12 hop path, it can be fetched from the last hop that received it
reliably.
## Crossing Continents
When a packet needs to travel farther than local network topology knowledge stretches, a system of
geographical or topological hinting is used to direct the packet towards a network segment with
direct knowledge of the intended destination. This functionality is currently left out of the protocol
for simplicity of testing other parts, but will be activated in a future release. For more information
on when, refer to the roadmap on the website.
## Resourceful Memory
In traditional networks, large amounts of data is rapidly exchanged with very low latency. Links of
several thousand kilometers will often only have round-trip latency in the tens of milliseconds, and
as such, traditional protocols are often designed to not store any transmitted data at intermediary
hops. If a transmission error occurs, the sending node will simply notice the lack of a packet
acknowledgement, and retransmit the packet all the way, until it hears back from the receiver that it
got the intended data.
In bandwidth-limited and high-latency conditions, such behaviour quickly causes congestion on the
network, and communications that span many hops become exceedingly expensive in terms of
bandwidth usage, due to the higher risk of some packets failing.
Reticulum alleviates this in part with its _path_ discovery methodology, and in part by implementing
_resource_ caching at all nodes that can support it. Network operation can be made much more
efficient by caching everything for a period of time, and given the availability of cheap memory and
storage, this is a very welcome tradeoff. A gigabyte of memory can store millions of Reticulum
packets, and since everything is encrypted by default, the storing poses very little privacy risk.
In a Reticulum network, any node that is able to do so, should cache as many packets as its
memory will allow for. When a packet is received, a timestamp and a hash of the packet is stored
along with the full packet itself, and it will be kept in storage until the allocated cache storage is
full, whereupon the packet that was last accessed in the cache will be deleted. If a packet is accessed
from the cache, its timestamp will be updated to the current time, to ensure that packets that are
used stay in the cache, and packets that are not used are dropped from memory.
Some packet types are stored in separate caching tables, that allow easier lookup for other nodes.
For example, an announce is stored in a way, that allows other nodes to request the public key for a
certain destination, and as such the network as a whole operates as a distributed key ledger.
For more details on how the caching works and is used, see the reference implementation source
code.
# Reference System Setup
This section will detail the recommended _Reference System Setup_ for Reticulum. It is important to
note that Reticulum is designed to be usable over more or less any medium that allows you to send
and receive data in a digital form, and satisfies some very low minimum requirements. The
communication channel must support at least half-duplex operation, and provide an average
throughput of around 1000 bits per second, and supports a physical layer MTU of 500 bytes. The
Reticulum software should be able to run on more or less any hardware that can provide a Python
runtime environment.
That being said, the reference setup has been outlined to provide a common platform for anyone
who wants to help in the development of Reticulum, and for everyone who wants to know a
recommended setup to get started. A reference system consists of three parts:
- **A channel access device**
Or _CAD_ , in short, provides access to the physical medium whereupon the communication
takes place, for example a radio with an integrated modem. A setup with a separate modem
connected to a radio would also be termed a “channel access device”.
- **A host device**
Some sort of computing device that can run the necessary software, communicates with the
channel access device, and provides user interaction.
- **A software stack**
The software implementing the Reticulum protocol and applications using it.
The reference setup can be considered a relatively stable platform to develop on, and also to start
building networks on. While details of the implementation might change at the current stage of
development, it is the goal to maintain hardware compatibility for as long as entirely possible, and
the current reference setup has been determined to provide a functional platform for many years
into the future. The current Reference System Setup is as follows:
- **Channel Access Device**
A data radio consisting of a LoRa radio module, and a microcontroller with open source
firmware, that can connect to host devices via USB. It operates in either the 430, 868 or 900
MHz frequency bands. More details on the exact parts and how to get/make one can be
found on the website.
- **Host device**
Any computer device running Linux and Python. A Raspberry Pi with Raspbian is
recommended.
- **Software stack**
The current Reference Implementation Release of Reticulum, running on a Debian based
operating system.
It is very important to note, that the reference channel access device **does not** use the LoRaWAN
standard, but uses a custom MAC layer on top of the plain LoRa modulation! As such, you will
need a plain LoRa radio module connected to an MCU with the correct Reticulum firmware. Full
details on how to get or make such a device is available on the website.
With the current reference setup, it should be possible to get on a Reticulum network for around 70$
even if you have none of the hardware already.
# Protocol Specifics
This chapter will detail protocol specific information that is essential to the implementation of
Reticulum, but non critical in understanding how the protocol works on a general level. It should be
treated more as a reference than as essential reading.
## Node Types
Currently Reticulum defines two node types, the _Station_ and the _Peer_. A node is a _station_ if it fixed
in one place, and if it is intended to be kept online at all times. Otherwise the node is a _peer_. This
distinction is made by the user configuring the node, and is used to determine what nodes on the
network will help forward traffic, and what nodes rely on other nodes for connectivity.
## Packet Prioritisation
_The packet prioritisation algorithms are subject to rapid change at the moment, and for now, they
are not documented here. See the reference implementation for more info on how this functionality
works._
## Path Calculation
_The path calculation algorithms are subject to rapid change at the moment, and for now, they are
not documented here. See the reference implementation for more info on how this functionality
works._
## Binary Packet Format
_The binary packet format is subject to rapid change at the moment, and for now, it is not
documented here. See the reference implementation for the specific details on this topic._
Binary file not shown.
+170
View File
@@ -0,0 +1,170 @@
##########################################################
# This RNS example demonstrates setting up announce #
# callbacks, which will let an application receive a #
# notification when an announce relevant for it arrives #
##########################################################
import argparse
import random
import RNS
# Let's define an app name. We'll use this for all
# destinations we create. Since this basic example
# is part of a range of example utilities, we'll put
# them all within the app namespace "example_utilities"
APP_NAME = "example_utilities"
# We initialise two lists of strings to use as app_data
fruits = ["Peach", "Quince", "Date", "Tangerine", "Pomelo", "Carambola", "Grape"]
noble_gases = ["Helium", "Neon", "Argon", "Krypton", "Xenon", "Radon", "Oganesson"]
# This initialisation is executed when the program is started
def program_setup(configpath):
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our example
identity = RNS.Identity()
# Using the identity we just created, we create two destinations
# in the "example_utilities.announcesample" application space.
#
# 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
# in the network.
destination_1 = RNS.Destination(
identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
APP_NAME,
"announcesample",
"fruits"
)
destination_2 = RNS.Destination(
identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
APP_NAME,
"announcesample",
"noble_gases"
)
# We configure the destinations to automatically prove all
# packets adressed 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
# communication was received correctly.
destination_1.set_proof_strategy(RNS.Destination.PROVE_ALL)
destination_2.set_proof_strategy(RNS.Destination.PROVE_ALL)
# We create an announce handler and configure it to only ask for
# announces from "example_utilities.announcesample.fruits".
# Try changing the filter and see what happens.
announce_handler = ExampleAnnounceHandler(
aspect_filter="example_utilities.announcesample.fruits"
)
# We register the announce handler with Reticulum
RNS.Transport.register_announce_handler(announce_handler)
# Everything's ready!
# Let's hand over control to the announce loop
announceLoop(destination_1, destination_2)
def announceLoop(destination_1, destination_2):
# Let the user know that everything is ready
RNS.log("Announce example running, hit enter to manually send an announce (Ctrl-C to quit)")
# We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server
# destination on the network, which will let clients
# know how to create messages directed towards it.
while True:
entered = input()
# Randomly select a fruit
fruit = fruits[random.randint(0,len(fruits)-1)]
# Send the announce including the app data
destination_1.announce(app_data=fruit.encode("utf-8"))
RNS.log(
"Sent announce from "+
RNS.prettyhexrep(destination_1.hash)+
" ("+destination_1.name+")"
)
# Randomly select a noble gas
noble_gas = noble_gases[random.randint(0,len(noble_gases)-1)]
# Send the announce including the app data
destination_2.announce(app_data=noble_gas.encode("utf-8"))
RNS.log(
"Sent announce from "+
RNS.prettyhexrep(destination_2.hash)+
" ("+destination_2.name+")"
)
# We will need to define an announce handler class that
# Reticulum can message when an announce arrives.
class ExampleAnnounceHandler:
# The initialisation method takes the optional
# aspect_filter argument. If aspect_filter is set to
# None, all announces will be passed to the instance.
# If only some announces are wanted, it can be set to
# an aspect string.
def __init__(self, aspect_filter=None):
self.aspect_filter = aspect_filter
# This method will be called by Reticulums Transport
# system when an announce arrives that matches the
# configured aspect filter. Filters must be specific,
# and cannot use wildcards.
def received_announce(self, destination_hash, announced_identity, app_data):
RNS.log(
"Received an announce from "+
RNS.prettyhexrep(destination_hash)
)
RNS.log(
"The announce contained the following app data: "+
app_data.decode("utf-8")
)
##########################################################
#### Program Startup #####################################
##########################################################
# This part of the program gets run at startup,
# and parses input from the user, and then starts
# the desired program mode.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(
description="Reticulum example that demonstrates announces and announce handlers"
)
parser.add_argument(
"--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
args = parser.parse_args()
if args.config:
configarg = args.config
else:
configarg = None
program_setup(configarg)
except KeyboardInterrupt:
print("")
exit()
+83 -55
View File
@@ -11,56 +11,67 @@ import RNS
# destinations we create. Since this basic example
# is part of a range of example utilities, we'll put
# them all within the app namespace "example_utilities"
APP_NAME = "example_utilitites"
APP_NAME = "example_utilities"
# This initialisation is executed when the program is started
def program_setup(configpath, channel=None):
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# If the user did not select a "channel" we use
# a default one called "public_information".
# This "channel" is added to the destination name-
# space, so the user can select different broadcast
# channels.
if channel == None:
channel = "public_information"
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# If the user did not select a "channel" we use
# a default one called "public_information".
# This "channel" is added to the destination name-
# space, so the user can select different broadcast
# channels.
if channel == None:
channel = "public_information"
# We create a PLAIN destination. This is an uncencrypted endpoint
# that anyone can listen to and send information to.
broadcast_destination = RNS.Destination(None, RNS.Destination.IN, RNS.Destination.PLAIN, APP_NAME, "broadcast", channel)
# We create a PLAIN destination. This is an uncencrypted endpoint
# that anyone can listen to and send information to.
broadcast_destination = RNS.Destination(
None,
RNS.Destination.IN,
RNS.Destination.PLAIN,
APP_NAME,
"broadcast",
channel
)
# We specify a callback that will get called every time
# the destination receives data.
broadcast_destination.packet_callback(packet_callback)
# Everything's ready!
# Let's hand over control to the main loop
broadcastLoop(broadcast_destination)
# We specify a callback that will get called every time
# the destination receives data.
broadcast_destination.set_packet_callback(packet_callback)
# Everything's ready!
# Let's hand over control to the main loop
broadcastLoop(broadcast_destination)
def packet_callback(data, packet):
# Simply print out the received data
print("")
print("Received data: "+data.decode("utf-8")+"\r\n> ", end="")
sys.stdout.flush()
# Simply print out the received data
print("")
print("Received data: "+data.decode("utf-8")+"\r\n> ", end="")
sys.stdout.flush()
def broadcastLoop(destination):
# Let the user know that everything is ready
RNS.log("Broadcast example "+RNS.prettyhexrep(destination.hash)+" running, enter text and hit enter to broadcast (Ctrl-C to quit)")
# Let the user know that everything is ready
RNS.log(
"Broadcast example "+
RNS.prettyhexrep(destination.hash)+
" running, enter text and hit enter to broadcast (Ctrl-C to quit)"
)
# We enter a loop that runs until the users exits.
# If the user hits enter, we will send the information
# that the user entered into the prompt.
while True:
print("> ", end="")
entered = input()
# We enter a loop that runs until the users exits.
# If the user hits enter, we will send the information
# that the user entered into the prompt.
while True:
print("> ", end="")
entered = input()
if entered != "":
data = entered.encode("utf-8")
packet = RNS.Packet(destination, data)
packet.send()
if entered != "":
data = entered.encode("utf-8")
packet = RNS.Packet(destination, data)
packet.send()
##########################################################
#### Program Startup #####################################
@@ -70,24 +81,41 @@ def broadcastLoop(destination):
# and parses input from the user, and then starts
# the program.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="Reticulum example that demonstrates sending and receiving unencrypted broadcasts")
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("--channel", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
args = parser.parse_args()
try:
parser = argparse.ArgumentParser(
description="Reticulum example demonstrating sending and receiving broadcasts"
)
if args.config:
configarg = args.config
else:
configarg = None
parser.add_argument(
"--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
if args.channel:
channelarg = args.channel
else:
channelarg = None
parser.add_argument(
"--channel",
action="store",
default=None,
help="broadcast channel name",
type=str
)
program_setup(configarg, channelarg)
args = parser.parse_args()
except KeyboardInterrupt:
print("")
exit()
if args.config:
configarg = args.config
else:
configarg = None
if args.channel:
channelarg = args.channel
else:
channelarg = None
program_setup(configarg, channelarg)
except KeyboardInterrupt:
print("")
exit()
+254 -149
View File
@@ -12,7 +12,7 @@ import RNS
# 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_utilitites"
APP_NAME = "example_utilities"
##########################################################
@@ -22,56 +22,90 @@ APP_NAME = "example_utilitites"
# 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 echo server
server_identity = RNS.Identity()
global reticulum
# We create a destination that clients can query. We want
# to be able to verify echo replies to our clients, so we
# create a "single" destination that can receive encrypted
# messages. This way the client can send a request and be
# certain that no-one else than this destination was able
# to read it.
echo_destination = RNS.Destination(server_identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "echo", "request")
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our echo server
server_identity = RNS.Identity()
# We configure the destination to automatically prove all
# packets adressed to it. By doing this, RNS will automatically
# generate a proof for each incoming packet and transmit it
# back to the sender of that packet.
echo_destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
# Tell the destination which function in our program to
# run when a packet is received. We do this so we can
# print a log message when the server receives a request
echo_destination.packet_callback(server_callback)
# We create a destination that clients can query. We want
# to be able to verify echo replies to our clients, so we
# create a "single" destination that can receive encrypted
# messages. This way the client can send a request and be
# certain that no-one else than this destination was able
# to read it.
echo_destination = RNS.Destination(
server_identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
APP_NAME,
"echo",
"request"
)
# Everything's ready!
# Let's Wait for client requests or user input
announceLoop(echo_destination)
# We configure the destination to automatically prove all
# packets adressed to it. By doing this, RNS will automatically
# generate a proof for each incoming packet and transmit it
# back to the sender of that packet.
echo_destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
# Tell the destination which function in our program to
# run when a packet is received. We do this so we can
# print a log message when the server receives a request
echo_destination.set_packet_callback(server_callback)
# Everything's ready!
# Let's Wait for client requests or user input
announceLoop(echo_destination)
def announceLoop(destination):
# Let the user know that everything is ready
RNS.log("Echo server "+RNS.prettyhexrep(destination.hash)+" running, hit enter to manually send an announce (Ctrl-C to quit)")
# Let the user know that everything is ready
RNS.log(
"Echo server "+
RNS.prettyhexrep(destination.hash)+
" running, hit enter to manually send an announce (Ctrl-C to quit)"
)
# We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server
# destination on the network, which will let clients
# know how to create messages directed towards it.
while True:
entered = input()
destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
# We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server
# destination on the network, which will let clients
# know how to create messages directed towards it.
while True:
entered = input()
destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
def server_callback(message, packet):
# Tell the user that we received an echo request, and
# that we are going to send a reply to the requester.
# Sending the proof is handled automatically, since we
# set up the destination to prove all incoming packets.
RNS.log("Received packet from echo client, proof sent")
global reticulum
# Tell the user that we received an echo request, and
# that we are going to send a reply to the requester.
# Sending the proof is handled automatically, since we
# set up the destination to prove all incoming packets.
reception_stats = ""
if reticulum.is_connected_to_shared_instance:
reception_rssi = reticulum.get_packet_rssi(packet.packet_hash)
reception_snr = reticulum.get_packet_snr(packet.packet_hash)
if reception_rssi != None:
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
if reception_snr != None:
reception_stats += " [SNR "+str(reception_snr)+" dBm]"
else:
if packet.rssi != None:
reception_stats += " [RSSI "+str(packet.rssi)+" dBm]"
if packet.snr != None:
reception_stats += " [SNR "+str(packet.snr)+" dB]"
RNS.log("Received packet from echo client, proof sent"+reception_stats)
##########################################################
@@ -81,103 +115,145 @@ def server_callback(message, packet):
# This initialisation is executed when the users chooses
# to run as a client
def client(destination_hexhash, configpath, timeout=None):
# We need a binary representation of the destination
# hash that was entered on the command line
try:
if len(destination_hexhash) != 20:
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
exit()
global reticulum
# We need a binary representation of the destination
# hash that was entered on the command line
try:
if len(destination_hexhash) != 20:
raise ValueError(
"Destination length is invalid, must be 20 hexadecimal characters (10 bytes)"
)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
exit()
# We override the loglevel to provide feedback when
# an announce is received
if RNS.loglevel < RNS.LOG_INFO:
RNS.loglevel = RNS.LOG_INFO
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Tell the user that the client is ready!
RNS.log("Echo client ready, hit enter to send echo request to "+destination_hexhash+" (Ctrl-C to quit)")
# We override the loglevel to provide feedback when
# an announce is received
if RNS.loglevel < RNS.LOG_INFO:
RNS.loglevel = RNS.LOG_INFO
# We enter a loop that runs until the user exits.
# If the user hits enter, we will try to send an
# echo request to the destination specified on the
# command line.
while True:
input()
# Let's first check if RNS knows a path to the destination.
# If it does, we'll load the server identity and create a packet
if RNS.Transport.hasPath(destination_hash):
# Tell the user that the client is ready!
RNS.log(
"Echo client ready, hit enter to send echo request to "+
destination_hexhash+
" (Ctrl-C to quit)"
)
# To address the server, we need to know it's public
# key, so we check if Reticulum knows this destination.
# This is done by calling the "recall" method of the
# Identity module. If the destination is known, it will
# return an Identity instance that can be used in
# outgoing destinations.
server_identity = RNS.Identity.recall(destination_hash)
# We enter a loop that runs until the user exits.
# If the user hits enter, we will try to send an
# echo request to the destination specified on the
# command line.
while True:
input()
# Let's first check if RNS knows a path to the destination.
# If it does, we'll load the server identity and create a packet
if RNS.Transport.has_path(destination_hash):
# We got the correct identity instance from the
# recall method, so let's create an outgoing
# destination. We use the naming convention:
# example_utilities.echo.request
# This matches the naming we specified in the
# server part of the code.
request_destination = RNS.Destination(server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "echo", "request")
# To address the server, we need to know it's public
# key, so we check if Reticulum knows this destination.
# This is done by calling the "recall" method of the
# Identity module. If the destination is known, it will
# return an Identity instance that can be used in
# outgoing destinations.
server_identity = RNS.Identity.recall(destination_hash)
# The destination is ready, so let's create a packet.
# We set the destination to the request_destination
# that was just created, and the only data we add
# is a random hash.
echo_request = RNS.Packet(request_destination, RNS.Identity.getRandomHash())
# We got the correct identity instance from the
# recall method, so let's create an outgoing
# destination. We use the naming convention:
# example_utilities.echo.request
# This matches the naming we specified in the
# server part of the code.
request_destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
APP_NAME,
"echo",
"request"
)
# Send the packet! If the packet is successfully
# sent, it will return a PacketReceipt instance.
packet_receipt = echo_request.send()
# The destination is ready, so let's create a packet.
# We set the destination to the request_destination
# that was just created, and the only data we add
# is a random hash.
echo_request = RNS.Packet(request_destination, RNS.Identity.get_random_hash())
# If the user specified a timeout, we set this
# timeout on the packet receipt, and configure
# a callback function, that will get called if
# the packet times out.
if timeout != None:
packet_receipt.set_timeout(timeout)
packet_receipt.timeout_callback(packet_timed_out)
# Send the packet! If the packet is successfully
# sent, it will return a PacketReceipt instance.
packet_receipt = echo_request.send()
# We can then set a delivery callback on the receipt.
# This will get automatically called when a proof for
# this specific packet is received from the destination.
packet_receipt.delivery_callback(packet_delivered)
# If the user specified a timeout, we set this
# timeout on the packet receipt, and configure
# a callback function, that will get called if
# the packet times out.
if timeout != None:
packet_receipt.set_timeout(timeout)
packet_receipt.set_timeout_callback(packet_timed_out)
# Tell the user that the echo request was sent
RNS.log("Sent echo request to "+RNS.prettyhexrep(request_destination.hash))
else:
# If we do not know this destination, tell the
# user to wait for an announce to arrive.
RNS.log("Destination is not yet known. Requesting path...")
RNS.Transport.requestPath(destination_hash)
# We can then set a delivery callback on the receipt.
# This will get automatically called when a proof for
# this specific packet is received from the destination.
packet_receipt.set_delivery_callback(packet_delivered)
# Tell the user that the echo request was sent
RNS.log("Sent echo request to "+RNS.prettyhexrep(request_destination.hash))
else:
# If we do not know this destination, tell the
# user to wait for an announce to arrive.
RNS.log("Destination is not yet known. Requesting path...")
RNS.Transport.request_path(destination_hash)
# This function is called when our reply destination
# receives a proof packet.
def packet_delivered(receipt):
if receipt.status == RNS.PacketReceipt.DELIVERED:
rtt = receipt.rtt()
if (rtt >= 1):
rtt = round(rtt, 3)
rttstring = str(rtt)+" seconds"
else:
rtt = round(rtt*1000, 3)
rttstring = str(rtt)+" milliseconds"
global reticulum
RNS.log("Valid reply received from "+RNS.prettyhexrep(receipt.destination.hash)+", round-trip time is "+rttstring)
if receipt.status == RNS.PacketReceipt.DELIVERED:
rtt = receipt.get_rtt()
if (rtt >= 1):
rtt = round(rtt, 3)
rttstring = str(rtt)+" seconds"
else:
rtt = round(rtt*1000, 3)
rttstring = str(rtt)+" milliseconds"
reception_stats = ""
if reticulum.is_connected_to_shared_instance:
reception_rssi = reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
reception_snr = reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
if reception_rssi != None:
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
if reception_snr != None:
reception_stats += " [SNR "+str(reception_snr)+" dB]"
else:
if receipt.proof_packet != None:
if receipt.proof_packet.rssi != None:
reception_stats += " [RSSI "+str(receipt.proof_packet.rssi)+" dBm]"
if receipt.proof_packet.snr != None:
reception_stats += " [SNR "+str(receipt.proof_packet.snr)+" dB]"
RNS.log(
"Valid reply received from "+
RNS.prettyhexrep(receipt.destination.hash)+
", round-trip time is "+rttstring+
reception_stats
)
# This function is called if a packet times out.
def packet_timed_out(receipt):
if receipt.status == RNS.PacketReceipt.FAILED:
RNS.log("Packet "+RNS.prettyhexrep(receipt.hash)+" timed out")
if receipt.status == RNS.PacketReceipt.FAILED:
RNS.log("Packet "+RNS.prettyhexrep(receipt.hash)+" timed out")
##########################################################
@@ -188,36 +264,65 @@ def packet_timed_out(receipt):
# and parses input from the user, and then starts
# the desired program mode.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="Simple echo server and client utility")
parser.add_argument("-s", "--server", action="store_true", help="wait for incoming packets from clients")
parser.add_argument("-t", "--timeout", action="store", metavar="s", default=None, help="set a reply timeout in seconds", type=float)
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the server destination", type=str)
args = parser.parse_args()
try:
parser = argparse.ArgumentParser(description="Simple echo server and client utility")
if args.server:
configarg=None
if args.config:
configarg = args.config
server(configarg)
else:
if args.config:
configarg = args.config
else:
configarg = None
parser.add_argument(
"-s",
"--server",
action="store_true",
help="wait for incoming packets from clients"
)
if args.timeout:
timeoutarg = float(args.timeout)
else:
timeoutarg = None
parser.add_argument(
"-t",
"--timeout",
action="store",
metavar="s",
default=None,
help="set a reply timeout in seconds",
type=float
)
if (args.destination == None):
print("")
parser.print_help()
print("")
else:
client(args.destination, configarg, timeout=timeoutarg)
except KeyboardInterrupt:
print("")
exit()
parser.add_argument("--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
parser.add_argument(
"destination",
nargs="?",
default=None,
help="hexadecimal hash of the server destination",
type=str
)
args = parser.parse_args()
if args.server:
configarg=None
if args.config:
configarg = args.config
server(configarg)
else:
if args.config:
configarg = args.config
else:
configarg = None
if args.timeout:
timeoutarg = float(args.timeout)
else:
timeoutarg = None
if (args.destination == None):
print("")
parser.print_help()
print("")
else:
client(args.destination, configarg, timeout=timeoutarg)
except KeyboardInterrupt:
print("")
exit()
+429 -321
View File
@@ -3,6 +3,17 @@
# server and client program. The server will serve a #
# directory of files, and the clients can list and #
# download files from the server. #
# #
# Please note that using RNS Resources for large file #
# transfers is not recommended, since compression, #
# encryption and hashmap sequencing can take a long time #
# on systems with slow CPUs, which will probably result #
# in the client timing out before the resource sender #
# can complete preparing the resource. #
# #
# If you need to transfer large files, use the Bundle #
# class instead, which will automatically slice the data #
# into chunks suitable for packing as a Resource. #
##########################################################
import os
@@ -17,10 +28,10 @@ import RNS.vendor.umsgpack as umsgpack
# 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_utilitites"
APP_NAME = "example_utilities"
# We'll also define a default timeout, in seconds
APP_TIMEOUT = 15.0
APP_TIMEOUT = 45.0
##########################################################
#### Server Part #########################################
@@ -31,260 +42,293 @@ serve_path = None
# This initialisation is executed when the users chooses
# to run as a server
def server(configpath, path):
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our file server
server_identity = RNS.Identity()
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our file server
server_identity = RNS.Identity()
global serve_path
serve_path = path
global serve_path
serve_path = path
# 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, "filetransfer", "server")
# 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,
"filetransfer",
"server"
)
# We configure a function that will get called every time
# a new client creates a link to this destination.
server_destination.link_established_callback(client_connected)
# 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
announceLoop(server_destination)
# Everything's ready!
# Let's Wait for client requests or user input
announceLoop(server_destination)
def announceLoop(destination):
# Let the user know that everything is ready
RNS.log("File server "+RNS.prettyhexrep(destination.hash)+" running")
RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
# Let the user know that everything is ready
RNS.log("File server "+RNS.prettyhexrep(destination.hash)+" running")
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))
# 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))
# Here's a convenience function for listing all files
# in our served directory
def list_files():
# We add all entries from the directory that are
# actual files, and does not start with "."
global serve_path
return [file for file in os.listdir(serve_path) if os.path.isfile(os.path.join(serve_path, file)) and file[:1] != "."]
# We add all entries from the directory that are
# actual files, and does not start with "."
global serve_path
return [file for file in os.listdir(serve_path) if os.path.isfile(os.path.join(serve_path, file)) and file[:1] != "."]
# When a client establishes a link to our server
# destination, this function will be called with
# a reference to the link. We then send the client
# a list of files hosted on the server.
def client_connected(link):
# Check if the served directory still exists
if os.path.isdir(serve_path):
RNS.log("Client connected, sending file list...")
# Check if the served directory still exists
if os.path.isdir(serve_path):
RNS.log("Client connected, sending file list...")
link.link_closed_callback(client_disconnected)
link.set_link_closed_callback(client_disconnected)
# We pack a list of files for sending in a packet
data = umsgpack.packb(list_files())
# We pack a list of files for sending in a packet
data = umsgpack.packb(list_files())
# Check the size of the packed data
if len(data) <= RNS.Link.MDU:
# If it fits in one packet, we will just
# send it as a single packet over the link.
list_packet = RNS.Packet(link, data)
list_receipt = list_packet.send()
list_receipt.set_timeout(APP_TIMEOUT)
list_receipt.delivery_callback(list_delivered)
list_receipt.timeout_callback(list_timeout)
else:
RNS.log("Too many files in served directory!", RNS.LOG_ERROR)
RNS.log("You should implement a function to split the filelist over multiple packets.", RNS.LOG_ERROR)
RNS.log("Hint: The client already supports it :)", RNS.LOG_ERROR)
# After this, we're just going to keep the link
# open until the client requests a file. We'll
# configure a function that get's called when
# the client sends a packet with a file request.
link.packet_callback(client_request)
else:
RNS.log("Client connected, but served path no longer exists!", RNS.LOG_ERROR)
link.teardown()
# Check the size of the packed data
if len(data) <= RNS.Link.MDU:
# If it fits in one packet, we will just
# send it as a single packet over the link.
list_packet = RNS.Packet(link, data)
list_receipt = list_packet.send()
list_receipt.set_timeout(APP_TIMEOUT)
list_receipt.set_delivery_callback(list_delivered)
list_receipt.set_timeout_callback(list_timeout)
else:
RNS.log("Too many files in served directory!", RNS.LOG_ERROR)
RNS.log("You should implement a function to split the filelist over multiple packets.", RNS.LOG_ERROR)
RNS.log("Hint: The client already supports it :)", RNS.LOG_ERROR)
# After this, we're just going to keep the link
# open until the client requests a file. We'll
# configure a function that get's called when
# the client sends a packet with a file request.
link.set_packet_callback(client_request)
else:
RNS.log("Client connected, but served path no longer exists!", RNS.LOG_ERROR)
link.teardown()
def client_disconnected(link):
RNS.log("Client disconnected")
RNS.log("Client disconnected")
def client_request(message, packet):
global serve_path
filename = message.decode("utf-8")
if filename in list_files():
try:
# If we have the requested file, we'll
# read it and pack it as a resource
RNS.log("Client requested \""+filename+"\"")
file = open(os.path.join(serve_path, filename), "rb")
file_data = file.read()
file.close()
global serve_path
file_resource = RNS.Resource(file_data, packet.link, callback=resource_sending_concluded)
file_resource.filename = filename
except:
# If somethign went wrong, we close
# the link
RNS.log("Error while reading file \""+filename+"\"", RNS.LOG_ERROR)
packet.link.teardown()
else:
# If we don't have it, we close the link
RNS.log("Client requested an unknown file")
packet.link.teardown()
try:
filename = message.decode("utf-8")
except Exception as e:
filename = None
if filename in list_files():
try:
# If we have the requested file, we'll
# read it and pack it as a resource
RNS.log("Client requested \""+filename+"\"")
file = open(os.path.join(serve_path, filename), "rb")
file_resource = RNS.Resource(
file,
packet.link,
callback=resource_sending_concluded
)
file_resource.filename = filename
except Exception as e:
# If somethign went wrong, we close
# the link
RNS.log("Error while reading file \""+filename+"\"", RNS.LOG_ERROR)
packet.link.teardown()
raise e
else:
# If we don't have it, we close the link
RNS.log("Client requested an unknown file")
packet.link.teardown()
# This function is called on the server when a
# resource transfer concludes.
def resource_sending_concluded(resource):
if hasattr(resource, "filename"):
name = resource.filename
else:
name = "resource"
if hasattr(resource, "filename"):
name = resource.filename
else:
name = "resource"
if resource.status == RNS.Resource.COMPLETE:
RNS.log("Done sending \""+name+"\" to client")
elif resource.status == RNS.Resource.FAILED:
RNS.log("Sending \""+name+"\" to client failed")
if resource.status == RNS.Resource.COMPLETE:
RNS.log("Done sending \""+name+"\" to client")
elif resource.status == RNS.Resource.FAILED:
RNS.log("Sending \""+name+"\" to client failed")
def list_delivered(receipt):
RNS.log("The file list was received by the client")
RNS.log("The file list was received by the client")
def list_timeout(receipt):
RNS.log("Sending list to client timed out, closing this link")
link = receipt.destination
link.teardown()
RNS.log("Sending list to client timed out, closing this link")
link = receipt.destination
link.teardown()
##########################################################
#### Client Part #########################################
##########################################################
# We store a global list of files available on the server
server_files = []
server_files = []
# A reference to the server link
server_link = None
server_link = None
# And a reference to the current download
current_download = None
current_filename = None
current_download = None
current_filename = None
# Variables to store download statistics
download_started = 0
download_finished = 0
download_time = 0
transfer_size = 0
file_size = 0
# 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:
if len(destination_hexhash) != 20:
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
exit()
# We need a binary representation of the destination
# hash that was entered on the command line
try:
if len(destination_hexhash) != 20:
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
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)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Check if we know a path to the destination
if not RNS.Transport.hasPath(destination_hash):
RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
RNS.Transport.requestPath(destination_hash)
while not RNS.Transport.hasPath(destination_hash):
time.sleep(0.1)
# 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)
# 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...")
# 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, "filetransfer", "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,
"filetransfer",
"server"
)
# We also want to automatically prove incoming packets
server_destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
# We also want to automatically prove incoming packets
server_destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
# And create a link
link = RNS.Link(server_destination)
# And create a link
link = RNS.Link(server_destination)
# We expect any normal data packets on the link
# to contain a list of served files, so we set
# a callback accordingly
link.packet_callback(filelist_received)
# We expect any normal data packets on the link
# to contain a list of served files, so we set
# a callback accordingly
link.set_packet_callback(filelist_received)
# We'll also set up functions to inform the
# user when the link is established or closed
link.link_established_callback(link_established)
link.link_closed_callback(link_closed)
# 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)
# And set the link to automatically begin
# downloading advertised resources
link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
link.resource_started_callback(download_began)
link.resource_concluded_callback(download_concluded)
# And set the link to automatically begin
# downloading advertised resources
link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
link.set_resource_started_callback(download_began)
link.set_resource_concluded_callback(download_concluded)
menu()
menu()
# Requests the specified file from the server
def download(filename):
global server_link, menu_mode, current_filename
current_filename = filename
global server_link, menu_mode, current_filename, transfer_size, download_started
current_filename = filename
download_started = 0
transfer_size = 0
# We just create a packet containing the
# requested filename, and send it down the
# link. We also specify we don't need a
# packet receipt.
request_packet = RNS.Packet(server_link, filename.encode("utf-8"), create_receipt=False)
request_packet.send()
print("")
print(("Requested \""+filename+"\" from server, waiting for download to begin..."))
menu_mode = "download_started"
# We just create a packet containing the
# requested filename, and send it down the
# link. We also specify we don't need a
# packet receipt.
request_packet = RNS.Packet(server_link, filename.encode("utf-8"), create_receipt=False)
request_packet.send()
print("")
print(("Requested \""+filename+"\" from server, waiting for download to begin..."))
menu_mode = "download_started"
# This function runs a simple menu for the user
# to select which files to download, or quit
menu_mode = None
def menu():
global server_files, server_link
# Wait until we have a filelist
while len(server_files) == 0:
time.sleep(0.1)
RNS.log("Ready!")
time.sleep(0.5)
global server_files, server_link
# Wait until we have a filelist
while len(server_files) == 0:
time.sleep(0.1)
RNS.log("Ready!")
time.sleep(0.5)
global menu_mode
menu_mode = "main"
should_quit = False
while (not should_quit):
print_menu()
global menu_mode
menu_mode = "main"
should_quit = False
while (not should_quit):
print_menu()
while not menu_mode == "main":
# Wait
time.sleep(0.25)
while not menu_mode == "main":
# Wait
time.sleep(0.25)
user_input = input()
if user_input == "q" or user_input == "quit" or user_input == "exit":
should_quit = True
print("")
else:
if user_input in server_files:
download(user_input)
else:
try:
if 0 <= int(user_input) < len(server_files):
download(server_files[int(user_input)])
except:
pass
user_input = input()
if user_input == "q" or user_input == "quit" or user_input == "exit":
should_quit = True
print("")
else:
if user_input in server_files:
download(user_input)
else:
try:
if 0 <= int(user_input) < len(server_files):
download(server_files[int(user_input)])
except:
pass
if should_quit:
server_link.teardown()
if should_quit:
server_link.teardown()
# Prints out menus or screens for the
# various states of the client program.
@@ -292,164 +336,204 @@ def menu():
# I won't go into detail here. Just
# strings basically.
def print_menu():
global menu_mode
global menu_mode, download_time, download_started, download_finished, transfer_size, file_size
if menu_mode == "main":
clear_screen()
print_filelist()
print("")
print("Select a file to download by entering name or number, or q to quit")
print(("> "), end=' ')
elif menu_mode == "download_started":
download_began = time.time()
while menu_mode == "download_started":
time.sleep(0.1)
if time.time() > download_began+APP_TIMEOUT:
print("The download timed out")
time.sleep(1)
server_link.teardown()
if menu_mode == "main":
clear_screen()
print_filelist()
print("")
print("Select a file to download by entering name or number, or q to quit")
print(("> "), end=' ')
elif menu_mode == "download_started":
download_began = time.time()
while menu_mode == "download_started":
time.sleep(0.1)
if time.time() > download_began+APP_TIMEOUT:
print("The download timed out")
time.sleep(1)
server_link.teardown()
if menu_mode == "downloading":
print("Download started")
print("")
while menu_mode == "downloading":
global current_download
percent = round(current_download.progress() * 100.0, 1)
print(("\rProgress: "+str(percent)+" % "), end=' ')
sys.stdout.flush()
time.sleep(0.1)
if menu_mode == "downloading":
print("Download started")
print("")
while menu_mode == "downloading":
global current_download
percent = round(current_download.get_progress() * 100.0, 1)
print(("\rProgress: "+str(percent)+" % "), end=' ')
sys.stdout.flush()
time.sleep(0.1)
if menu_mode == "save_error":
print(("\rProgress: 100.0 %"), end=' ')
sys.stdout.flush()
print("")
print("Could not write downloaded file to disk")
current_download.status = RNS.Resource.FAILED
menu_mode = "download_concluded"
if menu_mode == "save_error":
print(("\rProgress: 100.0 %"), end=' ')
sys.stdout.flush()
print("")
print("Could not write downloaded file to disk")
current_download.status = RNS.Resource.FAILED
menu_mode = "download_concluded"
if menu_mode == "download_concluded":
if current_download.status == RNS.Resource.COMPLETE:
print(("\rProgress: 100.0 %"), end=' ')
sys.stdout.flush()
print("")
print("The download completed! Press enter to return to the menu.")
input()
if menu_mode == "download_concluded":
if current_download.status == RNS.Resource.COMPLETE:
print(("\rProgress: 100.0 %"), end=' ')
sys.stdout.flush()
else:
print("")
print("The download failed! Press enter to return to the menu.")
input()
# Print statistics
hours, rem = divmod(download_time, 3600)
minutes, seconds = divmod(rem, 60)
timestring = "{:0>2}:{:0>2}:{:05.2f}".format(int(hours),int(minutes),seconds)
print("")
print("")
print("--- Statistics -----")
print("\tTime taken : "+timestring)
print("\tFile size : "+size_str(file_size))
print("\tData transferred : "+size_str(transfer_size))
print("\tEffective rate : "+size_str(file_size/download_time, suffix='b')+"/s")
print("\tTransfer rate : "+size_str(transfer_size/download_time, suffix='b')+"/s")
print("")
print("The download completed! Press enter to return to the menu.")
print("")
input()
current_download = None
menu_mode = "main"
print_menu()
else:
print("")
print("The download failed! Press enter to return to the menu.")
input()
current_download = None
menu_mode = "main"
print_menu()
# This function prints out a list of files
# on the connected server.
def print_filelist():
global server_files
print("Files on server:")
for index,file in enumerate(server_files):
print("\t("+str(index)+")\t"+file)
global server_files
print("Files on server:")
for index,file in enumerate(server_files):
print("\t("+str(index)+")\t"+file)
def filelist_received(filelist_data, packet):
global server_files, menu_mode
try:
# Unpack the list and extend our
# local list of available files
filelist = umsgpack.unpackb(filelist_data)
for file in filelist:
if not file in server_files:
server_files.append(file)
global server_files, menu_mode
try:
# Unpack the list and extend our
# local list of available files
filelist = umsgpack.unpackb(filelist_data)
for file in filelist:
if not file in server_files:
server_files.append(file)
# If the menu is already visible,
# we'll update it with what was
# just received
if menu_mode == "main":
print_menu()
except:
RNS.log("Invalid file list data received, closing link")
packet.link.teardown()
# If the menu is already visible,
# we'll update it with what was
# just received
if menu_mode == "main":
print_menu()
except:
RNS.log("Invalid file list data received, closing link")
packet.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
# We store a reference to the link
# instance for later use
global server_link
server_link = link
# Inform the user that the server is
# connected
RNS.log("Link established with server")
RNS.log("Waiting for filelist...")
# Inform the user that the server is
# connected
RNS.log("Link established with server")
RNS.log("Waiting for filelist...")
# 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.start()
# 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.start()
# This job just sleeps for the specified
# time, and then checks if the file list
# was received. If not, the program will
# exit.
def filelist_timeout_job():
time.sleep(APP_TIMEOUT)
time.sleep(APP_TIMEOUT)
global server_files
if len(server_files) == 0:
RNS.log("Timed out waiting for filelist, exiting")
os._exit(0)
global server_files
if len(server_files) == 0:
RNS.log("Timed out waiting for filelist, exiting")
os._exit(0)
# 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)
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 RNS detects that the download has
# started, we'll update our menu state
# so the user can be shown a progress of
# the download.
def download_began(resource):
global menu_mode, current_download
current_download = resource
menu_mode = "downloading"
global menu_mode, current_download, download_started, transfer_size, file_size
current_download = resource
if download_started == 0:
download_started = time.time()
transfer_size += resource.size
file_size = resource.total_size
menu_mode = "downloading"
# When the download concludes, successfully
# or not, we'll update our menu state and
# inform the user about how it all went.
def download_concluded(resource):
global menu_mode, current_filename
saved_filename = current_filename
global menu_mode, current_filename, download_started, download_finished, download_time
download_finished = time.time()
download_time = download_finished - download_started
if resource.status == RNS.Resource.COMPLETE:
counter = 0
while os.path.isfile(saved_filename):
counter += 1
saved_filename = current_filename+"."+str(counter)
saved_filename = current_filename
try:
file = open(saved_filename, "wb")
file.write(resource.data)
file.close()
menu_mode = "download_concluded"
except:
menu_mode = "save_error"
else:
menu_mode = "download_concluded"
if resource.status == RNS.Resource.COMPLETE:
counter = 0
while os.path.isfile(saved_filename):
counter += 1
saved_filename = current_filename+"."+str(counter)
try:
file = open(saved_filename, "wb")
file.write(resource.data.read())
file.close()
menu_mode = "download_concluded"
except:
menu_mode = "save_error"
else:
menu_mode = "download_concluded"
# A convenience function for printing a human-
# readable file size
def size_str(num, suffix='B'):
units = ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']
last_unit = 'Yi'
if suffix == 'b':
num *= 8
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
for unit in units:
if abs(num) < 1024.0:
return "%3.2f %s%s" % (num, unit, suffix)
num /= 1024.0
return "%.2f %s%s" % (num, last_unit, suffix)
# A convenience function for clearing the screen
def clear_screen():
@@ -463,31 +547,55 @@ def clear_screen():
# and parses input of from the user, and then
# starts up the desired program mode.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="Simple file transfer server and client utility")
parser.add_argument("-s", "--serve", action="store", metavar="dir", help="serve a directory of files to 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()
try:
parser = argparse.ArgumentParser(
description="Simple file transfer server and client utility"
)
if args.config:
configarg = args.config
else:
configarg = None
parser.add_argument(
"-s",
"--serve",
action="store",
metavar="dir",
help="serve a directory of files to clients"
)
if args.serve:
if os.path.isdir(args.serve):
server(configarg, args.serve)
else:
RNS.log("The specified directory does not exist")
else:
if (args.destination == None):
print("")
parser.print_help()
print("")
else:
client(args.destination, configarg)
parser.add_argument(
"--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
except KeyboardInterrupt:
print("")
exit()
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.serve:
if os.path.isdir(args.serve):
server(configarg, args.serve)
else:
RNS.log("The specified directory does not exist")
else:
if (args.destination == None):
print("")
parser.print_help()
print("")
else:
client(args.destination, configarg)
except KeyboardInterrupt:
print("")
exit()
+310
View File
@@ -0,0 +1,310 @@
##########################################################
# This RNS example demonstrates how to set up a link to #
# a destination, and identify the initiator to it's peer #
##########################################################
import os
import sys
import time
import argparse
import RNS
# Let's define an app name. We'll use this for all
# destinations we create. Since this echo example
# is part of a range of example utilities, we'll put
# them all within the app namespace "example_utilities"
APP_NAME = "example_utilities"
##########################################################
#### Server Part #########################################
##########################################################
# A reference to the latest client link that connected
latest_client_link = None
# This initialisation is executed when the users chooses
# to run as a server
def server(configpath):
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our link example
server_identity = RNS.Identity()
# We create a destination that clients can connect to. We
# want clients to create links to this destination, so we
# need to create a "single" destination type.
server_destination = RNS.Destination(
server_identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
APP_NAME,
"identifyexample"
)
# 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 identification example "+
RNS.prettyhexrep(destination.hash)+
" running, waiting for a connection."
)
RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
# We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server
# destination on the network, which will let clients
# know how to create messages directed towards it.
while True:
entered = input()
destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
# When a client establishes a link to our server
# destination, this function will be called with
# a reference to the link.
def client_connected(link):
global latest_client_link
RNS.log("Client connected")
link.set_link_closed_callback(client_disconnected)
link.set_packet_callback(server_packet_received)
link.set_remote_identified_callback(remote_identified)
latest_client_link = link
def client_disconnected(link):
RNS.log("Client disconnected")
def remote_identified(identity):
RNS.log("Remote identified as: "+str(identity))
def server_packet_received(message, packet):
global latest_client_link
# Get the originating identity for display
remote_peer = "unidentified peer"
if packet.link.get_remote_identity() != None:
remote_peer = str(packet.link.get_remote_identity())
# When data is received over any active link,
# it will all be directed to the last client
# that connected.
text = message.decode("utf-8")
RNS.log("Received data from "+remote_peer+": "+text)
reply_text = "I received \""+text+"\" over the link from "+remote_peer
reply_data = reply_text.encode("utf-8")
RNS.Packet(latest_client_link, reply_data).send()
##########################################################
#### Client Part #########################################
##########################################################
# A reference to the server link
server_link = None
# A reference to the client identity
client_identity = None
# This initialisation is executed when the users chooses
# to run as a client
def client(destination_hexhash, configpath):
global client_identity
# We need a binary representation of the destination
# hash that was entered on the command line
try:
if len(destination_hexhash) != 20:
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
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)
# Create a new client identity
client_identity = RNS.Identity()
RNS.log(
"Client created new identity "+
str(client_identity)
)
# 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,
"identifyexample"
)
# And create a link
link = RNS.Link(server_destination)
# We set a callback that will get executed
# every time a packet is received over the
# link
link.set_packet_callback(client_packet_received)
# 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 != "":
data = text.encode("utf-8")
if len(data) <= RNS.Link.MDU:
RNS.Packet(server_link, data).send()
else:
RNS.log(
"Cannot send this packet, the data size of "+
str(len(data))+" bytes exceeds the link packet MDU of "+
str(RNS.Link.MDU)+" bytes",
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, client_identity
server_link = link
# Inform the user that the server is
# connected
RNS.log("Link established with server, identifying to remote peer...")
link.identify(client_identity)
# 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 link, we
# simply print out the data.
def client_packet_received(message, packet):
text = message.decode("utf-8")
RNS.log("Received data on the link: "+text)
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 link 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()
+191 -143
View File
@@ -13,7 +13,7 @@ import RNS
# 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_utilitites"
APP_NAME = "example_utilities"
##########################################################
#### Server Part #########################################
@@ -25,65 +25,76 @@ 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 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, "linkexample")
# 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,
"linkexample"
)
# We configure a function that will get called every time
# a new client creates a link to this destination.
server_destination.link_established_callback(client_connected)
# 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)
# 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)")
# Let the user know that everything is ready
RNS.log(
"Link example "+
RNS.prettyhexrep(destination.hash)+
" running, waiting for a connection."
)
# 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))
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
global latest_client_link
RNS.log("Client connected")
link.link_closed_callback(client_disconnected)
link.packet_callback(server_packet_received)
latest_client_link = link
RNS.log("Client connected")
link.set_link_closed_callback(client_disconnected)
link.set_packet_callback(server_packet_received)
latest_client_link = link
def client_disconnected(link):
RNS.log("Client disconnected")
RNS.log("Client disconnected")
def server_packet_received(message, packet):
global latest_client_link
global latest_client_link
# When data is received over any active link,
# it will all be directed to the last client
# that connected.
text = message.decode("utf-8")
RNS.log("Received data on the link: "+text)
reply_text = "I received \""+text+"\" over the link"
reply_data = reply_text.encode("utf-8")
RNS.Packet(latest_client_link, reply_data).send()
# When data is received over any active link,
# it will all be directed to the last client
# that connected.
text = message.decode("utf-8")
RNS.log("Received data on the link: "+text)
reply_text = "I received \""+text+"\" over the link"
reply_data = reply_text.encode("utf-8")
RNS.Packet(latest_client_link, reply_data).send()
##########################################################
@@ -96,112 +107,128 @@ 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:
if len(destination_hexhash) != 20:
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
destination_hash = bytes.fromhex(destination_hexhash)
except:
RNS.log("Invalid destination entered. Check your input!\n")
exit()
# We need a binary representation of the destination
# hash that was entered on the command line
try:
if len(destination_hexhash) != 20:
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
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)
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Check if we know a path to the destination
if not RNS.Transport.hasPath(destination_hash):
RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
RNS.Transport.requestPath(destination_hash)
while not RNS.Transport.hasPath(destination_hash):
time.sleep(0.1)
# 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)
# 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...")
# 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, "linkexample")
# 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,
"linkexample"
)
# And create a link
link = RNS.Link(server_destination)
# And create a link
link = RNS.Link(server_destination)
# We set a callback that will get executed
# every time a packet is received over the
# link
link.packet_callback(client_packet_received)
# We set a callback that will get executed
# every time a packet is received over the
# link
link.set_packet_callback(client_packet_received)
# We'll also set up functions to inform the
# user when the link is established or closed
link.link_established_callback(link_established)
link.link_closed_callback(link_closed)
# 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()
# 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
global server_link
# Wait for the link to become active
while not server_link:
time.sleep(0.1)
# 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()
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()
# 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 != "":
data = text.encode("utf-8")
RNS.Packet(server_link, data).send()
except Exception as e:
should_quit = True
server_link.teardown()
# If not, send the entered text over the link
if text != "":
data = text.encode("utf-8")
if len(data) <= RNS.Link.MDU:
RNS.Packet(server_link, data).send()
else:
RNS.log(
"Cannot send this packet, the data size of "+
str(len(data))+" bytes exceeds the link packet MDU of "+
str(RNS.Link.MDU)+" bytes",
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
# We store a reference to the link
# instance for later use
global server_link
server_link = link
# Inform the user that the server is
# connected
RNS.log("Link established with server, enter some text to send, or \"quit\" to quit")
# 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)
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 link, we
# simply print out the data.
def client_packet_received(message, packet):
text = message.decode("utf-8")
RNS.log("Received data on the link: "+text)
print("> ", end=" ")
sys.stdout.flush()
text = message.decode("utf-8")
RNS.log("Received data on the link: "+text)
print("> ", end=" ")
sys.stdout.flush()
##########################################################
@@ -212,28 +239,49 @@ def client_packet_received(message, packet):
# and parses input of from the user, and then
# starts up the desired program mode.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="Simple link 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()
try:
parser = argparse.ArgumentParser(description="Simple link example")
if args.config:
configarg = args.config
else:
configarg = None
parser.add_argument(
"-s",
"--server",
action="store_true",
help="wait for incoming link requests from clients"
)
if args.server:
server(configarg)
else:
if (args.destination == None):
print("")
parser.print_help()
print("")
else:
client(args.destination, configarg)
parser.add_argument(
"--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
except KeyboardInterrupt:
print("")
exit()
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()
+66 -46
View File
@@ -11,49 +11,59 @@ import RNS
# destinations we create. Since this basic example
# is part of a range of example utilities, we'll put
# them all within the app namespace "example_utilities"
APP_NAME = "example_utilitites"
APP_NAME = "example_utilities"
# This initialisation is executed when the program is started
def program_setup(configpath):
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our example
identity = RNS.Identity()
# We must first initialise Reticulum
reticulum = RNS.Reticulum(configpath)
# Randomly create a new identity for our example
identity = RNS.Identity()
# Using the identity we just created, we create a destination.
# 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
# in the network.
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "minimalsample")
# Using the identity we just created, we create a destination.
# 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
# in the network.
destination = RNS.Destination(
identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
APP_NAME,
"minimalsample"
)
# We configure the destination to automatically prove all
# packets adressed 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
# communication was received correctly.
destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
# Everything's ready!
# Let's hand over control to the announce loop
announceLoop(destination)
# We configure the destination to automatically prove all
# packets adressed 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
# communication was received correctly.
destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
# Everything's ready!
# Let's hand over control to the announce loop
announceLoop(destination)
def announceLoop(destination):
# Let the user know that everything is ready
RNS.log("Minimal example "+RNS.prettyhexrep(destination.hash)+" running, hit enter to manually send an announce (Ctrl-C to quit)")
# Let the user know that everything is ready
RNS.log(
"Minimal example "+
RNS.prettyhexrep(destination.hash)+
" running, hit enter to manually send an announce (Ctrl-C to quit)"
)
# We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server
# destination on the network, which will let clients
# know how to create messages directed towards it.
while True:
entered = input()
destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
# 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))
##########################################################
@@ -64,18 +74,28 @@ def announceLoop(destination):
# and parses input from the user, and then starts
# the desired program mode.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="Bare minimum example to start Reticulum and create a destination")
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
args = parser.parse_args()
try:
parser = argparse.ArgumentParser(
description="Minimal example to start Reticulum and create a destination"
)
if args.config:
configarg = args.config
else:
configarg = None
parser.add_argument(
"--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
program_setup(configarg)
args = parser.parse_args()
except KeyboardInterrupt:
print("")
exit()
if args.config:
configarg = args.config
else:
configarg = None
program_setup(configarg)
except KeyboardInterrupt:
print("")
exit()
+283
View File
@@ -0,0 +1,283 @@
##########################################################
# This RNS example demonstrates how to set perform #
# requests and receive responses over a link. #
##########################################################
import os
import sys
import time
import random
import argparse
import RNS
# Let's define an app name. We'll use this for all
# destinations we create. Since this echo example
# is part of a range of example utilities, we'll put
# them all within the app namespace "example_utilities"
APP_NAME = "example_utilities"
##########################################################
#### Server Part #########################################
##########################################################
# A reference to the latest client link that connected
latest_client_link = None
def random_text_generator(path, data, request_id, remote_identity, requested_at):
RNS.log("Generating response to request "+RNS.prettyhexrep(request_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)]
# 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,
"requestexample"
)
# 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)
# We register a request handler for handling incoming
# requests over any established links.
server_destination.register_request_handler(
"/random/text",
response_generator = random_text_generator,
allow = RNS.Destination.ALLOW_ALL
)
# 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(
"Request example "+
RNS.prettyhexrep(destination.hash)+
" running, waiting for a connection."
)
RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
# We enter a loop that runs until the users exits.
# If the user hits enter, we will announce our server
# destination on the network, which will let clients
# know how to create messages directed towards it.
while True:
entered = input()
destination.announce()
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
# When a client establishes a link to our server
# destination, this function will be called with
# a reference to the link.
def client_connected(link):
global latest_client_link
RNS.log("Client connected")
link.set_link_closed_callback(client_disconnected)
latest_client_link = link
def client_disconnected(link):
RNS.log("Client disconnected")
##########################################################
#### 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:
if len(destination_hexhash) != 20:
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
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,
"requestexample"
)
# And create a link
link = RNS.Link(server_destination)
# We'll set up functions to inform the
# user when the link is established or closed
link.set_link_established_callback(link_established)
link.set_link_closed_callback(link_closed)
# Everything is set up, so let's enter a loop
# for the user to interact with the example
client_loop()
def client_loop():
global server_link
# Wait for the link to become active
while not server_link:
time.sleep(0.1)
should_quit = False
while not should_quit:
try:
print("> ", end=" ")
text = input()
# Check if we should quit the example
if text == "quit" or text == "q" or text == "exit":
should_quit = True
server_link.teardown()
else:
server_link.request(
"/random/text",
data = None,
response_callback = got_response,
failed_callback = request_failed
)
except Exception as e:
RNS.log("Error while sending request over the link: "+str(e))
should_quit = True
server_link.teardown()
def got_response(request_receipt):
request_id = request_receipt.request_id
response = request_receipt.response
RNS.log("Got response for request "+RNS.prettyhexrep(request_id)+": "+str(response))
def request_received(request_receipt):
RNS.log("The request "+RNS.prettyhexrep(request_receipt.request_id)+" was received by the remote peer.")
def request_failed(request_receipt):
RNS.log("The request "+RNS.prettyhexrep(request_receipt.request_id)+" failed.")
# This function is called when a link
# has been established with the server
def link_established(link):
# We store a reference to the link
# instance for later use
global server_link
server_link = link
# Inform the user that the server is
# connected
RNS.log("Link established with server, hit enter to perform a request, or type in \"quit\" to quit")
# When a link is closed, we'll inform the
# user, and exit the program
def link_closed(link):
if link.teardown_reason == RNS.Link.TIMEOUT:
RNS.log("The link timed out, exiting now")
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
RNS.log("The link was closed by the server, exiting now")
else:
RNS.log("Link closed, exiting now")
RNS.Reticulum.exit_handler()
time.sleep(1.5)
os._exit(0)
##########################################################
#### Program Startup #####################################
##########################################################
# This part of the program runs at startup,
# and parses input of from the user, and then
# starts up the desired program mode.
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description="Simple request/response example")
parser.add_argument(
"-s",
"--server",
action="store_true",
help="wait for incoming 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()
+343
View File
@@ -0,0 +1,343 @@
##########################################################
# This RNS example demonstrates a simple speedtest #
# program to measure link throughput. #
# #
# The current configuration is suited for testing fast #
# links. If you want to measure slow links like LoRa or #
# packet radio, you must significantly lower the #
# data_cap variable, which defines how much data is sent #
# for each test. #
##########################################################
import os
import sys
import time
import argparse
import RNS
# Let's define an app name. We'll use this for all
# destinations we create.
APP_NAME = "example_utilities"
##########################################################
#### Server Part #########################################
##########################################################
latest_client_link = None
first_packet_at = None
last_packet_at = None
received_data = 0
rc = 0
data_cap = 2*1024*1024
printed = False
# 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,
"speedtest"
)
# 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(
"Speedtest "+
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, first_packet_at, rc
RNS.log("Client connected")
first_packet_at = time.time()
rc = 0
link.set_link_closed_callback(client_disconnected)
link.set_packet_callback(server_packet_received)
latest_client_link = link
def client_disconnected(link):
RNS.log("Client disconnected")
# A convenience function for printing a human-
# readable file size
def size_str(num, suffix='B'):
units = ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']
last_unit = 'Yi'
if suffix == 'b':
num *= 8
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
for unit in units:
if abs(num) < 1024.0:
return "%3.2f %s%s" % (num, unit, suffix)
num /= 1024.0
return "%.2f %s%s" % (num, last_unit, suffix)
def server_packet_received(message, packet):
global latest_client_link, first_packet_at, last_packet_at, received_data, rc, data_cap
received_data += len(packet.data)
rc += 1
if rc >= 50:
RNS.log(size_str(received_data))
rc = 0
if received_data > data_cap:
rcv_d = received_data
received_data = 0
rc = 0
last_packet_at = time.time()
# Print statistics
download_time = last_packet_at-first_packet_at
hours, rem = divmod(download_time, 3600)
minutes, seconds = divmod(rem, 60)
timestring = "{:0>2}:{:0>2}:{:05.2f}".format(int(hours),int(minutes),seconds)
print("")
print("")
print("--- Statistics -----")
print("\tTime taken : "+timestring)
print("\tData transferred : "+size_str(rcv_d))
print("\tTransfer rate : "+size_str(rcv_d/download_time, suffix='b')+"/s")
print("")
sys.stdout.flush()
latest_client_link.teardown()
time.sleep(0.2)
rc = 0
received_data = 0
# latest_client_link.teardown()
# os._exit(0)
##########################################################
#### 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:
if len(destination_hexhash) != 20:
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
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,
"speedtest"
)
# 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:
text = input()
# Check if we should quit the example
if text == "quit" or text == "q" or text == "exit":
should_quit = True
server_link.teardown()
except Exception as e:
raise e
# 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, data_cap, printed
server_link = link
data_sent = 0
# Inform the user that the server is
# connected
RNS.log("Link established with server,sending...")
rd = os.urandom(RNS.Link.MDU)
started = time.time()
while link.status == RNS.Link.ACTIVE and data_sent < data_cap*1.25:
RNS.Packet(server_link, rd, create_receipt=False).send()
data_sent += len(rd)
if data_sent > data_cap and not printed:
printed = True
ended = time.time()
# Print statistics
download_time = ended-started
hours, rem = divmod(download_time, 3600)
minutes, seconds = divmod(rem, 60)
timestring = "{:0>2}:{:0>2}:{:05.2f}".format(int(hours),int(minutes),seconds)
print("")
print("")
print("--- Statistics -----")
print("\tTime taken : "+timestring)
print("\tData transferred : "+size_str(data_sent))
print("\tTransfer rate : "+size_str(data_sent/download_time, suffix='b')+"/s")
print("")
sys.stdout.flush()
time.sleep(0.1)
# 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)
def client_packet_received(message, packet):
pass
##########################################################
#### 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="Speedtest example")
parser.add_argument(
"-s",
"--server",
action="store_true",
help="wait for incoming 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()
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License, unless otherwise noted
Copyright (c) 2018 Mark Qvist / unsigned.io
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
+25
View File
@@ -0,0 +1,25 @@
all: release
clean:
@echo Cleaning...
-rm -r ./build
-rm -r ./dist
remove_symlinks:
@echo Removing symlinks for build...
-rm Examples/RNS
-rm RNS/Utilities/RNS
create_symlinks:
@echo Creating symlinks...
-ln -s ../RNS ./Examples/
-ln -s ../../RNS ./RNS/Utilities/
build_wheel:
python3 setup.py sdist bdist_wheel
release: remove_symlinks build_wheel create_symlinks
upload:
@echo Uploading to PyPi...
twine upload dist/*
-54
View File
@@ -1,54 +0,0 @@
Reticulum Wire Format
Header Types
-----------------
type 1 00 Two byte header, one 10 byte address field
type 2 01 Two byte header, two 10 byte address fields
type 3 10 Reserved
type 4 11 Reserved for extended header format
Propagation Types
-----------------
broadcast 00
transport 01
relay 10
tunnel 11
Destination Types
-----------------
single 00
group 01
plain 10
link 11
Packet Types
-----------------
data 00
announce 01
link request 10
proof 11
+- Packet Example -+
01010000 00000100 [ADDR1, 10 bytes] [ADDR2, 10 bytes] [CONTEXT, 1 byte] [DATA]
| | | | |
| | | | +-- Hops = 4
| | | +------- DATA packet
| | +--------- SINGLE destination
| +----------- TRANSPORT propagation type
+------------- HEADER_2, two byte header, two address fields
+- Packet Example -+
00000000 00000111 [ADDR1, 10 bytes] [CONTEXT, 1 byte] [DATA]
| | | | |
| | | | +-- Hops = 7
| | | +------- DATA packet
| | +--------- SINGLE destination
| +----------- BROADCAST propagation type
+------------- HEADER_1, two byte header, one address field
-837
View File
@@ -1,837 +0,0 @@
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><style>@font-face {
font-family: octicons-anchor;
src: url(https://cdnjs.cloudflare.com/ajax/libs/octicons/4.4.0/font/octicons.woff) format('woff');
}
* {
box-sizing: border-box;
}
body {
width: 980px;
margin-right: auto;
margin-left: auto;
color:#333;
background:#fff;
}
body .markdown-body {
padding: 45px;
border: 1px solid #ddd;
border-radius: 3px;
word-wrap: break-word;
}
pre {
font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
.markdown-body {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
color: #333;
font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 16px;
line-height: 1.6;
word-wrap: break-word;
}
.markdown-body a {
background-color: transparent;
}
.markdown-body a:active,
.markdown-body a:hover {
outline: 0;
}
.markdown-body strong {
font-weight: bold;
}
.markdown-body h1 {
font-size: 2em;
margin: 0.67em 0;
}
.markdown-body img {
border: 0;
}
.markdown-body hr {
box-sizing: content-box;
height: 0;
}
.markdown-body pre {
overflow: auto;
}
.markdown-body code,
.markdown-body kbd,
.markdown-body pre {
font-family: monospace, monospace;
font-size: 1em;
}
.markdown-body input {
color: inherit;
font: inherit;
margin: 0;
}
.markdown-body html input[disabled] {
cursor: default;
}
.markdown-body input {
line-height: normal;
}
.markdown-body input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
.markdown-body table {
border-collapse: collapse;
border-spacing: 0;
}
.markdown-body td,
.markdown-body th {
padding: 0;
}
.markdown-body input {
font: 13px / 1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
.markdown-body a {
color: #4078c0;
text-decoration: none;
}
.markdown-body a:hover,
.markdown-body a:active {
text-decoration: underline;
}
.markdown-body hr {
height: 0;
margin: 15px 0;
overflow: hidden;
background: transparent;
border: 0;
border-bottom: 1px solid #ddd;
}
.markdown-body hr:before {
display: table;
content: "";
}
.markdown-body hr:after {
display: table;
clear: both;
content: "";
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 15px;
margin-bottom: 15px;
line-height: 1.1;
}
.markdown-body h1 {
font-size: 30px;
}
.markdown-body h2 {
font-size: 21px;
}
.markdown-body h3 {
font-size: 16px;
}
.markdown-body h4 {
font-size: 14px;
}
.markdown-body h5 {
font-size: 12px;
}
.markdown-body h6 {
font-size: 11px;
}
.markdown-body blockquote {
margin: 0;
}
.markdown-body ul,
.markdown-body ol {
padding: 0;
margin-top: 0;
margin-bottom: 0;
}
.markdown-body ol ol,
.markdown-body ul ol {
list-style-type: lower-roman;
}
.markdown-body ul ul ol,
.markdown-body ul ol ol,
.markdown-body ol ul ol,
.markdown-body ol ol ol {
list-style-type: lower-alpha;
}
.markdown-body dd {
margin-left: 0;
}
.markdown-body code {
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
.markdown-body .select::-ms-expand {
opacity: 0;
}
.markdown-body .octicon {
font: normal normal normal 16px/1 octicons-anchor;
display: inline-block;
text-decoration: none;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.markdown-body .octicon-link:before {
content: '\f05c';
}
.markdown-body:before {
display: table;
content: "";
}
.markdown-body:after {
display: table;
clear: both;
content: "";
}
.markdown-body>*:first-child {
margin-top: 0 !important;
}
.markdown-body>*:last-child {
margin-bottom: 0 !important;
}
.markdown-body a:not([href]) {
color: inherit;
text-decoration: none;
}
.markdown-body .anchor {
display: inline-block;
padding-right: 2px;
margin-left: -18px;
}
.markdown-body .anchor:focus {
outline: none;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 1em;
margin-bottom: 16px;
font-weight: bold;
line-height: 1.4;
}
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
color: #000;
vertical-align: middle;
visibility: hidden;
}
.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
text-decoration: none;
}
.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
visibility: visible;
}
.markdown-body h1 {
padding-bottom: 0.3em;
font-size: 2.25em;
line-height: 1.2;
border-bottom: 1px solid #eee;
}
.markdown-body h1 .anchor {
line-height: 1;
}
.markdown-body h2 {
padding-bottom: 0.3em;
font-size: 1.75em;
line-height: 1.225;
border-bottom: 1px solid #eee;
}
.markdown-body h2 .anchor {
line-height: 1;
}
.markdown-body h3 {
font-size: 1.5em;
line-height: 1.43;
}
.markdown-body h3 .anchor {
line-height: 1.2;
}
.markdown-body h4 {
font-size: 1.25em;
}
.markdown-body h4 .anchor {
line-height: 1.2;
}
.markdown-body h5 {
font-size: 1em;
}
.markdown-body h5 .anchor {
line-height: 1.1;
}
.markdown-body h6 {
font-size: 1em;
color: #777;
}
.markdown-body h6 .anchor {
line-height: 1.1;
}
.markdown-body p,
.markdown-body blockquote,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body pre {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body hr {
height: 4px;
padding: 0;
margin: 16px 0;
background-color: #e7e7e7;
border: 0 none;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 2em;
}
.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body li>p {
margin-top: 16px;
}
.markdown-body dl {
padding: 0;
}
.markdown-body dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: bold;
}
.markdown-body dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.markdown-body blockquote {
padding: 0 15px;
color: #777;
border-left: 4px solid #ddd;
}
.markdown-body blockquote>:first-child {
margin-top: 0;
}
.markdown-body blockquote>:last-child {
margin-bottom: 0;
}
.markdown-body table {
display: block;
width: 100%;
overflow: auto;
word-break: normal;
word-break: keep-all;
}
.markdown-body table th {
font-weight: bold;
}
.markdown-body table th,
.markdown-body table td {
padding: 6px 13px;
border: 1px solid #ddd;
}
.markdown-body table tr {
background-color: #fff;
border-top: 1px solid #ccc;
}
.markdown-body table tr:nth-child(2n) {
background-color: #f8f8f8;
}
.markdown-body img {
max-width: 100%;
box-sizing: content-box;
background-color: #fff;
}
.markdown-body code {
padding: 0;
padding-top: 0.2em;
padding-bottom: 0.2em;
margin: 0;
font-size: 85%;
background-color: rgba(0,0,0,0.04);
border-radius: 3px;
}
.markdown-body code:before,
.markdown-body code:after {
letter-spacing: -0.2em;
content: "\00a0";
}
.markdown-body pre>code {
padding: 0;
margin: 0;
font-size: 100%;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.markdown-body .highlight {
margin-bottom: 16px;
}
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f7f7f7;
border-radius: 3px;
}
.markdown-body .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.markdown-body pre {
word-wrap: normal;
}
.markdown-body pre code {
display: inline;
max-width: initial;
padding: 0;
margin: 0;
overflow: initial;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.markdown-body pre code:before,
.markdown-body pre code:after {
content: normal;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
color: #555;
vertical-align: middle;
background-color: #fcfcfc;
border: solid 1px #ccc;
border-bottom-color: #bbb;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #bbb;
}
.markdown-body .pl-c {
color: #969896;
}
.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
color: #0086b3;
}
.markdown-body .pl-e,
.markdown-body .pl-en {
color: #795da3;
}
.markdown-body .pl-s .pl-s1,
.markdown-body .pl-smi {
color: #333;
}
.markdown-body .pl-ent {
color: #63a35c;
}
.markdown-body .pl-k {
color: #a71d5d;
}
.markdown-body .pl-pds,
.markdown-body .pl-s,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sra,
.markdown-body .pl-sr .pl-sre {
color: #183691;
}
.markdown-body .pl-v {
color: #ed6a43;
}
.markdown-body .pl-id {
color: #b52a1d;
}
.markdown-body .pl-ii {
background-color: #b52a1d;
color: #f8f8f8;
}
.markdown-body .pl-sr .pl-cce {
color: #63a35c;
font-weight: bold;
}
.markdown-body .pl-ml {
color: #693a17;
}
.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
color: #1d3e81;
font-weight: bold;
}
.markdown-body .pl-mq {
color: #008080;
}
.markdown-body .pl-mi {
color: #333;
font-style: italic;
}
.markdown-body .pl-mb {
color: #333;
font-weight: bold;
}
.markdown-body .pl-md {
background-color: #ffecec;
color: #bd2c00;
}
.markdown-body .pl-mi1 {
background-color: #eaffea;
color: #55a532;
}
.markdown-body .pl-mdr {
color: #795da3;
font-weight: bold;
}
.markdown-body .pl-mo {
color: #1d3e81;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
line-height: 10px;
color: #555;
vertical-align: middle;
background-color: #fcfcfc;
border: solid 1px #ccc;
border-bottom-color: #bbb;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #bbb;
}
.markdown-body .plan-price-unit {
color: #767676;
font-weight: normal;
}
.markdown-body .task-list-item {
list-style-type: none;
}
.markdown-body .task-list-item+.task-list-item {
margin-top: 3px;
}
.markdown-body .task-list-item input {
margin: 0 0.35em 0.25em -1.6em;
vertical-align: middle;
}
.markdown-body .plan-choice {
padding: 15px;
padding-left: 40px;
display: block;
border: 1px solid #e0e0e0;
position: relative;
font-weight: normal;
background-color: #fafafa;
}
.markdown-body .plan-choice.open {
background-color: #fff;
}
.markdown-body .plan-choice.open .plan-choice-seat-breakdown {
display: block;
}
.markdown-body .plan-choice-free {
border-radius: 3px 3px 0 0;
}
.markdown-body .plan-choice-paid {
border-radius: 0 0 3px 3px;
border-top: 0;
margin-bottom: 20px;
}
.markdown-body .plan-choice-radio {
position: absolute;
left: 15px;
top: 18px;
}
.markdown-body .plan-choice-exp {
color: #999;
font-size: 12px;
margin-top: 5px;
}
.markdown-body .plan-choice-seat-breakdown {
margin-top: 10px;
display: none;
}
.markdown-body :checked+.radio-label {
z-index: 1;
position: relative;
border-color: #4078c0;
}
@media print {
body .markdown-body {
padding: 0;
border: none;
}
}
</style><title>README</title></head><body><article class="markdown-body"><h1>
<a id="user-content-reticulum-network-stack-α" class="anchor" href="#reticulum-network-stack-%CE%B1" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Reticulum Network Stack α</h1>
<p>Reticulum is a cryptography-based networking stack for high-latency, wide-area networks built on readily available hardware. Reticulum allows you to build very wide-area networks with off-the-shelf tools, and offers end-to-end encryption, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, resource caching, unforgeable packet acknowledgements and much more.</p>
<p>Reticulum is a complete networking stack, and does not use IP or higher layers, although it can be easily tunnelled through conventional IP networks. This frees up overhead, that has been utilised to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality in open and trustless networks.</p>
<p>No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python 3.</p>
<p>For more info, see <a href="https://unsigned.io/projects/reticulum/" rel="nofollow">unsigned.io/projects/reticulum</a></p>
<h2>
<a id="user-content-where-can-reticulum-be-used" class="anchor" href="#where-can-reticulum-be-used" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Where can Reticulum be used?</h2>
<p>On practically any hardware that can support at least a half-duplex channel with 1.000 bits per second throughput, and an MTU of 500 bytes. Data radios, modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, free-space optical links and similar systems are all examples of the types of interfaces Reticulum was designed for.</p>
<p>An open-source LoRa-based interface called <a href="https://unsigned.io/projects/rnode/" rel="nofollow">RNode</a> has been designed specifically for use with Reticulum. It is possible to build yourself, or can be purchased as a complete transceiver that just needs a USB connection to the host.</p>
<p>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 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.</p>
<p>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.</p>
<h2>
<a id="user-content-current-status" class="anchor" href="#current-status" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Current Status</h2>
<p>Consider Reticulum experimental at this stage. Most features are implemented and working, but at this point the protocol may still change significantly, and is made publicly available for development collaboration, previewing and testing.</p>
<p>An API- and wireformat-stable alpha release is coming in the near future. Until then expect things to change unexpectedly if something warrants it.</p>
<h2>
<a id="user-content-what-is-implemented-at-this-point" class="anchor" href="#what-is-implemented-at-this-point" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>What is implemented at this point?</h2>
<ul>
<li>Adressing and identification</li>
<li>Fully self-configuring multi-hop routing</li>
<li>RSA assymetric encryption and signatures as basis for all communication</li>
<li>AES-128 symmetric encryption for group destinations</li>
<li>Elliptic curve encryption for links (on the SECP256R1 curve)</li>
<li>Perfect Forward Secrecy on links with ephemereal ECDH keys</li>
<li>Unforgeable packet delivery confirmations</li>
<li>A variety of supported interface types</li>
<li>Efficient and easy resource transfers</li>
<li>A simple and easy-to-use API</li>
<li>Some basic programming examples</li>
</ul>
<h2>
<a id="user-content-supported-interface-types-and-devices" class="anchor" href="#supported-interface-types-and-devices" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Supported interface types and devices</h2>
<p>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. Currently, the following interfaces are supported:</p>
<ul>
<li>Any ethernet device</li>
<li>LoRa using <a href="https://unsigned.io/projects/rnode/" rel="nofollow">RNode</a>
</li>
<li>Packet Radio TNCs (with or without AX.25)</li>
<li>Any device with a serial port</li>
<li>TCP over IP networks</li>
<li>UDP over IP networks</li>
</ul>
<h2>
<a id="user-content-what-is-currently-being-worked-on" class="anchor" href="#what-is-currently-being-worked-on" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>What is currently being worked on?</h2>
<ul>
<li>Delay/disruption tolerant bundle transfers</li>
<li>Useful example programs and utilities</li>
<li>API documentation</li>
<li>A messaging protocol built on Reticulum, see <a href="https://github.com/markqvist/lxmf">LXMF</a>
</li>
<li>A few useful-in-the-real-world apps built with Reticulum</li>
</ul>
<h2>
<a id="user-content-can-i-use-reticulum-on-amateur-radio-spectrum" class="anchor" href="#can-i-use-reticulum-on-amateur-radio-spectrum" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Can I use Reticulum on amateur radio spectrum?</h2>
<p>Some countries still ban the use of encryption when operating under an amateur radio license. Reticulum offers several encryptionless modes, while still using cryptographic principles for station verification, link establishment, data integrity verification, acknowledgements and routing. It is therefore perfectly possible to include Reticulum in amateur radio use, even if your country bans encryption.</p>
<h2>
<a id="user-content-dependencies" class="anchor" href="#dependencies" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Dependencies:</h2>
<ul>
<li>Python 3</li>
<li>cryptography.io</li>
<li>pyserial</li>
</ul>
<h2>
<a id="user-content-how-do-i-get-started" class="anchor" href="#how-do-i-get-started" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>How do I get started?</h2>
<p>Full documentation and tutorials are coming with the stable alpha release. Until then, you are mostly on your own. If you want to experiment already, you could take a look in the "Examples" folder, for some well-documented example programs. The default configuration file created by Reticulum on the first run is also worth reading. Be sure to also read the <a href="http://unsigned.io/wp-content/uploads/2018/04/Reticulum_Overview_v0.4.pdf" rel="nofollow">Reticulum Overview Document</a>.</p>
<p>If you just need Reticulum as a dependency for another application, the easiest way is probably via pip:</p>
<div class="highlight highlight-source-shell"><pre>pip3 install rns</pre></div>
<p>For development, you might want to get the latest source from GitHub. In that case, don't use pip, but try this recipe:</p>
<div class="highlight highlight-source-shell"><pre><span class="pl-c"><span class="pl-c">#</span> Install dependencies</span>
pip3 install cryptography pyserial
<span class="pl-c"><span class="pl-c">#</span> Clone repository</span>
git clone https://github.com/markqvist/Reticulum.git
<span class="pl-c"><span class="pl-c">#</span> Move into Reticulum folder and symlink library to examples folder</span>
<span class="pl-c1">cd</span> Reticulum
ln -s ../RNS ./Examples/
<span class="pl-c"><span class="pl-c">#</span> Run an example</span>
python3 Examples/Echo.py -s
<span class="pl-c"><span class="pl-c">#</span> Unless you've manually created a config file, Reticulum will do so now,</span>
<span class="pl-c"><span class="pl-c">#</span> and immediately exit. Make any necessary changes to the file:</span>
nano <span class="pl-k">~</span>/.reticulum/config
<span class="pl-c"><span class="pl-c">#</span> ... and launch the example again.</span>
python3 Examples/Echo.py -s
<span class="pl-c"><span class="pl-c">#</span> You can now repeat the process on another computer,</span>
<span class="pl-c"><span class="pl-c">#</span> and run the same example with -h to get command line options.</span>
python3 Examples/Echo.py -h
<span class="pl-c"><span class="pl-c">#</span> Run the example in client mode to "ping" the server.</span>
<span class="pl-c"><span class="pl-c">#</span> Replace the hash below with the actual destination hash of your server.</span>
python3 Examples/Echo.py 3e12fc71692f8ec47bc5
<span class="pl-c"><span class="pl-c">#</span> Have a look at another example</span>
python3 Examples/Filetransfer.py -h</pre></div>
<p>The default config file contains examples for using Reticulum with LoRa transceivers (specifically <a href="https://unsigned.io/projects/rnode/" rel="nofollow">RNode</a>), packet radio TNCs/modems and UDP. By default a UDP interface is already enabled in the default config, which will enable Reticulum communication in your local ethernet broadcast domain.</p>
<p>You can use the examples in the config file to expand communication over other mediums such as packet radio or LoRa, or over fast IP links using the UDP interface. I'll add in-depth tutorials and explanations on these topics later. For now, the included examples will hopefully be enough to get started.</p>
<h2>
<a id="user-content-caveat-emptor" class="anchor" href="#caveat-emptor" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Caveat Emptor</h2>
<p>Reticulum is alpha software, and should be considered experimental. While it has been built with cryptography best-practices very foremost in mind, it <em>has not</em> been externally security audited, and there could very well be privacy-breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch.</p>
</article></body></html>
+148 -79
View File
@@ -1,114 +1,183 @@
Reticulum Network Stack α
Reticulum Network Stack β
==========
Reticulum is a cryptography-based networking stack for high-latency, wide-area networks built on readily available hardware. Reticulum allows you to build very wide-area networks with off-the-shelf tools, and offers end-to-end encryption, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, resource caching, unforgeable packet acknowledgements and much more.
<p align="center"><img width="200" src="https://unsigned.io/wp-content/uploads/2022/03/reticulum_logo_512.png"></p>
Reticulum is a complete networking stack, and does not use IP or higher layers, although it can be easily tunnelled through conventional IP networks. This frees up overhead, that has been utilised to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality in open and trustless networks.
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.
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, interconnectable and autonomous networks. Reticulum **is not** *one* network. It is **a tool** for building *thousands of networks*. Networks without kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate with each other, and require no central oversight. Networks for human beings. *Networks for the people*.
Reticulum is a complete networking stack, and does not rely on IP or higher layers, but it is possible to use IP as the underlying carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the Internet or private IP networks.
Having no dependencies on traditional networking stacks free up overhead that has been utilised to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality in open and trustless networks.
No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python 3.
## Read The Manual
The full documentation for Reticulum is available 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)
For more info, see [unsigned.io/projects/reticulum](https://unsigned.io/projects/reticulum/)
## Where can Reticulum be used?
On practically any hardware that can support at least a half-duplex channel with 1.000 bits per second throughput, and an MTU of 500 bytes. Data radios, modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, free-space optical links and similar systems are all examples of the types of interfaces Reticulum was designed for.
## Notable Features
- Coordination-less globally unique adressing and identification
- Fully self-configuring multi-hop routing
- Complete initiator anonymity, communicate without revealing your identity
- Asymmetric X25519 encryption and Ed25519 signatures as a basis for all communication
- Forward Secrecy with ephemereal Elliptic Curve Diffie-Hellman keys on Curve25519
- Reticulum uses the [Fernet](https://github.com/fernet/spec/blob/master/Spec.md) specification for on-the-wire / over-the-air encryption
- Keys are ephemeral and derived from an ECDH key exchange on Curve25519
- AES-128 in CBC mode with PKCS7 padding
- HMAC using SHA256 for authentication
- IVs are generated through os.urandom()
- Unforgeable packet delivery confirmations
- A variety of supported interface types
- An intuitive and easy-to-use API
- Reliable and efficient transfer of arbritrary amounts of data
- Reticulum can handle a few bytes of data or files of many gigabytes
- Sequencing, transfer coordination and checksumming is automatic
- The API is very easy to use, and provides transfer progress
- Lightweight, flexible and expandable Request/Response mechanism
- Efficient link establishment
- Total bandwidth cost of setting up an encrypted link is 3 packets totalling 237 bytes
- Low cost of keeping links open at only 0.62 bits per second
An open-source LoRa-based interface called [RNode](https://unsigned.io/projects/rnode/) has been designed specifically for use with Reticulum. It is possible to build yourself, or can be purchased as a complete transceiver that just needs a USB connection to the host.
## Examples of Reticulum Applications
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
- 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.
## 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, ad-hoc WiFi, free-space optical links and similar systems are all examples of the types of interfaces Reticulum was designed for.
An open-source LoRa-based interface called [RNode](https://unsigned.io/projects/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 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.
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.
## Current Status
Consider Reticulum experimental at this stage. Most features are implemented and working, but at this point the protocol may still change significantly, and is made publicly available for development collaboration, previewing and testing.
An API- and wireformat-stable alpha release is coming in the near future. Until then expect things to change unexpectedly if something warrants it.
## What is implemented at this point?
- Adressing and identification
- Fully self-configuring multi-hop routing
- RSA assymetric encryption and signatures as basis for all communication
- AES-128 symmetric encryption for group destinations
- Elliptic curve encryption for links (on the SECP256R1 curve)
- Perfect Forward Secrecy on links with ephemereal ECDH keys
- Unforgeable packet delivery confirmations
- A variety of supported interface types
- Efficient and easy resource transfers
- A simple and easy-to-use API
- Some basic programming examples
## 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. Currently, the following interfaces are supported:
- Any ethernet device
- LoRa using [RNode](https://unsigned.io/projects/rnode/)
- Packet Radio TNCs (with or without AX.25)
- Any device with a serial port
- TCP over IP networks
- UDP over IP networks
## What is currently being worked on?
- Delay/disruption tolerant bundle transfers
- Useful example programs and utilities
- API documentation
- A messaging protocol built on Reticulum, see [LXMF](https://github.com/markqvist/lxmf)
- A few useful-in-the-real-world apps built with Reticulum
## Can I use Reticulum on amateur radio spectrum?
Some countries still ban the use of encryption when operating under an amateur radio license. Reticulum offers several encryptionless modes, while still using cryptographic principles for station verification, link establishment, data integrity verification, acknowledgements and routing. It is therefore perfectly possible to include Reticulum in amateur radio use, even if your country bans encryption.
## Dependencies:
- Python 3
- cryptography.io
- pyserial
## How do I get started?
Full documentation and tutorials are coming with the stable alpha release. Until then, you are mostly on your own. If you want to experiment already, you could take a look in the "Examples" folder, for some well-documented example programs. The default configuration file created by Reticulum on the first run is also worth reading. Be sure to also read the [Reticulum Overview Document](http://unsigned.io/wp-content/uploads/2018/04/Reticulum_Overview_v0.4.pdf).
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/).
If you just need Reticulum as a dependency for another application, the easiest way is probably via pip:
To simply install Reticulum and related utilities on your system, the easiest way is via pip:
```bash
pip3 install rns
```
For development, you might want to get the latest source from GitHub. In that case, don't use pip, but try this recipe:
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
# Install dependencies
pip3 install cryptography pyserial
When first started, Reticulum will create a default configuration file, providing basic connectivity to other Reticulum peers. The default config file contains examples for using Reticulum with LoRa transceivers (specifically [RNode](https://unsigned.io/projects/rnode/)), packet radio TNCs/modems, TCP and UDP.
# Clone repository
git clone https://github.com/markqvist/Reticulum.git
You can use the examples in the config file to expand communication over many mediums such as packet radio or LoRa (with [RNode](https://unsigned.io/projects/rnode/)), serial ports, or over fast IP links and the Internet using the UDP and TCP interfaces. For more detailed examples, 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/).
# Move into Reticulum folder and symlink library to examples folder
cd Reticulum
ln -s ../RNS ./Examples/
## 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/).
# Run an example
python3 Examples/Echo.py -s
- 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 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 easy to copy files to remote systems
- The remote command execution program `rnx` that let's you run commands and programs and retrieve output from remote systems
# 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
All tools, including `rnx` and `rncp`, work reliably and well even over very low-bandwidth links like LoRa or Packet Radio.
# ... and launch the example again.
python3 Examples/Echo.py -s
## 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.
# 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
## Supported interface types and devices
# 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
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.
# Have a look at another example
python3 Examples/Filetransfer.py -h
Currently, the following interfaces are supported:
- Any ethernet device
- LoRa using [RNode](https://unsigned.io/projects/rnode/)
- Packet Radio TNCs (with or without AX.25)
- KISS-compatible hardware and software modems
- Any device with a serial port
- TCP over IP networks
- UDP over IP networks
- External programs via stdio or pipes
- Custom hardware via stdio or pipes
## Development Roadmap
- Version 0.3.8
- Improving [the manual](https://markqvist.github.io/Reticulum/manual/) with sections specifically for beginners
- Utilities for managing identities, signing and encryption
- Support for radio and modem interfaces on Android
- User friendly interface configuration tool
- Easy way to share interface configurations, see [#19](https://github.com/markqvist/Reticulum/discussions/19)
- More interface types for even broader compatibility
- Plain ESP32 devices (ESP-Now, WiFi, Bluetooth, etc.)
- More LoRa transceivers
- AT-compatible modems
- IR Transceivers
- AWDL / OWL
- HF Modems
- CAN-bus
- ZeroMQ
- MQTT
- IrDA / IrPHY
- SPI
- i²c
- Version 0.3.9
- A portable cryptography core, supporting multiple backends
- Performance optimisations
- Memory optimisations
- Planned, but not yet scheduled
- Globally routable multicast
- Bindings for other programming languages
- A portable Reticulum implementation in C, see [#21](https://github.com/markqvist/Reticulum/discussions/21)
## Dependencies:
- Python 3.6
- cryptography.io
- netifaces
- pyserial
## Public Testnet
If you just want to get started experimenting without building any physical networks, you are welcome to join the Unsigned.io RNS Testnet. The testnet is just that, an informal network for testing and experimenting. It will be up most of the time, and anyone can join, but it also means that there's no guarantees for service availability.
The testnet runs the very latest version of Reticulum (often even a short while before it is publicly released). Sometimes experimental versions of Reticulum might be deployed to nodes on the testnet, which means strange behaviour might occur. If none of that scares you, you can join the testnet via eihter TCP or I2P. Just add one of the following interfaces to your Reticulum configuration file:
```
# For connecting over TCP/IP:
[[RNS Testnet Frankfurt]]
type = TCPClientInterface
interface_enabled = yes
outgoing = True
target_host = frankfurt.rns.unsigned.io
target_port = 4965
# For connecting over I2P:
[[RNS Testnet I2P Node A]]
type = I2PInterface
interface_enabled = yes
peers = ykzlw5ujbaqc2xkec4cpvgyxj257wcrmmgkuxqmqcur7cq3w3lha.b32.i2p
```
The default config file contains examples for using Reticulum with LoRa transceivers (specifically [RNode](https://unsigned.io/projects/rnode/)), packet radio TNCs/modems and UDP. By default a UDP interface is already enabled in the default config, which will enable Reticulum communication in your local ethernet broadcast domain.
The testnet also contains a number of [Nomad Network](https://github.com/markqvist/nomadnet) nodes, and LXMF propagation nodes.
You can use the examples in the config file to expand communication over other mediums such as packet radio or LoRa, or over fast IP links using the UDP interface. I'll add in-depth tutorials and explanations on these topics later. For now, the included examples will hopefully be enough to get started.
## Support Reticulum
You can help support the continued development of open, free and private communications systems by donating via one of the following channels:
- Ethereum: 0x81F7B979fEa6134bA9FD5c701b3501A2e61E897a
- Bitcoin: 3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq
- 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.
## Caveat Emptor
Reticulum is alpha software, and should be considered experimental. 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. 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-breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch.
+379 -181
View File
@@ -1,237 +1,435 @@
# 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 base64
import math
import time
import RNS
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import padding
class Callbacks:
def __init__(self):
self.link_established = None
self.packet = None
self.proof_requested = None
def __init__(self):
self.link_established = None
self.packet = None
self.proof_requested = None
class Destination:
KEYSIZE = RNS.Identity.KEYSIZE;
PADDINGSIZE= RNS.Identity.PADDINGSIZE;
"""
A class used to describe endpoints in a Reticulum Network. Destination
instances are used both to create outgoing and incoming endpoints. The
destination type will decide if encryption, and what type, is used in
communication with the endpoint. A destination can also announce its
presence on the network, which will also distribute necessary keys for
encrypted communication with it.
# Constants
SINGLE = 0x00
GROUP = 0x01
PLAIN = 0x02
LINK = 0x03
types = [SINGLE, GROUP, PLAIN, LINK]
:param identity: An instance of :ref:`RNS.Identity<api-identity>`. Can hold only public keys for an outgoing destination, or holding private keys for an ingoing.
:param direction: ``RNS.Destination.IN`` or ``RNS.Destination.OUT``.
:param type: ``RNS.Destination.SINGLE``, ``RNS.Destination.GROUP`` or ``RNS.Destination.PLAIN``.
:param app_name: A string specifying the app name.
:param \*aspects: Any non-zero number of string arguments.
"""
PROVE_NONE = 0x21
PROVE_APP = 0x22
PROVE_ALL = 0x23
proof_strategies = [PROVE_NONE, PROVE_APP, PROVE_ALL]
# Constants
SINGLE = 0x00
GROUP = 0x01
PLAIN = 0x02
LINK = 0x03
types = [SINGLE, GROUP, PLAIN, LINK]
IN = 0x11;
OUT = 0x12;
directions = [IN, OUT]
PROVE_NONE = 0x21
PROVE_APP = 0x22
PROVE_ALL = 0x23
proof_strategies = [PROVE_NONE, PROVE_APP, PROVE_ALL]
@staticmethod
def getDestinationName(app_name, *aspects):
# Check input values and build name string
if "." in app_name: raise ValueError("Dots can't be used in app names")
ALLOW_NONE = 0x00
ALLOW_ALL = 0x01
ALLOW_LIST = 0x02
request_policies = [ALLOW_NONE, ALLOW_ALL, ALLOW_LIST]
name = app_name
for aspect in aspects:
if "." in aspect: raise ValueError("Dots can't be used in aspects")
name = name + "." + aspect
IN = 0x11;
OUT = 0x12;
directions = [IN, OUT]
return name
@staticmethod
def full_name(app_name, *aspects):
"""
:returns: A string containing the full human-readable name of the destination, for an app_name and a number of aspects.
"""
# Check input values and build name string
if "." in app_name: raise ValueError("Dots can't be used in app names")
name = app_name
for aspect in aspects:
if "." in aspect: raise ValueError("Dots can't be used in aspects")
name = name + "." + aspect
return name
@staticmethod
def getDestinationHash(app_name, *aspects):
name = Destination.getDestinationName(app_name, *aspects)
@staticmethod
def hash(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)
# Create a digest for the destination
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(name.encode("UTF-8"))
# Create a digest for the destination
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(name.encode("UTF-8"))
return digest.finalize()[:10]
return digest.finalize()[:10]
@staticmethod
def app_and_aspects_from_name(full_name):
"""
:returns: A tuple containing the app name and a list of aspects, for a full-name string.
"""
components = full_name.split(".")
return (components[0], components[1:])
@staticmethod
def hash_from_name_and_identity(full_name, identity):
"""
: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)
def __init__(self, identity, direction, type, app_name, *aspects):
# Check input values and build name string
if "." in app_name: raise ValueError("Dots can't be used in app names")
if not type in Destination.types: raise ValueError("Unknown destination type")
if not direction in Destination.directions: raise ValueError("Unknown destination direction")
self.callbacks = Callbacks()
self.request_handlers = {}
self.type = type
self.direction = direction
self.proof_strategy = Destination.PROVE_NONE
self.mtu = 0
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,)
if identity != None and self.type == Destination.PLAIN:
raise TypeError("Selected destination type PLAIN cannot hold an identity")
self.identity = identity
self.name = Destination.full_name(app_name, *aspects)
self.hash = Destination.hash(app_name, *aspects)
self.hexhash = self.hash.hex()
self.default_app_data = None
self.callback = None
self.proofcallback = None
RNS.Transport.register_destination(self)
def __init__(self, identity, direction, type, app_name, *aspects):
# Check input values and build name string
if "." in app_name: raise ValueError("Dots can't be used in app names")
if not type in Destination.types: raise ValueError("Unknown destination type")
if not direction in Destination.directions: raise ValueError("Unknown destination direction")
self.callbacks = Callbacks()
self.type = type
self.direction = direction
self.proof_strategy = Destination.PROVE_NONE
self.mtu = 0
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,)
self.identity = identity
self.name = Destination.getDestinationName(app_name, *aspects)
self.hash = Destination.getDestinationHash(app_name, *aspects)
self.hexhash = self.hash.hex()
self.callback = None
self.proofcallback = None
RNS.Transport.registerDestination(self)
def __str__(self):
"""
:returns: A human-readable representation of the destination including addressable hash and full name.
"""
return "<"+self.name+"/"+self.hexhash+">"
def __str__(self):
return "<"+self.name+"/"+self.hexhash+">"
def announce(self, app_data=None, path_response=False):
"""
Creates an announce packet for this destination and broadcasts it on all
relevant interfaces. Application specific data can be added to the announce.
:param app_data: *bytes* containing the app_data.
:param path_response: Internal flag used by :ref:`RNS.Transport<api-transport>`. Ignore.
"""
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
signed_data = self.hash+self.identity.get_public_key()+random_hash
if app_data != None:
signed_data += app_data
signature = self.identity.sign(signed_data)
announce_data = self.identity.get_public_key()+random_hash+signature
if app_data != None:
announce_data += app_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()
def link_established_callback(self, callback):
self.callbacks.link_established = callback
def set_link_established_callback(self, callback):
"""
Registers a function to be called when a link has been established to
this destination.
def packet_callback(self, callback):
self.callbacks.packet = callback
:param callback: A function or method with the signature *callback(link)* to be called when a new link is established with this destination.
"""
self.callbacks.link_established = callback
def proof_requested_callback(self, callback):
self.callbacks.proof_requested = callback
def set_packet_callback(self, callback):
"""
Registers a function to be called when a packet has been received by
this destination.
def set_proof_strategy(self, proof_strategy):
if not proof_strategy in Destination.proof_strategies:
raise TypeError("Unsupported proof strategy")
else:
self.proof_strategy = proof_strategy
:param callback: A function or method with the signature *callback(data, packet)* to be called when this destination receives a packet.
"""
self.callbacks.packet = callback
def receive(self, packet):
plaintext = self.decrypt(packet.data)
if plaintext != None:
if packet.packet_type == RNS.Packet.LINKREQUEST:
self.incomingLinkRequest(plaintext, packet)
def set_proof_requested_callback(self, callback):
"""
Registers a function to be called when a proof has been requested for
a packet sent to this destination. Allows control over when and if
proofs should be returned for received packets.
if packet.packet_type == RNS.Packet.DATA:
if self.callbacks.packet != None:
self.callbacks.packet(plaintext, packet)
:param callback: A function or method to with the signature *callback(packet)* be called when a packet that requests a proof is received. The callback must return one of True or False. If the callback returns True, a proof will be sent. If it returns False, a proof will not be sent.
"""
self.callbacks.proof_requested = callback
def incomingLinkRequest(self, data, packet):
link = RNS.Link.validateRequest(self, data, packet)
if link != None:
self.links.append(link)
def set_proof_strategy(self, proof_strategy):
"""
Sets the destinations proof strategy.
def createKeys(self):
if self.type == Destination.PLAIN:
raise TypeError("A plain destination does not hold any keys")
if self.type == Destination.SINGLE:
raise TypeError("A single destination holds keys through an Identity instance")
if self.type == Destination.GROUP:
self.prv_bytes = Fernet.generate_key()
self.prv = Fernet(self.prv_bytes)
:param proof_strategy: One of ``RNS.Destination.PROVE_NONE``, ``RNS.Destination.PROVE_ALL`` or ``RNS.Destination.PROVE_APP``. If ``RNS.Destination.PROVE_APP`` is set, the `proof_requested_callback` will be called to determine whether a proof should be sent or not.
"""
if not proof_strategy in Destination.proof_strategies:
raise TypeError("Unsupported proof strategy")
else:
self.proof_strategy = proof_strategy
def getPrivateKey(self):
if self.type == Destination.PLAIN:
raise TypeError("A plain destination does not hold any keys")
elif self.type == Destination.SINGLE:
raise TypeError("A single destination holds keys through an Identity instance")
else:
return self.prv_bytes
def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None):
"""
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 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.
"""
if path == None or path == "":
raise ValueError("Invalid path specified")
elif not callable(response_generator):
raise ValueError("Invalid response generator specified")
elif not allow in Destination.request_policies:
raise ValueError("Invalid request policy")
else:
path_hash = RNS.Identity.truncated_hash(path.encode("utf-8"))
request_handler = [path, response_generator, allow, allowed_list]
self.request_handlers[path_hash] = request_handler
def loadPrivateKey(self, key):
if self.type == Destination.PLAIN:
raise TypeError("A plain destination does not hold any keys")
def deregister_request_handler(self, path):
"""
Deregisters a request handler.
if self.type == Destination.SINGLE:
raise TypeError("A single destination holds keys through an Identity instance")
:param path: The path for the request handler to be deregistered.
:returns: True if the handler was deregistered, otherwise False.
"""
path_hash = RNS.Identity.truncated_hash(path.encode("utf-8"))
if path_hash in self.request_handlers:
self.request_handlers.pop(path_hash)
return True
else:
return False
if self.type == Destination.GROUP:
self.prv_bytes = key
self.prv = Fernet(self.prv_bytes)
def loadPublicKey(self, key):
if self.type != Destination.SINGLE:
raise TypeError("Only the \"single\" destination type can hold a public key")
else:
raise TypeError("A single destination holds keys through an Identity instance")
def receive(self, packet):
if packet.packet_type == RNS.Packet.LINKREQUEST:
plaintext = packet.data
self.incoming_link_request(plaintext, packet)
else:
plaintext = self.decrypt(packet.data)
if plaintext != None:
if packet.packet_type == RNS.Packet.DATA:
if self.callbacks.packet != None:
try:
self.callbacks.packet(plaintext, packet)
except Exception as e:
RNS.log("Error while executing receive callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
def encrypt(self, plaintext):
if self.type == Destination.PLAIN:
return plaintext
def incoming_link_request(self, data, packet):
link = RNS.Link.validate_request(self, data, packet)
if link != None:
self.links.append(link)
if self.type == Destination.SINGLE and self.identity != None:
return self.identity.encrypt(plaintext)
def create_keys(self):
"""
For a ``RNS.Destination.GROUP`` type destination, creates a new symmetric key.
if self.type == Destination.GROUP:
if hasattr(self, "prv") and self.prv != None:
try:
return base64.urlsafe_b64decode(self.prv.encrypt(plaintext))
except Exception as e:
RNS.log("The GROUP destination could not encrypt data", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
raise ValueError("No private key held by GROUP destination. Did you create or load one?")
:raises: ``TypeError`` if called on an incompatible type of destination.
"""
if self.type == Destination.PLAIN:
raise TypeError("A plain destination does not hold any keys")
if self.type == Destination.SINGLE:
raise TypeError("A single destination holds keys through an Identity instance")
if self.type == Destination.GROUP:
self.prv_bytes = base64.urlsafe_b64decode(Fernet.generate_key())
self.prv = Fernet(base64.urlsafe_b64encode(self.prv_bytes))
def get_private_key(self):
"""
For a ``RNS.Destination.GROUP`` type destination, returns the symmetric private key.
:raises: ``TypeError`` if called on an incompatible type of destination.
"""
if self.type == Destination.PLAIN:
raise TypeError("A plain destination does not hold any keys")
elif self.type == Destination.SINGLE:
raise TypeError("A single destination holds keys through an Identity instance")
else:
return self.prv_bytes
def load_private_key(self, key):
"""
For a ``RNS.Destination.GROUP`` type destination, loads a symmetric private key.
:param key: A *bytes-like* containing the symmetric key.
:raises: ``TypeError`` if called on an incompatible type of destination.
"""
if self.type == Destination.PLAIN:
raise TypeError("A plain destination does not hold any keys")
if self.type == Destination.SINGLE:
raise TypeError("A single destination holds keys through an Identity instance")
if self.type == Destination.GROUP:
self.prv_bytes = key
self.prv = Fernet(base64.urlsafe_b64encode(self.prv_bytes))
def load_public_key(self, key):
if self.type != Destination.SINGLE:
raise TypeError("Only the \"single\" destination type can hold a public key")
else:
raise TypeError("A single destination holds keys through an Identity instance")
def encrypt(self, plaintext):
"""
Encrypts information for ``RNS.Destination.SINGLE`` or ``RNS.Destination.GROUP`` type destination.
:param plaintext: A *bytes-like* containing the plaintext to be encrypted.
:raises: ``ValueError`` if destination does not hold a necessary key for encryption.
"""
if self.type == Destination.PLAIN:
return plaintext
if self.type == Destination.SINGLE and self.identity != None:
return self.identity.encrypt(plaintext)
if self.type == Destination.GROUP:
if hasattr(self, "prv") and self.prv != None:
try:
return base64.urlsafe_b64decode(self.prv.encrypt(plaintext))
except Exception as e:
RNS.log("The GROUP destination could not encrypt data", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
raise ValueError("No private key held by GROUP destination. Did you create or load one?")
def decrypt(self, ciphertext):
if self.type == Destination.PLAIN:
return ciphertext
def decrypt(self, ciphertext):
"""
Decrypts information for ``RNS.Destination.SINGLE`` or ``RNS.Destination.GROUP`` type destination.
if self.type == Destination.SINGLE and self.identity != None:
return self.identity.decrypt(ciphertext)
:param ciphertext: *Bytes* containing the ciphertext to be decrypted.
:raises: ``ValueError`` if destination does not hold a necessary key for decryption.
"""
if self.type == Destination.PLAIN:
return ciphertext
if self.type == Destination.GROUP:
if hasattr(self, "prv") and self.prv != None:
try:
return self.prv.decrypt(base64.urlsafe_b64encode(ciphertext))
except Exception as e:
RNS.log("The GROUP destination could not decrypt data", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
raise ValueError("No private key held by GROUP destination. Did you create or load one?")
if self.type == Destination.SINGLE and self.identity != None:
return self.identity.decrypt(ciphertext)
if self.type == Destination.GROUP:
if hasattr(self, "prv") and self.prv != None:
try:
return self.prv.decrypt(base64.urlsafe_b64encode(ciphertext))
except Exception as e:
RNS.log("The GROUP destination could not decrypt data", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
raise ValueError("No private key held by GROUP destination. Did you create or load one?")
def sign(self, message):
if self.type == Destination.SINGLE and self.identity != None:
return self.identity.sign(message)
else:
return None
def sign(self, message):
"""
Signs information for ``RNS.Destination.SINGLE`` type destination.
:param message: *Bytes* containing the message to be signed.
:returns: A *bytes-like* containing the message signature, or *None* if the destination could not sign the message.
"""
if self.type == Destination.SINGLE and self.identity != None:
return self.identity.sign(message)
else:
return None
# Creates an announce packet for this destination.
# Application specific data can be added to the announce.
def announce(self, app_data=None, path_response=False):
destination_hash = self.hash
random_hash = RNS.Identity.getRandomHash()
signed_data = self.hash+self.identity.getPublicKey()+random_hash
if app_data != None:
signed_data += app_data
def set_default_app_data(self, app_data=None):
"""
Sets the default app_data for the destination. If set, the default
app_data will be included in every announce sent by the destination,
unless other app_data is specified in the *announce* method.
signature = self.identity.sign(signed_data)
# TODO: Check if this could be optimised by only
# carrying the hash in the destination field, not
# also redundantly inside the signed blob as here
announce_data = self.hash+self.identity.getPublicKey()+random_hash+signature
if app_data != None:
announce_data += app_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()
:param app_data: A *bytes-like* containing the default app_data, or a *callable* returning a *bytes-like* containing the app_data.
"""
self.default_app_data = app_data
def clear_default_app_data(self):
"""
Clears default app_data previously set for the destination.
"""
self.set_default_app_data(app_data=None)
+480 -267
View File
@@ -1,326 +1,539 @@
# 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 base64
import math
import os
import RNS
import time
import atexit
import base64
from .vendor import umsgpack as umsgpack
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import load_der_public_key
from cryptography.hazmat.primitives.serialization import load_der_private_key
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.fernet import Fernet
cio_default_backend = default_backend()
class Identity:
#KEYSIZE = 1536
KEYSIZE = 1024
DERKEYSIZE = KEYSIZE+272
"""
This class is used to manage identities in Reticulum. It provides methods
for encryption, decryption, signatures and verification, and is the basis
for all encrypted communication over Reticulum networks.
# Non-configurable constants
PADDINGSIZE = 336 # In bits
HASHLENGTH = 256 # In bits
SIGLENGTH = KEYSIZE
:param create_keys: Specifies whether new encryption and signing keys should be generated.
"""
ENCRYPT_CHUNKSIZE = (KEYSIZE-PADDINGSIZE)//8
DECRYPT_CHUNKSIZE = KEYSIZE//8
CURVE = "Curve25519"
"""
The curve used for Elliptic Curve DH key exchanges
"""
TRUNCATED_HASHLENGTH = 80 # In bits
KEYSIZE = 256*2
"""
X25519 key size in bits. A complete key is the concatenation of a 256 bit encryption key, and a 256 bit signing key.
"""
# Storage
known_destinations = {}
# Non-configurable constants
FERNET_VERSION = 0x80
FERNET_OVERHEAD = 57 # In bytes
OPTIMISED_FERNET_OVERHEAD = 54 # In bytes
AES128_BLOCKSIZE = 16 # In bytes
HASHLENGTH = 256 # In bits
SIGLENGTH = KEYSIZE # In bits
@staticmethod
def remember(packet_hash, destination_hash, public_key, app_data = None):
Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data]
TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH
"""
Constant specifying the truncated hash length (in bits) used by Reticulum
for addressable hashes and other purposes. Non-configurable.
"""
# Storage
known_destinations = {}
@staticmethod
def remember(packet_hash, destination_hash, public_key, app_data = None):
if len(public_key) != Identity.KEYSIZE//8:
raise TypeError("Can't remember "+RNS.prettyhexrep(destination_hash)+", the public key size of "+str(len(public_key))+" is not valid.", RNS.LOG_ERROR)
else:
Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data]
@staticmethod
def recall(destination_hash):
RNS.log("Searching for "+RNS.prettyhexrep(destination_hash)+"...", RNS.LOG_EXTREME)
if destination_hash in Identity.known_destinations:
identity_data = Identity.known_destinations[destination_hash]
identity = Identity(public_only=True)
identity.loadPublicKey(identity_data[2])
RNS.log("Found "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME)
return identity
else:
RNS.log("Could not find "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME)
return None
@staticmethod
def recall(destination_hash):
"""
Recall identity for a destination hash.
@staticmethod
def saveKnownDestinations():
RNS.log("Saving known destinations to storage...", RNS.LOG_VERBOSE)
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)
:param destination_hash: Destination hash as *bytes*.
:returns: An :ref:`RNS.Identity<api-identity>` instance that can be used to create an outgoing :ref:`RNS.Destination<api-destination>`, or *None* if the destination is unknown.
"""
if destination_hash in Identity.known_destinations:
identity_data = Identity.known_destinations[destination_hash]
identity = Identity(create_keys=False)
identity.load_public_key(identity_data[2])
identity.app_data = identity_data[3]
return identity
else:
return None
@staticmethod
def loadKnownDestinations():
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
try:
file = open(RNS.Reticulum.storagepath+"/known_destinations","rb")
Identity.known_destinations = umsgpack.load(file)
file.close()
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE)
except:
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, so no known destinations loaded", RNS.LOG_VERBOSE)
@staticmethod
def recall_app_data(destination_hash):
"""
Recall last heard app_data for a destination hash.
@staticmethod
def fullHash(data):
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(data)
:param destination_hash: Destination hash as *bytes*.
:returns: *Bytes* containing app_data, or *None* if the destination is unknown.
"""
if destination_hash in Identity.known_destinations:
app_data = Identity.known_destinations[destination_hash][3]
return app_data
else:
return None
return digest.finalize()
@staticmethod
def save_known_destinations():
try:
storage_known_destinations = {}
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
try:
file = open(RNS.Reticulum.storagepath+"/known_destinations","rb")
storage_known_destinations = umsgpack.load(file)
file.close()
except:
pass
@staticmethod
def truncatedHash(data):
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(data)
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]
return digest.finalize()[:(Identity.TRUNCATED_HASHLENGTH//8)]
RNS.log("Saving known destinations to storage...", RNS.LOG_VERBOSE)
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)
except Exception as e:
RNS.log("Error while saving known destinations to disk, the contained exception was: "+str(e), RNS.LOG_ERROR)
@staticmethod
def getRandomHash():
return Identity.truncatedHash(os.urandom(10))
@staticmethod
def load_known_destinations():
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
try:
file = open(RNS.Reticulum.storagepath+"/known_destinations","rb")
Identity.known_destinations = umsgpack.load(file)
file.close()
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE)
except:
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)
@staticmethod
def validateAnnounce(packet):
if packet.packet_type == RNS.Packet.ANNOUNCE:
RNS.log("Validating announce from "+RNS.prettyhexrep(packet.destination_hash), RNS.LOG_DEBUG)
destination_hash = packet.destination_hash
public_key = packet.data[10:Identity.DERKEYSIZE//8+10]
random_hash = packet.data[Identity.DERKEYSIZE//8+10:Identity.DERKEYSIZE//8+20]
signature = packet.data[Identity.DERKEYSIZE//8+20:Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8]
app_data = b""
if len(packet.data) > Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8:
app_data = packet.data[Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8:]
@staticmethod
def full_hash(data):
"""
Get a SHA-256 hash of passed data.
signed_data = destination_hash+public_key+random_hash+app_data
:param data: Data to be hashed as *bytes*.
:returns: SHA-256 hash as *bytes*
"""
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(data)
announced_identity = Identity(public_only=True)
announced_identity.loadPublicKey(public_key)
return digest.finalize()
if announced_identity.pub != None and announced_identity.validate(signature, signed_data):
RNS.Identity.remember(packet.getHash(), destination_hash, public_key)
RNS.log("Stored valid announce from "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG)
del announced_identity
return True
else:
RNS.log("Received invalid announce", RNS.LOG_DEBUG)
del announced_identity
return False
@staticmethod
def truncated_hash(data):
"""
Get a truncated SHA-256 hash of passed data.
@staticmethod
def exitHandler():
Identity.saveKnownDestinations()
:param data: Data to be hashed as *bytes*.
:returns: Truncated SHA-256 hash as *bytes*
"""
return Identity.full_hash(data)[:(Identity.TRUNCATED_HASHLENGTH//8)]
@staticmethod
def get_random_hash():
"""
Get a random SHA-256 hash.
:param data: Data to be hashed as *bytes*.
:returns: Truncated SHA-256 hash of random data as *bytes*
"""
return Identity.truncated_hash(os.urandom(Identity.TRUNCATED_HASHLENGTH//8))
@staticmethod
def validate_announce(packet):
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]
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:]
signed_data = destination_hash+public_key+random_hash+app_data
if not len(packet.data) > Identity.KEYSIZE//8+10+Identity.KEYSIZE//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 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
else:
RNS.log("Received invalid announce for "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG)
del announced_identity
return False
except Exception as e:
RNS.log("Error occurred while validating announce. The contained exception was: "+str(e), RNS.LOG_ERROR)
return False
@staticmethod
def exit_handler():
Identity.save_known_destinations()
@staticmethod
def from_file(path):
identity = Identity(public_only=True)
if identity.load(path):
return identity
else:
return None
@staticmethod
def from_bytes(prv_bytes):
"""
Create a new :ref:`RNS.Identity<api-identity>` instance from *bytes* of private key.
Can be used to load previously created and saved identities into Reticulum.
:param prv_bytes: The *bytes* of private a saved private key. **HAZARD!** Never use this to generate a new key by feeding random data in prv_bytes.
:returns: A :ref:`RNS.Identity<api-identity>` instance, or *None* if the *bytes* data was invalid.
"""
identity = Identity(create_keys=False)
if identity.load_private_key(prv_bytes):
return identity
else:
return None
def __init__(self,public_only=False):
# Initialize keys to none
self.prv = None
self.pub = None
self.prv_bytes = None
self.pub_bytes = None
self.hash = None
self.hexhash = None
@staticmethod
def from_file(path):
"""
Create a new :ref:`RNS.Identity<api-identity>` instance from a file.
Can be used to load previously created and saved identities into Reticulum.
if not public_only:
self.createKeys()
:param path: The full path to the saved :ref:`RNS.Identity<api-identity>` data
:returns: A :ref:`RNS.Identity<api-identity>` instance, or *None* if the loaded data was invalid.
"""
identity = Identity(create_keys=False)
if identity.load(path):
return identity
else:
return None
def createKeys(self):
self.prv = rsa.generate_private_key(
public_exponent=65337,
key_size=Identity.KEYSIZE,
backend=default_backend()
)
self.prv_bytes = self.prv.private_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
self.pub = self.prv.public_key()
self.pub_bytes = self.pub.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
def to_file(self, path):
"""
Saves the identity to a file. This will write the private key to disk,
and anyone with access to this file will be able to decrypt all
communication for the identity. Be very careful with this method.
self.updateHashes()
:param path: The full path specifying where to save the identity.
:returns: True if the file was saved, otherwise False.
"""
try:
with open(path, "wb") as key_file:
key_file.write(self.get_private_key())
return True
return False
except Exception as e:
RNS.log("Error while saving identity to "+str(path), RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e))
RNS.log("Identity keys created for "+RNS.prettyhexrep(self.hash), RNS.LOG_VERBOSE)
def __init__(self,create_keys=True):
# Initialize keys to none
self.prv = None
self.prv_bytes = None
self.sig_prv = None
self.sig_prv_bytes = None
def getPrivateKey(self):
return self.prv_bytes
self.pub = None
self.pub_bytes = None
self.sig_pub = None
self.sig_pub_bytes = None
def getPublicKey(self):
return self.pub_bytes
self.hash = None
self.hexhash = None
def loadPrivateKey(self, prv_bytes):
try:
self.prv_bytes = prv_bytes
self.prv = serialization.load_der_private_key(
self.prv_bytes,
password=None,
backend=default_backend()
)
self.pub = self.prv.public_key()
self.pub_bytes = self.pub.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
self.updateHashes()
if create_keys:
self.create_keys()
return True
def create_keys(self):
self.prv = X25519PrivateKey.generate()
self.prv_bytes = self.prv.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption()
)
except Exception as e:
RNS.log("Failed to load identity key", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e))
return False
self.sig_prv = Ed25519PrivateKey.generate()
self.sig_prv_bytes = self.sig_prv.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption()
)
def loadPublicKey(self, key):
try:
self.pub_bytes = key
self.pub = load_der_public_key(self.pub_bytes, backend=default_backend())
self.updateHashes()
except Exception as e:
RNS.log("Error while loading public key, the contained exception was: "+str(e), RNS.LOG_ERROR)
self.pub = self.prv.public_key()
self.pub_bytes = self.pub.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
def updateHashes(self):
self.hash = Identity.truncatedHash(self.pub_bytes)
self.hexhash = self.hash.hex()
self.sig_pub = self.sig_prv.public_key()
self.sig_pub_bytes = self.sig_pub.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
def save(self, path):
try:
with open(path, "wb") as key_file:
key_file.write(self.prv_bytes)
return True
return False
except Exception as e:
RNS.log("Error while saving identity to "+str(path), RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e))
self.update_hashes()
def load(self, path):
try:
with open(path, "rb") as key_file:
prv_bytes = key_file.read()
return self.loadPrivateKey(prv_bytes)
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("Identity keys created for "+RNS.prettyhexrep(self.hash), RNS.LOG_VERBOSE)
def encrypt(self, plaintext):
if self.pub != None:
chunksize = Identity.ENCRYPT_CHUNKSIZE
chunks = int(math.ceil(len(plaintext)/(float(chunksize))))
def get_private_key(self):
"""
:returns: The private key as *bytes*
"""
return self.prv_bytes+self.sig_prv_bytes
ciphertext = b"";
for chunk in range(chunks):
start = chunk*chunksize
end = (chunk+1)*chunksize
if (chunk+1)*chunksize > len(plaintext):
end = len(plaintext)
ciphertext += self.pub.encrypt(
plaintext[start:end],
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None
)
)
return ciphertext
else:
raise KeyError("Encryption failed because identity does not hold a public key")
def get_public_key(self):
"""
:returns: The public key as *bytes*
"""
return self.pub_bytes+self.sig_pub_bytes
def load_private_key(self, prv_bytes):
"""
Load a private key into the instance.
:param prv_bytes: The private key as *bytes*.
:returns: True if the key was loaded, otherwise False.
"""
try:
self.prv_bytes = prv_bytes[:Identity.KEYSIZE//8//2]
self.prv = X25519PrivateKey.from_private_bytes(self.prv_bytes)
self.sig_prv_bytes = prv_bytes[Identity.KEYSIZE//8//2:]
self.sig_prv = Ed25519PrivateKey.from_private_bytes(self.sig_prv_bytes)
self.pub = self.prv.public_key()
self.pub_bytes = self.pub.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
self.sig_pub = self.sig_prv.public_key()
self.sig_pub_bytes = self.sig_pub.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
self.update_hashes()
return True
except Exception as e:
raise e
RNS.log("Failed to load identity key", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
return False
def load_public_key(self, pub_bytes):
"""
Load a public key into the instance.
:param pub_bytes: The public key as *bytes*.
:returns: True if the key was loaded, otherwise False.
"""
try:
self.pub_bytes = pub_bytes[:Identity.KEYSIZE//8//2]
self.sig_pub_bytes = pub_bytes[Identity.KEYSIZE//8//2:]
self.pub = X25519PublicKey.from_public_bytes(self.pub_bytes)
self.sig_pub = Ed25519PublicKey.from_public_bytes(self.sig_pub_bytes)
self.update_hashes()
except Exception as e:
RNS.log("Error while loading public key, the contained exception was: "+str(e), RNS.LOG_ERROR)
def update_hashes(self):
self.hash = Identity.truncated_hash(self.get_public_key())
self.hexhash = self.hash.hex()
def load(self, path):
try:
with open(path, "rb") as key_file:
prv_bytes = key_file.read()
return self.load_private_key(prv_bytes)
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))
def get_salt(self):
return self.hash
def get_context(self):
return None
def encrypt(self, plaintext):
"""
Encrypts information for the identity.
:param plaintext: The plaintext to be encrypted as *bytes*.
:returns: Ciphertext token as *bytes*.
:raises: *KeyError* if the instance does not hold a public key.
"""
if self.pub != None:
ephemeral_key = X25519PrivateKey.generate()
ephemeral_pub_bytes = ephemeral_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
shared_key = ephemeral_key.exchange(self.pub)
# TODO: Improve this re-allocation of HKDF
derived_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=self.get_salt(),
info=self.get_context(),
backend=cio_default_backend,
).derive(shared_key)
fernet = Fernet(base64.urlsafe_b64encode(derived_key))
ciphertext = base64.urlsafe_b64decode(fernet.encrypt(plaintext))
token = ephemeral_pub_bytes+ciphertext
return token
else:
raise KeyError("Encryption failed because identity does not hold a public key")
def decrypt(self, ciphertext):
if self.prv != None:
plaintext = None
try:
chunksize = Identity.DECRYPT_CHUNKSIZE
chunks = int(math.ceil(len(ciphertext)/(float(chunksize))))
def decrypt(self, ciphertext_token):
"""
Decrypts information for the identity.
plaintext = b"";
for chunk in range(chunks):
start = chunk*chunksize
end = (chunk+1)*chunksize
if (chunk+1)*chunksize > len(ciphertext):
end = len(ciphertext)
:param ciphertext: The ciphertext to be decrypted as *bytes*.
:returns: Plaintext as *bytes*, or *None* if decryption fails.
:raises: *KeyError* if the instance does not hold a private key.
"""
if self.prv != None:
if len(ciphertext_token) > Identity.KEYSIZE//8//2:
plaintext = None
try:
peer_pub_bytes = ciphertext_token[:Identity.KEYSIZE//8//2]
peer_pub = X25519PublicKey.from_public_bytes(peer_pub_bytes)
plaintext += self.prv.decrypt(
ciphertext[start:end],
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None
)
)
except:
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed", RNS.LOG_VERBOSE)
return plaintext;
else:
raise KeyError("Decryption failed because identity does not hold a private key")
shared_key = self.prv.exchange(peer_pub)
# TODO: Improve this re-allocation of HKDF
derived_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=self.get_salt(),
info=self.get_context(),
backend=cio_default_backend,
).derive(shared_key)
fernet = Fernet(base64.urlsafe_b64encode(derived_key))
ciphertext = ciphertext_token[Identity.KEYSIZE//8//2:]
plaintext = fernet.decrypt(base64.urlsafe_b64encode(ciphertext))
except Exception as e:
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed: "+str(e), RNS.LOG_DEBUG)
return plaintext;
else:
RNS.log("Decryption failed because the token size was invalid.", RNS.LOG_DEBUG)
return None
else:
raise KeyError("Decryption failed because identity does not hold a private key")
def sign(self, message):
if self.prv != None:
signature = self.prv.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return signature
else:
raise KeyError("Signing failed because identity does not hold a private key")
def sign(self, message):
"""
Signs information by the identity.
def validate(self, signature, message):
if self.pub != None:
try:
self.pub.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return True
except Exception as e:
return False
else:
raise KeyError("Signature validation failed because identity does not hold a public key")
:param message: The message to be signed as *bytes*.
:returns: Signature as *bytes*.
:raises: *KeyError* if the instance does not hold a private key.
"""
if self.sig_prv != None:
try:
return self.sig_prv.sign(message)
except Exception as e:
RNS.log("The identity "+str(self)+" could not sign the requested message. The contained exception was: "+str(e), RNS.LOG_ERROR)
raise e
else:
raise KeyError("Signing failed because identity does not hold a private key")
def prove(self, packet, destination=None):
signature = self.sign(packet.packet_hash)
if RNS.Reticulum.should_use_implicit_proof():
proof_data = signature
else:
proof_data = packet.packet_hash + signature
if destination == None:
destination = packet.generateProofDestination()
def validate(self, signature, message):
"""
Validates the signature of a signed message.
proof = RNS.Packet(destination, proof_data, RNS.Packet.PROOF, attached_interface = packet.receiving_interface)
proof.send()
:param signature: The signature to be validated as *bytes*.
:param message: The message to be validated as *bytes*.
:returns: True if the signature is valid, otherwise False.
:raises: *KeyError* if the instance does not hold a public key.
"""
if self.pub != None:
try:
self.sig_pub.verify(signature, message)
return True
except Exception as e:
return False
else:
raise KeyError("Signature validation failed because identity does not hold a public key")
def __str__(self):
return RNS.prettyhexrep(self.hash)
def prove(self, packet, destination=None):
signature = self.sign(packet.packet_hash)
if RNS.Reticulum.should_use_implicit_proof():
proof_data = signature
else:
proof_data = packet.packet_hash + signature
if destination == None:
destination = packet.generate_proof_destination()
proof = RNS.Packet(destination, proof_data, RNS.Packet.PROOF, attached_interface = packet.receiving_interface)
proof.send()
def __str__(self):
return RNS.prettyhexrep(self.hash)
+325 -259
View File
@@ -1,305 +1,371 @@
# 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 .Interface import Interface
from time import sleep
import sys
import serial
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
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
@staticmethod
def escape(data):
data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
return data
class AX25():
PID_NOLAYER3 = 0xF0
CTRL_UI = 0x03
CRC_CORRECT = bytes([0xF0])+bytes([0xB8])
HEADER_SIZE = 16
PID_NOLAYER3 = 0xF0
CTRL_UI = 0x03
CRC_CORRECT = bytes([0xF0])+bytes([0xB8])
HEADER_SIZE = 16
class AX25KISSInterface(Interface):
MAX_CHUNK = 32768
MAX_CHUNK = 32768
BITRATE_GUESS = 1200
owner = None
port = None
speed = None
databits = None
parity = None
stopbits = None
serial = None
owner = None
port = None
speed = None
databits = None
parity = None
stopbits = None
serial = None
def __init__(self, owner, name, callsign, ssid, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control):
self.serial = None
self.owner = owner
self.name = name
self.src_call = callsign.upper().encode("ascii")
self.src_ssid = ssid
self.dst_call = "APZRNS".encode("ascii")
self.dst_ssid = 0
self.port = port
self.speed = speed
self.databits = databits
self.parity = serial.PARITY_NONE
self.stopbits = stopbits
self.timeout = 100
self.online = False
# TODO: Sane default and make this configurable
# TODO: Changed to 25ms instead of 100ms, check it
self.txdelay = 0.025
def __init__(self, owner, name, callsign, ssid, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control):
import importlib
if importlib.util.find_spec('serial') != None:
import serial
else:
RNS.log("Using the AX.25 KISS interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
RNS.panic()
self.packet_queue = []
self.flow_control = flow_control
self.interface_ready = False
self.rxb = 0
self.txb = 0
if (len(self.src_call) < 3 or len(self.src_call) > 6):
raise ValueError("Invalid callsign for "+str(self))
self.HW_MTU = 564
self.pyserial = serial
self.serial = None
self.owner = owner
self.name = name
self.src_call = callsign.upper().encode("ascii")
self.src_ssid = ssid
self.dst_call = "APZRNS".encode("ascii")
self.dst_ssid = 0
self.port = port
self.speed = speed
self.databits = databits
self.parity = serial.PARITY_NONE
self.stopbits = stopbits
self.timeout = 100
self.online = False
self.bitrate = KISSInterface.BITRATE_GUESS
if (self.src_ssid < 0 or self.src_ssid > 15):
raise ValueError("Invalid SSID for "+str(self))
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 (len(self.src_call) < 3 or len(self.src_call) > 6):
raise ValueError("Invalid callsign for "+str(self))
if parity.lower() == "e" or parity.lower() == "even":
self.parity = serial.PARITY_EVEN
if (self.src_ssid < 0 or self.src_ssid > 15):
raise ValueError("Invalid SSID for "+str(self))
if parity.lower() == "o" or parity.lower() == "odd":
self.parity = serial.PARITY_ODD
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;
try:
RNS.log("Opening serial port "+self.port+"...")
self.serial = serial.Serial(
port = self.port,
baudrate = self.speed,
bytesize = self.databits,
parity = self.parity,
stopbits = self.stopbits,
xonxoff = False,
rtscts = False,
timeout = 0,
inter_byte_timeout = None,
write_timeout = None,
dsrdtr = False,
)
except Exception as e:
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
raise e
if parity.lower() == "e" or parity.lower() == "even":
self.parity = serial.PARITY_EVEN
if self.serial.is_open:
# Allow time for interface to initialise before config
sleep(2.0)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True)
thread.start()
self.online = True
RNS.log("Serial port "+self.port+" is now open")
RNS.log("Configuring AX.25 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("AX.25 KISS interface configured")
sleep(2)
else:
raise IOError("Could not open serial port")
if parity.lower() == "o" or parity.lower() == "odd":
self.parity = serial.PARITY_ODD
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+"...", RNS.LOG_VERBOSE)
self.serial = self.pyserial.Serial(
port = self.port,
baudrate = self.speed,
bytesize = self.databits,
parity = self.parity,
stopbits = self.stopbits,
xonxoff = False,
rtscts = False,
timeout = 0,
inter_byte_timeout = None,
write_timeout = None,
dsrdtr = False,
)
def configure_device(self):
# Allow time for interface to initialise before config
sleep(2.0)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True)
thread.start()
self.online = True
RNS.log("Serial port "+self.port+" is now open")
RNS.log("Configuring AX.25 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("AX.25 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 AX.25 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 AX.25 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 AX.25 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 AX.25 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 AX.25 KISS interface flow control")
else:
raise IOError("Could not enable AX.25 KISS interface flow control")
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 AX.25 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 AX.25 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 AX.25 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 AX.25 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 AX.25 KISS interface flow control")
else:
raise IOError("Could not enable AX.25 KISS interface flow control")
def processIncoming(self, data):
if (len(data) > AX25.HEADER_SIZE):
self.rxb += len(data)
self.owner.inbound(data[AX25.HEADER_SIZE:], self)
def processIncoming(self, data):
if (len(data) > AX25.HEADER_SIZE):
self.owner.inbound(data[AX25.HEADER_SIZE:], self)
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()
encoded_dst_ssid = bytes([0x60 | (self.dst_ssid << 1)])
encoded_src_ssid = bytes([0x60 | (self.src_ssid << 1) | 0x01])
def processOutgoing(self,data):
if self.online:
if self.interface_ready:
if self.flow_control:
self.interface_ready = False
addr = b""
encoded_dst_ssid = bytes([0x60 | (self.dst_ssid << 1)])
encoded_src_ssid = bytes([0x60 | (self.src_ssid << 1) | 0x01])
for i in range(0,6):
if (i < len(self.dst_call)):
addr += bytes([self.dst_call[i]<<1])
else:
addr += bytes([0x20])
addr += encoded_dst_ssid
addr = b""
for i in range(0,6):
if (i < len(self.src_call)):
addr += bytes([self.src_call[i]<<1])
else:
addr += bytes([0x20])
addr += encoded_src_ssid
for i in range(0,6):
if (i < len(self.dst_call)):
addr += bytes([self.dst_call[i]<<1])
else:
addr += bytes([0x20])
addr += encoded_dst_ssid
data = addr+bytes([AX25.CTRL_UI])+bytes([AX25.PID_NOLAYER3])+data
for i in range(0,6):
if (i < len(self.src_call)):
addr += bytes([self.src_call[i]<<1])
else:
addr += bytes([0x20])
addr += encoded_src_ssid
data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc]))
kiss_frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND])
data = addr+bytes([AX25.CTRL_UI])+bytes([AX25.PID_NOLAYER3])+data
written = self.serial.write(kiss_frame)
self.txb += datalen
data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc]))
kiss_frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND])
if written != len(kiss_frame):
if self.flow_control:
self.interface_ready = True
raise IOError("AX.25 interface only wrote "+str(written)+" bytes of "+str(len(kiss_frame)))
else:
self.queue(data)
if (self.txdelay > 0):
RNS.log(str(self.name)+" delaying TX for "+str(self.txdelay)+" seconds", RNS.LOG_EXTREME)
sleep(self.txdelay)
def queue(self, data):
self.packet_queue.append(data)
written = self.serial.write(kiss_frame)
if written != len(kiss_frame):
if self.flow_control:
self.interface_ready = True
raise IOError("AX.25 interface only wrote "+str(written)+" bytes of "+str(len(kiss_frame)))
else:
self.queue(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 queue(self, data):
self.packet_queue.append(data)
def readLoop(self):
try:
in_frame = False
escape = False
command = KISS.CMD_UNKNOWN
data_buffer = b""
last_read_ms = int(time.time()*1000)
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
while self.serial.is_open:
if self.serial.in_waiting:
byte = ord(self.serial.read(1))
last_read_ms = int(time.time()*1000)
def readLoop(self):
try:
in_frame = False
escape = False
command = KISS.CMD_UNKNOWN
data_buffer = b""
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+AX25.HEADER_SIZE):
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()
else:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
data_buffer = b""
in_frame = False
command = KISS.CMD_UNKNOWN
escape = False
sleep(0.05)
while self.serial.is_open:
if self.serial.in_waiting:
byte = ord(self.serial.read(1))
last_read_ms = int(time.time()*1000)
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 (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) < RNS.Reticulum.MTU+AX25.HEADER_SIZE):
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):
# TODO: add timeout and reset if ready
# command never arrives
self.process_queue()
else:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
data_buffer = b""
in_frame = False
command = KISS.CMD_UNKNOWN
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()
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.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR)
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
def __str__(self):
return "AX25KISSInterface["+self.name+"]"
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 __str__(self):
return "AX25KISSInterface["+self.name+"]"
+355
View File
@@ -0,0 +1,355 @@
# 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 .Interface import Interface
import socketserver
import threading
import socket
import struct
import time
import sys
import RNS
class AutoInterface(Interface):
DEFAULT_DISCOVERY_PORT = 29716
DEFAULT_DATA_PORT = 42671
DEFAULT_GROUP_ID = "reticulum".encode("utf-8")
SCOPE_LINK = "2"
SCOPE_ADMIN = "4"
SCOPE_SITE = "5"
SCOPE_ORGANISATION = "8"
SCOPE_GLOBAL = "e"
PEERING_TIMEOUT = 7.5
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()
self.netifaces = netifaces
self.rxb = 0
self.txb = 0
self.HW_MTU = 1064
self.IN = True
self.OUT = False
self.name = name
self.online = False
self.peers = {}
self.link_local_addresses = []
self.adopted_interfaces = {}
self.multicast_echoes = {}
self.timed_out_interfaces = {}
self.outbound_udp_socket = 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
if allowed_interfaces == None:
self.allowed_interfaces = []
else:
self.allowed_interfaces = allowed_interfaces
if ignored_interfaces == None:
self.ignored_interfaces = []
else:
self.ignored_interfaces = ignored_interfaces
if group_id == None:
self.group_id = AutoInterface.DEFAULT_GROUP_ID
else:
self.group_id = group_id.encode("utf-8")
if discovery_port == None:
self.discovery_port = AutoInterface.DEFAULT_DISCOVERY_PORT
else:
self.discovery_port = discovery_port
if data_port == None:
self.data_port = AutoInterface.DEFAULT_DATA_PORT
else:
self.data_port = data_port
if discovery_scope == None:
self.discovery_scope = AutoInterface.SCOPE_LINK
elif str(discovery_scope).lower() == "link":
self.discovery_scope = AutoInterface.SCOPE_LINK
elif str(discovery_scope).lower() == "admin":
self.discovery_scope = AutoInterface.SCOPE_ADMIN
elif str(discovery_scope).lower() == "site":
self.discovery_scope = AutoInterface.SCOPE_SITE
elif str(discovery_scope).lower() == "organisation":
self.discovery_scope = AutoInterface.SCOPE_ORGANISATION
elif str(discovery_scope).lower() == "global":
self.discovery_scope = AutoInterface.SCOPE_GLOBAL
self.group_hash = RNS.Identity.full_hash(self.group_id)
g = self.group_hash
#gt = "{:02x}".format(g[1]+(g[0]<<8))
gt = "0"
gt += ":"+"{:02x}".format(g[3]+(g[2]<<8))
gt += ":"+"{:02x}".format(g[5]+(g[4]<<8))
gt += ":"+"{:02x}".format(g[7]+(g[6]<<8))
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
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)
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 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))
# Set up multicast socket
discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, if_struct)
# Join multicast group
mcast_group = socket.inet_pton(socket.AF_INET6, mcast_addr) + if_struct
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mcast_group)
# Bind socket
addr_info = socket.getaddrinfo(mcast_addr+"%"+ifname, 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.setDaemon(True)
thread.start()
suitable_interfaces += 1
if suitable_interfaces == 0:
RNS.log(str(self)+" could not autoconfigure. This interface currently provides no connectivity.", RNS.LOG_WARNING)
else:
self.receives = True
peering_wait = self.announce_interval*1.2
RNS.log(str(self)+" discovering peers for "+str(round(peering_wait, 2))+" seconds...", RNS.LOG_VERBOSE)
def handlerFactory(callback):
def createHandler(*args, **keys):
return AutoInterfaceHandler(callback, *args, **keys)
return createHandler
self.owner = owner
socketserver.UDPServer.address_family = socket.AF_INET6
for ifname in self.adopted_interfaces:
local_addr = self.adopted_interfaces[ifname]+"%"+ifname
addr_info = socket.getaddrinfo(local_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
address = addr_info[0][4]
self.server = socketserver.UDPServer(address, handlerFactory(self.processIncoming))
thread = threading.Thread(target=self.server.serve_forever)
thread.setDaemon(True)
thread.start()
job_thread = threading.Thread(target=self.peer_jobs)
job_thread.setDaemon(True)
job_thread.start()
time.sleep(peering_wait)
if configured_bitrate != None:
self.bitrate = configured_bitrate
else:
self.bitrate = AutoInterface.BITRATE_GUESS
self.online = True
def discovery_handler(self, socket, ifname):
def announce_loop():
self.announce_handler(ifname)
thread = threading.Thread(target=announce_loop)
thread.setDaemon(True)
thread.start()
while True:
data, ipv6_src = socket.recvfrom(1024)
expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
if data == expected_hash:
self.add_peer(ipv6_src[0], ifname)
else:
RNS.log(str(self)+" received peering packet on "+str(ifname)+" from "+str(ipv6_src[0])+", but authentication hash was incorrect.", RNS.LOG_DEBUG)
def peer_jobs(self):
while True:
time.sleep(self.peer_job_interval)
now = time.time()
timed_out_peers = []
# Check for timed out peers
for peer_addr in self.peers:
peer = self.peers[peer_addr]
last_heard = peer[1]
if now > last_heard+self.peering_timeout:
timed_out_peers.append(peer_addr)
# Remove any timed out peers
for peer_addr in timed_out_peers:
removed_peer = self.peers.pop(peer_addr)
RNS.log(str(self)+" removed peer "+str(peer_addr)+" on "+str(removed_peer[0]), RNS.LOG_DEBUG)
for ifname in self.adopted_interfaces:
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:
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:
RNS.log(str(self)+" Carrier recovered on "+str(ifname), RNS.LOG_WARNING)
self.timed_out_interfaces[ifname] = False
def announce_handler(self, ifname):
while True:
self.peer_announce(ifname)
time.sleep(self.announce_interval)
def peer_announce(self, ifname):
try:
link_local_address = self.adopted_interfaces[ifname]
discovery_token = RNS.Identity.full_hash(self.group_id+link_local_address.encode("utf-8"))
announce_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
addr_info = socket.getaddrinfo(self.mcast_discovery_address, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
ifis = struct.pack("I", socket.if_nametoindex(ifname))
announce_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifis)
announce_socket.sendto(discovery_token, addr_info[0][4])
except Exception as e:
if (ifname in self.timed_out_interfaces and self.timed_out_interfaces[ifname] == False) or not ifname in self.timed_out_interfaces:
RNS.log(str(self)+" Detected possible carrier loss on "+str(ifname)+": "+str(e), RNS.LOG_WARNING)
else:
pass
def add_peer(self, addr, ifname):
if addr in self.link_local_addresses:
ifname = None
for interface_name in self.adopted_interfaces:
if self.adopted_interfaces[interface_name] == addr:
ifname = interface_name
if ifname != None:
self.multicast_echoes[ifname] = time.time()
else:
RNS.log(str(self)+" received multicast echo on unexpected interface "+str(ifname), RNS.LOG_WARNING)
else:
if not addr in self.peers:
self.peers[addr] = [ifname, time.time()]
RNS.log(str(self)+" added peer "+str(addr)+" on "+str(ifname), RNS.LOG_DEBUG)
else:
self.refresh_peer(addr)
def refresh_peer(self, addr):
self.peers[addr][1] = time.time()
def processIncoming(self, data):
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])
addr_info = socket.getaddrinfo(peer_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
self.outbound_udp_socket.sendto(data, addr_info[0][4])
except Exception as e:
RNS.log("Could not transmit on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
self.txb += len(data)
def __str__(self):
return "AutoInterface["+self.name+"]"
class AutoInterfaceHandler(socketserver.BaseRequestHandler):
def __init__(self, callback, *args, **keys):
self.callback = callback
socketserver.BaseRequestHandler.__init__(self, *args, **keys)
def handle(self):
data = self.request[0]
self.callback(data)
+786
View File
@@ -0,0 +1,786 @@
# 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 .Interface import Interface
import socketserver
import threading
import platform
import socket
import time
import sys
import os
import RNS
import asyncio
class HDLC():
FLAG = 0x7E
ESC = 0x7D
ESC_MASK = 0x20
@staticmethod
def escape(data):
data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
return data
class KISS():
FEND = 0xC0
FESC = 0xDB
TFEND = 0xDC
TFESC = 0xDD
CMD_DATA = 0x00
CMD_UNKNOWN = 0xFE
@staticmethod
def escape(data):
data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
return data
# TODO: Neater shutdown of the event loop and
# better error handling is needed. Sometimes
# errors occur in I2P that leave tunnel setup
# hanging indefinitely, and right now we have
# no way of catching it. Sometimes the server
# and client tasks are also not cancelled on
# shutdown, which leads to errors dumped to
# the console. This should also be remedied.
class I2PController:
def __init__(self, rns_storagepath):
import RNS.vendor.i2plib as i2plib
import RNS.vendor.i2plib.utils
self.client_tunnels = {}
self.server_tunnels = {}
self.i2plib_tunnels = {}
self.loop = None
self.i2plib = i2plib
self.utils = i2plib.utils
self.sam_address = i2plib.get_sam_address()
self.ready = False
self.storagepath = rns_storagepath+"/i2p"
if not os.path.isdir(self.storagepath):
os.makedirs(self.storagepath)
def start(self):
asyncio.set_event_loop(asyncio.new_event_loop())
self.loop = asyncio.get_event_loop()
time.sleep(0.10)
if self.loop == None:
RNS.log("Could not get event loop for "+str(self)+", waiting for event loop to appear", RNS.LOG_VERBOSE)
while self.loop == None:
self.loop = asyncio.get_event_loop()
sleep(0.25)
try:
self.ready = True
self.loop.run_forever()
except Exception as e:
self.ready = False
RNS.log("Exception on event loop for "+str(self)+": "+str(e), RNS.LOG_ERROR)
finally:
self.loop.close()
def stop(self):
for task in asyncio.Task.all_tasks(loop=self.loop):
task.cancel()
self.loop.stop()
def get_free_port(self):
return self.i2plib.utils.get_free_port()
def client_tunnel(self, owner, i2p_destination):
self.client_tunnels[i2p_destination] = False
self.i2plib_tunnels[i2p_destination] = None
while True:
if not self.client_tunnels[i2p_destination]:
try:
async def tunnel_up():
RNS.log("Bringing up I2P tunnel to "+str(owner)+", this may take a while...", RNS.LOG_INFO)
tunnel = self.i2plib.ClientTunnel(i2p_destination, owner.local_addr, sam_address=self.sam_address, loop=self.loop)
self.i2plib_tunnels[i2p_destination] = tunnel
await tunnel.run()
self.loop.ext_owner = self
result = asyncio.run_coroutine_threadsafe(tunnel_up(), self.loop).result()
if not i2p_destination in self.i2plib_tunnels:
raise IOError("No tunnel control instance was created")
else:
tn = self.i2plib_tunnels[i2p_destination]
if tn != None and hasattr(tn, "status"):
RNS.log("Waiting for status from I2P control process", RNS.LOG_EXTREME)
while not tn.status["setup_ran"]:
time.sleep(0.1)
RNS.log("Got status from I2P control process", RNS.LOG_EXTREME)
if tn.status["setup_failed"]:
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):
try:
owner.socket.shutdown(socket.SHUT_RDWR)
except Exception as e:
RNS.log("Error while shutting down socket for "+str(owner)+": "+str(e))
try:
owner.socket.close()
except Exception as e:
RNS.log("Error while closing socket for "+str(owner)+": "+str(e))
RNS.log(str(owner)+" tunnel setup complete", RNS.LOG_VERBOSE)
else:
raise IOError("Got no status response from SAM API")
except ConnectionRefusedError as e:
raise e
except ConnectionAbortedError as e:
raise e
except Exception as e:
RNS.log("Unexpected error type from I2P SAM: "+str(e), RNS.LOG_ERROR)
raise e
else:
i2ptunnel = self.i2plib_tunnels[i2p_destination]
if hasattr(i2ptunnel, "status"):
# TODO: Remove
# RNS.log(str(i2ptunnel.status))
i2p_exception = i2ptunnel.status["exception"]
if i2ptunnel.status["setup_ran"] == False:
RNS.log(str(self)+" I2P tunnel setup did not complete", RNS.LOG_ERROR)
return False
elif i2p_exception != None:
RNS.log(str(self)+" An error ocurred while setting up I2P tunnel. The contained exception was: "+str(i2p_exception), RNS.LOG_ERROR)
RNS.log("Resetting I2P tunnel", RNS.LOG_ERROR)
return False
elif i2ptunnel.status["setup_failed"] == True:
RNS.log(str(self)+" Unspecified I2P tunnel setup error, resetting I2P tunnel", RNS.LOG_ERROR)
return False
else:
RNS.log(str(self)+" Got no status from SAM API, resetting I2P tunnel", RNS.LOG_ERROR)
return False
time.sleep(5)
def server_tunnel(self, owner):
while RNS.Transport.identity == None:
time.sleep(1)
# Old format
i2p_dest_hash_of = RNS.Identity.full_hash(RNS.Identity.full_hash(owner.name.encode("utf-8")))
i2p_keyfile_of = self.storagepath+"/"+RNS.hexrep(i2p_dest_hash_of, delimit=False)+".i2p"
# New format
i2p_dest_hash_nf = RNS.Identity.full_hash(RNS.Identity.full_hash(owner.name.encode("utf-8"))+RNS.Identity.full_hash(RNS.Transport.identity.hash))
i2p_keyfile_nf = self.storagepath+"/"+RNS.hexrep(i2p_dest_hash_nf, delimit=False)+".i2p"
# Use old format if a key is already present
if os.path.isfile(i2p_keyfile_of):
i2p_keyfile = i2p_keyfile_of
else:
i2p_keyfile = i2p_keyfile_nf
i2p_dest = None
if not os.path.isfile(i2p_keyfile):
coro = self.i2plib.new_destination(sam_address=self.sam_address, loop=self.loop)
i2p_dest = asyncio.run_coroutine_threadsafe(coro, self.loop).result()
key_file = open(i2p_keyfile, "w")
key_file.write(i2p_dest.private_key.base64)
key_file.close()
else:
key_file = open(i2p_keyfile, "r")
prvd = key_file.read()
key_file.close()
i2p_dest = self.i2plib.Destination(data=prvd, has_private_key=True)
i2p_b32 = i2p_dest.base32
owner.b32 = i2p_b32
self.server_tunnels[i2p_b32] = False
self.i2plib_tunnels[i2p_b32] = None
while True:
if self.server_tunnels[i2p_b32] == False:
try:
async def tunnel_up():
RNS.log(str(owner)+" Bringing up I2P endpoint, this may take a while...", RNS.LOG_INFO)
tunnel = self.i2plib.ServerTunnel((owner.bind_ip, owner.bind_port), loop=self.loop, destination=i2p_dest, sam_address=self.sam_address)
self.i2plib_tunnels[i2p_b32] = tunnel
await tunnel.run()
RNS.log(str(owner)+ " endpoint setup complete. Now reachable at: "+str(i2p_dest.base32)+".b32.i2p", RNS.LOG_VERBOSE)
asyncio.run_coroutine_threadsafe(tunnel_up(), self.loop).result()
self.server_tunnels[i2p_b32] = True
except Exception as e:
raise e
else:
i2ptunnel = self.i2plib_tunnels[i2p_b32]
if hasattr(i2ptunnel, "status"):
# TODO: Remove
# RNS.log(str(i2ptunnel.status))
i2p_exception = i2ptunnel.status["exception"]
if i2ptunnel.status["setup_ran"] == False:
RNS.log(str(self)+" I2P tunnel setup did not complete", RNS.LOG_ERROR)
return False
elif i2p_exception != None:
RNS.log(str(self)+" An error ocurred while setting up I2P tunnel. The contained exception was: "+str(i2p_exception), RNS.LOG_ERROR)
RNS.log("Resetting I2P tunnel", RNS.LOG_ERROR)
return False
elif i2ptunnel.status["setup_failed"] == True:
RNS.log(str(self)+" Unspecified I2P tunnel setup error, resetting I2P tunnel", RNS.LOG_ERROR)
return False
else:
RNS.log(str(self)+" Got no status from SAM API, resetting I2P tunnel", RNS.LOG_ERROR)
return False
time.sleep(5)
def get_loop(self):
return asyncio.get_event_loop()
class ThreadingI2PServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
class I2PInterfacePeer(Interface):
RECONNECT_WAIT = 15
RECONNECT_MAX_TRIES = None
# TCP socket options
I2P_USER_TIMEOUT = 40
I2P_PROBE_AFTER = 10
I2P_PROBE_INTERVAL = 5
I2P_PROBES = 6
def __init__(self, parent_interface, owner, name, target_i2p_dest=None, connected_socket=None, max_reconnect_tries=None):
self.rxb = 0
self.txb = 0
self.HW_MTU = 1064
self.IN = True
self.OUT = False
self.socket = None
self.parent_interface = parent_interface
self.parent_count = True
self.name = name
self.initiator = False
self.reconnecting = False
self.never_connected = True
self.owner = owner
self.writing = False
self.online = False
self.detached = False
self.kiss_framing = False
self.i2p_tunneled = True
self.i2p_dest = None
self.i2p_tunnel_ready = False
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
self.bitrate = I2PInterface.BITRATE_GUESS
self.announce_rate_target = None
self.announce_rate_grace = None
self.announce_rate_penalty = None
if max_reconnect_tries == None:
self.max_reconnect_tries = I2PInterfacePeer.RECONNECT_MAX_TRIES
else:
self.max_reconnect_tries = max_reconnect_tries
if connected_socket != None:
self.receives = True
self.target_ip = None
self.target_port = None
self.socket = connected_socket
if platform.system() == "Linux":
self.set_timeouts_linux()
elif platform.system() == "Darwin":
self.set_timeouts_osx()
elif target_i2p_dest != None:
self.receives = True
self.initiator = True
self.bind_ip = "127.0.0.1"
self.awaiting_i2p_tunnel = True
def tunnel_job():
while self.awaiting_i2p_tunnel:
try:
self.bind_port = self.parent_interface.i2p.get_free_port()
self.local_addr = (self.bind_ip, self.bind_port)
self.target_ip = self.bind_ip
self.target_port = self.bind_port
if not self.parent_interface.i2p.client_tunnel(self, target_i2p_dest):
RNS.log(str(self)+" I2P control process experienced an error, requesting new tunnel...", RNS.LOG_ERROR)
self.awaiting_i2p_tunnel = True
except Exception as e:
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)
thread = threading.Thread(target=tunnel_job)
thread.setDaemon(True)
thread.start()
def wait_job():
while self.awaiting_i2p_tunnel:
time.sleep(0.25)
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.start()
else:
thread = threading.Thread(target=self.read_loop)
thread.setDaemon(True)
thread.start()
thread = threading.Thread(target=wait_job)
thread.setDaemon(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))
def set_timeouts_osx(self):
if hasattr(socket, "TCP_KEEPALIVE"):
TCP_KEEPIDLE = socket.TCP_KEEPALIVE
else:
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))
def detach(self):
if self.socket != None:
if hasattr(self.socket, "close"):
if callable(self.socket.close):
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
self.detached = True
try:
self.socket.shutdown(socket.SHUT_RDWR)
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))
self.socket = None
def connect(self, initial=False):
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((self.target_ip, self.target_port))
self.online = True
except Exception as e:
if initial:
if not self.awaiting_i2p_tunnel:
RNS.log("Initial connection for "+str(self)+" could not be established: "+str(e), RNS.LOG_ERROR)
RNS.log("Leaving unconnected and retrying connection in "+str(I2PInterfacePeer.RECONNECT_WAIT)+" seconds.", RNS.LOG_ERROR)
return False
else:
raise e
if platform.system() == "Linux":
self.set_timeouts_linux()
elif platform.system() == "Darwin":
self.set_timeouts_osx()
self.online = True
self.writing = False
self.never_connected = False
if not self.kiss_framing and self.wants_tunnel:
RNS.Transport.synthesize_tunnel(self)
return True
def reconnect(self):
if self.initiator:
if not self.reconnecting:
self.reconnecting = True
attempts = 0
while not self.online:
time.sleep(I2PInterfacePeer.RECONNECT_WAIT)
attempts += 1
if self.max_reconnect_tries != None and attempts > self.max_reconnect_tries:
RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_ERROR)
self.teardown()
break
try:
self.connect()
except Exception as e:
if not self.awaiting_i2p_tunnel:
RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
else:
RNS.log(str(self)+" still waiting for I2P tunnel to appear", RNS.LOG_VERBOSE)
if not self.never_connected:
RNS.log(str(self)+" Re-established connection via I2P tunnel", RNS.LOG_INFO)
self.reconnecting = False
thread = threading.Thread(target=self.read_loop)
thread.setDaemon(True)
thread.start()
if not self.kiss_framing:
RNS.Transport.synthesize_tunnel(self)
else:
RNS.log("Attempt to reconnect on a non-initiator I2P interface. This should not happen.", RNS.LOG_ERROR)
raise IOError("Attempt to reconnect on a non-initiator I2P interface")
def processIncoming(self, data):
self.rxb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None and self.parent_count:
self.parent_interface.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self, data):
if self.online:
while self.writing:
time.sleep(0.01)
try:
self.writing = True
if self.kiss_framing:
data = bytes([KISS.FEND])+bytes([KISS.CMD_DATA])+KISS.escape(data)+bytes([KISS.FEND])
else:
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
self.socket.sendall(data)
self.writing = False
self.txb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None and self.parent_count:
self.parent_interface.txb += len(data)
except Exception as e:
RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
self.teardown()
def read_loop(self):
try:
in_frame = False
escape = False
data_buffer = b""
command = KISS.CMD_UNKNOWN
while True:
data_in = self.socket.recv(4096)
if len(data_in) > 0:
pointer = 0
while pointer < len(data_in):
byte = data_in[pointer]
pointer += 1
if self.kiss_framing:
# Read loop for KISS framing
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])
else:
# Read loop for HDLC framing
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.processIncoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
elif (in_frame and len(data_buffer) < self.HW_MTU):
if (byte == HDLC.ESC):
escape = True
else:
if (escape):
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
byte = HDLC.FLAG
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
byte = HDLC.ESC
escape = False
data_buffer = data_buffer+bytes([byte])
else:
self.online = False
if self.initiator and not self.detached:
RNS.log("Socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
self.reconnect()
else:
RNS.log("Socket for remote client "+str(self)+" was closed.", RNS.LOG_VERBOSE)
self.teardown()
break
except Exception as e:
self.online = False
RNS.log("An interface error occurred for "+str(self)+", the contained exception was: "+str(e), RNS.LOG_WARNING)
if self.initiator:
RNS.log("Attempting to reconnect...", RNS.LOG_WARNING)
self.reconnect()
else:
self.teardown()
def teardown(self):
if self.initiator and not self.detached:
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error:
RNS.panic()
else:
RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_VERBOSE)
self.online = False
self.OUT = False
self.IN = False
if hasattr(self, "parent_interface") and self.parent_interface != None:
if self.parent_interface.clients > 0:
self.parent_interface.clients -= 1
if self in RNS.Transport.interfaces:
if not self.initiator:
RNS.Transport.interfaces.remove(self)
def __str__(self):
return "I2PInterfacePeer["+str(self.name)+"]"
class I2PInterface(Interface):
BITRATE_GUESS = 256*1000
def __init__(self, owner, name, rns_storagepath, peers, connectable = False):
self.rxb = 0
self.txb = 0
self.HW_MTU = 1064
self.online = False
self.clients = 0
self.owner = owner
self.connectable = connectable
self.i2p_tunneled = True
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
self.b32 = None
self.i2p = I2PController(rns_storagepath)
self.IN = True
self.OUT = False
self.name = name
self.receives = True
self.bind_ip = "127.0.0.1"
self.bind_port = self.i2p.get_free_port()
self.address = (self.bind_ip, self.bind_port)
self.bitrate = I2PInterface.BITRATE_GUESS
self.online = False
i2p_thread = threading.Thread(target=self.i2p.start)
i2p_thread.setDaemon(True)
i2p_thread.start()
i2p_notready_warning = False
time.sleep(0.25)
if not self.i2p.ready:
RNS.log("I2P controller did not become available in time, waiting for controller", RNS.LOG_VERBOSE)
i2p_notready_warning = True
while not self.i2p.ready:
time.sleep(0.25)
if i2p_notready_warning == True:
RNS.log("I2P controller ready, continuing setup", RNS.LOG_VERBOSE)
def handlerFactory(callback):
def createHandler(*args, **keys):
return I2PInterfaceHandler(callback, *args, **keys)
return createHandler
ThreadingI2PServer.allow_reuse_address = True
self.server = ThreadingI2PServer(self.address, handlerFactory(self.incoming_connection))
thread = threading.Thread(target=self.server.serve_forever)
thread.setDaemon(True)
thread.start()
if self.connectable:
def tunnel_job():
while True:
try:
if not self.i2p.server_tunnel(self):
RNS.log(str(self)+" I2P control process experienced an error, requesting new tunnel...", RNS.LOG_ERROR)
self.online = False
except Exception as e:
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)
thread = threading.Thread(target=tunnel_job)
thread.setDaemon(True)
thread.start()
if peers != None:
for peer_addr in peers:
interface_name = self.name+" to "+peer_addr
peer_interface = I2PInterfacePeer(self, self.owner, interface_name, peer_addr)
peer_interface.OUT = True
peer_interface.IN = True
peer_interface.parent_interface = self
peer_interface.parent_count = False
RNS.Transport.interfaces.append(peer_interface)
def incoming_connection(self, handler):
RNS.log("Accepting incoming I2P connection", RNS.LOG_VERBOSE)
interface_name = "Connected peer on "+self.name
spawned_interface = I2PInterfacePeer(self, self.owner, interface_name, connected_socket=handler.request)
spawned_interface.OUT = True
spawned_interface.IN = True
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
spawned_interface.announce_rate_target = self.announce_rate_target
spawned_interface.announce_rate_grace = self.announce_rate_grace
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
spawned_interface.mode = self.mode
spawned_interface.HW_MTU = self.HW_MTU
RNS.log("Spawned new I2PInterface Peer: "+str(spawned_interface), RNS.LOG_VERBOSE)
RNS.Transport.interfaces.append(spawned_interface)
self.clients += 1
spawned_interface.read_loop()
def processOutgoing(self, data):
pass
def detach(self):
self.i2p.stop()
def __str__(self):
return "I2PInterface["+self.name+"]"
class I2PInterfaceHandler(socketserver.BaseRequestHandler):
def __init__(self, callback, *args, **keys):
self.callback = callback
socketserver.BaseRequestHandler.__init__(self, *args, **keys)
def handle(self):
self.callback(handler=self)
+84 -3
View File
@@ -1,4 +1,28 @@
# 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 RNS
import time
import threading
class Interface:
IN = False
@@ -7,9 +31,66 @@ class Interface:
RPT = False
name = None
# Interface mode definitions
MODE_FULL = 0x01
MODE_POINT_TO_POINT = 0x02
MODE_ACCESS_POINT = 0x03
MODE_ROAMING = 0x04
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]
def __init__(self):
pass
self.rxb = 0
self.txb = 0
self.online = False
def get_hash(self):
# TODO: Maybe expand this to something more unique
return RNS.Identity.fullHash(str(self).encode("utf-8"))
return RNS.Identity.full_hash(str(self).encode("utf-8"))
def process_announce_queue(self):
if not hasattr(self, "announce_cap"):
self.announce_cap = RNS.Reticulum.ANNOUNCE_CAP
if hasattr(self, "announce_queue"):
try:
now = time.time()
stale = []
for a in self.announce_queue:
if now > a["time"]+RNS.Reticulum.QUEUED_ANNOUNCE_LIFE:
stale.append(a)
for s in stale:
if s in self.announce_queue:
self.announce_queue.remove(s)
if len(self.announce_queue) > 0:
min_hops = min(entry["hops"] for entry in self.announce_queue)
entries = list(filter(lambda e: e["hops"] == min_hops, self.announce_queue))
entries.sort(key=lambda e: e["time"])
selected = entries[0]
now = time.time()
tx_time = (len(selected["raw"])*8) / self.bitrate
wait_time = (tx_time / self.announce_cap)
self.announce_allowed_at = now + wait_time
self.processOutgoing(selected["raw"])
if selected in self.announce_queue:
self.announce_queue.remove(selected)
if len(self.announce_queue) > 0:
timer = threading.Timer(wait_time, self.process_announce_queue)
timer.start()
except Exception as e:
self.announce_queue = []
RNS.log("Error while processing announce queue on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("The announce queue for this interface has been cleared.", RNS.LOG_ERROR)
def detach(self):
pass
+318 -221
View File
@@ -1,256 +1,353 @@
# 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 .Interface import Interface
from time import sleep
import sys
import serial
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
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
@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
MAX_CHUNK = 32768
BITRATE_GUESS = 1200
owner = None
port = None
speed = None
databits = None
parity = None
stopbits = None
serial = None
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):
self.serial = None
self.owner = owner
self.name = name
self.port = port
self.speed = speed
self.databits = databits
self.parity = serial.PARITY_NONE
self.stopbits = stopbits
self.timeout = 100
self.online = False
def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control, beacon_interval, beacon_data):
import importlib
if importlib.util.find_spec('serial') != None:
import serial
else:
RNS.log("Using the KISS interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
RNS.panic()
self.packet_queue = []
self.flow_control = flow_control
self.interface_ready = False
self.rxb = 0
self.txb = 0
self.HW_MTU = 564
if beacon_data == None:
beacon_data = ""
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;
self.pyserial = serial
self.serial = None
self.owner = owner
self.name = name
self.port = port
self.speed = speed
self.databits = databits
self.parity = serial.PARITY_NONE
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
if parity.lower() == "e" or parity.lower() == "even":
self.parity = serial.PARITY_EVEN
self.packet_queue = []
self.flow_control = flow_control
self.interface_ready = False
self.flow_control_timeout = 5
self.flow_control_locked = time.time()
if parity.lower() == "o" or parity.lower() == "odd":
self.parity = serial.PARITY_ODD
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;
try:
RNS.log("Opening serial port "+self.port+"...")
self.serial = serial.Serial(
port = self.port,
baudrate = self.speed,
bytesize = self.databits,
parity = self.parity,
stopbits = self.stopbits,
xonxoff = False,
rtscts = False,
timeout = 0,
inter_byte_timeout = None,
write_timeout = None,
dsrdtr = False,
)
except Exception as e:
RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR)
raise e
if parity.lower() == "e" or parity.lower() == "even":
self.parity = serial.PARITY_EVEN
if self.serial.is_open:
# Allow time for interface to initialise before config
sleep(2.0)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(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")
else:
raise IOError("Could not open serial port")
if parity.lower() == "o" or parity.lower() == "odd":
self.parity = serial.PARITY_ODD
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 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 open_port(self):
RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE)
self.serial = self.pyserial.Serial(
port = self.port,
baudrate = self.speed,
bytesize = self.databits,
parity = self.parity,
stopbits = self.stopbits,
xonxoff = False,
rtscts = False,
timeout = 0,
inter_byte_timeout = None,
write_timeout = None,
dsrdtr = False,
)
def processIncoming(self, data):
self.owner.inbound(data, self)
def configure_device(self):
# Allow time for interface to initialise before config
sleep(2.0)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(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 processOutgoing(self,data):
if self.online:
if self.interface_ready:
if self.flow_control:
self.interface_ready = False
def setPreamble(self, preamble):
preamble_ms = preamble
preamble = int(preamble_ms / 10)
if preamble < 0:
preamble = 0
if preamble > 255:
preamble = 255
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])
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)+")")
written = self.serial.write(frame)
if written != len(frame):
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
def setTxTail(self, txtail):
txtail_ms = txtail
txtail = int(txtail_ms / 10)
if txtail < 0:
txtail = 0
if txtail > 255:
txtail = 255
else:
self.queue(data)
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 queue(self, data):
self.packet_queue.append(data)
def setPersistence(self, persistence):
if persistence < 0:
persistence = 0
if persistence > 255:
persistence = 255
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
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 readLoop(self):
try:
in_frame = False
escape = False
command = KISS.CMD_UNKNOWN
data_buffer = b""
last_read_ms = int(time.time()*1000)
def setSlotTime(self, slottime):
slottime_ms = slottime
slottime = int(slottime_ms / 10)
if slottime < 0:
slottime = 0
if slottime > 255:
slottime = 255
while self.serial.is_open:
if self.serial.in_waiting:
byte = ord(self.serial.read(1))
last_read_ms = int(time.time()*1000)
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)+")")
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) < RNS.Reticulum.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):
# TODO: add timeout and reset if ready
# command never arrives
self.process_queue()
else:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
data_buffer = b""
in_frame = False
command = KISS.CMD_UNKNOWN
escape = False
sleep(0.08)
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")
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.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR)
def __str__(self):
return "KISSInterface["+self.name+"]"
def processIncoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self)
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:
if self.serial.in_waiting:
byte = ord(self.serial.read(1))
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()
else:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
data_buffer = b""
in_frame = False
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 __str__(self):
return "KISSInterface["+self.name+"]"
+154 -11
View File
@@ -1,3 +1,25 @@
# 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 .Interface import Interface
import socketserver
import threading
@@ -22,13 +44,25 @@ class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
class LocalClientInterface(Interface):
RECONNECT_WAIT = 3
def __init__(self, owner, name, target_port = None, connected_socket=None):
self.rxb = 0
self.txb = 0
self.HW_MTU = 1064
self.online = False
self.IN = True
self.OUT = False
self.socket = None
self.parent_interface = None
self.reconnecting = False
self.never_connected = True
self.detached = False
self.name = name
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
if connected_socket != None:
self.receives = True
@@ -36,28 +70,77 @@ class LocalClientInterface(Interface):
self.target_port = None
self.socket = connected_socket
self.is_connected_to_shared_instance = False
elif target_port != None:
self.receives = True
self.target_ip = "127.0.0.1"
self.target_port = target_port
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((self.target_ip, self.target_port))
self.is_connected_to_shared_instance = True
self.connect()
self.owner = owner
self.bitrate = 1000*1000*1000
self.online = True
self.writing = 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.start()
def connect(self):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((self.target_ip, self.target_port))
self.online = True
self.is_connected_to_shared_instance = True
self.never_connected = False
return True
def reconnect(self):
if self.is_connected_to_shared_instance:
if not self.reconnecting:
self.reconnecting = True
attempts = 0
while not self.online:
time.sleep(LocalClientInterface.RECONNECT_WAIT)
attempts += 1
try:
self.connect()
except Exception as e:
RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
if not self.never_connected:
RNS.log("Reconnected TCP socket for "+str(self)+".", RNS.LOG_INFO)
self.reconnecting = False
thread = threading.Thread(target=self.read_loop)
thread.setDaemon(True)
thread.start()
RNS.Transport.shared_connection_reappeared()
else:
RNS.log("Attempt to reconnect on a non-initiator shared local interface. This should not happen.", RNS.LOG_ERROR)
raise IOError("Attempt to reconnect on a non-initiator local interface")
def processIncoming(self, data):
self.rxb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self, data):
if self.online:
while self.writing:
@@ -68,6 +151,10 @@ class LocalClientInterface(Interface):
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
self.socket.sendall(data)
self.writing = False
self.txb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.txb += len(data)
except Exception as e:
RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
@@ -93,7 +180,7 @@ class LocalClientInterface(Interface):
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU):
elif (in_frame and len(data_buffer) < self.HW_MTU):
if (byte == HDLC.ESC):
escape = True
else:
@@ -105,8 +192,14 @@ class LocalClientInterface(Interface):
escape = False
data_buffer = data_buffer+bytes([byte])
else:
RNS.log("Socket for "+str(self)+" was closed, tearing down interface", RNS.LOG_VERBOSE)
self.teardown()
self.online = False
if self.is_connected_to_shared_instance and not self.detached:
RNS.log("Socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
RNS.Transport.shared_connection_disappeared()
self.reconnect()
else:
self.teardown(nowarning=True)
break
@@ -116,7 +209,26 @@ class LocalClientInterface(Interface):
RNS.log("Tearing down "+str(self), RNS.LOG_ERROR)
self.teardown()
def teardown(self):
def detach(self):
if self.socket != None:
if hasattr(self.socket, "close"):
if callable(self.socket.close):
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
self.detached = True
try:
self.socket.shutdown(socket.SHUT_RDWR)
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))
self.socket = None
def teardown(self, nowarning=False):
self.online = False
self.OUT = False
self.IN = False
@@ -126,6 +238,19 @@ class LocalClientInterface(Interface):
if self in RNS.Transport.local_client_interfaces:
RNS.Transport.local_client_interfaces.remove(self)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.clients -= 1
if 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)
if RNS.Reticulum.panic_on_interface_error:
RNS.panic()
if self.is_connected_to_shared_instance:
if nowarning == False:
RNS.log("Permanently lost connection to local shared RNS instance. Exiting now.", RNS.LOG_CRITICAL)
RNS.exit()
def __str__(self):
@@ -135,9 +260,15 @@ class LocalClientInterface(Interface):
class LocalServerInterface(Interface):
def __init__(self, owner, bindport=None):
self.rxb = 0
self.txb = 0
self.online = False
self.clients = 0
self.IN = True
self.OUT = False
self.name = "Reticulum"
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
if (bindport != None):
self.receives = True
@@ -153,12 +284,22 @@ class LocalServerInterface(Interface):
self.is_local_shared_instance = True
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.start()
self.announce_rate_target = None
self.announce_rate_grace = None
self.announce_rate_penalty = None
self.bitrate = 1000*1000*1000
self.online = True
def incoming_connection(self, handler):
interface_name = str(str(handler.client_address[1]))
@@ -168,16 +309,18 @@ class LocalServerInterface(Interface):
spawned_interface.target_ip = handler.client_address[0]
spawned_interface.target_port = str(handler.client_address[1])
spawned_interface.parent_interface = self
RNS.log("Accepting new connection to shared instance: "+str(spawned_interface), RNS.LOG_VERBOSE)
spawned_interface.bitrate = self.bitrate
RNS.log("Accepting new connection to shared instance: "+str(spawned_interface), RNS.LOG_EXTREME)
RNS.Transport.interfaces.append(spawned_interface)
RNS.Transport.local_client_interfaces.append(spawned_interface)
self.clients += 1
spawned_interface.read_loop()
def processOutgoing(self, data):
pass
def __str__(self):
return "Shared Instance ["+str(self.bind_port)+"]"
return "Shared Instance["+str(self.bind_port)+"]"
class LocalInterfaceHandler(socketserver.BaseRequestHandler):
def __init__(self, callback, *args, **keys):
+189
View File
@@ -0,0 +1,189 @@
# 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 .Interface import Interface
from time import sleep
import sys
import threading
import time
import RNS
import subprocess
import shlex
class HDLC():
# The Pipe 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 PipeInterface(Interface):
MAX_CHUNK = 32768
BITRATE_GUESS = 1*1000*1000
owner = None
command = None
def __init__(self, owner, name, command, respawn_delay):
if respawn_delay == None:
respawn_delay = 5
self.rxb = 0
self.txb = 0
self.HW_MTU = 1064
self.owner = owner
self.name = name
self.command = command
self.process = None
self.timeout = 100
self.online = False
self.pipe_is_open = False
self.bitrate = PipeInterface.BITRATE_GUESS
self.respawn_delay = respawn_delay
try:
self.open_pipe()
except Exception as e:
RNS.log("Could connect pipe for interface "+str(self), RNS.LOG_ERROR)
raise e
if self.pipe_is_open:
self.configure_pipe()
else:
raise IOError("Could not connect pipe")
def open_pipe(self):
RNS.log("Connecting subprocess pipe for "+str(self)+"...", RNS.LOG_VERBOSE)
try:
self.process = subprocess.Popen(shlex.split(self.command), stdin=subprocess.PIPE, stdout=subprocess.PIPE)
self.pipe_is_open = True
except Exception as e:
raise e
self.pipe_is_open = False
def configure_pipe(self):
sleep(0.01)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True)
thread.start()
self.online = True
RNS.log("Subprocess pipe for "+str(self)+" is now connected", RNS.LOG_VERBOSE)
def processIncoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self,data):
if self.online:
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
written = self.process.stdin.write(data)
self.process.stdin.flush()
self.txb += len(data)
if written != len(data):
raise IOError("Pipe 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 True:
process_output = self.process.stdout.read(1)
if len(process_output) == 0 and self.process.poll() is not None:
break
else:
byte = ord(process_output)
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])
RNS.log("Subprocess terminated on "+str(self))
self.process.kill()
except Exception as e:
self.online = False
try:
self.process.kill()
except Exception as e:
pass
RNS.log("A pipe 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.reconnect_pipe()
def reconnect_pipe(self):
while not self.online:
try:
time.sleep(self.respawn_delay)
RNS.log("Attempting to respawn subprocess for "+str(self)+"...", RNS.LOG_VERBOSE)
self.open_pipe()
if self.pipe_is_open:
self.configure_pipe()
except Exception as e:
RNS.log("Error while spawning subprocess, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("Reconnected pipe for "+str(self))
def __str__(self):
return "PipeInterface["+self.name+"]"
File diff suppressed because it is too large Load Diff
+179 -110
View File
@@ -1,136 +1,205 @@
# 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 .Interface import Interface
from time import sleep
import sys
import serial
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
# 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
@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
MAX_CHUNK = 32768
owner = None
port = None
speed = None
databits = None
parity = None
stopbits = None
serial = None
owner = None
port = None
speed = None
databits = None
parity = None
stopbits = None
serial = None
def __init__(self, owner, name, port, speed, databits, parity, stopbits):
self.serial = None
self.owner = owner
self.name = name
self.port = port
self.speed = speed
self.databits = databits
self.parity = serial.PARITY_NONE
self.stopbits = stopbits
self.timeout = 100
self.online = False
def __init__(self, owner, name, port, speed, databits, parity, stopbits):
import importlib
if importlib.util.find_spec('serial') != None:
import serial
else:
RNS.log("Using the Serial interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
RNS.panic()
if parity.lower() == "e" or parity.lower() == "even":
self.parity = serial.PARITY_EVEN
self.rxb = 0
self.txb = 0
if parity.lower() == "o" or parity.lower() == "odd":
self.parity = serial.PARITY_ODD
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 = serial.PARITY_NONE
self.stopbits = stopbits
self.timeout = 100
self.online = False
self.bitrate = self.speed
try:
RNS.log("Opening serial port "+self.port+"...")
self.serial = serial.Serial(
port = self.port,
baudrate = self.speed,
bytesize = self.databits,
parity = self.parity,
stopbits = self.stopbits,
xonxoff = False,
rtscts = False,
timeout = 0,
inter_byte_timeout = None,
write_timeout = None,
dsrdtr = False,
)
except Exception as e:
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
raise e
if parity.lower() == "e" or parity.lower() == "even":
self.parity = serial.PARITY_EVEN
if self.serial.is_open:
sleep(0.5)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True)
thread.start()
self.online = True
RNS.log("Serial port "+self.port+" is now open")
else:
raise IOError("Could not open serial port")
if parity.lower() == "o" or parity.lower() == "odd":
self.parity = serial.PARITY_ODD
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 processIncoming(self, data):
self.owner.inbound(data, self)
def open_port(self):
RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE)
self.serial = self.pyserial.Serial(
port = self.port,
baudrate = self.speed,
bytesize = self.databits,
parity = self.parity,
stopbits = self.stopbits,
xonxoff = False,
rtscts = False,
timeout = 0,
inter_byte_timeout = None,
write_timeout = None,
dsrdtr = False,
)
def processOutgoing(self,data):
if self.online:
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
written = self.serial.write(data)
if written != len(data):
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
def configure_device(self):
sleep(0.5)
thread = threading.Thread(target=self.readLoop)
thread.setDaemon(True)
thread.start()
self.online = True
RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
def readLoop(self):
try:
in_frame = False
escape = False
data_buffer = b""
last_read_ms = int(time.time()*1000)
def processIncoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self)
while self.serial.is_open:
if self.serial.in_waiting:
byte = ord(self.serial.read(1))
last_read_ms = int(time.time()*1000)
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.processIncoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU):
if (byte == HDLC.ESC):
escape = True
else:
if (escape):
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
byte = HDLC.FLAG
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
byte = HDLC.ESC
escape = False
data_buffer = data_buffer+bytes([byte])
else:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
data_buffer = b""
in_frame = False
escape = False
sleep(0.08)
except Exception as e:
self.online = False
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("The interface "+str(self.name)+" is now offline. Restart Reticulum to attempt reconnection.", RNS.LOG_ERROR)
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 __str__(self):
return "SerialInterface["+self.name+"]"
def readLoop(self):
try:
in_frame = False
escape = False
data_buffer = b""
last_read_ms = int(time.time()*1000)
while self.serial.is_open:
if self.serial.in_waiting:
byte = ord(self.serial.read(1))
last_read_ms = int(time.time()*1000)
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.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])
else:
time_since_last = int(time.time()*1000) - last_read_ms
if len(data_buffer) > 0 and time_since_last > self.timeout:
data_buffer = b""
in_frame = False
escape = False
sleep(0.08)
except Exception as e:
self.online = False
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error:
RNS.panic()
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
self.online = False
self.serial.close()
self.reconnect_port()
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 __str__(self):
return "SerialInterface["+self.name+"]"
+340 -33
View File
@@ -1,6 +1,29 @@
# 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 .Interface import Interface
import socketserver
import threading
import platform
import socket
import time
import sys
@@ -18,17 +41,67 @@ class HDLC():
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
return data
class KISS():
FEND = 0xC0
FESC = 0xDB
TFEND = 0xDC
TFESC = 0xDD
CMD_DATA = 0x00
CMD_UNKNOWN = 0xFE
@staticmethod
def escape(data):
data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
return data
class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
class TCPClientInterface(Interface):
BITRATE_GUESS = 10*1000*1000
def __init__(self, owner, name, target_ip=None, target_port=None, connected_socket=None):
RECONNECT_WAIT = 5
RECONNECT_MAX_TRIES = None
# TCP socket options
TCP_USER_TIMEOUT = 20
TCP_PROBE_AFTER = 5
TCP_PROBE_INTERVAL = 3
TCP_PROBES = 5
I2P_USER_TIMEOUT = 40
I2P_PROBE_AFTER = 10
I2P_PROBE_INTERVAL = 5
I2P_PROBES = 6
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
self.HW_MTU = 1064
self.IN = True
self.OUT = False
self.socket = None
self.parent_interface = None
self.name = name
self.initiator = False
self.reconnecting = False
self.never_connected = True
self.owner = owner
self.writing = False
self.online = False
self.detached = False
self.kiss_framing = kiss_framing
self.i2p_tunneled = i2p_tunneled
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
self.bitrate = TCPClientInterface.BITRATE_GUESS
if max_reconnect_tries == None:
self.max_reconnect_tries = TCPClientInterface.RECONNECT_MAX_TRIES
else:
self.max_reconnect_tries = max_reconnect_tries
if connected_socket != None:
self.receives = True
@@ -36,24 +109,141 @@ class TCPClientInterface(Interface):
self.target_port = None
self.socket = connected_socket
if platform.system() == "Linux":
self.set_timeouts_linux()
elif platform.system() == "Darwin":
self.set_timeouts_osx()
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
def set_timeouts_linux(self):
if not self.i2p_tunneled:
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(TCPClientInterface.TCP_USER_TIMEOUT * 1000))
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(TCPClientInterface.TCP_PROBE_AFTER))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(TCPClientInterface.TCP_PROBE_INTERVAL))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(TCPClientInterface.TCP_PROBES))
else:
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(TCPClientInterface.I2P_USER_TIMEOUT * 1000))
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(TCPClientInterface.I2P_PROBE_AFTER))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(TCPClientInterface.I2P_PROBE_INTERVAL))
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(TCPClientInterface.I2P_PROBES))
def set_timeouts_osx(self):
if hasattr(socket, "TCP_KEEPALIVE"):
TCP_KEEPIDLE = socket.TCP_KEEPALIVE
else:
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(TCPClientInterface.TCP_PROBE_AFTER))
else:
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(TCPClientInterface.I2P_PROBE_AFTER))
def detach(self):
if self.socket != None:
if hasattr(self.socket, "close"):
if callable(self.socket.close):
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
self.detached = True
try:
self.socket.shutdown(socket.SHUT_RDWR)
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))
self.socket = None
def connect(self, initial=False):
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((self.target_ip, self.target_port))
self.online = True
except Exception as e:
if initial:
RNS.log("Initial connection for "+str(self)+" could not be established: "+str(e), RNS.LOG_ERROR)
RNS.log("Leaving unconnected and retrying connection in "+str(TCPClientInterface.RECONNECT_WAIT)+" seconds.", RNS.LOG_ERROR)
return False
else:
raise e
self.owner = owner
if platform.system() == "Linux":
self.set_timeouts_linux()
elif platform.system() == "Darwin":
self.set_timeouts_osx()
self.online = True
self.writing = False
self.never_connected = False
if connected_socket == None:
thread = threading.Thread(target=self.read_loop)
thread.setDaemon(True)
thread.start()
return True
def reconnect(self):
if self.initiator:
if not self.reconnecting:
self.reconnecting = True
attempts = 0
while not self.online:
time.sleep(TCPClientInterface.RECONNECT_WAIT)
attempts += 1
if self.max_reconnect_tries != None and attempts > self.max_reconnect_tries:
RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_ERROR)
self.teardown()
break
try:
self.connect()
except Exception as e:
RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
if not self.never_connected:
RNS.log("Reconnected TCP socket for "+str(self)+".", RNS.LOG_INFO)
self.reconnecting = False
thread = threading.Thread(target=self.read_loop)
thread.setDaemon(True)
thread.start()
if not self.kiss_framing:
RNS.Transport.synthesize_tunnel(self)
else:
RNS.log("Attempt to reconnect on a non-initiator TCP interface. This should not happen.", RNS.LOG_ERROR)
raise IOError("Attempt to reconnect on a non-initiator TCP interface")
def processIncoming(self, data):
self.rxb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self, data):
@@ -63,9 +253,18 @@ class TCPClientInterface(Interface):
try:
self.writing = True
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
if self.kiss_framing:
data = bytes([KISS.FEND])+bytes([KISS.CMD_DATA])+KISS.escape(data)+bytes([KISS.FEND])
else:
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
self.socket.sendall(data)
self.writing = False
self.txb += len(data)
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.txb += len(data)
except Exception as e:
RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
@@ -77,6 +276,7 @@ class TCPClientInterface(Interface):
in_frame = False
escape = False
data_buffer = b""
command = KISS.CMD_UNKNOWN
while True:
data_in = self.socket.recv(4096)
@@ -85,41 +285,94 @@ class TCPClientInterface(Interface):
while pointer < len(data_in):
byte = data_in[pointer]
pointer += 1
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.processIncoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
elif (in_frame and len(data_buffer) < RNS.Reticulum.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 self.kiss_framing:
# Read loop for KISS framing
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])
else:
# Read loop for HDLC framing
if (in_frame and byte == HDLC.FLAG):
in_frame = False
self.processIncoming(data_buffer)
elif (byte == HDLC.FLAG):
in_frame = True
data_buffer = b""
elif (in_frame and len(data_buffer) < self.HW_MTU):
if (byte == HDLC.ESC):
escape = True
else:
if (escape):
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
byte = HDLC.FLAG
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
byte = HDLC.ESC
escape = False
data_buffer = data_buffer+bytes([byte])
else:
RNS.log("TCP socket for "+str(self)+" was closed, tearing down interface", RNS.LOG_VERBOSE)
self.teardown()
self.online = False
if self.initiator and not self.detached:
RNS.log("TCP socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
self.reconnect()
else:
RNS.log("TCP socket for remote client "+str(self)+" was closed.", RNS.LOG_VERBOSE)
self.teardown()
break
except Exception as e:
self.online = False
RNS.log("An interface error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("Tearing down "+str(self), RNS.LOG_ERROR)
self.teardown()
RNS.log("An interface error occurred for "+str(self)+", the contained exception was: "+str(e), RNS.LOG_WARNING)
if self.initiator:
RNS.log("Attempting to reconnect...", RNS.LOG_WARNING)
self.reconnect()
else:
self.teardown()
def teardown(self):
if self.initiator and not self.detached:
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error:
RNS.panic()
else:
RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_VERBOSE)
self.online = False
self.OUT = False
self.IN = False
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.clients -= 1
if self in RNS.Transport.interfaces:
RNS.Transport.interfaces.remove(self)
if not self.initiator:
RNS.Transport.interfaces.remove(self)
def __str__(self):
@@ -127,12 +380,49 @@ class TCPClientInterface(Interface):
class TCPServerInterface(Interface):
BITRATE_GUESS = 10*1000*1000
def __init__(self, owner, name, bindip=None, bindport=None):
@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()
@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()
def __init__(self, owner, name, device=None, bindip=None, bindport=None, i2p_tunneled=False):
self.rxb = 0
self.txb = 0
self.HW_MTU = 1064
self.online = False
self.clients = 0
self.IN = True
self.OUT = False
self.name = name
self.i2p_tunneled = i2p_tunneled
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
if device != None:
bindip = TCPServerInterface.get_address_for_if(device)
if (bindip != None and bindport != None):
self.receives = True
self.bind_ip = bindip
@@ -145,24 +435,41 @@ class TCPServerInterface(Interface):
self.owner = owner
address = (self.bind_ip, self.bind_port)
ThreadingTCPServer.allow_reuse_address = True
self.server = ThreadingTCPServer(address, handlerFactory(self.incoming_connection))
self.bitrate = TCPServerInterface.BITRATE_GUESS
thread = threading.Thread(target=self.server.serve_forever)
thread.setDaemon(True)
thread.start()
self.online = True
def incoming_connection(self, handler):
RNS.log("Accepting incoming TCP connection", RNS.LOG_VERBOSE)
interface_name = "Client on "+self.name
spawned_interface = TCPClientInterface(self.owner, interface_name, target_ip=None, target_port=None, connected_socket=handler.request)
spawned_interface = TCPClientInterface(self.owner, interface_name, target_ip=None, target_port=None, connected_socket=handler.request, i2p_tunneled=self.i2p_tunneled)
spawned_interface.OUT = self.OUT
spawned_interface.IN = self.IN
spawned_interface.target_ip = handler.client_address[0]
spawned_interface.target_port = str(handler.client_address[1])
spawned_interface.parent_interface = self
spawned_interface.bitrate = self.bitrate
spawned_interface.ifac_size = self.ifac_size
spawned_interface.ifac_netname = self.ifac_netname
spawned_interface.ifac_netkey = self.ifac_netkey
spawned_interface.announce_rate_target = self.announce_rate_target
spawned_interface.announce_rate_grace = self.announce_rate_grace
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
spawned_interface.mode = self.mode
spawned_interface.HW_MTU = self.HW_MTU
spawned_interface.online = True
RNS.log("Spawned new TCPClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
RNS.Transport.interfaces.append(spawned_interface)
self.clients += 1
spawned_interface.read_loop()
def processOutgoing(self, data):
@@ -177,4 +484,4 @@ class TCPInterfaceHandler(socketserver.BaseRequestHandler):
socketserver.BaseRequestHandler.__init__(self, *args, **keys)
def handle(self):
self.callback(handler=self)
self.callback(handler=self)
+128
View File
@@ -0,0 +1,128 @@
# 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 .Interface import Interface
import socketserver
import threading
import socket
import time
import sys
import RNS
class UDPInterface(Interface):
BITRATE_GUESS = 10*1000*1000
@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()
@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()
def __init__(self, owner, name, device=None, bindip=None, bindport=None, forwardip=None, forwardport=None):
self.rxb = 0
self.txb = 0
self.HW_MTU = 1064
self.IN = True
self.OUT = False
self.name = name
self.online = False
self.bitrate = UDPInterface.BITRATE_GUESS
if device != None:
if bindip == None:
bindip = UDPInterface.get_broadcast_for_if(device)
if forwardip == None:
forwardip = UDPInterface.get_broadcast_for_if(device)
if (bindip != None and bindport != None):
self.receives = True
self.bind_ip = bindip
self.bind_port = bindport
def handlerFactory(callback):
def createHandler(*args, **keys):
return UDPInterfaceHandler(callback, *args, **keys)
return createHandler
self.owner = owner
address = (self.bind_ip, self.bind_port)
socketserver.UDPServer.address_family = socket.AF_INET
self.server = socketserver.UDPServer(address, handlerFactory(self.processIncoming))
thread = threading.Thread(target=self.server.serve_forever)
thread.setDaemon(True)
thread.start()
self.online = True
if (forwardip != None and forwardport != None):
self.forwards = True
self.forward_ip = forwardip
self.forward_port = forwardport
def processIncoming(self, data):
self.rxb += len(data)
self.owner.inbound(data, self)
def processOutgoing(self,data):
try:
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
udp_socket.sendto(data, (self.forward_ip, self.forward_port))
self.txb += len(data)
except Exception as e:
RNS.log("Could not transmit on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
def __str__(self):
return "UDPInterface["+self.name+"/"+self.bind_ip+":"+str(self.bind_port)+"]"
class UDPInterfaceHandler(socketserver.BaseRequestHandler):
def __init__(self, callback, *args, **keys):
self.callback = callback
socketserver.BaseRequestHandler.__init__(self, *args, **keys)
def handle(self):
data = self.request[0]
self.callback(data)
-64
View File
@@ -1,64 +0,0 @@
from .Interface import Interface
import socketserver
import threading
import socket
import time
import sys
import RNS
class UdpInterface(Interface):
def __init__(self, owner, name, bindip=None, bindport=None, forwardip=None, forwardport=None):
self.IN = True
self.OUT = False
# TODO: Optimise so this is not needed
self.transmit_delay = 0.001
self.name = name
if (bindip != None and bindport != None):
self.receives = True
self.bind_ip = bindip
self.bind_port = bindport
def handlerFactory(callback):
def createHandler(*args, **keys):
return UdpInterfaceHandler(callback, *args, **keys)
return createHandler
self.owner = owner
address = (self.bind_ip, self.bind_port)
self.server = socketserver.UDPServer(address, handlerFactory(self.processIncoming))
thread = threading.Thread(target=self.server.serve_forever)
thread.setDaemon(True)
thread.start()
if (forwardip != None and forwardport != None):
self.forwards = True
self.forward_ip = forwardip
self.forward_port = forwardport
def processIncoming(self, data):
self.owner.inbound(data, self)
def processOutgoing(self,data):
time.sleep(self.transmit_delay)
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
udp_socket.sendto(data, (self.forward_ip, self.forward_port))
def __str__(self):
return "UdpInterface["+self.name+"/"+self.bind_ip+":"+str(self.bind_port)+"]"
class UdpInterfaceHandler(socketserver.BaseRequestHandler):
def __init__(self, callback, *args, **keys):
self.callback = callback
socketserver.BaseRequestHandler.__init__(self, *args, **keys)
def handle(self):
data = self.request[0]
self.callback(data)
+22
View File
@@ -1,3 +1,25 @@
# 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
+1112 -516
View File
File diff suppressed because it is too large Load Diff
+464 -351
View File
@@ -1,3 +1,25 @@
# 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 threading
import struct
import math
@@ -5,407 +27,498 @@ import time
import RNS
class Packet:
# Packet types
DATA = 0x00 # Data packets
ANNOUNCE = 0x01 # Announces
LINKREQUEST = 0x02 # Link requests
PROOF = 0x03 # Proofs
types = [DATA, ANNOUNCE, LINKREQUEST, PROOF]
"""
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,
``RNS.Destination.GROUP`` destination or a :ref:`RNS.Link<api-link>`.
# 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]
For ``RNS.Destination.GROUP`` destinations, Reticulum will use the
pre-shared key configured for the destination.
# Data packet context types
NONE = 0x00 # Generic data packet
RESOURCE = 0x01 # Packet is part of a resource
RESOURCE_ADV = 0x02 # Packet is a resource advertisement
RESOURCE_REQ = 0x03 # Packet is a resource part request
RESOURCE_HMU = 0x04 # Packet is a resource hashmap update
RESOURCE_PRF = 0x05 # Packet is a resource proof
RESOURCE_ICL = 0x06 # Packet is a resource initiator cancel message
RESOURCE_RCL = 0x07 # Packet is a resource receiver cancel message
CACHE_REQUEST = 0x08 # Packet is a cache request
REQUEST = 0x09 # Packet is a request
RESPONSE = 0x0A # Packet is a response to a request
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
KEEPALIVE = 0xFB # Packet is a keepalive packet
LINKCLOSE = 0xFC # Packet is a link close message
LINKPROOF = 0xFD # Packet is a link packet proof
LRRTT = 0xFE # Packet is a link request round-trip time measurement
LRPROOF = 0xFF # Packet is a link request proof
For ``RNS.Destination.SINGLE`` destinations and :ref:`RNS.Link<api-link>`
destinations, reticulum will use ephemeral keys, and offers **Forward Secrecy**.
# This is used to calculate allowable
# payload sizes
HEADER_MAXSIZE = 23
MDU = RNS.Reticulum.MDU
: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*.
:param create_receipt: Specifies whether a :ref:`RNS.PacketReceipt<api-packetreceipt>` should be created when instantiating the packet.
"""
# With an MTU of 500, the maximum RSA-encrypted
# amount of data we can send in a single packet
# is given by the below calculation; 258 bytes.
RSA_MDU = math.floor(MDU/RNS.Identity.DECRYPT_CHUNKSIZE)*RNS.Identity.ENCRYPT_CHUNKSIZE
PLAIN_MDU = MDU
# Packet types
DATA = 0x00 # Data packets
ANNOUNCE = 0x01 # Announces
LINKREQUEST = 0x02 # Link requests
PROOF = 0x03 # Proofs
types = [DATA, ANNOUNCE, LINKREQUEST, PROOF]
# TODO: This should be calculated
# more intelligently
# Default packet timeout
TIMEOUT = 60
# 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]
def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True):
if destination != None:
if transport_type == None:
transport_type = RNS.Transport.BROADCAST
# Packet context types
NONE = 0x00 # Generic data packet
RESOURCE = 0x01 # Packet is part of a resource
RESOURCE_ADV = 0x02 # Packet is a resource advertisement
RESOURCE_REQ = 0x03 # Packet is a resource part request
RESOURCE_HMU = 0x04 # Packet is a resource hashmap update
RESOURCE_PRF = 0x05 # Packet is a resource proof
RESOURCE_ICL = 0x06 # Packet is a resource initiator cancel message
RESOURCE_RCL = 0x07 # Packet is a resource receiver cancel message
CACHE_REQUEST = 0x08 # Packet is a cache request
REQUEST = 0x09 # Packet is a request
RESPONSE = 0x0A # Packet is a response to a request
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
KEEPALIVE = 0xFA # Packet is a keepalive packet
LINKIDENTIFY = 0xFB # Packet is a link peer identification proof
LINKCLOSE = 0xFC # Packet is a link close message
LINKPROOF = 0xFD # Packet is a link packet proof
LRRTT = 0xFE # Packet is a link request round-trip time measurement
LRPROOF = 0xFF # Packet is a link request proof
self.header_type = header_type
self.packet_type = packet_type
self.transport_type = transport_type
self.context = context
# This is used to calculate allowable
# payload sizes
HEADER_MAXSIZE = RNS.Reticulum.HEADER_MAXSIZE
MDU = RNS.Reticulum.MDU
self.hops = 0;
self.destination = destination
self.transport_id = transport_id
self.data = data
self.flags = self.getPackedFlags()
# With an MTU of 500, the maximum of data we can
# send in a single encrypted packet is given by
# the below calculation; 383 bytes.
ENCRYPTED_MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.FERNET_OVERHEAD-RNS.Identity.KEYSIZE//16)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
"""
The maximum size of the payload data in a single encrypted packet
"""
PLAIN_MDU = MDU
"""
The maximum size of the payload data in a single unencrypted packet
"""
self.raw = None
self.packed = False
self.sent = False
self.create_receipt = create_receipt
self.receipt = None
self.fromPacked = False
else:
self.raw = data
self.packed = True
self.fromPacked = True
self.create_receipt = False
TIMEOUT_PER_HOP = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT
self.MTU = RNS.Reticulum.MTU
self.sent_at = None
self.packet_hash = None
def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True):
if destination != None:
if transport_type == None:
transport_type = RNS.Transport.BROADCAST
self.attached_interface = attached_interface
self.header_type = header_type
self.packet_type = packet_type
self.transport_type = transport_type
self.context = context
def getPackedFlags(self):
if self.context == Packet.LRPROOF:
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | RNS.Destination.LINK | self.packet_type
else:
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
return packed_flags
self.hops = 0;
self.destination = destination
self.transport_id = transport_id
self.data = data
self.flags = self.get_packed_flags()
def pack(self):
self.destination_hash = self.destination.hash
self.header = b""
self.header += struct.pack("!B", self.flags)
self.header += struct.pack("!B", self.hops)
self.raw = None
self.packed = False
self.sent = False
self.create_receipt = create_receipt
self.receipt = None
self.fromPacked = False
else:
self.raw = data
self.packed = True
self.fromPacked = True
self.create_receipt = False
if self.context == Packet.LRPROOF:
self.header += self.destination.link_id
self.ciphertext = self.data
else:
if self.header_type == Packet.HEADER_1:
self.header += self.destination.hash
self.MTU = RNS.Reticulum.MTU
self.sent_at = None
self.packet_hash = None
if self.packet_type == Packet.ANNOUNCE:
# Announce packets are not encrypted
self.ciphertext = self.data
elif self.packet_type == Packet.PROOF and self.context == Packet.RESOURCE_PRF:
# Resource proofs are not encrypted
self.ciphertext = self.data
elif self.packet_type == Packet.PROOF and self.destination.type == RNS.Destination.LINK:
# 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
self.ciphertext = self.data
elif self.context == Packet.KEEPALIVE:
# Keepalive packets contain no actual
# data
self.ciphertext = self.data
else:
# In all other cases, we encrypt the packet
# with the destination's public key
self.ciphertext = self.destination.encrypt(self.data)
self.attached_interface = attached_interface
self.receiving_interface = None
self.rssi = None
self.snr = None
if self.header_type == Packet.HEADER_2:
if self.transport_id != None:
self.header += self.transport_id
self.header += self.destination.hash
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
else:
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
return packed_flags
if self.packet_type == Packet.ANNOUNCE:
# Announce packets are not encrypted
self.ciphertext = self.data
else:
raise IOError("Packet with header type 2 must have a transport ID")
def pack(self):
self.destination_hash = self.destination.hash
self.header = b""
self.header += struct.pack("!B", self.flags)
self.header += struct.pack("!B", self.hops)
if self.context == Packet.LRPROOF:
self.header += self.destination.link_id
self.ciphertext = self.data
else:
if self.header_type == Packet.HEADER_1:
self.header += self.destination.hash
if self.packet_type == Packet.ANNOUNCE:
# Announce packets are not encrypted
self.ciphertext = self.data
elif self.packet_type == Packet.LINKREQUEST:
# Link request packets are not encrypted
self.ciphertext = self.data
elif self.packet_type == Packet.PROOF and self.context == Packet.RESOURCE_PRF:
# Resource proofs are not encrypted
self.ciphertext = self.data
elif self.packet_type == Packet.PROOF and self.destination.type == RNS.Destination.LINK:
# 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
self.ciphertext = self.data
elif self.context == Packet.KEEPALIVE:
# Keepalive packets contain no actual
# data
self.ciphertext = self.data
elif self.context == Packet.CACHE_REQUEST:
# Cache-requests are not encrypted
self.ciphertext = self.data
else:
# In all other cases, we encrypt the packet
# with the destination's encryption method
self.ciphertext = self.destination.encrypt(self.data)
if self.header_type == Packet.HEADER_2:
if self.transport_id != None:
self.header += self.transport_id
self.header += self.destination.hash
if self.packet_type == Packet.ANNOUNCE:
# Announce packets are not encrypted
self.ciphertext = self.data
else:
raise IOError("Packet with header type 2 must have a transport ID")
self.header += bytes([self.context])
self.raw = self.header + self.ciphertext
self.header += bytes([self.context])
self.raw = self.header + self.ciphertext
if len(self.raw) > self.MTU:
raise IOError("Packet size of "+str(len(self.raw))+" exceeds MTU of "+str(self.MTU)+" bytes")
if len(self.raw) > self.MTU:
raise IOError("Packet size of "+str(len(self.raw))+" exceeds MTU of "+str(self.MTU)+" bytes")
self.packed = True
self.updateHash()
self.packed = True
self.update_hash()
def unpack(self):
self.flags = self.raw[0]
self.hops = self.raw[1]
self.header_type = (self.flags & 0b11000000) >> 6
self.transport_type = (self.flags & 0b00110000) >> 4
self.destination_type = (self.flags & 0b00001100) >> 2
self.packet_type = (self.flags & 0b00000011)
def unpack(self):
try:
self.flags = self.raw[0]
self.hops = self.raw[1]
if self.header_type == Packet.HEADER_2:
self.transport_id = self.raw[2:12]
self.destination_hash = self.raw[12:22]
self.context = ord(self.raw[22:23])
self.data = self.raw[23:]
else:
self.transport_id = None
self.destination_hash = self.raw[2:12]
self.context = ord(self.raw[12:13])
self.data = self.raw[13:]
self.header_type = (self.flags & 0b11000000) >> 6
self.transport_type = (self.flags & 0b00110000) >> 4
self.destination_type = (self.flags & 0b00001100) >> 2
self.packet_type = (self.flags & 0b00000011)
self.packed = False
self.updateHash()
if self.header_type == Packet.HEADER_2:
self.transport_id = self.raw[2:12]
self.destination_hash = self.raw[12:22]
self.context = ord(self.raw[22:23])
self.data = self.raw[23:]
else:
self.transport_id = None
self.destination_hash = self.raw[2:12]
self.context = ord(self.raw[12:13])
self.data = self.raw[13:]
# Sends the packet. Returns a receipt if one is generated,
# or None if no receipt is available. Returns False if the
# packet could not be sent.
def send(self):
if not self.sent:
if self.destination.type == RNS.Destination.LINK:
if self.destination.status == RNS.Link.CLOSED:
raise IOError("Attempt to transmit over a closed link")
else:
self.destination.last_outbound = time.time()
self.destination.tx += 1
self.destination.txbytes += len(self.data)
self.packed = False
self.update_hash()
return True
if not self.packed:
self.pack()
if RNS.Transport.outbound(self):
return self.receipt
else:
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
self.sent = False
self.receipt = None
return False
else:
raise IOError("Packet was already sent")
except Exception as e:
RNS.log("Received malformed packet, dropping it. The contained exception was: "+str(e), RNS.LOG_EXTREME)
return False
def resend(self):
if self.sent:
if RNS.Transport.outbound(self):
return self.receipt
else:
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
self.sent = False
self.receipt = None
return False
else:
raise IOError("Packet was not sent yet")
def send(self):
"""
Sends the 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 not self.sent:
if self.destination.type == RNS.Destination.LINK:
if self.destination.status == RNS.Link.CLOSED:
raise IOError("Attempt to transmit over a closed link")
else:
self.destination.last_outbound = time.time()
self.destination.tx += 1
self.destination.txbytes += len(self.data)
def prove(self, destination=None):
if self.fromPacked and hasattr(self, "destination") and self.destination:
if self.destination.identity and self.destination.identity.prv:
self.destination.identity.prove(self, destination)
elif self.fromPacked and hasattr(self, "link") and self.link:
self.link.prove_packet(self)
else:
RNS.log("Could not prove packet associated with neither a destination nor a link", RNS.LOG_ERROR)
if not self.packed:
self.pack()
if RNS.Transport.outbound(self):
return self.receipt
else:
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
self.sent = False
self.receipt = None
return False
else:
raise IOError("Packet was already sent")
# Generates a special destination that allows Reticulum
# to direct the proof back to the proved packet's sender
def generateProofDestination(self):
return ProofDestination(self)
def resend(self):
"""
Re-sends the 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:
if RNS.Transport.outbound(self):
return self.receipt
else:
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
self.sent = False
self.receipt = None
return False
else:
raise IOError("Packet was not sent yet")
def validateProofPacket(self, proof_packet):
return self.receipt.validateProofPacket(proof_packet)
def prove(self, destination=None):
if self.fromPacked and hasattr(self, "destination") and self.destination:
if self.destination.identity and self.destination.identity.prv:
self.destination.identity.prove(self, destination)
elif self.fromPacked and hasattr(self, "link") and self.link:
self.link.prove_packet(self)
else:
RNS.log("Could not prove packet associated with neither a destination nor a link", RNS.LOG_ERROR)
def validateProof(self, proof):
return self.receipt.validateProof(proof)
# Generates a special destination that allows Reticulum
# to direct the proof back to the proved packet's sender
def generate_proof_destination(self):
return ProofDestination(self)
def updateHash(self):
self.packet_hash = self.getHash()
def validate_proof_packet(self, proof_packet):
return self.receipt.validate_proof_packet(proof_packet)
def getHash(self):
return RNS.Identity.fullHash(self.getHashablePart())
def validate_proof(self, proof):
return self.receipt.validate_proof(proof)
def getTruncatedHash(self):
return RNS.Identity.truncatedHash(self.getHashablePart())
def update_hash(self):
self.packet_hash = self.get_hash()
def getHashablePart(self):
hashable_part = bytes([self.raw[0] & 0b00001111])
if self.header_type == Packet.HEADER_2:
hashable_part += self.raw[12:]
else:
hashable_part += self.raw[2:]
def get_hash(self):
return RNS.Identity.full_hash(self.get_hashable_part())
return hashable_part
def getTruncatedHash(self):
return RNS.Identity.truncated_hash(self.get_hashable_part())
def get_hashable_part(self):
hashable_part = bytes([self.raw[0] & 0b00001111])
if self.header_type == Packet.HEADER_2:
hashable_part += self.raw[12:]
else:
hashable_part += self.raw[2:]
return hashable_part
class ProofDestination:
def __init__(self, packet):
self.hash = packet.getHash()[:10];
self.type = RNS.Destination.SINGLE
def __init__(self, packet):
self.hash = packet.get_hash()[:10];
self.type = RNS.Destination.SINGLE
def encrypt(self, plaintext):
return plaintext
def encrypt(self, plaintext):
return plaintext
class PacketReceipt:
# Receipt status constants
FAILED = 0x00
SENT = 0x01
DELIVERED = 0x02
CULLED = 0xFF
"""
The PacketReceipt class is used to receive notifications about
:ref:`RNS.Packet<api-packet>` instances sent over the network. Instances
of this class are never created manually, but always returned from
the *send()* method of a :ref:`RNS.Packet<api-packet>` instance.
"""
# Receipt status constants
FAILED = 0x00
SENT = 0x01
DELIVERED = 0x02
CULLED = 0xFF
EXPL_LENGTH = RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8
IMPL_LENGTH = RNS.Identity.SIGLENGTH//8
EXPL_LENGTH = RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8
IMPL_LENGTH = RNS.Identity.SIGLENGTH//8
# Creates a new packet receipt from a sent packet
def __init__(self, packet):
self.hash = packet.getHash()
self.sent = True
self.sent_at = time.time()
self.timeout = Packet.TIMEOUT
self.proved = False
self.status = PacketReceipt.SENT
self.destination = packet.destination
self.callbacks = PacketReceiptCallbacks()
self.concluded_at = None
# Creates a new packet receipt from a sent packet
def __init__(self, packet):
self.hash = packet.get_hash()
self.truncated_hash = packet.getTruncatedHash()
self.sent = True
self.sent_at = time.time()
self.proved = False
self.status = PacketReceipt.SENT
self.destination = packet.destination
self.callbacks = PacketReceiptCallbacks()
self.concluded_at = None
self.proof_packet = None
# Validate a proof packet
def validateProofPacket(self, proof_packet):
if hasattr(proof_packet, "link") and proof_packet.link:
return self.validate_link_proof(proof_packet.data, proof_packet.link)
else:
return self.validateProof(proof_packet.data)
# Validate a raw proof for a link
def validate_link_proof(self, proof, link):
# TODO: Hardcoded as explicit proofs for now
if True or len(proof) == PacketReceipt.EXPL_LENGTH:
# This is an explicit proof
proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
if proof_hash == self.hash:
proof_valid = link.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
self.proved = True
self.concluded_at = time.time()
if self.callbacks.delivery != None:
self.callbacks.delivery(self)
return True
else:
return False
else:
return False
elif len(proof) == PacketReceipt.IMPL_LENGTH:
pass
# TODO: Why is this disabled?
# signature = proof[:RNS.Identity.SIGLENGTH//8]
# proof_valid = self.link.validate(signature, self.hash)
# if proof_valid:
# self.status = PacketReceipt.DELIVERED
# self.proved = True
# self.concluded_at = time.time()
# if self.callbacks.delivery != None:
# self.callbacks.delivery(self)
# RNS.log("valid")
# return True
# else:
# RNS.log("invalid")
# return False
else:
return False
# Validate a raw proof
def validateProof(self, proof):
if len(proof) == PacketReceipt.EXPL_LENGTH:
# This is an explicit proof
proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
if proof_hash == self.hash:
proof_valid = self.destination.identity.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
self.proved = True
self.concluded_at = time.time()
if self.callbacks.delivery != None:
self.callbacks.delivery(self)
return True
else:
return False
else:
return False
elif len(proof) == PacketReceipt.IMPL_LENGTH:
# This is an implicit proof
if self.destination.identity == None:
return False
signature = proof[:RNS.Identity.SIGLENGTH//8]
proof_valid = self.destination.identity.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
self.proved = True
self.concluded_at = time.time()
if self.callbacks.delivery != None:
self.callbacks.delivery(self)
return True
else:
return False
else:
return False
def rtt(self):
return self.concluded_at - self.sent_at
def is_timed_out(self):
return (self.sent_at+self.timeout < time.time())
def check_timeout(self):
if self.is_timed_out():
if self.timeout == -1:
self.status = PacketReceipt.CULLED
else:
self.status = PacketReceipt.FAILED
self.concluded_at = time.time()
if self.callbacks.timeout:
thread = threading.Thread(target=self.callbacks.timeout, args=(self,))
thread.setDaemon(True)
thread.start()
#self.callbacks.timeout(self)
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)
# Set the timeout in seconds
def set_timeout(self, timeout):
self.timeout = float(timeout)
def get_status(self):
"""
:returns: The status of the associated :ref:`RNS.Packet<api-packet>` instance. Can be one of ``RNS.PacketReceipt.SENT``, ``RNS.PacketReceipt.DELIVERED``, ``RNS.PacketReceipt.FAILED`` or ``RNS.PacketReceipt.CULLED``.
"""
return self.status
# Set a function that gets called when
# a successfull delivery has been proved
def delivery_callback(self, callback):
self.callbacks.delivery = callback
# Validate a proof packet
def validate_proof_packet(self, proof_packet):
if hasattr(proof_packet, "link") and proof_packet.link:
return self.validate_link_proof(proof_packet.data, proof_packet.link, proof_packet)
else:
return self.validate_proof(proof_packet.data, proof_packet)
# Set a function that gets called if the
# delivery times out
def timeout_callback(self, callback):
self.callbacks.timeout = callback
# Validate a raw proof for a link
def validate_link_proof(self, proof, link, proof_packet=None):
# TODO: Hardcoded as explicit proofs for now
if True or len(proof) == PacketReceipt.EXPL_LENGTH:
# This is an explicit proof
proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
if proof_hash == self.hash:
proof_valid = link.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
self.proved = True
self.concluded_at = time.time()
self.proof_packet = proof_packet
if self.callbacks.delivery != None:
self.callbacks.delivery(self)
return True
else:
return False
else:
return False
elif len(proof) == PacketReceipt.IMPL_LENGTH:
pass
# TODO: Why is this disabled?
# signature = proof[:RNS.Identity.SIGLENGTH//8]
# proof_valid = self.link.validate(signature, self.hash)
# if proof_valid:
# self.status = PacketReceipt.DELIVERED
# self.proved = True
# self.concluded_at = time.time()
# if self.callbacks.delivery != None:
# self.callbacks.delivery(self)
# RNS.log("valid")
# return True
# else:
# RNS.log("invalid")
# return False
else:
return False
# Validate a raw proof
def validate_proof(self, proof, proof_packet=None):
if len(proof) == PacketReceipt.EXPL_LENGTH:
# This is an explicit proof
proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
if proof_hash == self.hash:
proof_valid = self.destination.identity.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
self.proved = True
self.concluded_at = time.time()
self.proof_packet = proof_packet
if self.callbacks.delivery != None:
try:
self.callbacks.delivery(self)
except Exception as e:
RNS.log("Error while executing proof validated callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
return True
else:
return False
else:
return False
elif len(proof) == PacketReceipt.IMPL_LENGTH:
# This is an implicit proof
if self.destination.identity == None:
return False
signature = proof[:RNS.Identity.SIGLENGTH//8]
proof_valid = self.destination.identity.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
self.proved = True
self.concluded_at = time.time()
self.proof_packet = proof_packet
if self.callbacks.delivery != None:
try:
self.callbacks.delivery(self)
except Exception as e:
RNS.log("Error while executing proof validated callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
return True
else:
return False
else:
return False
def get_rtt(self):
"""
:returns: The round-trip-time in seconds
"""
return self.concluded_at - self.sent_at
def is_timed_out(self):
return (self.sent_at+self.timeout < time.time())
def check_timeout(self):
if self.status == PacketReceipt.SENT and self.is_timed_out():
if self.timeout == -1:
self.status = PacketReceipt.CULLED
else:
self.status = PacketReceipt.FAILED
self.concluded_at = time.time()
if self.callbacks.timeout:
thread = threading.Thread(target=self.callbacks.timeout, args=(self,))
thread.setDaemon(True)
thread.start()
def set_timeout(self, timeout):
"""
Sets a timeout in seconds
:param timeout: The timeout in seconds.
"""
self.timeout = float(timeout)
def set_delivery_callback(self, callback):
"""
Sets a function that gets called if a successfull delivery has been proven.
:param callback: A *callable* with the signature *callback(packet_receipt)*
"""
self.callbacks.delivery = callback
# Set a function that gets called if the
# delivery times out
def set_timeout_callback(self, callback):
"""
Sets a function that gets called if the delivery times out.
:param callback: A *callable* with the signature *callback(packet_receipt)*
"""
self.callbacks.timeout = callback
class PacketReceiptCallbacks:
def __init__(self):
self.delivery = None
self.timeout = None
def __init__(self):
self.delivery = None
self.timeout = None
+991 -657
View File
File diff suppressed because it is too large Load Diff
+1102 -541
View File
File diff suppressed because it is too large Load Diff
+2151 -1104
View File
File diff suppressed because it is too large Load Diff
+27
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')]
+381
View File
@@ -0,0 +1,381 @@
#!/usr/bin/env python3
# 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 RNS
import argparse
import time
import sys
import os
from RNS._version import __version__
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):
global allow_all, allowed_identity_hashes
identity = None
targetloglevel = 3+verbosity-quietness
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
if os.path.isfile(identity_path):
identity = RNS.Identity.from_file(identity_path)
if identity == None:
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
identity = RNS.Identity()
identity.to_file(identity_path)
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "receive")
if display_identity:
print("Identity : "+str(identity))
print("Receiving on : "+RNS.prettyhexrep(destination.hash))
exit(0)
if disable_auth:
allow_all = True
else:
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:
destination_hash = bytes.fromhex(a)
allowed_identity_hashes.append(destination_hash)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
exit(1)
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))
if not disable_announce:
destination.announce()
while True:
time.sleep(1)
def receive_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)
link.set_resource_callback(receive_resource_callback)
link.set_resource_started_callback(receive_resource_started)
link.set_resource_concluded_callback(receive_resource_concluded)
def receive_sender_identified(link, identity):
if identity.hash in allowed_identity_hashes:
RNS.log("Authenticated sender", RNS.LOG_VERBOSE)
else:
RNS.log("Sender not allowed, tearing down link", RNS.LOG_VERBOSE)
link.teardown()
def receive_resource_callback(resource):
sender_identity = resource.link.get_remote_identity()
if sender_identity != None:
if sender_identity.hash in allowed_identity_hashes:
print("Allowing sender")
return True
print("Rejecting sender")
return False
def receive_resource_started(resource):
if resource.link.get_remote_identity():
id_str = " from "+RNS.prettyhexrep(resource.link.get_remote_identity().hash)
else:
id_str = ""
print("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str)
def receive_resource_concluded(resource):
if resource.status == RNS.Resource.COMPLETE:
print(str(resource)+" completed")
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()
else:
print("Invalid data received, ignoring resource")
else:
print("Resource failed")
resource_done = False
current_resource = None
stats = []
speed = 0.0
def sender_progress(resource):
stats_max = 32
global current_resource, stats, speed, resource_done
current_resource = resource
now = time.time()
got = current_resource.get_progress()*current_resource.total_size
entry = [now, got]
stats.append(entry)
while len(stats) > stats_max:
stats.pop(0)
span = now - stats[0][0]
if span == 0:
speed = 0
else:
diff = got - stats[0][1]
speed = diff/span
if resource.status < RNS.Resource.COMPLETE:
resource_done = False
else:
resource_done = True
link = None
def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT):
global current_resource, resource_done, link, speed
from tempfile import TemporaryFile
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)
file_path = os.path.expanduser(file)
if not os.path.isfile(file_path):
print("File not found")
exit(1)
temp_file = TemporaryFile()
real_file = open(file_path, "rb")
filename_bytes = os.path.basename(file_path).encode("utf-8")
filename_len = len(filename_bytes)
if filename_len > 0xFFFF:
print("Filename exceeds max size, cannot send")
exit(1)
else:
print("Preparing file...", end=" ")
temp_file.write(filename_len.to_bytes(2, "big"))
temp_file.write(filename_bytes)
temp_file.write(real_file.read())
temp_file.seek(0)
print("\r \r", end="")
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
if os.path.isfile(identity_path):
identity = RNS.Identity.from_file(identity_path)
if identity == None:
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
identity = RNS.Identity()
identity.to_file(identity_path)
if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash)
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 RNS.Transport.has_path(destination_hash):
print("\r \rPath not found")
exit(1)
else:
print("\r \rEstablishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=" ")
receiver_identity = RNS.Identity.recall(destination_hash)
receiver_destination = RNS.Destination(
receiver_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
APP_NAME,
"receive"
)
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 RNS.Transport.has_path(destination_hash):
print("\r \rCould not establish link with "+RNS.prettyhexrep(destination_hash))
exit(1)
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 resource.status > RNS.Resource.COMPLETE:
print("\r \rFile was not accepted by "+RNS.prettyhexrep(destination_hash))
exit(1)
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 current_resource.status != RNS.Resource.COMPLETE:
print("\r \rThe transfer failed")
exit(1)
else:
print("\r \r"+str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
link.teardown()
time.sleep(0.25)
real_file.close()
temp_file.close()
exit(0)
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum File Transfer Utility")
parser.add_argument("file", nargs="?", default=None, help="file to be transferred", type=str)
parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the receiver", type=str)
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('-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("-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(
configdir = args.config,
verbosity=args.verbose,
quietness=args.quiet,
allowed = args.allowed,
display_identity=args.print_identity,
# limit=args.limit,
disable_auth=args.no_auth,
disable_announce=args.no_announce,
)
elif args.destination != None and args.file != None:
send(
configdir = args.config,
verbosity = args.verbose,
quietness = args.quiet,
destination = args.destination,
file = args.file,
timeout = args.w,
)
else:
print("")
parser.print_help()
print("")
except KeyboardInterrupt:
print("")
if resource != None:
resource.cancel()
if link != None:
link.teardown()
exit()
def size_str(num, suffix='B'):
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
if suffix == 'b':
num *= 8
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
for unit in units:
if abs(num) < 1000.0:
if unit == "":
return "%.0f %s%s" % (num, unit, suffix)
else:
return "%.2f %s%s" % (num, unit, suffix)
num /= 1000.0
return "%.2f%s%s" % (num, last_unit, suffix)
if __name__ == "__main__":
main()
+338
View File
@@ -0,0 +1,338 @@
#!/usr/bin/env python3
# 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 RNS
import sys
import time
import argparse
from RNS._version import __version__
def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity, timeout, drop_queues):
if table:
destination_hash = None
if destination_hexhash != None:
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try:
destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
sys.exit(1)
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
table = sorted(reticulum.get_path_table(), key=lambda e: (e["interface"], e["hops"]) )
displayed = 0
for path in table:
if destination_hash == None or destination_hash == path["hash"]:
displayed += 1
exp_str = RNS.timestamp_str(path["expires"])
if path["hops"] == 1:
m_str = " "
else:
m_str = "s"
print(RNS.prettyhexrep(path["hash"])+" is "+str(path["hops"])+" hop"+m_str+" away via "+RNS.prettyhexrep(path["via"])+" on "+path["interface"]+" expires "+RNS.timestamp_str(path["expires"]))
if destination_hash != None and displayed == 0:
print("No path known")
sys.exit(1)
elif rates:
destination_hash = None
if destination_hexhash != None:
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try:
destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
sys.exit(1)
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
table = sorted(reticulum.get_rate_table(), key=lambda e: e["last"] )
if len(table) == 0:
print("No information available")
else:
displayed = 0
for entry in table:
if destination_hash == None or destination_hash == entry["hash"]:
displayed += 1
try:
last_str = pretty_date(int(entry["last"]))
start_ts = entry["timestamps"][0]
span = max(time.time() - start_ts, 3600.0)
span_hours = span/3600.0
span_str = pretty_date(int(entry["timestamps"][0]))
hour_rate = round(len(entry["timestamps"])/span_hours, 3)
if hour_rate-int(hour_rate) == 0:
hour_rate = int(hour_rate)
if entry["rate_violations"] > 0:
if entry["rate_violations"] == 1:
s_str = ""
else:
s_str = "s"
rv_str = ", "+str(entry["rate_violations"])+" active rate violation"+s_str
else:
rv_str = ""
if entry["blocked_until"] > time.time():
bli = time.time()-(int(entry["blocked_until"])-time.time())
bl_str = ", new announces allowed in "+pretty_date(int(bli))
else:
bl_str = ""
print(RNS.prettyhexrep(entry["hash"])+" last heard "+last_str+" ago, "+str(hour_rate)+" announces/hour in the last "+span_str+rv_str+bl_str)
except Exception as e:
print("Error while processing entry for "+RNS.prettyhexrep(entry["hash"]))
print(str(e))
if destination_hash != None and displayed == 0:
print("No information available")
sys.exit(1)
elif drop_queues:
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
RNS.log("Dropping announce queues on all interfaces...")
reticulum.drop_announce_queues()
elif drop:
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_path(destination_hash):
print("Dropped path to "+RNS.prettyhexrep(destination_hash))
else:
print("Unable to drop path to "+RNS.prettyhexrep(destination_hash)+". Does it exist?")
sys.exit(1)
else:
try:
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(destination_hexhash) != dest_len:
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try:
destination_hash = bytes.fromhex(destination_hexhash)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
sys.exit(1)
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash)
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
sys.stdout.flush()
i = 0
syms = "⢄⢂⢁⡁⡈⡐⡠"
limit = time.time()+timeout
while not RNS.Transport.has_path(destination_hash) and time.time()<limit:
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
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"
else:
ms = ""
print("\rPath found, destination "+RNS.prettyhexrep(destination_hash)+" is "+str(hops)+" hop"+ms+" away via "+next_hop+" on "+next_hop_interface)
else:
print("\r \rPath not found")
sys.exit(1)
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum Path Discovery Utility")
parser.add_argument("--config",
action="store",
default=None,
help="path to alternative Reticulum config directory",
type=str
)
parser.add_argument(
"--version",
action="version",
version="rnpath {version}".format(version=__version__)
)
parser.add_argument(
"-t",
"--table",
action="store_true",
help="show all known paths",
default=False
)
parser.add_argument(
"-r",
"--rates",
action="store_true",
help="show announce rate info",
default=False
)
parser.add_argument(
"-d",
"--drop",
action="store_true",
help="remove the path to a destination",
default=False
)
parser.add_argument(
"-D",
"--drop-announces",
action="store_true",
help="drop all queued announces",
default=False
)
parser.add_argument(
"-w",
action="store",
metavar="seconds",
type=float,
help="timeout before giving up",
default=RNS.Transport.PATH_REQUEST_TIMEOUT
)
parser.add_argument(
"destination",
nargs="?",
default=None,
help="hexadecimal hash of the destination",
type=str
)
parser.add_argument('-v', '--verbose', action='count', default=0)
args = parser.parse_args()
if args.config:
configarg = args.config
else:
configarg = None
if not args.drop_announces and not args.table and not args.rates and not args.destination:
print("")
parser.print_help()
print("")
else:
program_setup(
configdir = configarg,
table = args.table,
rates = args.rates,
drop = args.drop,
destination_hexhash = args.destination,
verbosity = args.verbose,
timeout = args.w,
drop_queues = args.drop_announces,
)
sys.exit(0)
except KeyboardInterrupt:
print("")
exit()
def pretty_date(time=False):
from datetime import datetime
now = datetime.now()
if type(time) is int:
diff = now - datetime.fromtimestamp(time)
elif isinstance(time,datetime):
diff = now - time
elif not time:
diff = now - now
second_diff = diff.seconds
day_diff = diff.days
if day_diff < 0:
return ''
if day_diff == 0:
if second_diff < 10:
return str(second_diff) + " seconds"
if second_diff < 60:
return str(second_diff) + " seconds"
if second_diff < 120:
return "1 minute"
if second_diff < 3600:
return str(int(second_diff / 60)) + " minutes"
if second_diff < 7200:
return "an hour"
if second_diff < 86400:
return str(int(second_diff / 3600)) + " hours"
if day_diff == 1:
return "1 day"
if day_diff < 7:
return str(day_diff) + " days"
if day_diff < 31:
return str(int(day_diff / 7)) + " weeks"
if day_diff < 365:
return str(int(day_diff / 30)) + " months"
return str(int(day_diff / 365)) + " years"
if __name__ == "__main__":
main()
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
# 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 RNS
import os
import sys
import time
import argparse
from RNS._version import __version__
DEFAULT_PROBE_SIZE = 16
def program_setup(configdir, destination_hexhash, size=DEFAULT_PROBE_SIZE, full_name = None, verbosity = 0):
if full_name == None:
print("The full destination name including application name aspects must be specified for the destination")
exit()
try:
app_name, aspects = RNS.Destination.app_and_aspects_from_name(full_name)
except Exception as e:
print(str(e))
exit()
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))
exit()
if verbosity > 0:
more_output = True
verbosity -= 1
else:
more_output = False
verbosity -= 1
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash)
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
sys.stdout.flush()
i = 0
syms = "⢄⢂⢁⡁⡈⡐⡠"
while not RNS.Transport.has_path(destination_hash):
time.sleep(0.1)
print(("\b\b"+syms[i]+" "), end="")
sys.stdout.flush()
i = (i+1)%len(syms)
server_identity = RNS.Identity.recall(destination_hash)
request_destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
app_name,
*aspects
)
probe = RNS.Packet(request_destination, os.urandom(size))
receipt = probe.send()
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 = ""
print("\rSent "+str(size)+" byte probe to "+RNS.prettyhexrep(destination_hash)+more+" ", end=" ")
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)
print("\b\b ")
sys.stdout.flush()
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)
if reception_rssi != None:
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
if reception_snr != None:
reception_stats += " [SNR "+str(reception_snr)+" dB]"
else:
if receipt.proof_packet != None:
if receipt.proof_packet.rssi != None:
reception_stats += " [RSSI "+str(receipt.proof_packet.rssi)+" dBm]"
if receipt.proof_packet.snr != None:
reception_stats += " [SNR "+str(receipt.proof_packet.snr)+" dB]"
print(
"Valid reply received from "+
RNS.prettyhexrep(receipt.destination.hash)+
"\nRound-trip time is "+rttstring+
" over "+str(hops)+" hop"+ms+
reception_stats
)
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('-v', '--verbose', action='count', default=0)
args = parser.parse_args()
if args.config:
configarg = args.config
else:
configarg = None
if not args.destination_hash:
print("")
parser.print_help()
print("")
else:
program_setup(
configdir = configarg,
destination_hexhash = args.destination_hash,
full_name = args.full_name,
verbosity = args.verbose
)
except KeyboardInterrupt:
print("")
exit()
if __name__ == "__main__":
main()
+413
View File
@@ -0,0 +1,413 @@
#!/usr/bin/env python3
# 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 RNS
import argparse
import time
from RNS._version import __version__
def program_setup(configdir, verbosity = 0, quietness = 0, service = False):
targetloglevel = 3+verbosity-quietness
if service:
RNS.logdest = RNS.LOG_FILE
RNS.logfile = RNS.Reticulum.configdir+"/logfile"
targetloglevel = None
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
RNS.log("Started rnsd version {version}".format(version=__version__), RNS.LOG_NOTICE)
while True:
time.sleep(1)
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum Network Stack Daemon")
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('-s', '--service', action='store_true', default=False, help="rnsd is running as a service and should log to file")
parser.add_argument("--exampleconfig", action='store_true', default=False, help="print verbose configuration example to stdout and exit")
parser.add_argument("--version", action="version", version="rnsd {version}".format(version=__version__))
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, service=args.service)
except KeyboardInterrupt:
print("")
exit()
__example_rns_config__ = '''# This is an example Reticulum config file.
# You should probably edit it to include any additional,
# interfaces and settings you might need.
[reticulum]
# If you enable Transport, your system will route traffic
# for other peers, pass announces and serve path requests.
# This should be done for systems that are suited to act
# as transport nodes, ie. if they are stationary and
# always-on. This directive is optional and can be removed
# for brevity.
enable_transport = False
# By default, the first program to launch the Reticulum
# Network Stack will create a shared instance, that other
# programs can communicate with. Only the shared instance
# opens all the configured interfaces directly, and other
# local programs communicate with the shared instance over
# a local socket. This is completely transparent to the
# user, and should generally be turned on. This directive
# is optional and can be removed for brevity.
share_instance = Yes
# If you want to run multiple *different* shared instances
# on the same system, you will need to specify different
# shared instance ports for each. The defaults are given
# below, and again, these options can be left out if you
# don't need them.
shared_instance_port = 37428
instance_control_port = 37429
# You can configure Reticulum to panic and forcibly close
# if an unrecoverable interface error occurs, such as the
# hardware device for an interface disappearing. This is
# an optional directive, and can be left out for brevity.
# This behaviour is disabled by default.
panic_on_interface_error = No
[logging]
# Valid log levels are 0 through 7:
# 0: Log only critical information
# 1: Log errors and lower log levels
# 2: Log warnings and lower log levels
# 3: Log notices and lower log levels
# 4: Log info and lower (this is the default)
# 5: Verbose logging
# 6: Debug logging
# 7: Extreme logging
loglevel = 4
# The interfaces section defines the physical and virtual
# interfaces Reticulum will use to communicate on. This
# section will contain examples for a variety of interface
# types. You can modify these or use them as a basis for
# your own config, or simply remove the unused ones.
[interfaces]
# This interface enables communication with other
# link-local Reticulum nodes over UDP. It does not
# need any functional IP infrastructure like routers
# or DHCP servers, but will require that at least link-
# local IPv6 is enabled in your operating system, which
# should be enabled by default in almost any OS. See
# the Reticulum Manual for more configuration options.
[[Default Interface]]
type = AutoInterface
enabled = yes
# The following example enables communication with other
# local Reticulum peers using UDP broadcasts.
[[UDP Interface]]
type = UDPInterface
enabled = no
listen_ip = 0.0.0.0
listen_port = 4242
forward_ip = 255.255.255.255
forward_port = 4242
# The above configuration will allow communication
# within the local broadcast domains of all local
# IP interfaces.
# Instead of specifying listen_ip, listen_port,
# forward_ip and forward_port, you can also bind
# to a specific network device like below.
# device = eth0
# port = 4242
# Assuming the eth0 device has the address
# 10.55.0.72/24, the above configuration would
# be equivalent to the following manual setup.
# Note that we are both listening and forwarding to
# the broadcast address of the network segments.
# listen_ip = 10.55.0.255
# listen_port = 4242
# forward_ip = 10.55.0.255
# forward_port = 4242
# You can of course also communicate only with
# a single IP address
# listen_ip = 10.55.0.15
# listen_port = 4242
# forward_ip = 10.55.0.16
# forward_port = 4242
# This example demonstrates a TCP server interface.
# It will listen for incoming connections on the
# specified IP address and port number.
[[TCP Server Interface]]
type = TCPServerInterface
enabled = no
# This configuration will listen on all IP
# interfaces on port 4242
listen_ip = 0.0.0.0
listen_port = 4242
# Alternatively you can bind to a specific IP
# listen_ip = 10.0.0.88
# listen_port = 4242
# Or a specific network device
# device = eth0
# port = 4242
# To connect to a TCP server interface, you would
# naturally use the TCP client interface. Here's
# an example. The target_host can either be an IP
# address or a hostname
[[TCP Client Interface]]
type = TCPClientInterface
enabled = no
target_host = 127.0.0.1
target_port = 4242
# This example shows how to make your Reticulum
# instance available over I2P, and connect to
# another I2P peer. Please be aware that you
# must have an I2P router running on your system
# with the SAMv3 API enabled for this to work.
[[I2P]]
type = I2PInterface
enabled = no
connectable = yes
peers = ykzlw5ujbaqc2xkec4cpvgyxj257wcrmmgkuxqmqcur7cq3w3lha.b32.i2p
# Here's an example of how to add a LoRa interface
# using the RNode LoRa transceiver.
[[RNode LoRa Interface]]
type = RNodeInterface
# Enable interface if you want use it!
enabled = no
# Serial port for the device
port = /dev/ttyUSB0
# Set frequency to 867.2 MHz
frequency = 867200000
# Set LoRa bandwidth to 125 KHz
bandwidth = 125000
# Set TX power to 7 dBm (5 mW)
txpower = 7
# Select spreading factor 8. Valid
# range is 7 through 12, with 7
# being the fastest and 12 having
# the longest range.
spreadingfactor = 8
# Select coding rate 5. Valid range
# is 5 throough 8, with 5 being the
# fastest, and 8 the longest range.
codingrate = 5
# You can configure the RNode to send
# out identification on the channel with
# a set interval by configuring the
# following two parameters. The trans-
# ceiver will only ID if the set
# interval has elapsed since it's last
# actual transmission. The interval is
# configured in seconds.
# This option is commented out and not
# used by default.
# id_callsign = MYCALL-0
# id_interval = 600
# For certain homebrew RNode interfaces
# with low amounts of RAM, using packet
# flow control can be useful. By default
# it is disabled.
flow_control = False
# An example KISS modem interface. Useful for running
# Reticulum over packet radio hardware.
[[Packet Radio KISS Interface]]
type = KISSInterface
# Enable interface if you want use it!
enabled = no
# Serial port for the device
port = /dev/ttyUSB1
# Set the serial baud-rate and other
# configuration parameters.
speed = 115200
databits = 8
parity = none
stopbits = 1
# Set the modem preamble. A 150ms
# preamble should be a reasonable
# default, but may need to be
# increased for radios with slow-
# opening squelch and long TX/RX
# turnaround
preamble = 150
# Set the modem TX tail. In most
# cases this should be kept as low
# as possible to not waste airtime.
txtail = 10
# Configure CDMA parameters. These
# settings are reasonable defaults.
persistence = 200
slottime = 20
# You can configure the interface to send
# out identification on the channel with
# a set interval by configuring the
# following two parameters. The KISS
# interface will only ID if the set
# interval has elapsed since it's last
# actual transmission. The interval is
# configured in seconds.
# This option is commented out and not
# used by default.
# id_callsign = MYCALL-0
# id_interval = 600
# Whether to use KISS flow-control.
# This is useful for modems that have
# a small internal packet buffer, but
# support packet flow control instead.
flow_control = false
# If you're using Reticulum on amateur radio spectrum,
# you might want to use the AX.25 KISS interface. This
# way, Reticulum will automatically encapsulate it's
# traffic in AX.25 and also identify your stations
# transmissions with your callsign and SSID.
#
# Only do this if you really need to! Reticulum doesn't
# need the AX.25 layer for anything, and it incurs extra
# overhead on every packet to encapsulate in AX.25.
#
# A more efficient way is to use the plain KISS interface
# with the beaconing functionality described above.
[[Packet Radio AX.25 KISS Interface]]
type = AX25KISSInterface
# Set the station callsign and SSID
callsign = NO1CLL
ssid = 0
# Enable interface if you want use it!
enabled = no
# Serial port for the device
port = /dev/ttyUSB2
# Set the serial baud-rate and other
# configuration parameters.
speed = 115200
databits = 8
parity = none
stopbits = 1
# Whether to use KISS flow-control.
# This is useful for modems with a
# small internal packet buffer.
flow_control = false
# Set the modem preamble. A 150ms
# preamble should be a reasonable
# default, but may need to be
# increased for radios with slow-
# opening squelch and long TX/RX
# turnaround
preamble = 150
# Set the modem TX tail. In most
# cases this should be kept as low
# as possible to not waste airtime.
txtail = 10
# Configure CDMA parameters. These
# settings are reasonable defaults.
persistence = 200
slottime = 20
'''
if __name__ == "__main__":
main()
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env python3
# 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 RNS
import argparse
from RNS._version import __version__
def size_str(num, suffix='B'):
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
if suffix == 'b':
num *= 8
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
for unit in units:
if abs(num) < 1000.0:
if unit == "":
return "%.0f %s%s" % (num, unit, suffix)
else:
return "%.2f %s%s" % (num, unit, suffix)
num /= 1000.0
return "%.2f%s%s" % (num, last_unit, suffix)
def program_setup(configdir, dispall=False, verbosity = 0):
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
stats = None
try:
stats = reticulum.get_interface_stats()
except Exception as e:
pass
if stats != None:
for ifstat in stats["interfaces"]:
name = ifstat["name"]
if dispall or not (
name.startswith("LocalInterface[") or
name.startswith("TCPInterface[Client") or
name.startswith("I2PInterfacePeer[Connected peer") or
(name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False))
):
if not (name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False)):
print("")
if ifstat["status"]:
ss = "Up"
else:
ss = "Down"
if ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ACCESS_POINT:
modestr = "Access Point"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_POINT_TO_POINT:
modestr = "Point-to-Point"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ROAMING:
modestr = "Roaming"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_BOUNDARY:
modestr = "Boundary"
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_GATEWAY:
modestr = "Gateway"
else:
modestr = "Full"
if ifstat["clients"] != None:
clients = ifstat["clients"]
if name.startswith("Shared Instance["):
cnum = max(clients-1,0)
if cnum == 1:
spec_str = " program"
else:
spec_str = " programs"
clients_string = "Serving : "+str(cnum)+spec_str
elif name.startswith("I2PInterface["):
if "i2p_connectable" in ifstat and ifstat["i2p_connectable"] == True:
cnum = clients
if cnum == 1:
spec_str = " connected I2P endpoint"
else:
spec_str = " connected I2P endpoints"
clients_string = "Peers : "+str(cnum)+spec_str
else:
clients_string = ""
else:
clients_string = "Clients : "+str(clients)
else:
clients = 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(" 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))
if "bitrate" in ifstat and ifstat["bitrate"] != None:
print(" Rate : {ss}".format(ss=speed_str(ifstat["bitrate"])))
if "peers" in ifstat and ifstat["peers"] != None:
print(" Peers : {np} reachable".format(np=ifstat["peers"]))
if "ifac_signature" in ifstat and ifstat["ifac_signature"] != None:
sigstr = "<…"+RNS.hexrep(ifstat["ifac_signature"][-5:], delimit=False)+">"
print(" Access : {nb}-bit IFAC by {sig}".format(nb=ifstat["ifac_size"]*8, sig=sigstr))
if "i2p_b32" in ifstat and ifstat["i2p_b32"] != None:
print(" I2P B32 : {ep}".format(ep=str(ifstat["i2p_b32"])))
if "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))
else:
print(" Queued : {np} announces".format(np=aqn))
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("")
else:
print("Could not get RNS status")
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum Network Stack Status")
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("--version", action="version", version="rnstatus {version}".format(version=__version__))
parser.add_argument(
"-a",
"--all",
action="store_true",
help="show all interfaces",
default=False
)
parser.add_argument('-v', '--verbose', action='count', default=0)
args = parser.parse_args()
if args.config:
configarg = args.config
else:
configarg = None
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose)
except KeyboardInterrupt:
print("")
exit()
def speed_str(num, suffix='bps'):
units = ['','k','M','G','T','P','E','Z']
last_unit = 'Y'
if suffix == 'Bps':
num /= 8
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
for unit in units:
if abs(num) < 1000.0:
return "%3.2f %s%s" % (num, unit, suffix)
num /= 1000.0
return "%.2f %s%s" % (num, last_unit, suffix)
if __name__ == "__main__":
main()
+697
View File
@@ -0,0 +1,697 @@
#!/usr/bin/env python3
# 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 RNS
import subprocess
import argparse
import shlex
import time
import sys
import tty
import os
from RNS._version import __version__
APP_NAME = "rnx"
identity = None
reticulum = None
allow_all = False
allowed_identity_hashes = []
def prepare_identity(identity_path):
global identity
if identity_path == None:
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
if os.path.isfile(identity_path):
identity = RNS.Identity.from_file(identity_path)
if identity == None:
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
identity = RNS.Identity()
identity.to_file(identity_path)
def listen(configdir, identitypath = None, verbosity = 0, quietness = 0, allowed = [], print_identity = False, disable_auth = None, disable_announce=False):
global identity, allow_all, allowed_identity_hashes, reticulum
targetloglevel = 3+verbosity-quietness
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
prepare_identity(identitypath)
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "execute")
if print_identity:
print("Identity : "+str(identity))
print("Listening on : "+RNS.prettyhexrep(destination.hash))
exit(0)
if disable_auth:
allow_all = True
else:
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:
destination_hash = bytes.fromhex(a)
allowed_identity_hashes.append(destination_hash)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
except Exception as e:
print(str(e))
exit(1)
if len(allowed_identity_hashes) < 1 and not disable_auth:
print("Warning: No allowed identities configured, rncx will not accept any commands!")
destination.set_link_established_callback(command_link_established)
if not allow_all:
destination.register_request_handler(
path = "command",
response_generator = execute_received_command,
allow = RNS.Destination.ALLOW_LIST,
allowed_list = allowed_identity_hashes
)
else:
destination.register_request_handler(
path = "command",
response_generator = execute_received_command,
allow = RNS.Destination.ALLOW_ALL,
)
RNS.log("rnx listening for commands on "+RNS.prettyhexrep(destination.hash))
if not disable_announce:
destination.announce()
while True:
time.sleep(1)
def command_link_established(link):
link.set_remote_identified_callback(initiator_identified)
link.set_link_closed_callback(command_link_closed)
RNS.log("Command link "+str(link)+" established")
def command_link_closed(link):
RNS.log("Command link "+str(link)+" closed")
def initiator_identified(link, identity):
global allow_all
RNS.log("Initiator of link "+str(link)+" identified as "+RNS.prettyhexrep(identity.hash))
if not allow_all and not identity.hash in allowed_identity_hashes:
RNS.log("Identity "+RNS.prettyhexrep(identity.hash)+" not allowed, tearing down link")
link.teardown()
def execute_received_command(path, data, request_id, remote_identity, requested_at):
command = data[0].decode("utf-8") # Command to execute
timeout = data[1] # Timeout in seconds
o_limit = data[2] # Size limit for stdout
e_limit = data[3] # Size limit for stderr
stdin = data[4] # Data passed to stdin
if remote_identity != None:
RNS.log("Executing command ["+command+"] for "+RNS.prettyhexrep(remote_identity.hash))
else:
RNS.log("Executing command ["+command+"] for unknown requestor")
result = [
False, # 0: Command was executed
None, # 1: Return value
None, # 2: Stdout
None, # 3: Stderr
None, # 4: Total stdout length
None, # 5: Total stderr length
time.time(), # 6: Started
None, # 7: Concluded
]
try:
process = subprocess.Popen(shlex.split(command), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result[0] = True
except Exception as e:
result[0] = False
return result
stdout = b""
stderr = b""
timed_out = False
if stdin != None:
process.stdin.write(stdin)
while True:
try:
stdout, stderr = process.communicate(timeout=1)
if process.poll() != None:
break
if len(stdout) > 0:
print(str(stdout))
sys.stdout.flush()
except subprocess.TimeoutExpired:
pass
if timeout != None and time.time() > result[6]+timeout:
RNS.log("Command ["+command+"] timed out and is being killed...")
process.terminate()
process.wait()
if process.poll() != None:
stdout, stderr = process.communicate()
else:
stdout = None
stderr = None
break
if timeout != None and time.time() < result[6]+timeout:
result[7] = time.time()
# Deliver result
result[1] = process.returncode
if o_limit != None and len(stdout) > o_limit:
if o_limit == 0:
result[2] = b""
else:
result[2] = stdout[0:o_limit]
else:
result[2] = stdout
if e_limit != None and len(stderr) > e_limit:
if e_limit == 0:
result[3] = b""
else:
result[3] = stderr[0:e_limit]
else:
result[3] = stderr
result[4] = len(stdout)
result[5] = len(stderr)
if timed_out:
RNS.log("Command timed out")
return result
if remote_identity != None:
RNS.log("Delivering result of command ["+str(command)+"] to "+RNS.prettyhexrep(remote_identity.hash))
else:
RNS.log("Delivering result of command ["+str(command)+"] to unknown requestor")
return result
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
current_progress = 0.0
stats = []
speed = 0.0
def spin_stat(until=None, timeout=None):
global current_progress, response_transfer_size, speed
i = 0
syms = "⢄⢂⢁⡁⡈⡐⡠"
if timeout != None:
timeout = time.time()+timeout
while (timeout == None or time.time()<timeout) and not until():
time.sleep(0.1)
prg = current_progress
percent = round(prg * 100.0, 1)
stat_str = str(percent)+"% - " + size_str(int(prg*response_transfer_size)) + " of " + size_str(response_transfer_size) + " - " +size_str(speed, "b")+"ps"
print("\r \rReceiving result "+syms[i]+" "+stat_str, end=" ")
sys.stdout.flush()
i = (i+1)%len(syms)
print("\r \r", end="")
if timeout != None and time.time() > timeout:
return False
else:
return True
def remote_execution_done(request_receipt):
pass
def remote_execution_progress(request_receipt):
stats_max = 32
global current_progress, response_transfer_size, speed
current_progress = request_receipt.progress
response_transfer_size = request_receipt.response_transfer_size
now = time.time()
got = current_progress*response_transfer_size
entry = [now, got]
stats.append(entry)
while len(stats) > stats_max:
stats.pop(0)
span = now - stats[0][0]
if span == 0:
speed = 0
else:
diff = got - stats[0][1]
speed = diff/span
link = None
listener_destination = None
remote_exec_grace = 2.0
def execute(configdir, identitypath = None, verbosity = 0, quietness = 0, detailed = False, mirror = False, noid = False, destination = None, command = None, stdin = None, stdoutl = None, stderrl = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, result_timeout = None, interactive = False):
global identity, reticulum, link, listener_destination, remote_exec_grace
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(241)
if reticulum == None:
targetloglevel = 3+verbosity-quietness
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
if identity == None:
prepare_identity(identitypath)
if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash)
if not spin(until=lambda: RNS.Transport.has_path(destination_hash), msg="Path to "+RNS.prettyhexrep(destination_hash)+" requested", timeout=timeout):
print("Path not found")
exit(242)
if listener_destination == None:
listener_identity = RNS.Identity.recall(destination_hash)
listener_destination = RNS.Destination(
listener_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
APP_NAME,
"execute"
)
if link == None or link.status == RNS.Link.CLOSED or link.status == RNS.Link.PENDING:
link = RNS.Link(listener_destination)
if not spin(until=lambda: link.status == RNS.Link.ACTIVE, msg="Establishing link with "+RNS.prettyhexrep(destination_hash), timeout=timeout):
print("Could not establish link with "+RNS.prettyhexrep(destination_hash))
exit(243)
if not noid:
link.identify(identity)
if stdin != None:
stdin = stdin.encode("utf-8")
request_data = [
command.encode("utf-8"), # Command to execute
timeout, # Timeout in seconds
stdoutl, # Size limit for stdout
stderrl, # Size limit for stderr
stdin, # Data passed to stdin
]
# TODO: Tune
rexec_timeout = timeout+link.rtt*4+remote_exec_grace
request_receipt = link.request(
path="command",
data=request_data,
response_callback=remote_execution_done,
failed_callback=remote_execution_done,
progress_callback=remote_execution_progress,
timeout=rexec_timeout
)
spin(
until=lambda:link.status == RNS.Link.CLOSED or (request_receipt.status != RNS.RequestReceipt.FAILED and request_receipt.status != RNS.RequestReceipt.SENT),
msg="Sending execution request",
timeout=rexec_timeout+0.5
)
if link.status == RNS.Link.CLOSED:
print("Could not request remote execution, link was closed")
exit(244)
if request_receipt.status == RNS.RequestReceipt.FAILED:
print("Could not request remote execution")
exit(244)
spin(
until=lambda:request_receipt.status != RNS.RequestReceipt.DELIVERED,
msg="Command delivered, awaiting result",
timeout=timeout
)
if request_receipt.status == RNS.RequestReceipt.FAILED:
print("No result was received")
exit(245)
spin_stat(
until=lambda:request_receipt.status != RNS.RequestReceipt.RECEIVING,
timeout=result_timeout
)
if request_receipt.status == RNS.RequestReceipt.FAILED:
print("Receiving result failed")
exit(246)
if request_receipt.response != None:
try:
executed = request_receipt.response[0]
retval = request_receipt.response[1]
stdout = request_receipt.response[2]
stderr = request_receipt.response[3]
outlen = request_receipt.response[4]
errlen = request_receipt.response[5]
started = request_receipt.response[6]
concluded = request_receipt.response[7]
except Exception as e:
print("Received invalid result")
exit(247)
if executed:
if detailed:
if stdout != None and len(stdout) > 0:
print(stdout.decode("utf-8"), end="")
if stderr != None and len(stderr) > 0:
print(stderr.decode("utf-8"), file=sys.stderr, end="")
sys.stdout.flush()
sys.stderr.flush()
print("\n--- End of remote output, rnx done ---")
if started != None and concluded != None:
cmd_duration = round(concluded - started, 3)
print("Remote command execution took "+str(cmd_duration)+" seconds")
total_size = request_receipt.response_size
if request_receipt.request_size != None:
total_size += request_receipt.request_size
transfer_duration = round(request_receipt.response_concluded_at - request_receipt.sent_at - cmd_duration, 3)
if transfer_duration == 1:
tdstr = " in 1 second"
elif transfer_duration < 10:
tdstr = " in "+str(transfer_duration)+" seconds"
else:
tdstr = " in "+pretty_time(transfer_duration)
spdstr = ", effective rate "+size_str(total_size/transfer_duration, "b")+"ps"
print("Transferred "+size_str(total_size)+tdstr+spdstr)
if outlen != None and stdout != None:
if len(stdout) < outlen:
tstr = ", "+str(len(stdout))+" bytes displayed"
else:
tstr = ""
print("Remote wrote "+str(outlen)+" bytes to stdout"+tstr)
if errlen != None and stderr != None:
if len(stderr) < errlen:
tstr = ", "+str(len(stderr))+" bytes displayed"
else:
tstr = ""
print("Remote wrote "+str(errlen)+" bytes to stderr"+tstr)
else:
if stdout != None and len(stdout) > 0:
print(stdout.decode("utf-8"), end="")
if stderr != None and len(stderr) > 0:
print(stderr.decode("utf-8"), file=sys.stderr, end="")
if (stdoutl != 0 and len(stdout) < outlen) or (stderrl != 0 and len(stderr) < errlen):
sys.stdout.flush()
sys.stderr.flush()
print("\nOutput truncated before being returned:")
if len(stdout) != 0 and len(stdout) < outlen:
print(" stdout truncated to "+str(len(stdout))+" bytes")
if len(stderr) != 0 and len(stderr) < errlen:
print(" stderr truncated to "+str(len(stderr))+" bytes")
else:
print("Remote could not execute command")
if interactive:
return
else:
exit(248)
else:
print("No response")
exit(249)
try:
if not interactive:
link.teardown()
except Exception as e:
pass
if not interactive and mirror:
if request_receipt.response[1] != None:
exit(request_receipt.response[1])
else:
exit(240)
else:
if interactive:
if mirror:
return request_receipt.response[1]
else:
return None
else:
exit(0)
def main():
try:
parser = argparse.ArgumentParser(description="Reticulum Remote Execution Utility")
parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the listener", type=str)
parser.add_argument("command", nargs="?", default=None, help="command to be execute", type=str)
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("-l", '--listen', action='store_true', default=False, help="listen for incoming commands")
parser.add_argument('-i', metavar="identity", action='store', dest="identity", default=None, help="path to identity to use", type=str)
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', '--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")
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="connect and request timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
parser.add_argument("-W", action="store", metavar="seconds", type=float, help="max result download time", default=None)
parser.add_argument("--stdin", action='store', default=None, help="pass input to stdin", type=str)
parser.add_argument("--stdout", action='store', default=None, help="max size in bytes of returned stdout", type=int)
parser.add_argument("--stderr", action='store', default=None, help="max size in bytes of returned stderr", type=int)
parser.add_argument("--version", action="version", version="rnx {version}".format(version=__version__))
args = parser.parse_args()
if args.listen or args.print_identity:
listen(
configdir = args.config,
identitypath = args.identity,
verbosity=args.verbose,
quietness=args.quiet,
allowed = args.allowed,
print_identity=args.print_identity,
disable_auth=args.noauth,
disable_announce=args.no_announce,
)
elif args.destination != None and args.command != None:
execute(
configdir = args.config,
identitypath = args.identity,
verbosity = args.verbose,
quietness = args.quiet,
detailed = args.detailed,
mirror = args.mirror,
noid = args.noid,
destination = args.destination,
command = args.command,
stdin = args.stdin,
stdoutl = args.stdout,
stderrl = args.stderr,
timeout = args.w,
result_timeout = args.W,
interactive = args.interactive,
)
if args.destination != None and args.interactive:
# command_history_max = 5000
# command_history = []
# command_current = ""
# history_idx = 0
# tty.setcbreak(sys.stdin.fileno())
code = None
while True:
try:
cstr = str(code) if code and code != 0 else ""
prompt = cstr+"> "
print(prompt,end="")
# cmdbuf = b""
# while True:
# ch = sys.stdin.read(1)
# cmdbuf += ch.encode("utf-8")
# print("\r"+prompt+cmdbuf.decode("utf-8"), end="")
command = input()
if command.lower() == "exit" or command.lower() == "quit":
exit(0)
except KeyboardInterrupt:
exit(0)
except EOFError:
exit(0)
if command.lower() == "clear":
print('\033c', end='')
# command_history.append(command)
# while len(command_history) > command_history_max:
# command_history.pop(0)
else:
code = execute(
configdir = args.config,
identitypath = args.identity,
verbosity = args.verbose,
quietness = args.quiet,
detailed = args.detailed,
mirror = args.mirror,
noid = args.noid,
destination = args.destination,
command = command,
stdin = None,
stdoutl = args.stdout,
stderrl = args.stderr,
timeout = args.w,
result_timeout = args.W,
interactive = True,
)
else:
print("")
parser.print_help()
print("")
except KeyboardInterrupt:
# tty.setnocbreak(sys.stdin.fileno())
print("")
if link != None:
link.teardown()
exit()
def size_str(num, suffix='B'):
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
if suffix == 'b':
num *= 8
units = ['','K','M','G','T','P','E','Z']
last_unit = 'Y'
for unit in units:
if abs(num) < 1000.0:
if unit == "":
return "%.0f %s%s" % (num, unit, suffix)
else:
return "%.2f %s%s" % (num, unit, suffix)
num /= 1000.0
return "%.2f%s%s" % (num, last_unit, suffix)
def pretty_time(time, verbose=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)
ss = "" if seconds == 1 else "s"
sm = "" if minutes == 1 else "s"
sh = "" if hours == 1 else "s"
sd = "" if days == 1 else "s"
components = []
if days > 0:
components.append(str(days)+" day"+sd if verbose else str(days)+"d")
if hours > 0:
components.append(str(hours)+" hour"+sh if verbose else str(hours)+"h")
if minutes > 0:
components.append(str(minutes)+" minute"+sm if verbose else str(minutes)+"m")
if seconds > 0:
components.append(str(seconds)+" second"+ss if verbose else str(seconds)+"s")
i = 0
tstr = ""
for c in components:
i += 1
if i == 1:
pass
elif i < len(components):
tstr += ", "
elif i == len(components):
tstr += " and "
tstr += c
return tstr
if __name__ == "__main__":
main()
+115 -45
View File
@@ -1,17 +1,42 @@
# 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 sys
import glob
import time
import random
import threading
from ._version import __version__
from .Reticulum import Reticulum
from .Identity import Identity
from .Link import Link
from .Link import Link, RequestReceipt
from .Transport import Transport
from .Destination import Destination
from .Packet import Packet
from .Packet import PacketReceipt
from .Resource import Resource
from .Resource import Resource, ResourceAdvertisement
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
@@ -25,65 +50,110 @@ LOG_VERBOSE = 5
LOG_DEBUG = 6
LOG_EXTREME = 7
LOG_STDOUT = 0x91
LOG_STDOUT = 0x91
LOG_FILE = 0x92
loglevel = LOG_NOTICE
LOG_MAXSIZE = 5*1024*1024
loglevel = LOG_NOTICE
logfile = None
logdest = LOG_STDOUT
logtimefmt = "%Y-%m-%d %H:%M:%S"
random.seed(os.urandom(10))
instance_random = random.Random()
instance_random.seed(os.urandom(10))
_always_override_destination = False
logging_lock = threading.Lock()
def loglevelname(level):
if (level == LOG_CRITICAL):
return "Critical"
if (level == LOG_ERROR):
return "Error"
if (level == LOG_WARNING):
return "Warning"
if (level == LOG_NOTICE):
return "Notice"
if (level == LOG_INFO):
return "Info"
if (level == LOG_VERBOSE):
return "Verbose"
if (level == LOG_DEBUG):
return "Debug"
if (level == LOG_EXTREME):
return "Extra"
return "Unknown"
if (level == LOG_CRITICAL):
return "Critical"
if (level == LOG_ERROR):
return "Error"
if (level == LOG_WARNING):
return "Warning"
if (level == LOG_NOTICE):
return "Notice"
if (level == LOG_INFO):
return "Info"
if (level == LOG_VERBOSE):
return "Verbose"
if (level == LOG_DEBUG):
return "Debug"
if (level == LOG_EXTREME):
return "Extra"
return "Unknown"
def log(msg, level=3):
# TODO: not thread safe
if loglevel >= level:
timestamp = time.time()
logstring = "["+time.strftime(logtimefmt)+"] ["+loglevelname(level)+"] "+msg
def version():
return __version__
if (logdest == LOG_STDOUT):
print(logstring)
def host_os():
from .vendor.platformutils import get_platform
return get_platform()
if (logdest == LOG_FILE and logfile != None):
file = open(logfile, "a")
file.write(logstring+"\n")
file.close()
def timestamp_str(time_s):
timestamp = time.localtime(time_s)
return time.strftime(logtimefmt, timestamp)
def log(msg, level=3, _override_destination = False):
global _always_override_destination
if loglevel >= level:
logstring = "["+timestamp_str(time.time())+"] ["+loglevelname(level)+"] "+msg
logging_lock.acquire()
if (logdest == LOG_STDOUT or _always_override_destination or _override_destination):
print(logstring)
logging_lock.release()
elif (logdest == LOG_FILE and logfile != None):
try:
file = open(logfile, "a")
file.write(logstring+"\n")
file.close()
if os.path.getsize(logfile) > LOG_MAXSIZE:
prevfile = logfile+".1"
if os.path.isfile(prevfile):
os.unlink(prevfile)
os.rename(logfile, prevfile)
logging_lock.release()
except Exception as e:
logging_lock.release()
_always_override_destination = True
log("Exception occurred while writing log message to log file: "+str(e), LOG_CRITICAL)
log("Dumping future log events to console!", LOG_CRITICAL)
log(msg, level)
def rand():
result = random.random()
return result
result = instance_random.random()
return result
def hexrep(data, delimit=True):
delimiter = ":"
if not delimit:
delimiter = ""
hexrep = delimiter.join("{:02x}".format(c) for c in data)
return hexrep
try:
iter(data)
except TypeError:
data = [data]
delimiter = ":"
if not delimit:
delimiter = ""
hexrep = delimiter.join("{:02x}".format(c) for c in data)
return hexrep
def prettyhexrep(data):
delimiter = ""
hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">"
return hexrep
delimiter = ""
hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">"
return hexrep
def panic():
os._exit(255)
os._exit(255)
def exit():
print("")
sys.exit(0)
+1
View File
@@ -0,0 +1 @@
__version__ = "0.3.7"
+1 -1
View File
@@ -19,7 +19,7 @@ import sys
from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE
import six
import RNS.vendor.six as six
__version__ = '5.0.6'
# imported lazily to avoid startup performance hit if it isn't used
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2018 Viktor Villainov
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.
+25
View File
@@ -0,0 +1,25 @@
"""
A modern asynchronous library for building I2P applications.
"""
from .__version__ import (
__title__, __description__, __url__, __version__,
__author__, __author_email__, __license__, __copyright__
)
from .sam import Destination, PrivateKey
from .aiosam import (
get_sam_socket, dest_lookup, new_destination,
create_session, stream_connect, stream_accept,
Session, StreamConnection, StreamAcceptor
)
from .tunnel import ClientTunnel, ServerTunnel
from .utils import get_sam_address
from .exceptions import (
CantReachPeer, DuplicatedDest, DuplicatedId, I2PError,
InvalidId, InvalidKey, KeyNotFound, PeerNotFound, Timeout,
)
+8
View File
@@ -0,0 +1,8 @@
__title__ = 'i2plib'
__description__ = 'A modern asynchronous library for building I2P applications.'
__url__ = 'https://github.com/l-n-s/i2plib'
__version__ = '0.0.14'
__author__ = 'Viktor Villainov'
__author_email__ = 'supervillain@riseup.net'
__license__ = 'MIT'
__copyright__ = 'Copyright 2018 Viktor Villainov'
+258
View File
@@ -0,0 +1,258 @@
import asyncio
from . import sam
from . import exceptions
from . import utils
from .log import logger
def parse_reply(data):
if not data:
raise ConnectionAbortedError("Empty response: SAM API went offline")
try:
msg = sam.Message(data.decode().strip())
logger.debug("SAM reply: "+str(msg))
except:
raise ConnectionAbortedError("Invalid SAM response")
return msg
async def get_sam_socket(sam_address=sam.DEFAULT_ADDRESS, loop=None):
"""A couroutine used to create a new SAM socket.
:param sam_address: (optional) SAM API address
:param loop: (optional) event loop instance
:return: A (reader, writer) pair
"""
reader, writer = await asyncio.open_connection(*sam_address)
writer.write(sam.hello("3.1", "3.1"))
reply = parse_reply(await reader.readline())
if reply.ok:
return (reader, writer)
else:
writer.close()
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
async def dest_lookup(domain, sam_address=sam.DEFAULT_ADDRESS,
loop=None):
"""A coroutine used to lookup a full I2P destination by .i2p domain or
.b32.i2p address.
:param domain: Address to be resolved, can be a .i2p domain or a .b32.i2p
address.
:param sam_address: (optional) SAM API address
:param loop: (optional) Event loop instance
:return: An instance of :class:`Destination`
"""
reader, writer = await get_sam_socket(sam_address, loop)
writer.write(sam.naming_lookup(domain))
reply = parse_reply(await reader.readline())
writer.close()
if reply.ok:
return sam.Destination(reply["VALUE"])
else:
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
async def new_destination(sam_address=sam.DEFAULT_ADDRESS, loop=None,
sig_type=sam.Destination.default_sig_type):
"""A coroutine used to generate a new destination with a private key of a
chosen signature type.
:param sam_address: (optional) SAM API address
:param loop: (optional) Event loop instance
:param sig_type: (optional) Signature type
:return: An instance of :class:`Destination`
"""
reader, writer = await get_sam_socket(sam_address, loop)
writer.write(sam.dest_generate(sig_type))
reply = parse_reply(await reader.readline())
writer.close()
return sam.Destination(reply["PRIV"], has_private_key=True)
async def create_session(session_name, sam_address=sam.DEFAULT_ADDRESS,
loop=None, style="STREAM",
signature_type=sam.Destination.default_sig_type,
destination=None, options={}):
"""A coroutine used to create a new SAM session.
:param session_name: Session nick name
:param sam_address: (optional) SAM API address
:param loop: (optional) Event loop instance
:param style: (optional) Session style, can be STREAM, DATAGRAM, RAW
:param signature_type: (optional) If the destination is TRANSIENT, this
signature type is used
:param destination: (optional) Destination to use in this session. Can be
a base64 encoded string, :class:`Destination`
instance or None. TRANSIENT destination is used when it
is None.
:param options: (optional) A dict object with i2cp options
:return: A (reader, writer) pair
"""
logger.debug("Creating session {}".format(session_name))
if destination:
if type(destination) == sam.Destination:
destination = destination
else:
destination = sam.Destination(
destination, has_private_key=True)
dest_string = destination.private_key.base64
else:
dest_string = sam.TRANSIENT_DESTINATION
options = " ".join(["{}={}".format(k, v) for k, v in options.items()])
reader, writer = await get_sam_socket(sam_address, loop)
writer.write(sam.session_create(
style, session_name, dest_string, options))
reply = parse_reply(await reader.readline())
if reply.ok:
if not destination:
destination = sam.Destination(
reply["DESTINATION"], has_private_key=True)
logger.debug(destination.base32)
logger.debug("Session created {}".format(session_name))
return (reader, writer)
else:
writer.close()
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
async def stream_connect(session_name, destination,
sam_address=sam.DEFAULT_ADDRESS, loop=None):
"""A coroutine used to connect to a remote I2P destination.
:param session_name: Session nick name
:param destination: I2P destination to connect to
:param sam_address: (optional) SAM API address
:param loop: (optional) Event loop instance
:return: A (reader, writer) pair
"""
logger.debug("Connecting stream {}".format(session_name))
if isinstance(destination, str) and not destination.endswith(".i2p"):
destination = sam.Destination(destination)
elif isinstance(destination, str):
destination = await dest_lookup(destination, sam_address, loop)
reader, writer = await get_sam_socket(sam_address, loop)
writer.write(sam.stream_connect(session_name, destination.base64,
silent="false"))
reply = parse_reply(await reader.readline())
if reply.ok:
logger.debug("Stream connected {}".format(session_name))
return (reader, writer)
else:
writer.close()
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
async def stream_accept(session_name, sam_address=sam.DEFAULT_ADDRESS,
loop=None):
"""A coroutine used to accept a connection from the I2P network.
:param session_name: Session nick name
:param sam_address: (optional) SAM API address
:param loop: (optional) Event loop instance
:return: A (reader, writer) pair
"""
reader, writer = await get_sam_socket(sam_address, loop)
writer.write(sam.stream_accept(session_name, silent="false"))
reply = parse_reply(await reader.readline())
if reply.ok:
return (reader, writer)
else:
writer.close()
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
### Context managers
class Session:
"""Async SAM session context manager.
:param session_name: Session nick name
:param sam_address: (optional) SAM API address
:param loop: (optional) Event loop instance
:param style: (optional) Session style, can be STREAM, DATAGRAM, RAW
:param signature_type: (optional) If the destination is TRANSIENT, this
signature type is used
:param destination: (optional) Destination to use in this session. Can be
a base64 encoded string, :class:`Destination`
instance or None. TRANSIENT destination is used when it
is None.
:param options: (optional) A dict object with i2cp options
:return: :class:`Session` object
"""
def __init__(self, session_name, sam_address=sam.DEFAULT_ADDRESS,
loop=None, style="STREAM",
signature_type=sam.Destination.default_sig_type,
destination=None, options={}):
self.session_name = session_name
self.sam_address = sam_address
self.loop = loop
self.style = style
self.signature_type = signature_type
self.destination = destination
self.options = options
async def __aenter__(self):
self.reader, self.writer = await create_session(self.session_name,
sam_address=self.sam_address, loop=self.loop, style=self.style,
signature_type=self.signature_type,
destination=self.destination, options=self.options)
return self
async def __aexit__(self, exc_type, exc, tb):
### TODO handle exceptions
self.writer.close()
class StreamConnection:
"""Async stream connection context manager.
:param session_name: Session nick name
:param destination: I2P destination to connect to
:param sam_address: (optional) SAM API address
:param loop: (optional) Event loop instance
:return: :class:`StreamConnection` object
"""
def __init__(self, session_name, destination,
sam_address=sam.DEFAULT_ADDRESS, loop=None):
self.session_name = session_name
self.sam_address = sam_address
self.loop = loop
self.destination = destination
async def __aenter__(self):
self.reader, self.writer = await stream_connect(self.session_name,
self.destination, sam_address=self.sam_address, loop=self.loop)
self.read = self.reader.read
self.write = self.writer.write
return self
async def __aexit__(self, exc_type, exc, tb):
### TODO handle exceptions
self.writer.close()
class StreamAcceptor:
"""Async stream acceptor context manager.
:param session_name: Session nick name
:param sam_address: (optional) SAM API address
:param loop: (optional) Event loop instance
:return: :class:`StreamAcceptor` object
"""
def __init__(self, session_name, sam_address=sam.DEFAULT_ADDRESS,
loop=None):
self.session_name = session_name
self.sam_address = sam_address
self.loop = loop
async def __aenter__(self):
self.reader, self.writer = await stream_accept(self.session_name,
sam_address=self.sam_address, loop=self.loop)
self.read = self.reader.read
self.write = self.writer.write
return self
async def __aexit__(self, exc_type, exc, tb):
### TODO handle exceptions
self.writer.close()
+44
View File
@@ -0,0 +1,44 @@
# SAM exceptions
class SAMException(IOError):
"""Base class for SAM exceptions"""
class CantReachPeer(SAMException):
"""The peer exists, but cannot be reached"""
class DuplicatedDest(SAMException):
"""The specified Destination is already in use"""
class DuplicatedId(SAMException):
"""The nickname is already associated with a session"""
class I2PError(SAMException):
"""A generic I2P error"""
class InvalidId(SAMException):
"""STREAM SESSION ID doesn't exist"""
class InvalidKey(SAMException):
"""The specified key is not valid (bad format, etc.)"""
class KeyNotFound(SAMException):
"""The naming system can't resolve the given name"""
class PeerNotFound(SAMException):
"""The peer cannot be found on the network"""
class Timeout(SAMException):
"""The peer cannot be found on the network"""
SAM_EXCEPTIONS = {
"CANT_REACH_PEER": CantReachPeer,
"DUPLICATED_DEST": DuplicatedDest,
"DUPLICATED_ID": DuplicatedId,
"I2P_ERROR": I2PError,
"INVALID_ID": InvalidId,
"INVALID_KEY": InvalidKey,
"KEY_NOT_FOUND": KeyNotFound,
"PEER_NOT_FOUND": PeerNotFound,
"TIMEOUT": Timeout,
}
+5
View File
@@ -0,0 +1,5 @@
"""Logging configuration."""
import logging
# Name the logger after the package.
logger = logging.getLogger(__package__)
+147
View File
@@ -0,0 +1,147 @@
from base64 import b64decode, b64encode, b32encode
from hashlib import sha256
import struct
import re
I2P_B64_CHARS = "-~"
def i2p_b64encode(x):
"""Encode I2P destination"""
return b64encode(x, altchars=I2P_B64_CHARS.encode()).decode()
def i2p_b64decode(x):
"""Decode I2P destination"""
return b64decode(x, altchars=I2P_B64_CHARS, validate=True)
SAM_BUFSIZE = 4096
DEFAULT_ADDRESS = ("127.0.0.1", 7656)
DEFAULT_MIN_VER = "3.1"
DEFAULT_MAX_VER = "3.1"
TRANSIENT_DESTINATION = "TRANSIENT"
VALID_BASE32_ADDRESS = re.compile(r"^([a-zA-Z0-9]{52}).b32.i2p$")
VALID_BASE64_ADDRESS = re.compile(r"^([a-zA-Z0-9-~=]{516,528})$")
class Message(object):
"""Parse SAM message to an object"""
def __init__(self, s):
self.opts = {}
if type(s) != str:
self._reply_string = s.decode().strip()
else:
self._reply_string = s
self.cmd, self.action, opts = self._reply_string.split(" ", 2)
for v in opts.split(" "):
data = v.split("=", 1) if "=" in v else (v, True)
self.opts[data[0]] = data[1]
def __getitem__(self, key):
return self.opts[key]
@property
def ok(self):
return self["RESULT"] == "OK"
def __repr__(self):
return self._reply_string
# SAM request messages
def hello(min_version, max_version):
return "HELLO VERSION MIN={} MAX={}\n".format(min_version,
max_version).encode()
def session_create(style, session_id, destination, options=""):
return "SESSION CREATE STYLE={} ID={} DESTINATION={} {}\n".format(
style, session_id, destination, options).encode()
def stream_connect(session_id, destination, silent="false"):
return "STREAM CONNECT ID={} DESTINATION={} SILENT={}\n".format(
session_id, destination, silent).encode()
def stream_accept(session_id, silent="false"):
return "STREAM ACCEPT ID={} SILENT={}\n".format(session_id, silent).encode()
def stream_forward(session_id, port, options=""):
return "STREAM FORWARD ID={} PORT={} {}\n".format(
session_id, port, options).encode()
def naming_lookup(name):
return "NAMING LOOKUP NAME={}\n".format(name).encode()
def dest_generate(signature_type):
return "DEST GENERATE SIGNATURE_TYPE={}\n".format(signature_type).encode()
class Destination(object):
"""I2P destination
https://geti2p.net/spec/common-structures#destination
:param data: (optional) Base64 encoded data or binary data
:param path: (optional) A path to a file with binary data
:param has_private_key: (optional) Does data have a private key?
"""
ECDSA_SHA256_P256 = 1
ECDSA_SHA384_P384 = 2
ECDSA_SHA512_P521 = 3
EdDSA_SHA512_Ed25519 = 7
default_sig_type = EdDSA_SHA512_Ed25519
_pubkey_size = 256
_signkey_size = 128
_min_cert_size = 3
def __init__(self, data=None, path=None, has_private_key=False):
#: Binary destination
self.data = bytes()
#: Base64 encoded destination
self.base64 = ""
#: :class:`RNS.vendor.i2plib.PrivateKey` instance or None
self.private_key = None
if path:
with open(path, "rb") as f: data = f.read()
if data and has_private_key:
self.private_key = PrivateKey(data)
cert_len = struct.unpack("!H", self.private_key.data[385:387])[0]
data = self.private_key.data[:387+cert_len]
if not data:
raise Exception("Can't create a destination with no data")
self.data = data if type(data) == bytes else i2p_b64decode(data)
self.base64 = data if type(data) == str else i2p_b64encode(data)
def __repr__(self):
return "<Destination: {}>".format(self.base32)
@property
def base32(self):
"""Base32 destination hash of this destination"""
desthash = sha256(self.data).digest()
return b32encode(desthash).decode()[:52].lower()
class PrivateKey(object):
"""I2P private key
https://geti2p.net/spec/common-structures#keysandcert
:param data: Base64 encoded data or binary data
"""
def __init__(self, data):
#: Binary private key
self.data = data if type(data) == bytes else i2p_b64decode(data)
#: Base64 encoded private key
self.base64 = data if type(data) == str else i2p_b64encode(data)
+233
View File
@@ -0,0 +1,233 @@
import logging
import asyncio
import argparse
from . import sam
from . import aiosam
from . import utils
from .log import logger
BUFFER_SIZE = 65536
async def proxy_data(reader, writer):
"""Proxy data from reader to writer"""
try:
while True:
data = await reader.read(BUFFER_SIZE)
if not data:
break
writer.write(data)
except Exception as e:
logger.debug('proxy_data_task exception {}'.format(e))
finally:
try:
writer.close()
except RuntimeError:
pass
logger.debug('close connection')
class I2PTunnel(object):
"""Base I2P Tunnel object, not to be used directly
:param local_address: A local address to use for a tunnel.
E.g. ("127.0.0.1", 6668)
:param destination: (optional) Destination to use for this tunnel. Can be
a base64 encoded string, :class:`Destination`
instance or None. A new destination is created when it
is None.
:param session_name: (optional) Session nick name. A new session nickname is
generated if not specified.
:param options: (optional) A dict object with i2cp options
:param loop: (optional) Event loop instance
:param sam_address: (optional) SAM API address
"""
def __init__(self, local_address, destination=None, session_name=None,
options={}, loop=None, sam_address=sam.DEFAULT_ADDRESS):
self.local_address = local_address
self.destination = destination
self.session_name = session_name or utils.generate_session_id()
self.options = options
self.loop = loop
self.sam_address = sam_address
async def _pre_run(self):
if not self.destination:
self.destination = await aiosam.new_destination(
sam_address=self.sam_address, loop=self.loop)
_, self.session_writer = await aiosam.create_session(
self.session_name, style=self.style, options=self.options,
sam_address=self.sam_address,
loop=self.loop, destination=self.destination)
def stop(self):
"""Stop the tunnel"""
self.session_writer.close()
class ClientTunnel(I2PTunnel):
"""Client tunnel, a subclass of tunnel.I2PTunnel
If you run a client tunnel with a local address ("127.0.0.1", 6668) and
a remote destination "irc.echelon.i2p", all connections to 127.0.0.1:6668
will be proxied to irc.echelon.i2p.
:param remote_destination: Remote I2P destination, can be either .i2p
domain, .b32.i2p address, base64 destination or
:class:`Destination` instance
"""
def __init__(self, remote_destination, *args, **kwargs):
super().__init__(*args, **kwargs)
self.style = "STREAM"
self.remote_destination = remote_destination
async def run(self):
"""A coroutine used to run the tunnel"""
await self._pre_run()
self.status = { "setup_ran": False, "setup_failed": False, "exception": None, "connect_tasks": [] }
async def handle_client(client_reader, client_writer):
"""Handle local client connection"""
try:
sc_task = aiosam.stream_connect(
self.session_name, self.remote_destination,
sam_address=self.sam_address, loop=self.loop)
self.status["connect_tasks"].append(sc_task)
remote_reader, remote_writer = await sc_task
asyncio.ensure_future(proxy_data(remote_reader, client_writer),
loop=self.loop)
asyncio.ensure_future(proxy_data(client_reader, remote_writer),
loop=self.loop)
except Exception as e:
self.status["setup_ran"] = True
self.status["setup_failed"] = True
self.status["exception"] = e
try:
self.server = await asyncio.start_server(handle_client, *self.local_address)
self.status["setup_ran"] = True
except Exception as e:
self.status["setup_ran"] = True
self.status["setup_failed"] = True
self.status["exception"] = e
def stop(self):
super().stop()
self.server.close()
class ServerTunnel(I2PTunnel):
"""Server tunnel, a subclass of tunnel.I2PTunnel
If you want to expose a local service 127.0.0.1:80 to the I2P network, run
a server tunnel with a local address ("127.0.0.1", 80). If you don't
provide a private key or a session name, it will use a TRANSIENT
destination.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.style = "STREAM"
async def run(self):
"""A coroutine used to run the tunnel"""
await self._pre_run()
self.status = { "setup_ran": False, "setup_failed": False, "exception": None, "connect_tasks": [] }
async def handle_client(incoming, client_reader, client_writer):
try:
# data and dest may come in one chunk
dest, data = incoming.split(b"\n", 1)
remote_destination = sam.Destination(dest.decode())
logger.debug("{} client connected: {}.b32.i2p".format(
self.session_name, remote_destination.base32))
except Exception as e:
self.status["exception"] = e
self.status["setup_failed"] = True
try:
sc_task = asyncio.wait_for(
asyncio.open_connection(
host=self.local_address[0],
port=self.local_address[1]),
timeout=5)
self.status["connect_tasks"].append(sc_task)
remote_reader, remote_writer = await sc_task
if data: remote_writer.write(data)
asyncio.ensure_future(proxy_data(remote_reader, client_writer),
loop=self.loop)
asyncio.ensure_future(proxy_data(client_reader, remote_writer),
loop=self.loop)
except ConnectionRefusedError:
client_writer.close()
self.status["exception"] = e
self.status["setup_failed"] = True
async def server_loop():
try:
while True:
client_reader, client_writer = await aiosam.stream_accept(
self.session_name, sam_address=self.sam_address,
loop=self.loop)
incoming = await client_reader.read(BUFFER_SIZE)
asyncio.ensure_future(handle_client(
incoming, client_reader, client_writer), loop=self.loop)
except asyncio.CancelledError:
pass
self.server_loop = asyncio.ensure_future(server_loop(), loop=self.loop)
self.status["setup_ran"] = True
def stop(self):
super().stop()
self.server_loop.cancel()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('type', metavar="TYPE", choices=('server', 'client'),
help="Tunnel type (server or client)")
parser.add_argument('address', metavar="ADDRESS",
help="Local address (e.g. 127.0.0.1:8000)")
parser.add_argument('--debug', '-d', action='store_true',
help='Debugging')
parser.add_argument('--key', '-k', default='', metavar='PRIVATE_KEY',
help='Path to private key file')
parser.add_argument('--destination', '-D', default='',
metavar='DESTINATION', help='Remote destination')
args = parser.parse_args()
SAM_ADDRESS = utils.get_sam_address()
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
loop = asyncio.get_event_loop()
loop.set_debug(args.debug)
if args.key:
destination = sam.Destination(path=args.key, has_private_key=True)
else:
destination = None
local_address = utils.address_from_string(args.address)
if args.type == "client":
tunnel = ClientTunnel(args.destination, local_address, loop=loop,
destination=destination, sam_address=SAM_ADDRESS)
elif args.type == "server":
tunnel = ServerTunnel(local_address, loop=loop, destination=destination,
sam_address=SAM_ADDRESS)
asyncio.ensure_future(tunnel.run(), loop=loop)
try:
loop.run_forever()
except KeyboardInterrupt:
tunnel.stop()
finally:
loop.stop()
loop.close()
+42
View File
@@ -0,0 +1,42 @@
import socket
import os
import random
import string
from . import sam
def get_free_port():
"""Get a free port on your local host"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', 0))
free_port = s.getsockname()[1]
s.close()
return free_port
def is_address_accessible(address):
"""Check if address is accessible or down"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
is_accessible = s.connect_ex(address) == 0
s.close()
return is_accessible
def address_from_string(address_string):
"""Address tuple from host:port string"""
address = address_string.split(":")
return (address[0], int(address[1]))
def get_sam_address():
"""
Get SAM address from environment variable I2P_SAM_ADDRESS, or use a default
value
"""
value = os.getenv("I2P_SAM_ADDRESS")
return address_from_string(value) if value else sam.DEFAULT_ADDRESS
def generate_session_id(length=6):
"""Generate random session id"""
rand = random.SystemRandom()
sid = [rand.choice(string.ascii_letters) for _ in range(length)]
return "reticulum-" + "".join(sid)
+45
View File
@@ -0,0 +1,45 @@
def get_platform():
from os import environ
if "ANDROID_ARGUMENT" in environ:
return "android"
elif "ANDROID_ROOT" in environ:
return "android"
else:
import sys
return sys.platform
def is_darwin():
if get_platform() == "darwin":
return True
else:
return False
def is_android():
if get_platform() == "android":
return True
else:
return False
def is_windows():
if str(get_platform()).startswith("win"):
return True
else:
return False
def platform_checks():
if is_windows():
import sys
if sys.version_info.major >= 3 and sys.version_info.minor >= 8:
pass
else:
import RNS
RNS.log("On Windows, Reticulum requires Python 3.8 or higher.", RNS.LOG_ERROR)
RNS.log("Please update Python to run Reticulum.", RNS.LOG_ERROR)
RNS.panic()
def cryptography_old_api():
import cryptography
if cryptography.__version__ == "2.8":
return True
else:
return False
+998
View File
@@ -0,0 +1,998 @@
# Copyright (c) 2010-2020 Benjamin Peterson
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Utilities for writing code that runs on Python 2 and 3"""
from __future__ import absolute_import
import functools
import itertools
import operator
import sys
import types
__author__ = "Benjamin Peterson <benjamin@python.org>"
__version__ = "1.16.0"
# Useful for very coarse version differentiation.
PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3
PY34 = sys.version_info[0:2] >= (3, 4)
if PY3:
string_types = str,
integer_types = int,
class_types = type,
text_type = str
binary_type = bytes
MAXSIZE = sys.maxsize
else:
string_types = basestring,
integer_types = (int, long)
class_types = (type, types.ClassType)
text_type = unicode
binary_type = str
if sys.platform.startswith("java"):
# Jython always uses 32 bits.
MAXSIZE = int((1 << 31) - 1)
else:
# It's possible to have sizeof(long) != sizeof(Py_ssize_t).
class X(object):
def __len__(self):
return 1 << 31
try:
len(X())
except OverflowError:
# 32-bit
MAXSIZE = int((1 << 31) - 1)
else:
# 64-bit
MAXSIZE = int((1 << 63) - 1)
del X
if PY34:
from importlib.util import spec_from_loader
else:
spec_from_loader = None
def _add_doc(func, doc):
"""Add documentation to a function."""
func.__doc__ = doc
def _import_module(name):
"""Import module, returning the module after the last dot."""
__import__(name)
return sys.modules[name]
class _LazyDescr(object):
def __init__(self, name):
self.name = name
def __get__(self, obj, tp):
result = self._resolve()
setattr(obj, self.name, result) # Invokes __set__.
try:
# This is a bit ugly, but it avoids running this again by
# removing this descriptor.
delattr(obj.__class__, self.name)
except AttributeError:
pass
return result
class MovedModule(_LazyDescr):
def __init__(self, name, old, new=None):
super(MovedModule, self).__init__(name)
if PY3:
if new is None:
new = name
self.mod = new
else:
self.mod = old
def _resolve(self):
return _import_module(self.mod)
def __getattr__(self, attr):
_module = self._resolve()
value = getattr(_module, attr)
setattr(self, attr, value)
return value
class _LazyModule(types.ModuleType):
def __init__(self, name):
super(_LazyModule, self).__init__(name)
self.__doc__ = self.__class__.__doc__
def __dir__(self):
attrs = ["__doc__", "__name__"]
attrs += [attr.name for attr in self._moved_attributes]
return attrs
# Subclasses should override this
_moved_attributes = []
class MovedAttribute(_LazyDescr):
def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
super(MovedAttribute, self).__init__(name)
if PY3:
if new_mod is None:
new_mod = name
self.mod = new_mod
if new_attr is None:
if old_attr is None:
new_attr = name
else:
new_attr = old_attr
self.attr = new_attr
else:
self.mod = old_mod
if old_attr is None:
old_attr = name
self.attr = old_attr
def _resolve(self):
module = _import_module(self.mod)
return getattr(module, self.attr)
class _SixMetaPathImporter(object):
"""
A meta path importer to import six.moves and its submodules.
This class implements a PEP302 finder and loader. It should be compatible
with Python 2.5 and all existing versions of Python3
"""
def __init__(self, six_module_name):
self.name = six_module_name
self.known_modules = {}
def _add_module(self, mod, *fullnames):
for fullname in fullnames:
self.known_modules[self.name + "." + fullname] = mod
def _get_module(self, fullname):
return self.known_modules[self.name + "." + fullname]
def find_module(self, fullname, path=None):
if fullname in self.known_modules:
return self
return None
def find_spec(self, fullname, path, target=None):
if fullname in self.known_modules:
return spec_from_loader(fullname, self)
return None
def __get_module(self, fullname):
try:
return self.known_modules[fullname]
except KeyError:
raise ImportError("This loader does not know module " + fullname)
def load_module(self, fullname):
try:
# in case of a reload
return sys.modules[fullname]
except KeyError:
pass
mod = self.__get_module(fullname)
if isinstance(mod, MovedModule):
mod = mod._resolve()
else:
mod.__loader__ = self
sys.modules[fullname] = mod
return mod
def is_package(self, fullname):
"""
Return true, if the named module is a package.
We need this method to get correct spec objects with
Python 3.4 (see PEP451)
"""
return hasattr(self.__get_module(fullname), "__path__")
def get_code(self, fullname):
"""Return None
Required, if is_package is implemented"""
self.__get_module(fullname) # eventually raises ImportError
return None
get_source = get_code # same as get_code
def create_module(self, spec):
return self.load_module(spec.name)
def exec_module(self, module):
pass
_importer = _SixMetaPathImporter(__name__)
class _MovedItems(_LazyModule):
"""Lazy loading of moved objects"""
__path__ = [] # mark as package
_moved_attributes = [
MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
MovedAttribute("intern", "__builtin__", "sys"),
MovedAttribute("map", "itertools", "builtins", "imap", "map"),
MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
MovedAttribute("getoutput", "commands", "subprocess"),
MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
MovedAttribute("reduce", "__builtin__", "functools"),
MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
MovedAttribute("StringIO", "StringIO", "io"),
MovedAttribute("UserDict", "UserDict", "collections"),
MovedAttribute("UserList", "UserList", "collections"),
MovedAttribute("UserString", "UserString", "collections"),
MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
MovedModule("builtins", "__builtin__"),
MovedModule("configparser", "ConfigParser"),
MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"),
MovedModule("copyreg", "copy_reg"),
MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"),
MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_thread"),
MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
MovedModule("http_cookies", "Cookie", "http.cookies"),
MovedModule("html_entities", "htmlentitydefs", "html.entities"),
MovedModule("html_parser", "HTMLParser", "html.parser"),
MovedModule("http_client", "httplib", "http.client"),
MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"),
MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
MovedModule("cPickle", "cPickle", "pickle"),
MovedModule("queue", "Queue"),
MovedModule("reprlib", "repr"),
MovedModule("socketserver", "SocketServer"),
MovedModule("_thread", "thread", "_thread"),
MovedModule("tkinter", "Tkinter"),
MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
MovedModule("tkinter_colorchooser", "tkColorChooser",
"tkinter.colorchooser"),
MovedModule("tkinter_commondialog", "tkCommonDialog",
"tkinter.commondialog"),
MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
MovedModule("tkinter_font", "tkFont", "tkinter.font"),
MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
"tkinter.simpledialog"),
MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
]
# Add windows specific modules.
if sys.platform == "win32":
_moved_attributes += [
MovedModule("winreg", "_winreg"),
]
for attr in _moved_attributes:
setattr(_MovedItems, attr.name, attr)
if isinstance(attr, MovedModule):
_importer._add_module(attr, "moves." + attr.name)
del attr
_MovedItems._moved_attributes = _moved_attributes
moves = _MovedItems(__name__ + ".moves")
_importer._add_module(moves, "moves")
class Module_six_moves_urllib_parse(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_parse"""
_urllib_parse_moved_attributes = [
MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
MovedAttribute("urljoin", "urlparse", "urllib.parse"),
MovedAttribute("urlparse", "urlparse", "urllib.parse"),
MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
MovedAttribute("quote", "urllib", "urllib.parse"),
MovedAttribute("quote_plus", "urllib", "urllib.parse"),
MovedAttribute("unquote", "urllib", "urllib.parse"),
MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"),
MovedAttribute("urlencode", "urllib", "urllib.parse"),
MovedAttribute("splitquery", "urllib", "urllib.parse"),
MovedAttribute("splittag", "urllib", "urllib.parse"),
MovedAttribute("splituser", "urllib", "urllib.parse"),
MovedAttribute("splitvalue", "urllib", "urllib.parse"),
MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
MovedAttribute("uses_params", "urlparse", "urllib.parse"),
MovedAttribute("uses_query", "urlparse", "urllib.parse"),
MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
]
for attr in _urllib_parse_moved_attributes:
setattr(Module_six_moves_urllib_parse, attr.name, attr)
del attr
Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
"moves.urllib_parse", "moves.urllib.parse")
class Module_six_moves_urllib_error(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_error"""
_urllib_error_moved_attributes = [
MovedAttribute("URLError", "urllib2", "urllib.error"),
MovedAttribute("HTTPError", "urllib2", "urllib.error"),
MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
]
for attr in _urllib_error_moved_attributes:
setattr(Module_six_moves_urllib_error, attr.name, attr)
del attr
Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
"moves.urllib_error", "moves.urllib.error")
class Module_six_moves_urllib_request(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_request"""
_urllib_request_moved_attributes = [
MovedAttribute("urlopen", "urllib2", "urllib.request"),
MovedAttribute("install_opener", "urllib2", "urllib.request"),
MovedAttribute("build_opener", "urllib2", "urllib.request"),
MovedAttribute("pathname2url", "urllib", "urllib.request"),
MovedAttribute("url2pathname", "urllib", "urllib.request"),
MovedAttribute("getproxies", "urllib", "urllib.request"),
MovedAttribute("Request", "urllib2", "urllib.request"),
MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
MovedAttribute("FileHandler", "urllib2", "urllib.request"),
MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
MovedAttribute("urlretrieve", "urllib", "urllib.request"),
MovedAttribute("urlcleanup", "urllib", "urllib.request"),
MovedAttribute("URLopener", "urllib", "urllib.request"),
MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
MovedAttribute("parse_http_list", "urllib2", "urllib.request"),
MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"),
]
for attr in _urllib_request_moved_attributes:
setattr(Module_six_moves_urllib_request, attr.name, attr)
del attr
Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
"moves.urllib_request", "moves.urllib.request")
class Module_six_moves_urllib_response(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_response"""
_urllib_response_moved_attributes = [
MovedAttribute("addbase", "urllib", "urllib.response"),
MovedAttribute("addclosehook", "urllib", "urllib.response"),
MovedAttribute("addinfo", "urllib", "urllib.response"),
MovedAttribute("addinfourl", "urllib", "urllib.response"),
]
for attr in _urllib_response_moved_attributes:
setattr(Module_six_moves_urllib_response, attr.name, attr)
del attr
Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
"moves.urllib_response", "moves.urllib.response")
class Module_six_moves_urllib_robotparser(_LazyModule):
"""Lazy loading of moved objects in six.moves.urllib_robotparser"""
_urllib_robotparser_moved_attributes = [
MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
]
for attr in _urllib_robotparser_moved_attributes:
setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
del attr
Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
"moves.urllib_robotparser", "moves.urllib.robotparser")
class Module_six_moves_urllib(types.ModuleType):
"""Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
__path__ = [] # mark as package
parse = _importer._get_module("moves.urllib_parse")
error = _importer._get_module("moves.urllib_error")
request = _importer._get_module("moves.urllib_request")
response = _importer._get_module("moves.urllib_response")
robotparser = _importer._get_module("moves.urllib_robotparser")
def __dir__(self):
return ['parse', 'error', 'request', 'response', 'robotparser']
_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
"moves.urllib")
def add_move(move):
"""Add an item to six.moves."""
setattr(_MovedItems, move.name, move)
def remove_move(name):
"""Remove item from six.moves."""
try:
delattr(_MovedItems, name)
except AttributeError:
try:
del moves.__dict__[name]
except KeyError:
raise AttributeError("no such move, %r" % (name,))
if PY3:
_meth_func = "__func__"
_meth_self = "__self__"
_func_closure = "__closure__"
_func_code = "__code__"
_func_defaults = "__defaults__"
_func_globals = "__globals__"
else:
_meth_func = "im_func"
_meth_self = "im_self"
_func_closure = "func_closure"
_func_code = "func_code"
_func_defaults = "func_defaults"
_func_globals = "func_globals"
try:
advance_iterator = next
except NameError:
def advance_iterator(it):
return it.next()
next = advance_iterator
try:
callable = callable
except NameError:
def callable(obj):
return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
if PY3:
def get_unbound_function(unbound):
return unbound
create_bound_method = types.MethodType
def create_unbound_method(func, cls):
return func
Iterator = object
else:
def get_unbound_function(unbound):
return unbound.im_func
def create_bound_method(func, obj):
return types.MethodType(func, obj, obj.__class__)
def create_unbound_method(func, cls):
return types.MethodType(func, None, cls)
class Iterator(object):
def next(self):
return type(self).__next__(self)
callable = callable
_add_doc(get_unbound_function,
"""Get the function out of a possibly unbound function""")
get_method_function = operator.attrgetter(_meth_func)
get_method_self = operator.attrgetter(_meth_self)
get_function_closure = operator.attrgetter(_func_closure)
get_function_code = operator.attrgetter(_func_code)
get_function_defaults = operator.attrgetter(_func_defaults)
get_function_globals = operator.attrgetter(_func_globals)
if PY3:
def iterkeys(d, **kw):
return iter(d.keys(**kw))
def itervalues(d, **kw):
return iter(d.values(**kw))
def iteritems(d, **kw):
return iter(d.items(**kw))
def iterlists(d, **kw):
return iter(d.lists(**kw))
viewkeys = operator.methodcaller("keys")
viewvalues = operator.methodcaller("values")
viewitems = operator.methodcaller("items")
else:
def iterkeys(d, **kw):
return d.iterkeys(**kw)
def itervalues(d, **kw):
return d.itervalues(**kw)
def iteritems(d, **kw):
return d.iteritems(**kw)
def iterlists(d, **kw):
return d.iterlists(**kw)
viewkeys = operator.methodcaller("viewkeys")
viewvalues = operator.methodcaller("viewvalues")
viewitems = operator.methodcaller("viewitems")
_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
_add_doc(iteritems,
"Return an iterator over the (key, value) pairs of a dictionary.")
_add_doc(iterlists,
"Return an iterator over the (key, [values]) pairs of a dictionary.")
if PY3:
def b(s):
return s.encode("latin-1")
def u(s):
return s
unichr = chr
import struct
int2byte = struct.Struct(">B").pack
del struct
byte2int = operator.itemgetter(0)
indexbytes = operator.getitem
iterbytes = iter
import io
StringIO = io.StringIO
BytesIO = io.BytesIO
del io
_assertCountEqual = "assertCountEqual"
if sys.version_info[1] <= 1:
_assertRaisesRegex = "assertRaisesRegexp"
_assertRegex = "assertRegexpMatches"
_assertNotRegex = "assertNotRegexpMatches"
else:
_assertRaisesRegex = "assertRaisesRegex"
_assertRegex = "assertRegex"
_assertNotRegex = "assertNotRegex"
else:
def b(s):
return s
# Workaround for standalone backslash
def u(s):
return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
unichr = unichr
int2byte = chr
def byte2int(bs):
return ord(bs[0])
def indexbytes(buf, i):
return ord(buf[i])
iterbytes = functools.partial(itertools.imap, ord)
import StringIO
StringIO = BytesIO = StringIO.StringIO
_assertCountEqual = "assertItemsEqual"
_assertRaisesRegex = "assertRaisesRegexp"
_assertRegex = "assertRegexpMatches"
_assertNotRegex = "assertNotRegexpMatches"
_add_doc(b, """Byte literal""")
_add_doc(u, """Text literal""")
def assertCountEqual(self, *args, **kwargs):
return getattr(self, _assertCountEqual)(*args, **kwargs)
def assertRaisesRegex(self, *args, **kwargs):
return getattr(self, _assertRaisesRegex)(*args, **kwargs)
def assertRegex(self, *args, **kwargs):
return getattr(self, _assertRegex)(*args, **kwargs)
def assertNotRegex(self, *args, **kwargs):
return getattr(self, _assertNotRegex)(*args, **kwargs)
if PY3:
exec_ = getattr(moves.builtins, "exec")
def reraise(tp, value, tb=None):
try:
if value is None:
value = tp()
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
finally:
value = None
tb = None
else:
def exec_(_code_, _globs_=None, _locs_=None):
"""Execute code in a namespace."""
if _globs_ is None:
frame = sys._getframe(1)
_globs_ = frame.f_globals
if _locs_ is None:
_locs_ = frame.f_locals
del frame
elif _locs_ is None:
_locs_ = _globs_
exec("""exec _code_ in _globs_, _locs_""")
exec_("""def reraise(tp, value, tb=None):
try:
raise tp, value, tb
finally:
tb = None
""")
if sys.version_info[:2] > (3,):
exec_("""def raise_from(value, from_value):
try:
raise value from from_value
finally:
value = None
""")
else:
def raise_from(value, from_value):
raise value
print_ = getattr(moves.builtins, "print", None)
if print_ is None:
def print_(*args, **kwargs):
"""The new-style print function for Python 2.4 and 2.5."""
fp = kwargs.pop("file", sys.stdout)
if fp is None:
return
def write(data):
if not isinstance(data, basestring):
data = str(data)
# If the file has an encoding, encode unicode with it.
if (isinstance(fp, file) and
isinstance(data, unicode) and
fp.encoding is not None):
errors = getattr(fp, "errors", None)
if errors is None:
errors = "strict"
data = data.encode(fp.encoding, errors)
fp.write(data)
want_unicode = False
sep = kwargs.pop("sep", None)
if sep is not None:
if isinstance(sep, unicode):
want_unicode = True
elif not isinstance(sep, str):
raise TypeError("sep must be None or a string")
end = kwargs.pop("end", None)
if end is not None:
if isinstance(end, unicode):
want_unicode = True
elif not isinstance(end, str):
raise TypeError("end must be None or a string")
if kwargs:
raise TypeError("invalid keyword arguments to print()")
if not want_unicode:
for arg in args:
if isinstance(arg, unicode):
want_unicode = True
break
if want_unicode:
newline = unicode("\n")
space = unicode(" ")
else:
newline = "\n"
space = " "
if sep is None:
sep = space
if end is None:
end = newline
for i, arg in enumerate(args):
if i:
write(sep)
write(arg)
write(end)
if sys.version_info[:2] < (3, 3):
_print = print_
def print_(*args, **kwargs):
fp = kwargs.get("file", sys.stdout)
flush = kwargs.pop("flush", False)
_print(*args, **kwargs)
if flush and fp is not None:
fp.flush()
_add_doc(reraise, """Reraise an exception.""")
if sys.version_info[0:2] < (3, 4):
# This does exactly the same what the :func:`py3:functools.update_wrapper`
# function does on Python versions after 3.2. It sets the ``__wrapped__``
# attribute on ``wrapper`` object and it doesn't raise an error if any of
# the attributes mentioned in ``assigned`` and ``updated`` are missing on
# ``wrapped`` object.
def _update_wrapper(wrapper, wrapped,
assigned=functools.WRAPPER_ASSIGNMENTS,
updated=functools.WRAPPER_UPDATES):
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
continue
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
wrapper.__wrapped__ = wrapped
return wrapper
_update_wrapper.__doc__ = functools.update_wrapper.__doc__
def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
updated=functools.WRAPPER_UPDATES):
return functools.partial(_update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
wraps.__doc__ = functools.wraps.__doc__
else:
wraps = functools.wraps
def with_metaclass(meta, *bases):
"""Create a base class with a metaclass."""
# This requires a bit of explanation: the basic idea is to make a dummy
# metaclass for one level of class instantiation that replaces itself with
# the actual metaclass.
class metaclass(type):
def __new__(cls, name, this_bases, d):
if sys.version_info[:2] >= (3, 7):
# This version introduced PEP 560 that requires a bit
# of extra care (we mimic what is done by __build_class__).
resolved_bases = types.resolve_bases(bases)
if resolved_bases is not bases:
d['__orig_bases__'] = bases
else:
resolved_bases = bases
return meta(name, resolved_bases, d)
@classmethod
def __prepare__(cls, name, this_bases):
return meta.__prepare__(name, bases)
return type.__new__(metaclass, 'temporary_class', (), {})
def add_metaclass(metaclass):
"""Class decorator for creating a class with a metaclass."""
def wrapper(cls):
orig_vars = cls.__dict__.copy()
slots = orig_vars.get('__slots__')
if slots is not None:
if isinstance(slots, str):
slots = [slots]
for slots_var in slots:
orig_vars.pop(slots_var)
orig_vars.pop('__dict__', None)
orig_vars.pop('__weakref__', None)
if hasattr(cls, '__qualname__'):
orig_vars['__qualname__'] = cls.__qualname__
return metaclass(cls.__name__, cls.__bases__, orig_vars)
return wrapper
def ensure_binary(s, encoding='utf-8', errors='strict'):
"""Coerce **s** to six.binary_type.
For Python 2:
- `unicode` -> encoded to `str`
- `str` -> `str`
For Python 3:
- `str` -> encoded to `bytes`
- `bytes` -> `bytes`
"""
if isinstance(s, binary_type):
return s
if isinstance(s, text_type):
return s.encode(encoding, errors)
raise TypeError("not expecting type '%s'" % type(s))
def ensure_str(s, encoding='utf-8', errors='strict'):
"""Coerce *s* to `str`.
For Python 2:
- `unicode` -> encoded to `str`
- `str` -> `str`
For Python 3:
- `str` -> `str`
- `bytes` -> decoded to `str`
"""
# Optimization: Fast return for the common case.
if type(s) is str:
return s
if PY2 and isinstance(s, text_type):
return s.encode(encoding, errors)
elif PY3 and isinstance(s, binary_type):
return s.decode(encoding, errors)
elif not isinstance(s, (text_type, binary_type)):
raise TypeError("not expecting type '%s'" % type(s))
return s
def ensure_text(s, encoding='utf-8', errors='strict'):
"""Coerce *s* to six.text_type.
For Python 2:
- `unicode` -> `unicode`
- `str` -> `unicode`
For Python 3:
- `str` -> `str`
- `bytes` -> decoded to `str`
"""
if isinstance(s, binary_type):
return s.decode(encoding, errors)
elif isinstance(s, text_type):
return s
else:
raise TypeError("not expecting type '%s'" % type(s))
def python_2_unicode_compatible(klass):
"""
A class decorator that defines __unicode__ and __str__ methods under Python 2.
Under Python 3 it does nothing.
To support Python 2 and 3 with a single code base, define a __str__ method
returning text and apply this decorator to the class.
"""
if PY2:
if '__str__' not in klass.__dict__:
raise ValueError("@python_2_unicode_compatible cannot be applied "
"to %s because it doesn't define __str__()." %
klass.__name__)
klass.__unicode__ = klass.__str__
klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
return klass
# Complete the moves implementation.
# This code is at the end of this module to speed up module loading.
# Turn this module into a package.
__path__ = [] # required for PEP 302 and PEP 451
__package__ = __name__ # see PEP 366 @ReservedAssignment
if globals().get("__spec__") is not None:
__spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
# Remove other six meta path importers, since they cause problems. This can
# happen if six is removed from sys.modules and then reloaded. (Setuptools does
# this for some reason.)
if sys.meta_path:
for i, importer in enumerate(sys.meta_path):
# Here's some real nastiness: Another "instance" of the six module might
# be floating around. Therefore, we can't use isinstance() to check for
# the six meta path importer, since the other six instance will have
# inserted an importer with different class.
if (type(importer).__name__ == "_SixMetaPathImporter" and
importer.name == __name__):
del sys.meta_path[i]
break
del i, importer
# Finally, add the importer to the meta path import hook.
sys.meta_path.append(_importer)
+252 -128
View File
@@ -1,4 +1,4 @@
# u-msgpack-python v2.5.0 - v at sergeev.io
# u-msgpack-python v2.7.1 - v at sergeev.io
# https://github.com/vsergeev/u-msgpack-python
#
# u-msgpack-python is a lightweight MessagePack serializer and deserializer
@@ -10,7 +10,7 @@
#
# MIT License
#
# Copyright (c) 2013-2016 vsergeev / Ivan (Vanya) A. Sergeev
# Copyright (c) 2013-2020 vsergeev / Ivan (Vanya) A. Sergeev
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,7 +31,7 @@
# THE SOFTWARE.
#
"""
u-msgpack-python v2.5.0 - v at sergeev.io
u-msgpack-python v2.7.1 - v at sergeev.io
https://github.com/vsergeev/u-msgpack-python
u-msgpack-python is a lightweight MessagePack serializer and deserializer
@@ -49,10 +49,15 @@ import datetime
import sys
import io
__version__ = "2.5.0"
if sys.version_info[0:2] >= (3, 3):
from collections.abc import Hashable
else:
from collections import Hashable
__version__ = "2.7.1"
"Module version string"
version = (2, 5, 0)
version = (2, 7, 1)
"Module version tuple"
@@ -61,7 +66,7 @@ version = (2, 5, 0)
##############################################################################
# Extension type for application-defined types and data
class Ext:
class Ext(object):
"""
The Ext class facilitates creating a serializable extension object to store
an application-defined type and data byte array.
@@ -75,23 +80,33 @@ class Ext:
type: application-defined type integer
data: application-defined data byte array
TypeError:
Type is not an integer.
ValueError:
Type is out of range of -128 to 127.
TypeError::
Data is not type 'bytes' (Python 3) or not type 'str' (Python 2).
Example:
>>> foo = umsgpack.Ext(0x05, b"\x01\x02\x03")
>>> foo = umsgpack.Ext(5, b"\x01\x02\x03")
>>> umsgpack.packb({u"special stuff": foo, u"awesome": True})
'\x82\xa7awesome\xc3\xadspecial stuff\xc7\x03\x05\x01\x02\x03'
>>> bar = umsgpack.unpackb(_)
>>> print(bar["special stuff"])
Ext Object (Type: 0x05, Data: 01 02 03)
Ext Object (Type: 5, Data: 01 02 03)
>>>
"""
# Check type is type int
# Check type is type int and in range
if not isinstance(type, int):
raise TypeError("ext type is not type integer")
# Check data is type bytes
elif not (-2**7 <= type <= 2**7 - 1):
raise ValueError("ext type value {:d} is out of range (-128 to 127)".format(type))
# Check data is type bytes or str
elif sys.version_info[0] == 3 and not isinstance(data, bytes):
raise TypeError("ext data is not type \'bytes\'")
elif sys.version_info[0] == 2 and not isinstance(data, str):
raise TypeError("ext data is not type \'str\'")
self.type = type
self.data = data
@@ -99,9 +114,8 @@ class Ext:
"""
Compare this Ext object with another for equality.
"""
return (isinstance(other, self.__class__) and
self.type == other.type and
self.data == other.data)
return isinstance(other, self.__class__) \
and self.type == other.type and self.data == other.data
def __ne__(self, other):
"""
@@ -113,8 +127,8 @@ class Ext:
"""
String representation of this Ext object.
"""
s = "Ext Object (Type: 0x%02x, Data: " % self.type
s += " ".join(["0x%02x" % ord(self.data[i:i + 1])
s = "Ext Object (Type: {:d}, Data: ".format(self.type)
s += " ".join(["0x{:02}".format(ord(self.data[i:i + 1]))
for i in xrange(min(len(self.data), 8))])
if len(self.data) > 8:
s += " ..."
@@ -130,7 +144,52 @@ class Ext:
class InvalidString(bytes):
"""Subclass of bytes to hold invalid UTF-8 strings."""
pass
##############################################################################
# Ext Serializable Decorator
##############################################################################
_ext_class_to_type = {}
_ext_type_to_class = {}
def ext_serializable(ext_type):
"""
Return a decorator to register a class for automatic packing and unpacking
with the specified Ext type code. The application class should implement a
`packb()` method that returns serialized bytes, and an `unpackb()` class
method or static method that accepts serialized bytes and returns an
instance of the application class.
Args:
ext_type: application-defined Ext type code
Raises:
TypeError:
Ext type is not an integer.
ValueError:
Ext type is out of range of -128 to 127.
ValueError:
Ext type or class already registered.
"""
def wrapper(cls):
if not isinstance(ext_type, int):
raise TypeError("Ext type is not type integer")
elif not (-2**7 <= ext_type <= 2**7 - 1):
raise ValueError("Ext type value {:d} is out of range of -128 to 127".format(ext_type))
elif ext_type in _ext_type_to_class:
raise ValueError("Ext type {:d} already registered with class {:s}".format(ext_type, repr(_ext_type_to_class[ext_type])))
elif cls in _ext_class_to_type:
raise ValueError("Class {:s} already registered with Ext type {:d}".format(repr(cls), ext_type))
_ext_type_to_class[ext_type] = cls
_ext_class_to_type[cls] = ext_type
return cls
return wrapper
##############################################################################
# Exceptions
@@ -140,39 +199,32 @@ class InvalidString(bytes):
# Base Exception classes
class PackException(Exception):
"Base class for exceptions encountered during packing."
pass
class UnpackException(Exception):
"Base class for exceptions encountered during unpacking."
pass
# Packing error
class UnsupportedTypeException(PackException):
"Object type not supported for packing."
pass
# Unpacking error
class InsufficientDataException(UnpackException):
"Insufficient data to unpack the serialized object."
pass
class InvalidStringException(UnpackException):
"Invalid UTF-8 string encountered during unpacking."
pass
class UnsupportedTimestampException(UnpackException):
"Unsupported timestamp format encountered during unpacking."
pass
class ReservedCodeException(UnpackException):
"Reserved code encountered during unpacking."
pass
class UnhashableKeyException(UnpackException):
@@ -180,12 +232,10 @@ class UnhashableKeyException(UnpackException):
Unhashable key encountered during map unpacking.
The serialized map cannot be deserialized into a Python dictionary.
"""
pass
class DuplicateKeyException(UnpackException):
"Duplicate key encountered during map unpacking."
pass
# Backwards compatibility
@@ -250,15 +300,15 @@ def _pack_integer(obj, fp, options):
else:
raise UnsupportedTypeException("huge signed int")
else:
if obj <= 127:
if obj < 128:
fp.write(struct.pack("B", obj))
elif obj <= 2**8 - 1:
elif obj < 2**8:
fp.write(b"\xcc" + struct.pack("B", obj))
elif obj <= 2**16 - 1:
elif obj < 2**16:
fp.write(b"\xcd" + struct.pack(">H", obj))
elif obj <= 2**32 - 1:
elif obj < 2**32:
fp.write(b"\xce" + struct.pack(">I", obj))
elif obj <= 2**64 - 1:
elif obj < 2**64:
fp.write(b"\xcf" + struct.pack(">Q", obj))
else:
raise UnsupportedTypeException("huge unsigned int")
@@ -285,94 +335,99 @@ def _pack_float(obj, fp, options):
def _pack_string(obj, fp, options):
obj = obj.encode('utf-8')
if len(obj) <= 31:
fp.write(struct.pack("B", 0xa0 | len(obj)) + obj)
elif len(obj) <= 2**8 - 1:
fp.write(b"\xd9" + struct.pack("B", len(obj)) + obj)
elif len(obj) <= 2**16 - 1:
fp.write(b"\xda" + struct.pack(">H", len(obj)) + obj)
elif len(obj) <= 2**32 - 1:
fp.write(b"\xdb" + struct.pack(">I", len(obj)) + obj)
obj_len = len(obj)
if obj_len < 32:
fp.write(struct.pack("B", 0xa0 | obj_len) + obj)
elif obj_len < 2**8:
fp.write(b"\xd9" + struct.pack("B", obj_len) + obj)
elif obj_len < 2**16:
fp.write(b"\xda" + struct.pack(">H", obj_len) + obj)
elif obj_len < 2**32:
fp.write(b"\xdb" + struct.pack(">I", obj_len) + obj)
else:
raise UnsupportedTypeException("huge string")
def _pack_binary(obj, fp, options):
if len(obj) <= 2**8 - 1:
fp.write(b"\xc4" + struct.pack("B", len(obj)) + obj)
elif len(obj) <= 2**16 - 1:
fp.write(b"\xc5" + struct.pack(">H", len(obj)) + obj)
elif len(obj) <= 2**32 - 1:
fp.write(b"\xc6" + struct.pack(">I", len(obj)) + obj)
obj_len = len(obj)
if obj_len < 2**8:
fp.write(b"\xc4" + struct.pack("B", obj_len) + obj)
elif obj_len < 2**16:
fp.write(b"\xc5" + struct.pack(">H", obj_len) + obj)
elif obj_len < 2**32:
fp.write(b"\xc6" + struct.pack(">I", obj_len) + obj)
else:
raise UnsupportedTypeException("huge binary string")
def _pack_oldspec_raw(obj, fp, options):
if len(obj) <= 31:
fp.write(struct.pack("B", 0xa0 | len(obj)) + obj)
elif len(obj) <= 2**16 - 1:
fp.write(b"\xda" + struct.pack(">H", len(obj)) + obj)
elif len(obj) <= 2**32 - 1:
fp.write(b"\xdb" + struct.pack(">I", len(obj)) + obj)
obj_len = len(obj)
if obj_len < 32:
fp.write(struct.pack("B", 0xa0 | obj_len) + obj)
elif obj_len < 2**16:
fp.write(b"\xda" + struct.pack(">H", obj_len) + obj)
elif obj_len < 2**32:
fp.write(b"\xdb" + struct.pack(">I", obj_len) + obj)
else:
raise UnsupportedTypeException("huge raw string")
def _pack_ext(obj, fp, options):
if len(obj.data) == 1:
obj_len = len(obj.data)
if obj_len == 1:
fp.write(b"\xd4" + struct.pack("B", obj.type & 0xff) + obj.data)
elif len(obj.data) == 2:
elif obj_len == 2:
fp.write(b"\xd5" + struct.pack("B", obj.type & 0xff) + obj.data)
elif len(obj.data) == 4:
elif obj_len == 4:
fp.write(b"\xd6" + struct.pack("B", obj.type & 0xff) + obj.data)
elif len(obj.data) == 8:
elif obj_len == 8:
fp.write(b"\xd7" + struct.pack("B", obj.type & 0xff) + obj.data)
elif len(obj.data) == 16:
elif obj_len == 16:
fp.write(b"\xd8" + struct.pack("B", obj.type & 0xff) + obj.data)
elif len(obj.data) <= 2**8 - 1:
fp.write(b"\xc7" +
struct.pack("BB", len(obj.data), obj.type & 0xff) + obj.data)
elif len(obj.data) <= 2**16 - 1:
fp.write(b"\xc8" +
struct.pack(">HB", len(obj.data), obj.type & 0xff) + obj.data)
elif len(obj.data) <= 2**32 - 1:
fp.write(b"\xc9" +
struct.pack(">IB", len(obj.data), obj.type & 0xff) + obj.data)
elif obj_len < 2**8:
fp.write(b"\xc7" + struct.pack("BB", obj_len, obj.type & 0xff) + obj.data)
elif obj_len < 2**16:
fp.write(b"\xc8" + struct.pack(">HB", obj_len, obj.type & 0xff) + obj.data)
elif obj_len < 2**32:
fp.write(b"\xc9" + struct.pack(">IB", obj_len, obj.type & 0xff) + obj.data)
else:
raise UnsupportedTypeException("huge ext data")
def _pack_ext_timestamp(obj, fp, options):
delta = obj - _epoch
if not obj.tzinfo:
# Object is naive datetime, convert to aware date time,
# assuming UTC timezone
delta = obj.replace(tzinfo=_utc_tzinfo) - _epoch
else:
# Object is aware datetime
delta = obj - _epoch
seconds = delta.seconds + delta.days * 86400
microseconds = delta.microseconds
if microseconds == 0 and 0 <= seconds <= 2**32 - 1:
# 32-bit timestamp
fp.write(b"\xd6\xff" +
struct.pack(">I", seconds))
fp.write(b"\xd6\xff" + struct.pack(">I", seconds))
elif 0 <= seconds <= 2**34 - 1:
# 64-bit timestamp
value = ((microseconds * 1000) << 34) | seconds
fp.write(b"\xd7\xff" +
struct.pack(">Q", value))
fp.write(b"\xd7\xff" + struct.pack(">Q", value))
elif -2**63 <= abs(seconds) <= 2**63 - 1:
# 96-bit timestamp
fp.write(b"\xc7\x0c\xff" +
struct.pack(">I", microseconds * 1000) +
struct.pack(">q", seconds))
fp.write(b"\xc7\x0c\xff" + struct.pack(">Iq", microseconds * 1000, seconds))
else:
raise UnsupportedTypeException("huge timestamp")
def _pack_array(obj, fp, options):
if len(obj) <= 15:
fp.write(struct.pack("B", 0x90 | len(obj)))
elif len(obj) <= 2**16 - 1:
fp.write(b"\xdc" + struct.pack(">H", len(obj)))
elif len(obj) <= 2**32 - 1:
fp.write(b"\xdd" + struct.pack(">I", len(obj)))
obj_len = len(obj)
if obj_len < 16:
fp.write(struct.pack("B", 0x90 | obj_len))
elif obj_len < 2**16:
fp.write(b"\xdc" + struct.pack(">H", obj_len))
elif obj_len < 2**32:
fp.write(b"\xdd" + struct.pack(">I", obj_len))
else:
raise UnsupportedTypeException("huge array")
@@ -381,12 +436,13 @@ def _pack_array(obj, fp, options):
def _pack_map(obj, fp, options):
if len(obj) <= 15:
fp.write(struct.pack("B", 0x80 | len(obj)))
elif len(obj) <= 2**16 - 1:
fp.write(b"\xde" + struct.pack(">H", len(obj)))
elif len(obj) <= 2**32 - 1:
fp.write(b"\xdf" + struct.pack(">I", len(obj)))
obj_len = len(obj)
if obj_len < 16:
fp.write(struct.pack("B", 0x80 | obj_len))
elif obj_len < 2**16:
fp.write(b"\xde" + struct.pack(">H", obj_len))
elif obj_len < 2**32:
fp.write(b"\xdf" + struct.pack(">I", obj_len))
else:
raise UnsupportedTypeException("huge array")
@@ -435,9 +491,14 @@ def _pack2(obj, fp, **options):
_pack_nil(obj, fp, options)
elif ext_handlers and obj.__class__ in ext_handlers:
_pack_ext(ext_handlers[obj.__class__](obj), fp, options)
elif obj.__class__ in _ext_class_to_type:
try:
_pack_ext(Ext(_ext_class_to_type[obj.__class__], obj.packb()), fp, options)
except AttributeError:
raise NotImplementedError("Ext serializable class {:s} is missing implementation of packb()".format(repr(obj.__class__)))
elif isinstance(obj, bool):
_pack_boolean(obj, fp, options)
elif isinstance(obj, int) or isinstance(obj, long):
elif isinstance(obj, (int, long)):
_pack_integer(obj, fp, options)
elif isinstance(obj, float):
_pack_float(obj, fp, options)
@@ -449,7 +510,7 @@ def _pack2(obj, fp, **options):
_pack_string(obj, fp, options)
elif isinstance(obj, str):
_pack_binary(obj, fp, options)
elif isinstance(obj, list) or isinstance(obj, tuple):
elif isinstance(obj, (list, tuple)):
_pack_array(obj, fp, options)
elif isinstance(obj, dict):
_pack_map(obj, fp, options)
@@ -464,9 +525,19 @@ def _pack2(obj, fp, **options):
_pack_ext(ext_handlers[t](obj), fp, options)
else:
raise UnsupportedTypeException(
"unsupported type: %s" % str(type(obj)))
"unsupported type: {:s}".format(str(type(obj))))
elif _ext_class_to_type:
# Linear search for superclass
t = next((t for t in _ext_class_to_type if isinstance(obj, t)), None)
if t:
try:
_pack_ext(Ext(_ext_class_to_type[t], obj.packb()), fp, options)
except AttributeError:
raise NotImplementedError("Ext serializable class {:s} is missing implementation of packb()".format(repr(t)))
else:
raise UnsupportedTypeException("unsupported type: {:s}".format(str(type(obj))))
else:
raise UnsupportedTypeException("unsupported type: %s" % str(type(obj)))
raise UnsupportedTypeException("unsupported type: {:s}".format(str(type(obj))))
# Pack for Python 3, with unicode 'str' type, 'bytes' type, and no 'long' type
@@ -507,6 +578,11 @@ def _pack3(obj, fp, **options):
_pack_nil(obj, fp, options)
elif ext_handlers and obj.__class__ in ext_handlers:
_pack_ext(ext_handlers[obj.__class__](obj), fp, options)
elif obj.__class__ in _ext_class_to_type:
try:
_pack_ext(Ext(_ext_class_to_type[obj.__class__], obj.packb()), fp, options)
except AttributeError:
raise NotImplementedError("Ext serializable class {:s} is missing implementation of packb()".format(repr(obj.__class__)))
elif isinstance(obj, bool):
_pack_boolean(obj, fp, options)
elif isinstance(obj, int):
@@ -521,7 +597,7 @@ def _pack3(obj, fp, **options):
_pack_string(obj, fp, options)
elif isinstance(obj, bytes):
_pack_binary(obj, fp, options)
elif isinstance(obj, list) or isinstance(obj, tuple):
elif isinstance(obj, (list, tuple)):
_pack_array(obj, fp, options)
elif isinstance(obj, dict):
_pack_map(obj, fp, options)
@@ -536,10 +612,20 @@ def _pack3(obj, fp, **options):
_pack_ext(ext_handlers[t](obj), fp, options)
else:
raise UnsupportedTypeException(
"unsupported type: %s" % str(type(obj)))
"unsupported type: {:s}".format(str(type(obj))))
elif _ext_class_to_type:
# Linear search for superclass
t = next((t for t in _ext_class_to_type if isinstance(obj, t)), None)
if t:
try:
_pack_ext(Ext(_ext_class_to_type[t], obj.packb()), fp, options)
except AttributeError:
raise NotImplementedError("Ext serializable class {:s} is missing implementation of packb()".format(repr(t)))
else:
raise UnsupportedTypeException("unsupported type: {:s}".format(str(type(obj))))
else:
raise UnsupportedTypeException(
"unsupported type: %s" % str(type(obj)))
"unsupported type: {:s}".format(str(type(obj))))
def _packb2(obj, **options):
@@ -613,9 +699,20 @@ def _packb3(obj, **options):
def _read_except(fp, n):
if n == 0:
return b""
data = fp.read(n)
if len(data) < n:
if len(data) == 0:
raise InsufficientDataException()
while len(data) < n:
chunk = fp.read(n - len(data))
if len(chunk) == 0:
raise InsufficientDataException()
data += chunk
return data
@@ -640,21 +737,21 @@ def _unpack_integer(code, fp, options):
return struct.unpack(">I", _read_except(fp, 4))[0]
elif code == b'\xcf':
return struct.unpack(">Q", _read_except(fp, 8))[0]
raise Exception("logic error, not int: 0x%02x" % ord(code))
raise Exception("logic error, not int: 0x{:02x}".format(ord(code)))
def _unpack_reserved(code, fp, options):
if code == b'\xc1':
raise ReservedCodeException(
"encountered reserved code: 0x%02x" % ord(code))
"encountered reserved code: 0x{:02x}".format(ord(code)))
raise Exception(
"logic error, not reserved code: 0x%02x" % ord(code))
"logic error, not reserved code: 0x{:02x}".format(ord(code)))
def _unpack_nil(code, fp, options):
if code == b'\xc0':
return None
raise Exception("logic error, not nil: 0x%02x" % ord(code))
raise Exception("logic error, not nil: 0x{:02x}".format(ord(code)))
def _unpack_boolean(code, fp, options):
@@ -662,7 +759,7 @@ def _unpack_boolean(code, fp, options):
return False
elif code == b'\xc3':
return True
raise Exception("logic error, not boolean: 0x%02x" % ord(code))
raise Exception("logic error, not boolean: 0x{:02x}".format(ord(code)))
def _unpack_float(code, fp, options):
@@ -670,7 +767,7 @@ def _unpack_float(code, fp, options):
return struct.unpack(">f", _read_except(fp, 4))[0]
elif code == b'\xcb':
return struct.unpack(">d", _read_except(fp, 8))[0]
raise Exception("logic error, not float: 0x%02x" % ord(code))
raise Exception("logic error, not float: 0x{:02x}".format(ord(code)))
def _unpack_string(code, fp, options):
@@ -683,7 +780,7 @@ def _unpack_string(code, fp, options):
elif code == b'\xdb':
length = struct.unpack(">I", _read_except(fp, 4))[0]
else:
raise Exception("logic error, not string: 0x%02x" % ord(code))
raise Exception("logic error, not string: 0x{:02x}".format(ord(code)))
# Always return raw bytes in compatibility mode
global compatibility
@@ -707,7 +804,7 @@ def _unpack_binary(code, fp, options):
elif code == b'\xc6':
length = struct.unpack(">I", _read_except(fp, 4))[0]
else:
raise Exception("logic error, not binary: 0x%02x" % ord(code))
raise Exception("logic error, not binary: 0x{:02x}".format(ord(code)))
return _read_except(fp, length)
@@ -730,43 +827,48 @@ def _unpack_ext(code, fp, options):
elif code == b'\xc9':
length = struct.unpack(">I", _read_except(fp, 4))[0]
else:
raise Exception("logic error, not ext: 0x%02x" % ord(code))
raise Exception("logic error, not ext: 0x{:02x}".format(ord(code)))
ext_type = struct.unpack("b", _read_except(fp, 1))[0]
ext_data = _read_except(fp, length)
# Create extension object
ext = Ext(ext_type, ext_data)
# Unpack with ext handler, if we have one
ext_handlers = options.get("ext_handlers")
if ext_handlers and ext.type in ext_handlers:
return ext_handlers[ext.type](ext)
if ext_handlers and ext_type in ext_handlers:
return ext_handlers[ext_type](Ext(ext_type, ext_data))
# Unpack with ext classes, if type is registered
if ext_type in _ext_type_to_class:
try:
return _ext_type_to_class[ext_type].unpackb(ext_data)
except AttributeError:
raise NotImplementedError("Ext serializable class {:s} is missing implementation of unpackb()".format(repr(_ext_type_to_class[ext_type])))
# Timestamp extension
if ext.type == -1:
return _unpack_ext_timestamp(ext, options)
if ext_type == -1:
return _unpack_ext_timestamp(ext_data, options)
return ext
return Ext(ext_type, ext_data)
def _unpack_ext_timestamp(ext, options):
if len(ext.data) == 4:
def _unpack_ext_timestamp(ext_data, options):
obj_len = len(ext_data)
if obj_len == 4:
# 32-bit timestamp
seconds = struct.unpack(">I", ext.data)[0]
seconds = struct.unpack(">I", ext_data)[0]
microseconds = 0
elif len(ext.data) == 8:
elif obj_len == 8:
# 64-bit timestamp
value = struct.unpack(">Q", ext.data)[0]
value = struct.unpack(">Q", ext_data)[0]
seconds = value & 0x3ffffffff
microseconds = (value >> 34) // 1000
elif len(ext.data) == 12:
elif obj_len == 12:
# 96-bit timestamp
seconds = struct.unpack(">q", ext.data[4:12])[0]
microseconds = struct.unpack(">I", ext.data[0:4])[0] // 1000
seconds = struct.unpack(">q", ext_data[4:12])[0]
microseconds = struct.unpack(">I", ext_data[0:4])[0] // 1000
else:
raise UnsupportedTimestampException(
"unsupported timestamp with data length %d" % len(ext.data))
"unsupported timestamp with data length {:d}".format(len(ext_data)))
return _epoch + datetime.timedelta(seconds=seconds,
microseconds=microseconds)
@@ -780,7 +882,10 @@ def _unpack_array(code, fp, options):
elif code == b'\xdd':
length = struct.unpack(">I", _read_except(fp, 4))[0]
else:
raise Exception("logic error, not array: 0x%02x" % ord(code))
raise Exception("logic error, not array: 0x{:02x}".format(ord(code)))
if options.get('use_tuple'):
return tuple((_unpack(fp, options) for i in xrange(length)))
return [_unpack(fp, options) for i in xrange(length)]
@@ -799,10 +904,9 @@ def _unpack_map(code, fp, options):
elif code == b'\xdf':
length = struct.unpack(">I", _read_except(fp, 4))[0]
else:
raise Exception("logic error, not map: 0x%02x" % ord(code))
raise Exception("logic error, not map: 0x{:02x}".format(ord(code)))
d = {} if not options.get('use_ordered_dict') \
else collections.OrderedDict()
d = {} if not options.get('use_ordered_dict') else collections.OrderedDict()
for _ in xrange(length):
# Unpack key
k = _unpack(fp, options)
@@ -810,12 +914,12 @@ def _unpack_map(code, fp, options):
if isinstance(k, list):
# Attempt to convert list into a hashable tuple
k = _deep_list_to_tuple(k)
elif not isinstance(k, collections.Hashable):
elif not isinstance(k, Hashable):
raise UnhashableKeyException(
"encountered unhashable key: %s, %s" % (str(k), str(type(k))))
"encountered unhashable key: \"{:s}\" ({:s})".format(str(k), str(type(k))))
elif k in d:
raise DuplicateKeyException(
"encountered duplicate key: %s, %s" % (str(k), str(type(k))))
"encountered duplicate key: \"{:s}\" ({:s})".format(str(k), str(type(k))))
# Unpack value
v = _unpack(fp, options)
@@ -824,7 +928,7 @@ def _unpack_map(code, fp, options):
d[k] = v
except TypeError:
raise UnhashableKeyException(
"encountered unhashable key: %s" % str(k))
"encountered unhashable key: \"{:s}\"".format(str(k)))
return d
@@ -848,6 +952,8 @@ def _unpack2(fp, **options):
Ext into an object
use_ordered_dict (bool): unpack maps into OrderedDict, instead of
unordered dict (default False)
use_tuple (bool): unpacks arrays into tuples, instead of lists (default
False)
allow_invalid_utf8 (bool): unpack invalid strings into instances of
InvalidString, for access to the bytes
(default False)
@@ -892,6 +998,8 @@ def _unpack3(fp, **options):
Ext into an object
use_ordered_dict (bool): unpack maps into OrderedDict, instead of
unordered dict (default False)
use_tuple (bool): unpacks arrays into tuples, instead of lists (default
False)
allow_invalid_utf8 (bool): unpack invalid strings into instances of
InvalidString, for access to the bytes
(default False)
@@ -937,6 +1045,8 @@ def _unpackb2(s, **options):
Ext into an object
use_ordered_dict (bool): unpack maps into OrderedDict, instead of
unordered dict (default False)
use_tuple (bool): unpacks arrays into tuples, instead of lists (default
False)
allow_invalid_utf8 (bool): unpack invalid strings into instances of
InvalidString, for access to the bytes
(default False)
@@ -985,6 +1095,8 @@ def _unpackb3(s, **options):
Ext into an object
use_ordered_dict (bool): unpack maps into OrderedDict, instead of
unordered dict (default False)
use_tuple (bool): unpacks arrays into tuples, instead of lists (default
False)
allow_invalid_utf8 (bool): unpack invalid strings into instances of
InvalidString, for access to the bytes
(default False)
@@ -1045,9 +1157,21 @@ def __init():
if sys.version_info[0] == 3:
_utc_tzinfo = datetime.timezone.utc
else:
_utc_tzinfo = None
class UTC(datetime.tzinfo):
ZERO = datetime.timedelta(0)
# Calculate epoch datetime
def utcoffset(self, dt):
return UTC.ZERO
def tzname(self, dt):
return "UTC"
def dst(self, dt):
return UTC.ZERO
_utc_tzinfo = UTC()
# Calculate an aware epoch datetime
_epoch = datetime.datetime(1970, 1, 1, tzinfo=_utc_tzinfo)
# Auto-detect system float precision
View File
+32
View File
@@ -0,0 +1,32 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@if [ $@ = "html" ]; then \
rm -rf html; \
rm -rf manual; \
cp -r build/html ./; \
mv html manual; \
echo "HTML Manual Generated"; \
fi
@if [ $@ = "latexpdf" ]; then \
cp -r build/latex/reticulumnetworkstack.pdf ./Reticulum\ Manual.pdf; \
echo "PDF Manual Generated"; \
fi
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="refresh" content="0;URL='./manual/'" />
</head>
<body>
</body>
</html>
+4
View File
@@ -0,0 +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: 3ea52ff0bfd9431c8886e9a105e9d835
tags: 645f666f9bcd5a90fca523b33c5a78b7
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

+106
View File
@@ -0,0 +1,106 @@
.. _examples-main:
*************
Code Examples
*************
A number of examples are included in the source distribution of Reticulum.
You can use these examples to learn how to write your own programs.
.. _example-minimal:
Minimal
=======
The *Minimal* example demonstrates the bare-minimum setup required to connect to
a Reticulum network from your program. In about five lines of code, you will
have the Reticulum Network Stack initialised, and ready to pass traffic in your
program.
.. literalinclude:: ../../Examples/Minimal.py
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Minimal.py>`_.
.. _example-announce:
Announce
========
The *Announce* example builds upon the previous example by exploring how to
announce a destination on the network, and how to let your program receive
notifications about announces from relevant destinations.
.. literalinclude:: ../../Examples/Announce.py
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Announce.py>`_.
.. _example-broadcast:
Broadcast
=========
The *Broadcast* example explores how to transmit plaintext broadcast messages
over the network.
.. literalinclude:: ../../Examples/Broadcast.py
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Broadcast.py>`_.
.. _example-echo:
Echo
====
The *Echo* example demonstrates communication between two destinations using
the Packet interface.
.. literalinclude:: ../../Examples/Echo.py
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Echo.py>`_.
.. _example-link:
Link
====
The *Link* example explores establishing an encrypted link to a remote
destination, and passing traffic back and forth over the link.
.. literalinclude:: ../../Examples/Link.py
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Link.py>`_.
.. _example-identify:
Identification
==============
The *Identify* example explores identifying an intiator of a link, once
the link has been established.
.. literalinclude:: ../../Examples/Identify.py
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Identify.py>`_.
.. _example-request:
Requests & Responses
====================
The *Request* example explores sendig requests and receiving responses.
.. literalinclude:: ../../Examples/Request.py
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Request.py>`_.
.. _example-filetransfer:
Filetransfer
============
The *Filetransfer* example implements a basic file-server program that
allow clients to connect and download files. The program uses the Resource
interface to efficiently pass files of any size over a Reticulum :ref:`Link<api-link>`.
.. literalinclude:: ../../Examples/Filetransfer.py
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Filetransfer.py>`_.
@@ -0,0 +1,330 @@
********************
Getting Started Fast
********************
The best way to get started with the Reticulum Network Stack depends on what
you want to do. This guide will outline sensible starting paths for different
scenarios.
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.
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.
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.
Nomad Network
^^^^^^^^^^^^^
The terminal-based program `Nomad Network <https://github.com/markqvist/nomadnet>`_
provides a complete encrypted communications suite built with Reticulum. It features
encrypted messaging (both direct and delayed-delivery for offline users), file sharing,
and has a built-in text-browser and page server with support for dynamically rendered pages,
user authentication and more.
.. image:: screenshots/nomadnet_3.png
:target: _images/nomadnet_3.png
`Nomad Network <https://github.com/markqvist/nomadnet>`_ is a user-facing client
for the messaging and information-sharing protocol
`LXMF <https://github.com/markqvist/lxmf>`_, another project built with Reticulum.
You can install Nomad Network via pip:
.. code::
# Install ...
pip3 install nomadnet
# ... and run
nomadnet
**Please Note**: If this is the very first time you use pip to install a program
on your system, you might need to reboot your system for your program to become
available. If you get a "command not found" error or similar when running the
program, reboot your system and try again.
Sideband
^^^^^^^^
If you would rather use a program with a graphical user interface, you can take
a look at `Sideband <https://unsigned.io/sideband>`_, which is available for Android,
Linux and macOS.
.. image:: screenshots/sideband_1.png
:width: 400px
:align: center
:target: _images/sideband_1.png
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.
Using the Included Utilities
=============================================
Reticulum comes with a range of included utilities that make it easier to
manage your network, check connectivity and make Reticulum available to other
programs on your system.
You can use ``rnsd`` to run Reticulum as a background or foreground service,
and the ``rnstatus``, ``rnpath`` and ``rnprobe`` utilities to view and query
network status and connectivity.
To learn more about these utility programs, have a look at the
:ref:`Using Reticulum on Your System<using-main>` chapter of this manual.
Creating a Network With Reticulum
=============================================
To create a network, you will need to specify one or more *interfaces* for
Reticulum to use. This is done in the Reticulum configuration file, which by
default is located at ``~/.reticulum/config``. You can edit this file by hand,
or use the interactive ``rnsconfig`` utility.
When Reticulum is started for the first time, it will create a default
configuration file, with one active interface. This default interface uses
your existing ethernet and WiFi networks (if any), and only allows you to
communicate with other Reticulum peers within your local broadcast domains.
To communicate further, you will have to add one or more interfaces. The default
configuration includes a number of examples, ranging from using TCP over the
internet, to LoRa and Packet Radio interfaces.
With Reticulum, you only need to configure what interfaces you want to communicate
over. There is no need to configure address spaces, subnets, routing tables,
or other things you might be used to from other network types.
Once Reticulums knows which interfaces it should use, it will automatically
discover topography and configure transport of data to any destinations it
knows about.
In situations where you already have an established WiFi or ethernet network, and
many devices that want to utilise the same external Reticulum network (for example over
LoRa), it will often be sufficient to let one system act as a Reticulum gateway, by
adding any external interfaces to this systems configuration, and enabling transport. Any
other device on your local WiFi will then be able to connect to this wider Reticulum
network just using the default interface configuration.
Possibly, the examples in the config file are enough to get you started. If
you want more information, you can read the :ref:`Building Networks<networks-main>`
and :ref:`Interfaces<interfaces-main>` chapters of this manual.
Connecting Reticulum Instances Over the Internet
================================================
Reticulum currently offers 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.
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
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.
Hosting a publicly reachable instance over TCP also requires a publicly reachable IP address,
which most Internet connections don't offer anymore.
The ``I2PInterface`` routes messages through the `Invisible Internet Protocol
(I2P) <https://geti2p.net/en/>`_. To properly use this interface, users must also run an I2P daemon in
parallel to ``rnsd``. For always-on I2P nodes it is recommended to use `i2pd <https://i2pd.website/>`_.
By default, I2P will encrypt and mix all traffic sent over the Internet, and
hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node
will also relay other I2P user's encrypted packets, which will use extra
bandwidth and compute power, but also makes timing attacks and other forms of
deep-packet-inspection much more difficult.
I2P also allows users to host globally available Reticulum instances from non-public IPs and behind firewalls.
In general it is recommended to use an I2P node if you want to host a publically accessible
instance, while preserving anonymity. If you care more about performance, and a slightly
easier setup, use TCP.
Connect to the Public Testnet
===========================================
An experimental public testnet has been made accessible over both I2P and TCP. You can join it
by adding one of the following interfaces to your ``.reticulum/config`` file:
.. code::
# For connecting over TCP/IP:
[[RNS Testnet Frankfurt]]
type = TCPClientInterface
interface_enabled = yes
outgoing = True
target_host = frankfurt.rns.unsigned.io
target_port = 4965
# For connecting over I2P:
[[RNS Testnet I2P Node A]]
type = I2PInterface
interface_enabled = yes
peers = ykzlw5ujbaqc2xkec4cpvgyxj257wcrmmgkuxqmqcur7cq3w3lha.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
topography, usage or what types of instances connect. It will also occasionally be used
to test various failure scenarios, and there are no availability or service guarantees.
Develop a Program with Reticulum
===========================================
If you want to develop programs that use Reticulum, the easiest way to get
started is to install the latest release of Reticulum via pip:
.. code::
pip3 install rns
The above command will install Reticulum and dependencies, and you will be
ready to import and use RNS in your own programs. The next step will most
likely be to look at some :ref:`Example Programs<examples-main>`.
For extended functionality, you can install optional dependencies:
.. code::
pip3 install pyserial netifaces
Further information can be found in the :ref:`API Reference<api-main>`.
Participate in Reticulum Development
==============================================
If you want to participate in the development of Reticulum and associated
utilities, you'll want to get the latest source from GitHub. In that case,
don't use pip, but try this recipe:
.. code::
# Install dependencies
pip3 install cryptography pyserial netifaces
# Clone repository
git clone https://github.com/markqvist/Reticulum.git
# Move into Reticulum folder and symlink library to examples folder
cd Reticulum
ln -s ../RNS ./Examples/
# Run an example
python3 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
# 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
# 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
# Have a look at another example
python3 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.
Reticulum on ARM64
==============================================
On some architectures, including ARM64, not all dependencies have precompiled
binaries. On such systems, you will need to install ``python3-dev`` before
installing Reticulum or programs that depend on Reticulum.
.. code::
# Install Python and development packages
sudo apt update
sudo apt install python3 python3-pip python3-dev
# Install 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>`_.
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.
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:
.. 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.
pip3 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.
pip3 install cryptography
# If the above installation succeeds, you can now install
# Reticulum and any related software
pip3 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.
Adding Radio Interfaces
==============================================
Once you have Reticulum installed and working, you can add radio interfaces with
any compatible hardware you have available. For information on how to configure
this, see the :ref:`Interfaces<interfaces-main>` section of this manual.
A range of common LoRa development boards and transceiver modules can be used
as interfaces with Reticulum. You can refer to the following external resources
for more information:
* `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/>`_
* `Private, Secure and Uncensorable Messaging Over a LoRa Mesh <https://unsigned.io/private-messaging-over-lora/>`_
* `RNode Firmware <https://github.com/markqvist/RNode_Firmware/>`_
If you have communications hardware that you think would be suitable for use with Reticulum,
you are welcome to head over to the `GitHub discussion pages <https://github.com/markqvist/Reticulum/discussions>`_
and propose adding an interface for the hardware.
+25
View File
@@ -0,0 +1,25 @@
******************************
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.
.. toctree::
:maxdepth: 3
whatis
gettingstartedfast
using
networks
interfaces
understanding
reference
examples
Indices and Tables
==================
* :ref:`genindex`
* :ref:`search`
+752
View File
@@ -0,0 +1,752 @@
.. _interfaces-main:
********************
Supported 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
network topologies you can create with Reticulum is more or less endless, but
common to them all is that you will need to define one or more *interfaces*
for Reticulum to use.
The following sections describe the interfaces currently available in Reticulum,
and gives example configurations for the respective interface types.
For a high-level overview of how networks can be formed over different interface
types, have a look at the :ref:`Building Networks<networks-main>` chapter of this
manual.
.. _interfaces-auto:
Auto Interface
==============
The Auto Interface enables communication with other discoverable Reticulum
nodes over autoconfigured IPv6 and UDP. It does not need any functional IP
infrastructure like routers or DHCP servers, but will require at least some
sort of switching medium between peers (a wired switch, a hub, a WiFi access
point or similar), and that link-local IPv6 is enabled in your operating
system, which should be enabled by default in almost all OSes.
.. code::
# This example demonstrates a TCP server interface.
# It will listen for incoming connections on the
# specified IP address and port number.
[[Default Interface]]
type = AutoInterface
interface_enabled = True
# You can create multiple isolated Reticulum
# networks on the same physical LAN by
# specifying different Group IDs.
group_id = reticulum
# You can also select specifically which
# kernel networking devices to use.
devices = wlan0,eth1
# Or let AutoInterface use all suitable
# devices except for a list of ignored ones.
ignored_devices = tun0,eth0
If you are connected to the Internet with IPv6, and your provider will route
IPv6 multicast, you can potentially configure the Auto Interface to globally
autodiscover other Reticulum nodes within your selected Group ID. You can specify
the discovery scope by setting it to one of ``link``, ``admin``, ``site``,
``organisation`` or ``global``.
.. code::
[[Default Interface]]
type = AutoInterface
interface_enabled = True
# Configure global discovery
group_id = custom_network_name
discovery_scope = global
# Other configuration options
discovery_port = 48555
data_port = 49555
.. _interfaces-i2p:
I2P Interface
=============
The I2P interface lets you connect Reticulum instances over the
`Invisible Internet Protocol <https://i2pd.website>`_. This can be
especially useful in cases where you want to host a globally reachable
Reticulum instance, but do not have access to any public IP addresses,
have a frequently changing IP address, or have firewalls blocking
inbound traffic.
Using the I2P interface, you will get a globally reachable, portable
and persistent I2P address that your Reticulum instance can be reached
at.
To use the I2P interface, you must have an I2P router running
on your system. The easiest way to acheive this is to download and
install the `latest release <https://github.com/PurpleI2P/i2pd/releases/latest>`_
of the ``i2pd`` package. For more details about I2P, see the
`geti2p.net website <https://geti2p.net/en/about/intro>`_.
When an I2P router is running on your system, you can simply add
an I2P interface to reticulum:
.. code::
[[I2P]]
type = I2PInterface
interface_enabled = yes
connectable = yes
On the first start, Reticulum will generate a new I2P address for the
interface and start listening for inbound traffic on it. This can take
a while the first time, especially if your I2P router was also just
started, and is not yet well-connected to the I2P network. When ready,
you should see I2P base32 address printed to your log file. You can
also inspect the status of the interface using the ``rnstatus`` utility.
To connect to other Reticulum instances over I2P, just add a comma-separated
list of I2P base32 addresses to the ``peers`` option of the interface:
.. code::
[[I2P]]
type = I2PInterface
interface_enabled = yes
connectable = yes
peers = 5urvjicpzi7q3ybztsef4i5ow2aq4soktfj7zedz53s47r54jnqq.b32.i2p
It can take anywhere from a few seconds to a few minutes to establish
I2P connections to the desired peers, so Reticulum handles the process
in the background, and will output relevant events to the log.
**Please Note!** While the I2P interface is the simplest way to use
Reticulum over I2P, it is also possible to tunnel the TCP server and
client interfaces over I2P manually. This can be useful in situations
where more control is needed, but requires manual tunnel setup through
the I2P daemon configuration.
It is important to note that the two methods are *interchangably compatible*.
You can use the I2PInterface to connect to a TCPServerInterface that
was manually tunneled over I2P, for example. This offers a high degree
of flexibility in network setup, while retaining ease of use in simpler
use-cases.
.. _interfaces-tcps:
TCP Server Interface
====================
The TCP Server interface is suitable for allowing other peers to connect over
the Internet or private IP networks. When a TCP server interface has been
configured, other Reticulum peers can connect to it with a TCP Client interface.
.. code::
# This example demonstrates a TCP server interface.
# It will listen for incoming connections on the
# specified IP address and port number.
[[TCP Server Interface]]
type = TCPServerInterface
interface_enabled = True
# This configuration will listen on all IP
# interfaces on port 4242
listen_ip = 0.0.0.0
listen_port = 4242
# Alternatively you can bind to a specific IP
# listen_ip = 10.0.0.88
# listen_port = 4242
# Or a specific network device
# device = eth0
# port = 4242
**Please Note!** The TCP interfaces support tunneling over I2P, but to do so reliably,
you must use the i2p_tunneled option:
.. code::
[[TCP Server on I2P]]
type = TCPServerInterface
interface_enabled = yes
listen_ip = 127.0.0.1
listen_port = 5001
i2p_tunneled = yes
In almost all cases, it is easier to use the dedicated ``I2PInterface``, but for complete
control, and using I2P routers running on external systems, this option also exists.
.. _interfaces-tcpc:
TCP Client Interface
====================
To connect to a TCP server interface, you would naturally use the TCP client
interface. Many TCP Client interfaces from different peers can connect to the
same TCP Server interface at the same time.
The TCP interface types can also tolerate intermittency in the IP link layer.
This means that Reticulum will gracefully handle IP links that go up and down,
and restore connectivity after a failure, once the other end of a TCP interface reappears.
.. code::
# Here's an example of a TCP Client interface. The
# target_host can either be an IP address or a hostname.
[[TCP Client Interface]]
type = TCPClientInterface
interface_enabled = True
target_host = 127.0.0.1
target_port = 4242
It is also possible to use this interface type to connect via other programs
or hardware devices that expose a KISS interface on a TCP port, for example
software-based soundmodems. To do this, use the ``kiss_framing`` option:
.. code::
# Here's an example of a TCP Client interface that connects
# to a software TNC soundmodem on a KISS over TCP port.
[[TCP KISS Interface]]
type = TCPClientInterface
interface_enabled = True
kiss_framing = True
target_host = 127.0.0.1
target_port = 8001
**Caution!** Only use the KISS framing option when connecting to external devices
and programs like soundmodems and similar over TCP. When using the
``TCPClientInterface`` in conjunction with the ``TCPServerInterface`` you should
never enable ``kiss_framing``, since this will disable internal reliability and
recovery mechanisms that greatly improves performance over unreliable and
intermittent TCP links.
**Please Note!** The TCP interfaces support tunneling over I2P, but to do so reliably,
you must use the i2p_tunneled option:
.. code::
[[TCP Client over I2P]]
type = TCPClientInterface
interface_enabled = yes
target_host = 127.0.0.1
target_port = 5001
i2p_tunneled = yes
.. _interfaces-udp:
UDP Interface
=============
A UDP interface can be useful for communicating over IP networks, both
private and the internet. It can also allow broadcast communication
over IP networks, so it can provide an easy way to enable connectivity
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
:ref:`Auto Interface<interfaces-auto>` performs better, and is even
easier to use.
The below example is enabled by default on new Reticulum installations,
as it provides an easy way to get started and to test Reticulum on a
pre-existing LAN.
.. code::
# This example enables communication with other
# local Reticulum peers over UDP.
[[UDP Interface]]
type = UDPInterface
interface_enabled = True
listen_ip = 0.0.0.0
listen_port = 4242
forward_ip = 255.255.255.255
forward_port = 4242
# The above configuration will allow communication
# within the local broadcast domains of all local
# IP interfaces.
# Instead of specifying listen_ip, listen_port,
# forward_ip and forward_port, you can also bind
# to a specific network device like below.
# device = eth0
# port = 4242
# Assuming the eth0 device has the address
# 10.55.0.72/24, the above configuration would
# be equivalent to the following manual setup.
# Note that we are both listening and forwarding to
# the broadcast address of the network segments.
# listen_ip = 10.55.0.255
# listen_port = 4242
# forward_ip = 10.55.0.255
# forward_port = 4242
# You can of course also communicate only with
# a single IP address
# listen_ip = 10.55.0.15
# listen_port = 4242
# forward_ip = 10.55.0.16
# forward_port = 4242
.. _interfaces-rnode:
RNode LoRa Interface
====================
To use Reticulum over LoRa, the `RNode <https://unsigned.io/rnode/>`_ interface
can be used, and offers full control over LoRa parameters.
.. code::
# Here's an example of how to add a LoRa interface
# using the RNode LoRa transceiver.
[[RNode LoRa Interface]]
type = RNodeInterface
# Enable interface if you want use it!
interface_enabled = True
# Serial port for the device
port = /dev/ttyUSB0
# Set frequency to 867.2 MHz
frequency = 867200000
# Set LoRa bandwidth to 125 KHz
bandwidth = 125000
# Set TX power to 7 dBm (5 mW)
txpower = 7
# Select spreading factor 8. Valid
# range is 7 through 12, with 7
# being the fastest and 12 having
# the longest range.
spreadingfactor = 8
# Select coding rate 5. Valid range
# is 5 throough 8, with 5 being the
# fastest, and 8 the longest range.
codingrate = 5
# You can configure the RNode to send
# out identification on the channel with
# a set interval by configuring the
# following two parameters.
# id_callsign = MYCALL-0
# id_interval = 600
# For certain homebrew RNode interfaces
# with low amounts of RAM, using packet
# flow control can be useful. By default
# it is disabled.
flow_control = False
.. _interfaces-serial:
Serial Interface
================
Reticulum can be used over serial ports directly, or over any device with a
serial port, that will transparently pass data. Useful for communicating
directly over a wire-pair, or for using devices such as data radios and lasers.
.. code::
[[Serial Interface]]
type = SerialInterface
interface_enabled = True
# Serial port for the device
port = /dev/ttyUSB0
# Set the serial baud-rate and other
# configuration parameters.
speed = 115200
databits = 8
parity = none
stopbits = 1
.. _interfaces-pipe:
Pipe Interface
==============
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.
.. code::
[[Pipe Interface]]
type = PipeInterface
interface_enabled = True
# External command to execute
command = netcat -l 5757
# Optional respawn delay, in seconds
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,
Reticulum will try to respawn the program after waiting for ``respawn_interval`` seconds.
.. _interfaces-kiss:
KISS Interface
==============
With the KISS interface, you can use Reticulum over a variety of packet
radio modems and TNCs, including `OpenModem <https://unsigned.io/openmodem/>`_.
KISS interfaces can also be configured to periodically send out beacons
for station identification purposes.
.. code::
[[Packet Radio KISS Interface]]
type = KISSInterface
interface_enabled = True
# Serial port for the device
port = /dev/ttyUSB1
# Set the serial baud-rate and other
# configuration parameters.
speed = 115200
databits = 8
parity = none
stopbits = 1
# Set the modem preamble.
preamble = 150
# Set the modem TX tail.
txtail = 10
# Configure CDMA parameters. These
# settings are reasonable defaults.
persistence = 200
slottime = 20
# You can configure the interface to send
# out identification on the channel with
# a set interval by configuring the
# following two parameters. The KISS
# interface will only ID if the set
# interval has elapsed since it's last
# actual transmission. The interval is
# configured in seconds.
# This option is commented out and not
# used by default.
# id_callsign = MYCALL-0
# id_interval = 600
# Whether to use KISS flow-control.
# This is useful for modems that have
# a small internal packet buffer, but
# support packet flow control instead.
flow_control = false
.. _interfaces-ax25:
AX.25 KISS Interface
====================
If you're using Reticulum on amateur radio spectrum, you might want to
use the AX.25 KISS interface. This way, Reticulum will automatically
encapsulate it's traffic in AX.25 and also identify your stations
transmissions with your callsign and SSID.
Only do this if you really need to! Reticulum doesn't need the AX.25
layer for anything, and it incurs extra overhead on every packet to
encapsulate in AX.25.
A more efficient way is to use the plain KISS interface with the
beaconing functionality described above.
.. code::
[[Packet Radio AX.25 KISS Interface]]
type = AX25KISSInterface
# Set the station callsign and SSID
callsign = NO1CLL
ssid = 0
# Enable interface if you want use it!
interface_enabled = True
# Serial port for the device
port = /dev/ttyUSB2
# Set the serial baud-rate and other
# configuration parameters.
speed = 115200
databits = 8
parity = none
stopbits = 1
# Set the modem preamble. A 150ms
# preamble should be a reasonable
# default, but may need to be
# increased for radios with slow-
# opening squelch and long TX/RX
# turnaround
preamble = 150
# Set the modem TX tail. In most
# cases this should be kept as low
# as possible to not waste airtime.
txtail = 10
# Configure CDMA parameters. These
# settings are reasonable defaults.
persistence = 200
slottime = 20
# Whether to use KISS flow-control.
# This is useful for modems with a
# small internal packet buffer.
flow_control = false
.. _interfaces-options:
Common Interface Options
========================
A number of general configuration options are available on most interfaces.
These can be used to control various aspects of interface behaviour.
* | The ``enabled`` option tells Reticulum whether or not
to bring up the interface. Defaults to ``False``. For any
interface to be brought up, the ``enabled`` option
must be set to ``True`` or ``Yes``.
* | The ``mode`` option allows selecting the high-level behaviour
of the interface from a number of options.
- The default value is ``full``. In this mode, all discovery,
meshing and transport functionality is available.
- In the ``access_point`` (or shorthand ``ap``) mode, the
interface will operate as a network access point. In this
mode, announces will not be automatically broadcasted on
the interface, and paths to destinations on the interface
will have a much shorter expiry time. This mode is useful
for creating interfaces that are mostly quiet, unless when
someone is actually using them. An example of this could
be a radio interface serving a wide area, where users are
expected to connect momentarily, use the network, and then
disappear again.
* | The ``outgoing`` option sets whether an interface is allowed
to transmit. Defaults to ``True``. If set to ``False`` or ``No``
the interface will only receive data, and never transmit.
* | The ``network_name`` option sets the virtual network name for
the interface. This allows multiple separate network segments
to exist on the same physical channel or medium.
* | The ``passphrase`` option sets an authentication passphrase on
the interface. This option can be used in conjunction with the
``network_name`` option, or be used alone.
* | The ``ifac_size`` option allows customising the length of the
Interface Authentication Codes carried by each packet on named
and/or authenticated network segments. It is set by default to
a size suitable for the interface in question, but can be set
to a custom size between 8 and 512 bits by using this option.
In normal usage, this option should not be changed from the
default.
* | The ``announce_cap`` option lets you configure the maximum
bandwidth to allocate, at any given time, to propagating
announces and other network upkeep traffic. It is configured at
2% by default, and should normally not need to be changed. Can
be set to any value between ``1`` and ``100``.
*If an interface exceeds its announce cap, it will queue announces
for later transmission. Reticulum will always prioritise propagating
announces from nearby nodes first. This ensures that the local
topology is prioritised, and that slow networks are not overwhelmed
by interconnected fast networks.*
*Destinations that are rapidly re-announcing will be down-prioritised
further. Trying to get "first-in-line" by announce spamming will have
the exact opposite effect: Getting moved to the back of the queue every
time a new announce from the excessively announcing destination is received.*
*This means that it is always beneficial to select a balanced
announce rate, and not announce more often than is actually necesarry
for your application to function.*
* | The ``bitrate`` option configures the interface bitrate.
Reticulum will use interface speeds reported by hardware, or
try to guess a suitable rate when the hardware doesn't report
any. In most cases, the automatically found rate should be
sufficient, but it can be configured by using the ``bitrate``
option, to set the interface speed in *bits per second*.
.. _interfaces-modes:
Interface Modes
===============
The optional ``mode`` setting is available on all interfaces, and allows
selecting the high-level behaviour of the interface from a number of modes.
These modes affect how Reticulum selects paths in the network, how announces
are propagated, how long paths are valid and how paths are discovered.
Configuring modes on interfaces is **not** strictly necessary, but can be useful
when building or connecting to more complex networks. If your Reticulum
instance is not running a Transport Node, it is rarely useful to configure
interface modes, and in such cases interfaces should generally be left in
the default mode.
* | The default mode is ``full``. In this mode, all discovery,
meshing and transport functionality is activated.
* | The ``gateway`` mode (or shorthand ``gw``) also has all
discovery, meshing and transport functionality available,
but will additionally try to discover unknown paths on
behalf of other nodes residing on the ``gateway`` interface.
If Reticulum receives a path request for an unknown
destination, from a node on a ``gateway`` interface, it
will try to discover this path via all other active interfaces,
and forward the discovered path to the requestor if one is
found.
| If you want to allow other nodes to widely resolve paths or connect
to a network via an interface, it might be useful to put it in this
mode. By creating a chain of ``gateway`` interfaces, other
nodes will be able to immediately discover paths to any
destination along the chain.
| *Please note!* It is the interface *facing the clients* that
must be put into ``gateway`` mode for this to work, not
the interface facing the wider network (for this, the ``boundary``
mode can be useful, though).
* | In the ``access_point`` (or shorthand ``ap``) mode, the
interface will operate as a network access point. In this
mode, announces will not be automatically broadcasted on
the interface, and paths to destinations on the interface
will have a much shorter expiry time. In addition, path
requests from clients on the access point interface will
be handled in the same way as the ``gateway`` interface.
| This mode is useful for creating interfaces that remain
quiet, until someone actually starts using them. An example
of this could be a radio interface serving a wide area,
where users are expected to connect momentarily, use the
network, and then disappear again.
* | The ``roaming`` mode should be used on interfaces that are
roaming (physically mobile), seen from the perspective of
other nodes in the network. As an example, if a vehicle is
equipped with an external LoRa interface, and an internal,
WiFi-based interface, that serves devices that are moving
_with_ the vehicle, the external LoRa interface should be
configured as ``roaming``, and the internal interface can
be left in the default mode. With transport enabled, such
a setup will allow all internal devices to reach each other,
and all other devices that are available on the LoRa side
of the network, when they are in range. Devices on the LoRa
side of the network will also be able to reach devices
internal to the vehicle, when it is in range. Paths via
``roaming`` interfaces also expire faster.
* | The purpose of the ``boundary`` mode is to specify interfaces
that establish connectivity with network segments that are
significantly different than the one this node exists on.
As an example, if a Reticulum instance is part of a LoRa-based
network, but also has a high-speed connection to a
public Transport Node available on the Internet, the interface
connecting over the Internet should be set to ``boundary`` mode.
For a table describing the impact of all modes on announce propagation,
please see the :ref:`Announce Propagation Rules<understanding-announcepropagation>` section.
.. _interfaces-announcerates:
Announce Rate Control
=====================
The built-in announce control mechanisms and the default ``announce_cap``
option described above are sufficient most of the time, but in some cases, especially on fast
interfaces, it may be useful to control the target announce rate. Using the
``announce_rate_target``, ``announce_rate_grace`` and ``announce_rate_penalty``
options, this can be done on a per-interface basis, and moderates the *rate at
which received announces are re-broadcasted to other interfaces*.
* | The ``announce_rate_target`` option sets the minimum amount of time,
in seconds, that should pass between received announces, for any one
destination. As an example, setting this value to ``3600`` means that
announces *received* on this interface will only be re-transmitted and
propagated to other interfaces once every hour, no matter how often they
are received.
* | The optional ``announce_rate_grace`` defines the number of times a destination
can violate the announce rate before the target rate is enforced.
* | The optional ``announce_rate_penalty`` configures an extra amount of
time that is added to the normal rate target. As an example, if a penalty
of ``7200`` seconds is defined, once the rate target is enforced, the
destination in question will only have its announces propagated every
3 hours, until it lowers its actual announce rate to within the target.
These mechanisms, in conjunction with the ``annouce_cap`` mechanisms mentioned
above means that it is essential to select a balanced announce strategy for
your destinations. The more balanced you can make this decision, the easier
it will be for your destinations to make it into slower networks that many hops
away. Or you can prioritise only reaching high-capacity networks with more frequent
announces.
Current statistics and information about announce rates can be viewed using the
``rnpath -r`` command.
It is important to note that there is no one right or wrong way to set up announce
rates. Slower networks will naturally tend towards using less frequent announces to
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.
+169
View File
@@ -0,0 +1,169 @@
.. _networks-main:
*****************
Building Networks
*****************
This chapter will provide you with the knowledge needed to build networks with
Reticulum, which can often be easier than using traditional stacks, since you
don't have to worry about coordinating addresses, subnets and routing for an
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
automatically.
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
(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.
* | Reticulum was designed to handle both very small, and very large networks.
While the adress 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
interconnect with much larger and higher bandwidth networks without issue.
Reticulum automatically manages the flow of information to and from various
network segments, and when bandwidth is limited, local traffic is prioritised.
* | Reticulum provides sender/initiator anonymity by default. There is no way
to filter traffic or discriminate it based on the source of the traffic.
* | All traffic is encrypted using ephemeral keys generated by an Elliptic Curve
Diffie-Hellman key exchange on Curve25519. There is no way to inspect traffic
contents, and no way to prioritise or throttle certain kinds of traffic.
All transport and routing layers are thus completely agnostic to traffic type,
and will pass all traffic equally.
* | Reticulum can function both with and without infrastructure. When *transport
nodes* are available, they can route traffic over multiple hops for other
nodes, and will function as a distributed cryptographic keystore. When there
is no transport nodes available, all nodes that are within communication range
can still communicate.
* | Every node can become a transport node, simply by enabling it in it's
configuration, but there is no need for every node on the network to be a
transport node. Letting every node be a transport node will in most cases
degrade the performance and reliability of the network.
*In general terms, if a node is stationary, well-connected and kept running
most of the time, it is a good candidate to be a transport node. For optimal
performance, a network should contain the amount of transport nodes that
provides connectivity to the intended area / topography, and not many more
than that.*
* | Reticulum is designed to work reliably in open, trustless environments. This
means you can use it to create open-access networks, where participants can
join and leave in an free and unorganised manner. This property allows an
entirely new, and so far, mostly unexplored class of networked applications,
where networks, and the information flow within them can form and dissolve
organically.
* | You can just as easily create closed networks, since Reticulum allows you to
add authentication to any interface. This means you can restrict access on
any interface type, even when using legacy devices, such as modems. You can
also mix authenticated and open interfaces on the same system. See the
:ref:`Common Interface Options<interfaces-options>` section of the :ref:`Interfaces<interfaces-main>`
chapter of this manual for information on how to set up interface authentication.
Reticulum allows you to mix very different kinds of networking mediums into a
unified mesh, or to keep everything within one medium. You could build a "virtual
network" running entirely over the Internet, where all nodes communicate over TCP
and UDP "channels". You could also build such a network using other already-established
communications channels as the underlying carrier for Reticulum.
However, most real-world networks will probably involve either some form of
wireless or direct hardline communications. To allow Reticulum to communicate
over any type of medium, you must specify it in the configuration file, by default
located at ``~/.reticulum/config``. See the :ref:`Supported Interfaces<interfaces-main>`
chapter of this manual for interface configuration examples.
Any number of interfaces can be configured, and Reticulum will automatically
decide which are suitable to use in any given situation, depending on where
traffic needs to flow.
Example Scenarios
-----------------
This section illustrates a few example scenarios, and how they would, in general
terms, be planned, implemented and configured.
Interconnected LoRa Sites
=========================
An organisation wants to provide communication and information services to it's
members, which are located mainly in three separate areas. Three suitable hill-top
locations are found, where the organisation can install equipment: Site A, B and C.
Since the amount of data that needs to be exchanged between users is mainly text-
based, the bandwidth requirements are low, and LoRa radios are chosen to connect
users to the network.
Due to the hill-top locations found, there is radio line-of-sight between site A
and B, and also between site B and C. Because of this, the organisation does not
need to use the Internet to interconnect the sites, but purchases four Point-to-Point
WiFi based radios for interconnecting the sites.
At each site, a Raspberry Pi is installed to function as a gateway. A LoRa radio
is connected to the Pi with a USB cable, and the WiFi radio is connected to the
ethernet port of the Pi. At site B, two WiFi radios are needed to be able to reach
both site A and site C, so an extra ethernet adapter is connected to the Pi in
this location.
Once the hardware has been installed, Reticulum is installed on all the Pis, and at
site A and C, one interface is added for the LoRa radio, as well as one for the WiFi
radio. At site B, an interface for the LoRa radio, and one interface for each WiFi
radio is added to the Reticulum configuration file. The transport node option is
enabled in the configuration of all three gateways.
The network is now operational, and ready to serve users across all three areas.
The organisation prepares a LoRa radio that is supplied to the end users, along
with a Reticulum configuration file, that contains the right parameters for
communicating with the LoRa radios installed at the gateway sites.
Once users connect to the network, anyone will be able to communicate with anyone
else across all three sites.
Bridging Over the Internet
==========================
As the organisation grows, several new communities form in places too far away
from the core network to be reachable over WiFi links. New gateways similar to those
previously installed are set up for the new communities at the new sites D and E, but
they are islanded from the core network, and only serve the local users.
After investigating the options, it is found that it is possible to install an
Internet connection at site A, and an interface on the Internet connection is
configured for Reticulum on the Raspberry Pi at site A.
A member of the organisation at site D, named Dori, is willing to help by sharing
the Internet connection she already has in her home, and is able to leave a Raspberry
Pi running. A new Reticulum interface is configured on her Pi, connecting to the newly
enabled Internet interface on the gateway at site A. Dori is now connected to both
all the nodes at her own local site (through the hill-top LoRa gateway), and all the
combined users of sites A, B and C. She then enables transport on her node, and
traffic from site D can now reach everyone at site A, B and C, and vice versa.
Growth and Convergence
======================
As the organisation grows, more gateways are added to keep up with the growing user
base. Some local gateways even add VHF radios and packet modems to reach outlying users
and communities that are out of reach for the LoRa radios and WiFi backhauls.
As more sites, gateways and users are connected, the amount of coordination required
is kept to a minimum. If one community wants to add connectivity to the next one
over, it can simply be done without having to involve everyone or coordinate address
space or routing tables.
With the added geographical coverage, the operators at site A one day find that
the original internet bridged interfaces are no longer utilised. The network has
converged to be completely self-connected, and the sites that were once poorly
connected outliers are now an integral part of the network.
+83
View File
@@ -0,0 +1,83 @@
.. _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.
.. _api-reticulum:
Reticulum
---------
.. autoclass:: RNS.Reticulum
:members:
.. _api-identity:
Identity
--------
.. autoclass:: RNS.Identity
:members:
.. _api-destination:
Destination
-----------
.. autoclass:: RNS.Destination
:members:
.. _api-packet:
Packet
------
.. autoclass:: RNS.Packet(destination, data, create_receipt = True)
:members:
.. _api-packetreceipt:
Packet Receipt
--------------
.. autoclass:: RNS.PacketReceipt()
:members:
.. _api-link:
Link
----
.. autoclass:: RNS.Link(destination, established_callback=None, closed_callback = None)
:members:
.. _api-requestreceipt:
Request Receipt
---------------
.. autoclass:: RNS.RequestReceipt()
:members:
.. _api-resource:
Resource
--------
.. autoclass:: RNS.Resource(data, link, advertise=True, auto_compress=True, callback=None, progress_callback=None, timeout=None)
:members:
.. _api-transport:
Transport
---------
.. autoclass:: RNS.Transport
:members:
+841
View File
@@ -0,0 +1,841 @@
.. _understanding-main:
***********************
Understanding Reticulum
***********************
This chapter will briefly describe the overall purpose and operating principles of Reticulum.
It should give you an overview of how the stack works, and an understanding of how to
develop networked applications using Reticulum.
This chapter is not an exhaustive source of information on Reticulum, at least not yet. Currently,
the only complete repository, and final authority on how Reticulum actually functions, is the Python
reference implementation and API reference. That being said, this chapter is an essential resource in
understanding how Reticulum works from a high-level perspective, along with the general principles of
Reticulum, and how to apply them when creating your own networks or software.
After reading this document, you should be well-equipped to understand how a Reticulum network
operates, what it can achieve, and how you can use it yourself. If you want to help out with the
development, this is also the place to start, since it will provide a pretty clear overview of the
sentiments and the philosophy behind Reticulum, what problems it seeks to solve, and how it
approaches those solutions.
.. _understanding-motivation:
Motivation
==========
The primary motivation for designing and implementing Reticulum has been the current lack of
reliable, functional and secure minimal-infrastructure modes of digital communication. It is my
belief that it is highly desirable to create a reliable and efficient way to set up long-range digital
communication networks that can securely allow exchange of information between people and
machines, with no central point of authority, control, censorship or barrier to entry.
Almost all of the various networking systems in use today share a common limitation: They
require large amounts of coordination and centralised trust and power to function. To join such networks, you need approval
of gatekeepers in control. This need for coordination and trust inevitably leads to an environment of
central control, where it's very easy for infrastructure operators or governments to control or alter
traffic, and censor or persecute unwanted actors. It also makes it completely impossible to freely deploy
and use networks at will, like one would use other common tools that enhance individual agency and freedom.
Reticulum aims to require as little coordination and trust as possible. It aims to make secure,
anonymous and permissionless networking and information exchange a tool that anyone can just pick up and use.
Since Reticulum is completely medium agnostic, it can be used to build networks on whatever is best
suited to the situation, or whatever you have available. In some cases, this might be packet radio
links over VHF frequencies, in other cases it might be a 2.4 GHz
network using off-the-shelf radios, or it might be using common LoRa development boards.
At the time of release of this document, the fastest and easiest setup for development and testing is using
LoRa radio modules with an open source firmware (see the section :ref:`Reference Setup<understanding-referencesystem>`),
connected to any kind of computer or mobile device that Reticulum can run on.
The ultimate aim of Reticulum is to allow anyone to be their own network operator, and to make it
cheap and easy to cover vast areas with a myriad of independent, interconnectable and autonomous networks.
Reticulum **is not** *one network*, it **is a tool** to build *thousands of networks*. Networks without
kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate
with each other, and require no central oversight. Networks for human beings. *Networks for the people*.
.. _understanding-goals:
Goals
=====
To be as widely usable and efficient to deploy as possible, the following goals have been used to
guide the design of Reticulum:
* **Fully useable as open source software stack**
Reticulum must be implemented with, and be able to run using only open source software. This is
critical to ensuring the availability, security and transparency of the system.
* **Hardware layer agnosticism**
Reticulum must be fully hardware agnostic, and shall be useable over a wide range of
physical networking layers, such as data radios, serial lines, modems, handheld transceivers,
wired ethernet, wifi, or anything else that can carry a digital data stream. Hardware made for
dedicated Reticulum use shall be as cheap as possible and use off-the-shelf components, so
it can be easily modified and replicated by anyone interested in doing so.
* **Very low bandwidth requirements**
Reticulum should be able to function reliably over links with a transmission capacity as low
as *500 bits per second*.
* **Encryption by default**
Reticulum must use strong encryption by default for all communication.
* **Initiator Anonymity**
It must be possible to communicate over a Reticulum network without revealing any identifying
information about oneself.
* **Unlicensed use**
Reticulum shall be functional over physical communication mediums that do not require any
form of license to use. Reticulum must be designed in a way, so it is usable over ISM radio
frequency bands, and can provide functional long distance links in such conditions, for example
by connecting a modem to a PMR or CB radio, or by using LoRa or WiFi modules.
* **Supplied software**
In addition to the core networking stack and API, that allows a developer to build
applications with Reticulum, a basic set of Reticulum-based communication tools must be
implemented and released along with Reticulum itself. These shall serve both as a
functional, basic communication suite, and as an example and learning resource to others wishing
to build applications with Reticulum.
* **Ease of use**
The reference implementation of Reticulum is written in Python, to make it easy to use
and understand. A programmer with only basic experience should be able to use
Reticulum to write networked applications.
* **Low cost**
It shall be as cheap as possible to deploy a communication system based on Reticulum. This
should be achieved by using cheap off-the-shelf hardware that potential users might already
own. The cost of setting up a functioning node should be less than $100 even if all parts
needs to be purchased.
.. _understanding-basicfunctionality:
Introduction & Basic Functionality
==================================
Reticulum is a networking stack suited for high-latency, low-bandwidth links. Reticulum is at its
core a *message oriented* system. It is suited for both local point-to-point or point-to-multipoint
scenarios where alle nodes are within range of each other, as well as scenarios where packets need
to be transported over multiple hops in a complex network to reach the recipient.
Reticulum does away with the idea of addresses and ports known from IP, TCP and UDP. Instead
Reticulum uses the singular concept of *destinations*. Any application using Reticulum as its
networking stack will need to create one or more destinations to receive data, and know the
destinations it needs to send data to.
All destinations in Reticulum are represented as a 10 byte hash, derived from truncating a full
SHA-256 hash of identifying characteristics of the destination. To users, the destination addresses
will be displayed as 10 bytes in hexadecimal representation, as in the following example: ``<80e29bf7cccaf31431b3>``.
The truncation size of 10 bytes (80 bits) for destinations has been choosen as a reasonable tradeoff between address space
and packet overhead. The address space accomodated 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 80 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
channel to a destination with *Forward Secrecy* and *Initiator Anonymity* using a elliptic
curve cryptography and ephemeral keys derived from a Diffie Hellman exchange on Curve25519. In
Reticulum terminology, this is called a *Link*. 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 broadcast purposes, or situations where you need the communication to be in
plain text.
Reticulum can connect to a variety of interfaces such as radio modems, data radios and serial ports,
and offers the possibility to easily tunnel Reticulum traffic over IP links such as the Internet or
private IP networks.
.. _understanding-destinations:
Destinations
------------
To receive and send data with the Reticulum stack, an application needs to create one or more
destinations. Reticulum uses three different basic destination types, and one special:
* **Single**
The *single* destination type is the most common type in Reticulum, and should be used for
most purposes. It is always identified by a unique public key. Any data sent to this
destination will be encrypted using ephemeral keys derived from an ECDH key exchange, and will
only be readable by the creator of the destination, who holds the corresponding private key.
* **Plain**
A *plain* destination type is unencrypted, and suited for traffic that should be broadcast to a
number of users, or should be readable by anyone. Traffic to a *plain* destination is not encrypted.
Generally, *plain* destinations can be used for broadcast information intended to be public.
Plain destinations are only reachable directly, and packets adressed to plain destinations are
never transported over multiple hops in the network. To be transportable over multiple hops in Reticulum, information
*must* be encrypted, since Reticulum uses the per-packet encryption to verify routing paths and
keep them alive.
* **Group**
The *group* special destination type, that defines a symmetrically encrypted virtual destination.
Data sent to this destination will be encrypted with a symmetric key, and will be readable by
anyone in possession of the key, but as with the *plain* destination type, packets to this type
of destination are not currently transported over multiple hops, although a planned upgrade
to Reticulum will allow globally reachable *group* destinations.
* **Link**
A *link* is a special destination type, that serves as an abstract channel to a *single*
destination, directly connected or over multiple hops. The *link* also offers reliability and
more efficient encryption, forward secrecy, initiator anonymity, and as such can be useful even
when a node is directly reachable. It also offers a more capable API and allows easily carrying
out requests and responses, large data transfers and more.
.. _understanding-destinationnaming:
Destination Naming
^^^^^^^^^^^^^^^^^^
Destinations are created and named in an easy to understand dotted notation of *aspects*, and
represented on the network as a hash of this value. The hash is a SHA-256 truncated to 80 bits. The
top level aspect should always be a unique identifier for the application using the destination.
The next levels of aspects can be defined in any way by the creator of the application.
Aspects can be as long and as plentiful as required, and a resulting long destination name will not
impact efficiency, as names are always represented as truncated SHA-256 hashes on the network.
As an example, a destination for a environmental monitoring application could be made up of the
application name, a device type and measurement type, like this:
.. code-block:: text
app name : environmentlogger
aspects : remotesensor, temperature
full name : environmentlogger.remotesensor.temperature
hash : fa7ddfab5213f916dea
For the *single* destination, Reticulum will automatically append the associated public key as a
destination aspect before hashing. This is done to ensure only the correct destination is reached,
since anyone can listen to any destination name. Appending the public key ensures that a given
packet is only directed at the destination that holds the corresponding private key to decrypt the
packet.
**Take note!** There is a very important concept to understand here:
* Anyone can use the destination name ``environmentlogger.remotesensor.temperature``
* Each destination that does so will still have a unique destination hash, and thus be uniquely
addressable, because their public keys will differ.
In actual use of *single* destination naming, it is advisable not to use any uniquely identifying
features in aspect naming. Aspect names should be general terms describing what kind of destination
is represented. The uniquely identifying aspect is always acheived by the appending the public key,
which expands the destination into a uniquely identifyable one. Reticulum does this automatically.
Any destination on a Reticulum network can be addressed and reached simply by knowning its
destination hash (and public key, but if the public key is not known, it can be requested from the
network simply by knowing the destination hash). The use of app names and aspects makes it easy to
structure Reticulum programs and makes it possible to filter what information and data your program
receives.
To recap, the different destination types should be used in the following situations:
* **Single**
When private communication between two endpoints is needed. Supports multiple hops.
* **Group**
When private communication between two or more endpoints is needed. Supports multiple hops
indirectly, but must first be established through a *single* destination.
* **Plain**
When plain-text communication is desirable, for example when broadcasting information, or for local discovery purposes.
To communicate with a *single* destination, you need to know its public key. Any method for
obtaining the public key is valid, but Reticulum includes a simple mechanism for making other
nodes aware of your destinations public key, called the *announce*. It is also possible to request
an unknown public key from the network, as all transport instances serve as a distributed ledger
of public keys.
Note that public key information can be shared and verified in other ways than using the
built-in *announce* functionality, and that it is therefore not required to use the *announce* and *path request*
functionality to obtain public keys. It is by far the easiest though, and should definitely be used
if there is not a very good reason for doing it differently.
.. _understanding-keyannouncements:
Public Key Announcements
------------------------
An *announce* will send a special packet over any relevant interfaces, containing all needed
information about the destination hash and public key, and can also contain some additional,
application specific data. The entire packet is signed by the sender to ensure authenticity. It is not
required to use the announce functionality, but in many cases it will be the simplest way to share
public keys on the network. The announce mechanism also serves to establish end-to-end connectivity
to the announced destination, as the announce propagates through the network.
As an example, an announce in a simple messenger application might contain the following information:
* The announcers destination hash
* The announcers public key
* Application specific data, in this case the users nickname and availability status
* A random blob, making each new announce unique
* An Ed25519 signature of the above information, verifying authenticity
With this information, any Reticulum node that receives it will be able to reconstruct an outgoing
destination to securely communicate with that destination. You might have noticed that there is one
piece of information lacking to reconstruct full knowledge of the announced destination, and that is
the aspect names of the destination. These are intentionally left out to save bandwidth, since they
will be implicit in almost all cases. The receiving application will already know them. If a destination
name is not entirely implicit, information can be included in the application specific data part that
will allow the receiver to infer the naming.
It is important to note that announces will be forwarded throughout the network according to a
certain pattern. This will be detailed in the section
:ref:`The Announce Mechanism in Detail<understanding-announce>`.
In Reticulum, destinations are allowed to move around the network at will. This is very different from
protocols such as IP, where an address is always expected to stay within the network segment it was assigned in.
This limitation does not exist in Reticulum, and any destination is *completely portable* over the entire topography
of the network, and *can even be moved to other Reticulum networks* than the one it was created in, and
still become reachable. To update it's 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.
.. _understanding-identities:
Identities
----------
In Reticulum, an *identity* does not necessarily represent a personal identity, but is an abstraction that
can represent any kind of *verifiable entity*. This could very well be a person, but it could also be the
control interface of a machine, a program, robot, computer, sensor or something else entirely. In
general, any kind of agent that can act, or be acted upon, or store or manipulate information, can be
represented as an identity. An *identity* can be used to create any number of destinations.
A *single* destination will always have an *identity* tied to it, but not *plain* or *group*
destinations. Destinations and identities share a multilateral connection. You can create a
destination, and if it is not connected to an identity upon creation, it will just create a new one to use
automatically. This may be desirable in some situations, but often you will probably want to create
the identity first, and then use it to create new destinations.
As an example, we could use an identity to represent the user of a messaging application.
Destinations can then be created by this identity to allow communication to reach the user.
In all cases it is of great importance to store the private keys associated with any
Reticulum Identity securely and privately, since obtaining access to the identity keys equals
obtaining access and controlling reachability to any destinations created by that identity.
.. _understanding-gettingfurther:
Getting Further
---------------
The above functions and principles form the core of Reticulum, and would suffice to create
functional networked applications in local clusters, for example over radio links where all interested
nodes can directly hear each other. But to be truly useful, we need a way to direct traffic over multiple
hops in the network.
In the following sections, two concepts that allow this will be introduced, *paths* and *links*.
.. _understanding-transport:
Reticulum Transport
===================
The methods of routing used in traditional networks are fundamentally incompatible with the physical medium
types and circumstances that Reticulum was designed to handle. These mechanisms mostly assume trust at the physical layer,
and often needs a lot more bandwidth than Reticulum can assume is available. Since Reticulum is designed to
survive running over open radio spectrum, no such trust can be assumed, and bandwidth is often very limited.
To overcome such challenges, Reticulums *Transport* system uses asymmetric elliptic curve cryptography to
implement the concept of *paths* that allow discovery of how to get information closer to a certain
destination. It is important to note that no single node in a Reticulum network knows the complete
path to a destination. Every Transport node participating in a Reticulum network will only
know the most direct way to get a packet one hop closer to it's destination.
.. _understanding-nodetypes:
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
one place, and is intended to be kept available most of the time, it is a good contender to be a *Transport Node*.
Any Reticulum Instance can become a Transport Node by enabling it in the configuration.
This distinction is made by the user configuring the node, and is used to determine what nodes on the
network will help forward traffic, and what nodes rely on other nodes for wider connectivity.
If a node is an *Instance* it should be given the configuration directive ``enable_transport = No``, which
is the default setting.
If it is a *Transport Node*, it should be given the configuration directive ``enable_transport = Yes``.
.. _understanding-announce:
The Announce Mechanism in Detail
--------------------------------
When an *announce* for a destination is transmitted by from a Reticulum instance, it will be forwarded by
any transport node receiving it, but according to some specific rules:
* | If this exact announce has already been received before, ignore it.
* | If not, record into a table which Transport Node the announce was received from, and how many times in
total it has been retransmitted to get here.
* | If the announce has been retransmitted *m+1* times, it will not be forwarded any more. By default, *m* is
set to 128.
* | After a randomised delay, the announce will be retransmitted on all interfaces that have bandwidth
available for processing announces. By default, the maximum bandwidth allocation for processing
announces is set at 2%, but can be configured on a per-interface basis.
* | 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
into a queue managed by the interface.
* | When the interface has bandwidth available for processing an announce, it will prioritise announces
for destinations that are closest in terms of hops, thus prioritising reachability and connectivity
of local nodes, even on slow networks that connect to wider and faster networks.
* | After the announce has been re-transmitted, and if no other nodes are heard retransmitting the announce
with a greater hop count than when it left this node, transmitting it will be retried *r* times. By default,
*r* is set to 1.
* | If a newer announce from the same destination arrives, while an identical one is already waiting
to be transmitted, the newest announce is discarded. If the newest announce contains different
application specific data, it will replace the old announce.
Once an announce has reached a node in the network, any other node in direct contact with that
node will be able to reach the destination the announce originated from, simply by sending a packet
addressed to that destination. Any node with knowledge of the announce will be able to direct the
packet towards the destination by looking up the next node with the shortest amount of hops to the
destination.
According to these rules, an announce will propagate throughout the network in a predictable way,
and make the announced destination reachable in a short amount of time. Fast networks that have the
capacity to process many announces can reach full convergence very quickly, even when constantly adding
new destinations. Slower segments of such networks might take a bit longer to gain full knowledge about
the wide and fast networks they are connected to, but can still do so over time, while prioritising full
and quickly converging end-to-end connectivity for their local, slower segments.
In general, even extremely complex networks, that utilize the maximum 128 hops will converge to full
end-to-end connectivity in about one minute, given there is enough bandwidth available to process
the required amount of announces.
.. _understanding-paths:
Reaching the Destination
------------------------
In networks with changing topology and trustless connectivity, nodes need a way to establish
*verified connectivity* with each other. Since the network is assumed to be trustless, Reticulum
must provide a way to guarantee that the peer you are communicating with is actually who you
expect. Reticulum offers two ways to do this.
For exchanges of small amounts of information, Reticulum offers the *Packet* API, which works exactly like you would expect - on a per packet level. The following process is employed when sending a packet:
* | A packet is always created with an associated destination and some payload data. When the packet is sent
to a *single* destination type, Reticulum will automatically create an ephemeral encryption key, perform
an ECDH key exchange with the destinations public key, 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
key exchange locally, before sending the packet.
* | The public part of the newly generated ephemeral key-pair is included with the encrypted token, and sent
along with the encrypted payload data in the packet.
* | 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.
* | 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
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
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.
For exchanges of larger amounts of data, or when longer sessions of bidirectional communication is desired, Reticulum offers the *Link* API. To establish a *link*, the following process is employed:
* | First, the node that wishes to establish a link will send out a special packet, that
traverses the network and locates the desired destination. Along the way, the Transport Nodes that
forward the packet will take note of this *link request*.
* | Second, if the destination accepts the *link request* , it will send back a packet that proves the
authenticity of its identity (and the receipt of the link request) to the initiating node. All
nodes that initially forwarded the packet will also be able to verify this proof, and thus
accept the validity of the *link* throughout the network.
* | When the validity of the *link* has been accepted by forwarding nodes, these nodes will
remember the *link* , and it can subsequently be used by referring to a hash representing it.
* | As a part of the *link request*, an Elliptic Curve Diffie-Hellman key exchange takes place, that sets up an
efficiently encrypted tunnel between the two nodes. As such, this mode of communication is preferred,
even for situations when nodes can directly communicate, when the amount of data to be exchanged numbers
in the tens of packets, or whenever the use of the more advanced API functions is desired.
* | When a *link* has been set up, it automatically provides message receipt functionality, through
the same *proof* mechanism discussed before, so the sending node can obtain verified confirmation
that the information reached the intended recipient.
* | Once the *link* has been set up, the initiator can remain anonymous, or choose to authenticate towards
the destination using a Reticulum Identity. This authentication is happening inside the encrypted
link, and is only revealed to the verified destination, and no intermediaries.
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 237 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.62 bits per second. Even on a slow 1200 bits per second packet
radio channel, 100 concurrent links will still leave 95% channel capacity for actual data.
Link Establishment in Detail
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
After exploring the basics of the announce mechanism, finding a path through the network, and an overview
of the link establishment procedure, this section will go into greater detail about the Reticulum link
establishment process.
The *link* in Reticulum terminology should not be viewed as a direct node-to-node link on the
physical layer, but as an abstract channel, that can be open for any amount of time, and can span
an arbitrary number of hops, where information will be exchanged between two nodes.
* | When a node in the network wants to establish verified connectivity with another node, it
will randomly generate a new X25519 private/public key pair. It then creates a *link request*
packet, and broadcast it.
|
| *It should be noted that the X25519 public/private keypair mentioned above is two separate keypairs:
An encryption key pair, used for derivation of a shared symmetric key, and a signing key pair, used
for signing and verifying messages on the link. They are sent together over the wire, and can be
considered as single public key for simplicity in this explanation.*
* | The *link request* is addressed to the destination hash of the desired destination, and
contains the following data: The newly generated X25519 public key *LKi*.
* | The broadcasted packet will be directed through the network according to the rules laid out
previously.
* | Any node that forwards the link request will store a *link id* in its *link table* , along with the
amount of hops the packet had taken when received. The link id is a hash of the entire link
request packet. If the link request packet is not *proven* by the addressed destination within some
set amount of time, the entry will be dropped from the *link table* again.
* | When the destination receives the link request packet, it will decide whether to accept the request.
If it is accepted, the destination will also generate a new X25519 private/public key pair, and
perform a Diffie Hellman Key Exchange, deriving a new symmetric key that will be used to encrypt the
channel, once it has been established.
* | 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
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.
An abstract bi-directional communication channel has now been established along a path in the network.
* | When the source receives the *proof* , it will know unequivocally that a verified path has been
established to the destination. It can now also use the X25519 public key contained in the
*link proof* to perform it's own Diffie Hellman Key Exchange and derive the symmetric key
that is used to encrypt the channel. Information can now be exchanged reliably and securely.
Its important to note that this methodology ensures that the source of the request does not need to
reveal any identifying information about itself. The link initiator remains completely anonymous.
When using *links*, Reticulum will automatically verify all data sent over the link, and can also
automate retransmissions if *Resources* are used.
.. _understanding-resources:
Resources
---------
For exchanging small amounts of data over a Reticulum network, the :ref:`Packet<api-packet>` interface
is sufficient, but for exchanging data that would require many packets, an efficient way to coordinate
the transfer is needed.
This is the purpose of the Reticulum :ref:`Resource<api-resource>`. A *Resource* can automatically
handle the reliable transfer of an arbitrary amount of data over an established :ref:`Link<api-link>`.
Resources can auto-compress data, will handle breaking the data into individual packets, sequencing
the transfer, integrity verification and reassembling the data on the other end.
:ref:`Resources<api-resource>` are programmatically very simple to use, and only requires a few lines
of codes to reliably transfer any amount of data. They can be used to transfer data stored in memory,
or stream data directly from files.
.. _understanding-referencesystem:
Reference Setup
======================
This section will detail a recommended *Reference Setup* for Reticulum. It is important to
note that Reticulum is designed to be usable on more or less any computing device, and over more
or less any medium that allows you to send and receive data, which satisfies some very low
minimum requirements.
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
Reticulum stack should be able to run on more or less any hardware that can provide a Python 3.x
runtime environment.
That being said, this reference setup has been outlined to provide a common platform for anyone
who wants to help in the development of Reticulum, and for everyone who wants to know a
recommended setup to get started experimenting. A reference system consists of three parts:
* **An Interface Device**
Which provides access to the physical medium whereupon the communication
takes place, for example a radio with an integrated modem. A setup with a separate modem
connected to a radio would also be an interface device.
* **A Host Device**
Some sort of computing device that can run the necessary software, communicate with the
interface device, and provide user interaction.
* **A Software Stack**
The software implementing the Reticulum protocol and applications using it.
The reference setup can be considered a relatively stable platform to develop on, and also to start
building networks or applications on. While details of the implementation might change at the current stage of
development, it is the goal to maintain hardware compatibility for as long as entirely possible, and
the current reference setup has been determined to provide a functional platform for many years
into the future. The current Reference System Setup is as follows:
* **Interface Device**
A data radio consisting of a LoRa radio module, and a microcontroller with open source
firmware, that can connect to host devices via USB. It operates in either the 430, 868 or 900
MHz frequency bands. More details can be found on the `RNode Page <https://unsigned.io/rnode>`_.
* **Host Device**
Any computer device running Linux and Python. A Raspberry Pi with a Debian based OS is
recommended.
* **Software Stack**
The most recently released Python Implementation of Reticulum, running on a Debian based
operating system.
To avoid confusion, it is very important to note, that the reference interface device **does not**
use the LoRaWAN standard, but uses a custom MAC layer on top of the plain LoRa modulation! As such, you will
need a plain LoRa radio module connected to an controller with the correct firmware. Full details on how to
get or make such a device is available on the `RNode Page <https://unsigned.io/rnode>`_.
With the current reference setup, it should be possible to get on a Reticulum network for around 100$
even if you have none of the hardware already, and need to purchase everything.
This reference setup is of course just a recommendation for getting started easily, and you should
tailor it to your own specific needs, or whatever hardware you have available.
.. _understanding-protocolspecifics:
Protocol Specifics
==================
This chapter will detail protocol specific information that is essential to the implementation of
Reticulum, but non critical in understanding how the protocol works on a general level. It should be
treated more as a reference than as essential reading.
Packet Prioritisation
---------------------
Currently, Reticulum is completely priority-agnostic regarding general traffic. All traffic is handled
on a first-come, first-serve basis. Announce re-transmission are handled according to the re-transmission
times and priorities described earlier in this chapter.
Interface Access Codes
----------------------
Reticulum can create named virtual networks, and networks that are only accessible by knowing a preshared
passphrase. The configuration of this is detailed in the :ref:`Common Interface Options<interfaces-options>`
section. To implement these feature, Reticulum uses the concept of Interface Access Codes, that are calculated
and verified per packet.
An interface with a named virtual network or passphrase authentication enabled will derive a shared Ed25519
signing identity, and for every outbound packet generate a signature of the entire packet. This signature is
then inserted into the packet as an Interface Access Code before transmission. Depending on the speed and
capabilities of the interface, the IFAC can be the full 512-bit Ed25519 signature, or a truncated version.
Configured IFAC length can be inspected for all interfaces with the ``rnstatus`` utility.
Upon receipt, the interface will check that the signature matches the expected value, and drop the packet if it
does not. This ensures that only packets sent with the correct naming and/or passphrase parameters are allowed to
pass onto the network.
.. _understanding-packetformat:
Wire Format
-----------
.. code-block:: text
== Reticulum Wire Format ======
A Reticulum packet is composed of the following fields:
[HEADER 2 bytes] [ADDRESSES 10/20 bytes] [CONTEXT 1 byte] [DATA 0-477 bytes]
* The HEADER field is 2 bytes long.
* Byte 1: [IFAC Flag], [Header Type], [Propagation Type], [Destination Type] and [Packet Type]
* Byte 2: Number of hops
* Interface Access Code field if the IFAC flag was set.
* The length of the Interface Access Code can vary from
1 to 64 bytes according to physical interface
capabilities and configuration.
* The ADDRESSES field contains either 1 or 2 addresses.
* Each address is 10 bytes long.
* The Header Type flag in the HEADER field determines
whether the ADDRESSES field contains 1 or 2 addresses.
* Addresses are Reticulum hashes truncated to 10 bytes.
* The CONTEXT field is 1 byte.
* It is used by Reticulum to determine packet context.
* The DATA field is between 0 and 477 bytes.
* It contains the packets data payload.
IFAC Flag
-----------------
open 0 Packet for publically accessible interface
authenticated 1 Interface authentication is included in packet
Header Types
-----------------
type 1 0 Two byte header, one 10 byte address field
type 2 1 Two byte header, two 10 byte address fields
Propagation Types
-----------------
broadcast 00
transport 01
reserved 10
reserved 11
Destination Types
-----------------
single 00
group 01
plain 10
link 11
Packet Types
-----------------
data 00
announce 01
link request 10
proof 11
+- Packet Example -+
HEADER FIELD DESTINATION FIELDS CONTEXT FIELD DATA FIELD
_______|_______ ________________|________________ ________|______ __|_
| | | | | | | |
01010000 00000100 [HASH1, 10 bytes] [HASH2, 10 bytes] [CONTEXT, 1 byte] [DATA]
|| | | | |
|| | | | +-- Hops = 4
|| | | +------- Packet Type = DATA
|| | +--------- Destination Type = SINGLE
|| +----------- Propagation Type = TRANSPORT
|+------------- Header Type = HEADER_2 (two byte header, two address fields)
+-------------- Access Codes = DISABLED
+- Packet Example -+
HEADER FIELD DESTINATION FIELD CONTEXT FIELD DATA FIELD
_______|_______ _______|_______ ________|______ __|_
| | | | | | | |
00000000 00000111 [HASH1, 10 bytes] [CONTEXT, 1 byte] [DATA]
|| | | | |
|| | | | +-- Hops = 0
|| | | +------- Packet Type = DATA
|| | +--------- Destination Type = SINGLE
|| +----------- Propagation Type = BROADCAST
|+------------- Header Type = HEADER_1 (two byte header, one address field)
+-------------- Access Codes = DISABLED
+- Packet Example -+
HEADER FIELD IFAC FIELD DESTINATION FIELD CONTEXT FIELD DATA FIELD
_______|_______ ______|______ _______|_______ ________|______ __|_
| | | | | | | | | |
10000000 00000111 [IFAC, N bytes] [HASH1, 10 bytes] [CONTEXT, 1 byte] [DATA]
|| | | | |
|| | | | +-- Hops = 0
|| | | +------- Packet Type = DATA
|| | +--------- Destination Type = SINGLE
|| +----------- Propagation Type = BROADCAST
|+------------- Header Type = HEADER_1 (two byte header, one address field)
+-------------- Access Codes = ENABLED
Size examples of different packet types
---------------------------------------
The following table lists example sizes of various
packet types. The size listed are the complete on-
wire size counting all fields including headers,
but excluding any interface access codes.
- Path Request : 33 bytes
- Announce : 151 bytes
- Link Request : 77 bytes
- Link Proof : 77 bytes
- Link RTT packet : 83 bytes
- Link keepalive : 14 bytes
.. _understanding-announcepropagation:
Announce Propagation Rules
--------------------------
The following table illustrates the rules for automatically propagating announces
from one interface type to another, for all possible combinations. For the purpose
of announce propagation, the *Full* and *Gateway* modes are identical.
.. image:: graphics/if_mode_graph_b.png
See the :ref:`Interface Modes<interfaces-modes>` section for a conceptual overview
of the different interface modes, and how they are configured.
..
(.. code-block:: text)
Full ────── ✓ ──┐ ┌── ✓ ── Full
AP ──────── ✓ ──┼───> Full >───┼── ✕ ── AP
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
Roaming ─── ✓ ──┘ └── ✓ ── Roaming
Full ────── ✕ ──┐ ┌── ✓ ── Full
AP ──────── ✕ ──┼────> AP >────┼── ✕ ── AP
Boundary ── ✕ ──┤ ├── ✓ ── Boundary
Roaming ─── ✕ ──┘ └── ✓ ── Roaming
Full ────── ✓ ──┐ ┌── ✓ ── Full
AP ──────── ✓ ──┼─> Roaming >──┼── ✕ ── AP
Boundary ── ✕ ──┤ ├── ✕ ── Boundary
Roaming ─── ✕ ──┘ └── ✕ ── Roaming
Full ────── ✓ ──┐ ┌── ✓ ── Full
AP ──────── ✓ ──┼─> Boundary >─┼── ✕ ── AP
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
Roaming ─── ✕ ──┘ └── ✕ ── Roaming
+509
View File
@@ -0,0 +1,509 @@
.. _using-main:
******************************
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.
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.
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
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.
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
be brought up when a program needs it. In either case, any number of programs
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
configuration file is created. The default configuration looks like this:
.. code::
# This is the default Reticulum config file.
# You should probably edit it to include any additional,
# interfaces and settings you might need.
# Only the most basic options are included in this default
# configuration. To see a more verbose, and much longer,
# configuration example, you can run the command:
# rnsd --exampleconfig
[reticulum]
# If you enable Transport, your system will route traffic
# for other peers, pass announces and serve path requests.
# This should only be done for systems that are suited to
# act as transport nodes, ie. if they are stationary and
# always-on. This directive is optional and can be removed
# for brevity.
enable_transport = False
# By default, the first program to launch the Reticulum
# Network Stack will create a shared instance, that other
# programs can communicate with. Only the shared instance
# opens all the configured interfaces directly, and other
# local programs communicate with the shared instance over
# a local socket. This is completely transparent to the
# user, and should generally be turned on. This directive
# is optional and can be removed for brevity.
share_instance = Yes
# If you want to run multiple *different* shared instances
# on the same system, you will need to specify different
# shared instance ports for each. The defaults are given
# below, and again, these options can be left out if you
# don't need them.
shared_instance_port = 37428
instance_control_port = 37429
# You can configure Reticulum to panic and forcibly close
# if an unrecoverable interface error occurs, such as the
# hardware device for an interface disappearing. This is
# an optional directive, and can be left out for brevity.
# This behaviour is disabled by default.
panic_on_interface_error = No
[logging]
# Valid log levels are 0 through 7:
# 0: Log only critical information
# 1: Log errors and lower log levels
# 2: Log warnings and lower log levels
# 3: Log notices and lower log levels
# 4: Log info and lower (this is the default)
# 5: Verbose logging
# 6: Debug logging
# 7: Extreme logging
loglevel = 4
# The interfaces section defines the physical and virtual
# interfaces Reticulum will use to communicate on. This
# section will contain examples for a variety of interface
# types. You can modify these or use them as a basis for
# your own config, or simply remove the unused ones.
[interfaces]
# This interface enables communication with other
# link-local Reticulum nodes over UDP. It does not
# need any functional IP infrastructure like routers
# or DHCP servers, but will require that at least link-
# local IPv6 is enabled in your operating system, which
# should be enabled by default in almost any OS. See
# the Reticulum Manual for more configuration options.
[[Default Interface]]
type = AutoInterface
interface_enabled = True
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.
Included Utility Programs
-------------------------
Reticulum includes a range of useful utilities, both for managing your Reticulum
networks, and for carrying out common tasks over Reticulum networks, such as
transferring files to remote systems, and executing commands and programs remotely.
If you often use Reticulum from several different programs, or simply want
Reticulum to stay available all the time, for example if you are hosting
a transport node, you might want to run Reticulum as a separate service that
other programs, applications and services can utilise.
The rnsd Utility
================
It is very easy to run Reticulum as a service. Simply run the included ``rnsd`` command.
When ``rnsd`` is running, it will keep all configured interfaces open, handle transport if
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
the same system.
.. code:: text
# Install Reticulum
pip3 install rns
# Run rnsd
rnsd
.. code:: text
usage: rnsd [-h] [--config CONFIG] [-v] [-q] [--version]
Reticulum Network Stack Daemon
optional arguments:
-h, --help show this help message and exit
--config CONFIG path to alternative Reticulum config directory
-v, --verbose
-q, --quiet
--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>`.
The rnstatus Utility
====================
Using the ``rnstatus`` utility, you can view the status of configured Reticulum
interfaces, similar to the ``ifconfig`` program.
.. code:: text
# Run rnstatus
rnstatus
# Example output
Shared Instance[37428]
Status : Up
Serving : 1 program
Rate : 1.00 Gbps
Traffic : 83.13 KB↑
86.10 KB↓
AutoInterface[Local]
Status : Up
Mode : Full
Rate : 10.00 Mbps
Peers : 1 reachable
Traffic : 63.23 KB↑
80.17 KB↓
TCPInterface[RNS Testnet Frankfurt/frankfurt.rns.unsigned.io:4965]
Status : Up
Mode : Full
Rate : 10.00 Mbps
Traffic : 187.27 KB↑
74.17 KB↓
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 <5245a8efe1788c6a70e1> running
.. code:: text
usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-v]
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
-v, --verbose
The rnpath Utility
====================
With the ``rnpath`` utility, you can look up and view paths for
destinations on the Reticulum network.
.. code:: text
# Run rnpath
rnpath eca6f4e4dc26ae329e61
# Example output
Path found, destination <eca6f4e4dc26ae329e61> is 4 hops away via <56b115c30cd386cad69c> on TCPInterface[Testnet/frankfurt.rns.unsigned.io:4965]
.. code:: text
usage: rnpath [-h] [--config CONFIG] [--version] [-t] [-r] [-d] [-D] [-w seconds] [-v] [destination]
Reticulum Path Discovery Utility
positional arguments:
destination hexadecimal hash of the destination
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
-t, --table show all known paths
-r, --rates show announce rate info
-d, --drop remove the path to a destination
-D, --drop-announces drop all queued announces
-w seconds timeout before giving up
-v, --verbose
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.
.. code:: text
# Run rnprobe
rnprobe example_utilities.echo.request 9382f334de63217a4278
# Example output
Sent 16 byte probe to <9382f334de63217a4278>
Valid reply received from <9382f334de63217a4278>
Round-trip time is 38.469 milliseconds over 2 hops
.. code:: text
usage: rnprobe [-h] [--config CONFIG] [--version] [-v] [full_name] [destination_hash]
Reticulum Probe Utility
positional arguments:
full_name full destination name in dotted notation
destination_hash hexadecimal hash of the destination
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
-v, --verbose
The rncp Utility
================
The ``rncp`` utility is a simple file transfer tool. Using it, you can transfer
files through Reticulum.
.. code:: text
# Run rncp on the receiving system, specifying which identities
# are allowed to send files
rncp --receive -a 940ea3f9e1037d38758f -a e28d5aee4317c24a9041
# From another system, copy a file to the receiving system
rncp ~/path/to/file.tgz 256320d405d6d525d1e9
You can specify as many allowed senders as needed, or complete disable authentication.
.. code:: text
usage: rncp [-h] [--config path] [-v] [-q] [-p] [-r] [-b] [-a allowed_hash] [-n] [-w seconds] [--version] [file] [destination]
Reticulum File Transfer Utility
positional arguments:
file file to be transferred
destination hexadecimal hash of the receiver
optional arguments:
-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
-a allowed_hash accept from this identity
-n, --no-auth accept files from anyone
-w seconds sender timeout before giving up
--version show program's version number and exit
-v, --verbose
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.
.. code:: text
# Run rnx on the listening system, specifying which identities
# are allowed to execute commands
rncp --listen -a 8111c4ff2968ab0c1286 -a 590256654482b4ba4038
# From another system, run a command
rnx ad9a4c9da60089d41c29 "cat /proc/cpuinfo"
# Or enter the interactive mode pseudo-shell
rnx ad9a4c9da60089d41c29 -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 ad9a4c9da60089d41c29 -i /path/to/identity
You can specify as many allowed senders as needed, or complete disable authentication.
.. 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]
Reticulum Remote Execution Utility
positional arguments:
destination hexadecimal hash of the listener
command command to be execute
optional arguments:
-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
-l, --listen listen for incoming commands
-i identity path to identity to use
-x, --interactive enter interactive mode
-b, --no-announce don't announce at program start
-a allowed_hash accept from this identity
-n, --noauth accept files from anyone
-N, --noid don't identify to listener
-d, --detailed show detailed result output
-m mirror exit code of remote command
-w seconds connect and request timeout before giving up
-W seconds max result download time
--stdin STDIN pass input to stdin
--stdout STDOUT max size in bytes of returned stdout
--stderr STDERR max size in bytes of returned stderr
--version show program's version number and exit
Improving System Configuration
------------------------------
If you are setting up a system for permanent use with Reticulum, there is a
few system configuration changes that can make this easier to administrate.
These changes will be detailed here.
Fixed Serial Port Names
=======================
On a Reticulum instance with several serial port based interfaces, it can be
beneficial to use the fixed device names for the serial ports, instead
of the dynamically allocated shorthands such as ``/dev/ttyUSB0``. Under most
Debian-based distributions, including Ubuntu and Raspberry Pi OS, these nodes
can be found under ``/dev/serial/by-id``.
You can use such a device path directly in place of the numbered shorthands.
Here is an example of a packet radio TNC configured as such:
.. code:: text
[[Packet Radio KISS Interface]]
type = KISSInterface
interface_enabled = True
outgoing = true
port = /dev/serial/by-id/usb-FTDI_FT230X_Basic_UART_43891CKM-if00-port0
speed = 115200
databits = 8
parity = none
stopbits = 1
preamble = 150
txtail = 10
persistence = 200
slottime = 20
Using this methodology avoids potential naming mix-ups where physical devices
might be plugged and unplugged in different orders, or when device name
assignment varies from one boot to another.
.. _using-systemd:
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.
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
into a directory that is in systemd's path:
.. code:: text
sudo ln -s $(which rnsd) /usr/local/bin/
You can then create the service file ``/etc/systemd/system/rnsd.service`` with the
following content:
.. code:: text
[Unit]
Description=Reticulum Network Stack Daemon
After=multi-user.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
User=USERNAMEHERE
ExecStart=rnsd --service
[Install]
WantedBy=multi-user.target
Be sure to replace ``USERNAMEHERE`` with the user you want to run ``rnsd`` as.
To manually start ``rnsd`` run:
.. code:: text
sudo systemctl start rnsd
If you want to automatically start ``rnsd`` at boot, run:
.. code:: text
sudo systemctl enable rnsd
+138
View File
@@ -0,0 +1,138 @@
******************
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 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 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 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.
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.
What does Reticulum Offer?
==========================
* Coordination-less globally unique adressing and identification
* Fully self-configuring multi-hop routing
* Complete initiator anonymity, communicate without revealing your identity
* 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
* Reticulum uses the `Fernet <https://github.com/fernet/spec/blob/master/Spec.md>`_ specification for on-the-wire / over-the-air encryption
* All 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
* IVs are generated through os.urandom()
* Unforgeable packet delivery confirmations
* A variety of supported interface types
* An intuitive and developer-friendly API
* Efficient link establishment
* Total bandwidth cost of setting up a link is only 3 packets, totalling 237 bytes
* Low cost of keeping links open at only 0.62 bits per second
* Reliable and efficient transfer of arbritrary amounts of data
* Reticulum can handle a few bytes of data or files of many gigabytes
* Sequencing, transfer coordination and checksumming is automatic
* The API is very easy to use, and provides transfer progress
* Authentication and virtual network segmentation on all supported interface types
* Flexible scalability allowing extremely low-bandwidth networks to co-exist and interoperate with large, high-bandwidth networks
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,
ad-hoc WiFi, free-space optical links and similar systems are all examples
of the types of interfaces Reticulum was designed for.
An open-source LoRa-based interface called `RNode <https://unsigned.io/rnode>`_
has been designed as an example transceiver that is very suitable for
Reticulum. It is possible to build it yourself, to transform a common LoRa
development board into one, or it can be purchased as a complete transceiver.
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
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.
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
added, Reticulum will take care of the rest, and any device on the WiFi
network can communicate with nodes on the LoRa and packet radio sides of the
network, and vice versa.
Interface Types and Devices
===========================
Reticulum implements a range of generalised interface types that covers the communications hardware that Reticulum can run over. If your hardware is not supported, it's relatively simple to implement an interface class. Currently, Reticulum can use the following devices and communication mediums:
* Any ethernet device
* WiFi devices
* Wired ethernet devices
* Fibre-optic transceivers
* Data radios with ethernet ports
* LoRa using `RNode <https://unsigned.io/rnode>`_
* Can be installed on `many popular LoRa boards <https://github.com/markqvist/rnodeconfigutil#supported-devices>`_
* Can be purchased as a `ready to use transceiver <https://unsigned.io/rnode>`_
* Packet Radio TNCs, such as `OpenModem <https://unsigned.io/openmodem>`_
* Any packet radio TNC in KISS mode
* Ideal for VHF and UHF radio
* Any device with a serial port
* The I2P network
* TCP over IP networks
* UDP over IP networks
* Anything you can connect via stdio
* Reticulum can use external programs and pipes as interfaces
* This can be used to easily hack in virtual interfaces
* Or to quickly create interfaces with custom hardware
For a full list and more details, see the :ref:`Supported Interfaces<interfaces-main>` chapter.
Caveat Emptor
==============
Reticulum is an experimental networking stack, and should be considered as such. While it has been built with cryptography best-practices very foremost in mind, it has not 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.
+904
View File
@@ -0,0 +1,904 @@
/*
* basic.css
* ~~~~~~~~~
*
* Sphinx stylesheet -- basic theme.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
/* -- main layout ----------------------------------------------------------- */
div.clearer {
clear: both;
}
div.section::after {
display: block;
content: '';
clear: left;
}
/* -- relbar ---------------------------------------------------------------- */
div.related {
width: 100%;
font-size: 90%;
}
div.related h3 {
display: none;
}
div.related ul {
margin: 0;
padding: 0 0 0 10px;
list-style: none;
}
div.related li {
display: inline;
}
div.related li.right {
float: right;
margin-right: 5px;
}
/* -- sidebar --------------------------------------------------------------- */
div.sphinxsidebarwrapper {
padding: 10px 5px 0 10px;
}
div.sphinxsidebar {
float: left;
width: 230px;
margin-left: -100%;
font-size: 90%;
word-wrap: break-word;
overflow-wrap : break-word;
}
div.sphinxsidebar ul {
list-style: none;
}
div.sphinxsidebar ul ul,
div.sphinxsidebar ul.want-points {
margin-left: 20px;
list-style: square;
}
div.sphinxsidebar ul ul {
margin-top: 0;
margin-bottom: 0;
}
div.sphinxsidebar form {
margin-top: 10px;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
div.sphinxsidebar #searchbox form.search {
overflow: hidden;
}
div.sphinxsidebar #searchbox input[type="text"] {
float: left;
width: 80%;
padding: 0.25em;
box-sizing: border-box;
}
div.sphinxsidebar #searchbox input[type="submit"] {
float: left;
width: 20%;
border-left: none;
padding: 0.25em;
box-sizing: border-box;
}
img {
border: 0;
max-width: 100%;
}
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
}
ul.search li a {
font-weight: bold;
}
ul.search li p.context {
color: #888;
margin: 2px 0 0 30px;
text-align: left;
}
ul.keywordmatches li.goodmatch a {
font-weight: bold;
}
/* -- index page ------------------------------------------------------------ */
table.contentstable {
width: 90%;
margin-left: auto;
margin-right: auto;
}
table.contentstable p.biglink {
line-height: 150%;
}
a.biglink {
font-size: 1.3em;
}
span.linkdescr {
font-style: italic;
padding-top: 5px;
font-size: 90%;
}
/* -- general index --------------------------------------------------------- */
table.indextable {
width: 100%;
}
table.indextable td {
text-align: left;
vertical-align: top;
}
table.indextable ul {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
}
table.indextable > tbody > tr > td > ul {
padding-left: 0em;
}
table.indextable tr.pcap {
height: 10px;
}
table.indextable tr.cap {
margin-top: 10px;
background-color: #f2f2f2;
}
img.toggler {
margin-right: 3px;
margin-top: 3px;
cursor: pointer;
}
div.modindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
div.genindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
/* -- domain module index --------------------------------------------------- */
table.modindextable td {
padding: 2px;
border-collapse: collapse;
}
/* -- general body styles --------------------------------------------------- */
div.body {
min-width: 450px;
max-width: 800px;
}
div.body p, div.body dd, div.body li, div.body blockquote {
-moz-hyphens: auto;
-ms-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
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,
h4:hover > a.headerlink,
h5:hover > a.headerlink,
h6:hover > a.headerlink,
dt:hover > a.headerlink,
caption:hover > a.headerlink,
p.caption:hover > a.headerlink,
div.code-block-caption:hover > a.headerlink {
visibility: visible;
}
div.body p.caption {
text-align: inherit;
}
div.body td {
text-align: left;
}
.first {
margin-top: 0 !important;
}
p.rubric {
margin-top: 30px;
font-weight: bold;
}
img.align-left, figure.align-left, .figure.align-left, object.align-left {
clear: left;
float: left;
margin-right: 1em;
}
img.align-right, figure.align-right, .figure.align-right, object.align-right {
clear: right;
float: right;
margin-left: 1em;
}
img.align-center, figure.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
img.align-default, figure.align-default, .figure.align-default {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.align-default {
text-align: center;
}
.align-right {
text-align: right;
}
/* -- sidebars -------------------------------------------------------------- */
div.sidebar,
aside.sidebar {
margin: 0 0 0.5em 1em;
border: 1px solid #ddb;
padding: 7px;
background-color: #ffe;
width: 40%;
float: right;
clear: right;
overflow-x: auto;
}
p.sidebar-title {
font-weight: bold;
}
div.admonition, div.topic, blockquote {
clear: left;
}
/* -- topics ---------------------------------------------------------------- */
div.topic {
border: 1px solid #ccc;
padding: 7px;
margin: 10px 0 10px 0;
}
p.topic-title {
font-size: 1.1em;
font-weight: bold;
margin-top: 10px;
}
/* -- admonitions ----------------------------------------------------------- */
div.admonition {
margin-top: 10px;
margin-bottom: 10px;
padding: 7px;
}
div.admonition dt {
font-weight: bold;
}
p.admonition-title {
margin: 0px 10px 5px 0px;
font-weight: bold;
}
div.body p.centered {
text-align: center;
margin-top: 25px;
}
/* -- content of sidebars/topics/admonitions -------------------------------- */
div.sidebar > :last-child,
aside.sidebar > :last-child,
div.topic > :last-child,
div.admonition > :last-child {
margin-bottom: 0;
}
div.sidebar::after,
aside.sidebar::after,
div.topic::after,
div.admonition::after,
blockquote::after {
display: block;
content: '';
clear: both;
}
/* -- tables ---------------------------------------------------------------- */
table.docutils {
margin-top: 10px;
margin-bottom: 10px;
border: 0;
border-collapse: collapse;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
table.align-default {
margin-left: auto;
margin-right: auto;
}
table caption span.caption-number {
font-style: italic;
}
table caption span.caption-text {
}
table.docutils td, table.docutils th {
padding: 1px 8px 1px 5px;
border-top: 0;
border-left: 0;
border-right: 0;
border-bottom: 1px solid #aaa;
}
table.footnote td, table.footnote th {
border: 0 !important;
}
th {
text-align: left;
padding-right: 5px;
}
table.citation {
border-left: solid 1px gray;
margin-left: 1px;
}
table.citation td {
border-bottom: none;
}
th > :first-child,
td > :first-child {
margin-top: 0px;
}
th > :last-child,
td > :last-child {
margin-bottom: 0px;
}
/* -- figures --------------------------------------------------------------- */
div.figure, figure {
margin: 0.5em;
padding: 0.5em;
}
div.figure p.caption, figcaption {
padding: 0.3em;
}
div.figure p.caption span.caption-number,
figcaption span.caption-number {
font-style: italic;
}
div.figure p.caption span.caption-text,
figcaption span.caption-text {
}
/* -- field list styles ----------------------------------------------------- */
table.field-list td, table.field-list th {
border: 0 !important;
}
.field-list ul {
margin: 0;
padding-left: 1em;
}
.field-list p {
margin: 0;
}
.field-name {
-moz-hyphens: manual;
-ms-hyphens: manual;
-webkit-hyphens: manual;
hyphens: manual;
}
/* -- hlist styles ---------------------------------------------------------- */
table.hlist {
margin: 1em 0;
}
table.hlist td {
vertical-align: top;
}
/* -- object description styles --------------------------------------------- */
.sig {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
}
.sig-name, code.descname {
background-color: transparent;
font-weight: bold;
}
.sig-name {
font-size: 1.1em;
}
code.descname {
font-size: 1.2em;
}
.sig-prename, code.descclassname {
background-color: transparent;
}
.optional {
font-size: 1.3em;
}
.sig-paren {
font-size: larger;
}
.sig-param.n {
font-style: italic;
}
/* C++ specific styling */
.sig-inline.c-texpr,
.sig-inline.cpp-texpr {
font-family: unset;
}
.sig.c .k, .sig.c .kt,
.sig.cpp .k, .sig.cpp .kt {
color: #0033B3;
}
.sig.c .m,
.sig.cpp .m {
color: #1750EB;
}
.sig.c .s, .sig.c .sc,
.sig.cpp .s, .sig.cpp .sc {
color: #067D17;
}
/* -- other body styles ----------------------------------------------------- */
ol.arabic {
list-style: decimal;
}
ol.loweralpha {
list-style: lower-alpha;
}
ol.upperalpha {
list-style: upper-alpha;
}
ol.lowerroman {
list-style: lower-roman;
}
ol.upperroman {
list-style: upper-roman;
}
:not(li) > ol > li:first-child > :first-child,
:not(li) > ul > li:first-child > :first-child {
margin-top: 0px;
}
:not(li) > ol > li:last-child > :last-child,
:not(li) > ul > li:last-child > :last-child {
margin-bottom: 0px;
}
ol.simple ol p,
ol.simple ul p,
ul.simple ol p,
ul.simple ul p {
margin-top: 0;
}
ol.simple > li:not(:first-child) > p,
ul.simple > li:not(:first-child) > p {
margin-top: 0;
}
ol.simple p,
ul.simple p {
margin-bottom: 0;
}
dl.footnote > dt,
dl.citation > dt {
float: left;
margin-right: 0.5em;
}
dl.footnote > dd,
dl.citation > dd {
margin-bottom: 0em;
}
dl.footnote > dd:after,
dl.citation > dd:after {
content: "";
clear: both;
}
dl.field-list {
display: grid;
grid-template-columns: fit-content(30%) auto;
}
dl.field-list > dt {
font-weight: bold;
word-break: break-word;
padding-left: 0.5em;
padding-right: 5px;
}
dl.field-list > dt:after {
content: ":";
}
dl.field-list > dd {
padding-left: 0.5em;
margin-top: 0em;
margin-left: 0em;
margin-bottom: 0em;
}
dl {
margin-bottom: 15px;
}
dd > :first-child {
margin-top: 0px;
}
dd ul, dd table {
margin-bottom: 10px;
}
dd {
margin-top: 3px;
margin-bottom: 10px;
margin-left: 30px;
}
dl > dd:last-child,
dl > dd:last-child > :last-child {
margin-bottom: 0;
}
dt:target, span.highlighted {
background-color: #fbe54e;
}
rect.highlighted {
fill: #fbe54e;
}
dl.glossary dt {
font-weight: bold;
font-size: 1.1em;
}
.versionmodified {
font-style: italic;
}
.system-message {
background-color: #fda;
padding: 5px;
border: 3px solid red;
}
.footnote:target {
background-color: #ffa;
}
.line-block {
display: block;
margin-top: 1em;
margin-bottom: 1em;
}
.line-block .line-block {
margin-top: 0;
margin-bottom: 0;
margin-left: 1.5em;
}
.guilabel, .menuselection {
font-family: sans-serif;
}
.accelerator {
text-decoration: underline;
}
.classifier {
font-style: oblique;
}
.classifier:before {
font-style: normal;
margin: 0.5em;
content: ":";
}
abbr, acronym {
border-bottom: dotted 1px;
cursor: help;
}
/* -- code displays --------------------------------------------------------- */
pre {
overflow: auto;
overflow-y: hidden; /* fixes display issues on Chrome browsers */
}
pre, div[class*="highlight-"] {
clear: both;
}
span.pre {
-moz-hyphens: none;
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none;
}
div[class*="highlight-"] {
margin: 1em 0;
}
td.linenos pre {
border: 0;
background-color: transparent;
color: #aaa;
}
table.highlighttable {
display: block;
}
table.highlighttable tbody {
display: block;
}
table.highlighttable tr {
display: flex;
}
table.highlighttable td {
margin: 0;
padding: 0;
}
table.highlighttable td.linenos {
padding-right: 0.5em;
}
table.highlighttable td.code {
flex: 1;
overflow: hidden;
}
.highlight .hll {
display: block;
}
div.highlight pre,
table.highlighttable pre {
margin: 0;
}
div.code-block-caption + div {
margin-top: 0;
}
div.code-block-caption {
margin-top: 1em;
padding: 2px 5px;
font-size: small;
}
div.code-block-caption code {
background-color: transparent;
}
table.highlighttable td.linenos,
span.linenos,
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
-webkit-user-select: text; /* Safari fallback only */
-webkit-user-select: none; /* Chrome/Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+ */
}
div.code-block-caption span.caption-number {
padding: 0.1em 0.3em;
font-style: italic;
}
div.code-block-caption span.caption-text {
}
div.literal-block-wrapper {
margin: 1em 0;
}
code.xref, a code {
background-color: transparent;
font-weight: bold;
}
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
background-color: transparent;
}
.viewcode-link {
float: right;
}
.viewcode-back {
float: right;
font-family: sans-serif;
}
div.viewcode-block:target {
margin: -1px -10px;
padding: 0 10px;
}
/* -- math display ---------------------------------------------------------- */
img.math {
vertical-align: middle;
}
div.body div.math p {
text-align: center;
}
span.eqno {
float: right;
}
span.eqno a.headerlink {
position: absolute;
z-index: 1;
}
div.math:hover a.headerlink {
visibility: visible;
}
/* -- printout stylesheet --------------------------------------------------- */
@media print {
div.document,
div.documentwrapper,
div.bodywrapper {
margin: 0 !important;
width: 100%;
}
div.sphinxsidebar,
div.related,
div.footer,
#top-link {
display: none;
}
}
+266
View File
@@ -0,0 +1,266 @@
/*
* 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;
}
+321
View File
@@ -0,0 +1,321 @@
/*
* doctools.js
* ~~~~~~~~~~~
*
* Sphinx JavaScript utilities for all documentation.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
/**
* select a different prefix for underscore
*/
$u = _.noConflict();
/**
* 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
}
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();
}
},
/**
* i18n support
*/
TRANSLATIONS : {},
PLURAL_EXPR : function(n) { return 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];
},
ngettext : function(singular, plural, n) {
var translated = Documentation.TRANSLATIONS[singular];
if (typeof translated === 'undefined')
return (n == 1) ? singular : plural;
return translated[Documentation.PLURALEXPR(n)];
},
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;
},
/**
* add context elements like header anchor links
*/
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);
});
},
/**
* workaround a firefox stupidity
* see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
*/
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');
}
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'));
}
},
/**
* 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();
}
},
/**
* helper function to hide the search marks again
*/
hideSearchWords : function() {
$('#searchbox .highlight-link').fadeOut(300);
$('span.highlighted').removeClass('highlighted');
},
/**
* make the url absolute
*/
makeURL : function(relativeURL) {
return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
},
/**
* 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;
}
case 39: // right
var nextHref = $('link[rel="next"]').prop('href');
if (nextHref) {
window.location.href = nextHref;
return false;
}
}
}
});
}
};
// quick alias for translations
_ = Documentation.gettext;
$(document).ready(function() {
Documentation.init();
});
@@ -0,0 +1,12 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '0.3.7 beta',
LANGUAGE: 'None',
COLLAPSE_INDEX: false,
BUILDER: 'html',
FILE_SUFFIX: '.html',
LINK_SUFFIX: '.html',
HAS_SOURCE: true,
SOURCELINK_SUFFIX: '.txt',
NAVIGATION_WITH_KEYS: false
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

+10872
View File
File diff suppressed because it is too large Load Diff
+2
View File
File diff suppressed because one or more lines are too long
+297
View File
@@ -0,0 +1,297 @@
/*
* language_data.js
* ~~~~~~~~~~~~~~~~
*
* This script contains the language-specific data used by searchtools.js,
* namely the list of stopwords, stemmer, scorer and splitter.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"];
/* Non-minified version is copied as a separate JS file, is available */
/**
* Porter Stemmer
*/
var Stemmer = function() {
var step2list = {
ational: 'ate',
tional: 'tion',
enci: 'ence',
anci: 'ance',
izer: 'ize',
bli: 'ble',
alli: 'al',
entli: 'ent',
eli: 'e',
ousli: 'ous',
ization: 'ize',
ation: 'ate',
ator: 'ate',
alism: 'al',
iveness: 'ive',
fulness: 'ful',
ousness: 'ous',
aliti: 'al',
iviti: 'ive',
biliti: 'ble',
logi: 'log'
};
var step3list = {
icate: 'ic',
ative: '',
alize: 'al',
iciti: 'ic',
ical: 'ic',
ful: '',
ness: ''
};
var c = "[^aeiou]"; // consonant
var v = "[aeiouy]"; // vowel
var C = c + "[^aeiouy]*"; // consonant sequence
var V = v + "[aeiou]*"; // vowel sequence
var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0
var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1
var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1
var s_v = "^(" + C + ")?" + v; // vowel in stem
this.stemWord = function (w) {
var stem;
var suffix;
var firstch;
var origword = w;
if (w.length < 3)
return w;
var re;
var re2;
var re3;
var re4;
firstch = w.substr(0,1);
if (firstch == "y")
w = firstch.toUpperCase() + w.substr(1);
// Step 1a
re = /^(.+?)(ss|i)es$/;
re2 = /^(.+?)([^s])s$/;
if (re.test(w))
w = w.replace(re,"$1$2");
else if (re2.test(w))
w = w.replace(re2,"$1$2");
// Step 1b
re = /^(.+?)eed$/;
re2 = /^(.+?)(ed|ing)$/;
if (re.test(w)) {
var fp = re.exec(w);
re = new RegExp(mgr0);
if (re.test(fp[1])) {
re = /.$/;
w = w.replace(re,"");
}
}
else if (re2.test(w)) {
var fp = re2.exec(w);
stem = fp[1];
re2 = new RegExp(s_v);
if (re2.test(stem)) {
w = stem;
re2 = /(at|bl|iz)$/;
re3 = new RegExp("([^aeiouylsz])\\1$");
re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
if (re2.test(w))
w = w + "e";
else if (re3.test(w)) {
re = /.$/;
w = w.replace(re,"");
}
else if (re4.test(w))
w = w + "e";
}
}
// Step 1c
re = /^(.+?)y$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
re = new RegExp(s_v);
if (re.test(stem))
w = stem + "i";
}
// Step 2
re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
suffix = fp[2];
re = new RegExp(mgr0);
if (re.test(stem))
w = stem + step2list[suffix];
}
// Step 3
re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
suffix = fp[2];
re = new RegExp(mgr0);
if (re.test(stem))
w = stem + step3list[suffix];
}
// Step 4
re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
re2 = /^(.+?)(s|t)(ion)$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
re = new RegExp(mgr1);
if (re.test(stem))
w = stem;
}
else if (re2.test(w)) {
var fp = re2.exec(w);
stem = fp[1] + fp[2];
re2 = new RegExp(mgr1);
if (re2.test(stem))
w = stem;
}
// Step 5
re = /^(.+?)e$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
re = new RegExp(mgr1);
re2 = new RegExp(meq1);
re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))
w = stem;
}
re = /ll$/;
re2 = new RegExp(mgr1);
if (re.test(w) && re2.test(w)) {
re = /.$/;
w = w.replace(re,"");
}
// and turn initial Y back to y
if (firstch == "y")
w = firstch.toLowerCase() + w.substr(1);
return w;
}
}
var splitChars = (function() {
var result = {};
var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648,
1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702,
2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971,
2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345,
3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761,
3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823,
4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125,
8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695,
11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587,
43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141];
var i, j, start, end;
for (i = 0; i < singles.length; i++) {
result[singles[i]] = true;
}
var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709],
[722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161],
[1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568],
[1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807],
[1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047],
[2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383],
[2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450],
[2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547],
[2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673],
[2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820],
[2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946],
[2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023],
[3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173],
[3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332],
[3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481],
[3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718],
[3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791],
[3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095],
[4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205],
[4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687],
[4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968],
[4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869],
[5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102],
[6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271],
[6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592],
[6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822],
[6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167],
[7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959],
[7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143],
[8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318],
[8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483],
[8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101],
[10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567],
[11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292],
[12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444],
[12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783],
[12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311],
[19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511],
[42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774],
[42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071],
[43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263],
[43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519],
[43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647],
[43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967],
[44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295],
[57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274],
[64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007],
[65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381],
[65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]];
for (i = 0; i < ranges.length; i++) {
start = ranges[i][0];
end = ranges[i][1];
for (j = start; j <= end; j++) {
result[j] = true;
}
}
return result;
})();
function splitQuery(query) {
var result = [];
var start = -1;
for (var i = 0; i < query.length; i++) {
if (splitChars[query.charCodeAt(i)]) {
if (start !== -1) {
result.push(query.slice(start, i));
start = -1;
}
} else if (start === -1) {
start = i;
}
}
if (start !== -1) {
result.push(query.slice(start));
}
return result;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

+74
View File
@@ -0,0 +1,74 @@
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight { background: #eeffcc; }
.highlight .c { color: #408090; font-style: italic } /* Comment */
.highlight .err { border: 1px solid #FF0000 } /* Error */
.highlight .k { color: #007020; font-weight: bold } /* Keyword */
.highlight .o { color: #666666 } /* Operator */
.highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #007020 } /* Comment.Preproc */
.highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */
.highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */
.highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #A00000 } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #FF0000 } /* Generic.Error */
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.highlight .gi { color: #00A000 } /* Generic.Inserted */
.highlight .go { color: #333333 } /* Generic.Output */
.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.highlight .gt { color: #0044DD } /* Generic.Traceback */
.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #007020 } /* Keyword.Pseudo */
.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #902000 } /* Keyword.Type */
.highlight .m { color: #208050 } /* Literal.Number */
.highlight .s { color: #4070a0 } /* Literal.String */
.highlight .na { color: #4070a0 } /* Name.Attribute */
.highlight .nb { color: #007020 } /* Name.Builtin */
.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */
.highlight .no { color: #60add5 } /* Name.Constant */
.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */
.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */
.highlight .ne { color: #007020 } /* Name.Exception */
.highlight .nf { color: #06287e } /* Name.Function */
.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */
.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */
.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #bb60d5 } /* Name.Variable */
.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #208050 } /* Literal.Number.Bin */
.highlight .mf { color: #208050 } /* Literal.Number.Float */
.highlight .mh { color: #208050 } /* Literal.Number.Hex */
.highlight .mi { color: #208050 } /* Literal.Number.Integer */
.highlight .mo { color: #208050 } /* Literal.Number.Oct */
.highlight .sa { color: #4070a0 } /* Literal.String.Affix */
.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */
.highlight .sc { color: #4070a0 } /* Literal.String.Char */
.highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */
.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */
.highlight .s2 { color: #4070a0 } /* Literal.String.Double */
.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */
.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */
.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */
.highlight .sx { color: #c65d09 } /* Literal.String.Other */
.highlight .sr { color: #235388 } /* Literal.String.Regex */
.highlight .s1 { color: #4070a0 } /* Literal.String.Single */
.highlight .ss { color: #517918 } /* Literal.String.Symbol */
.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #06287e } /* Name.Function.Magic */
.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */
.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */
.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */
.highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */
.highlight .il { color: #208050 } /* Literal.Number.Integer.Long */
+522
View File
@@ -0,0 +1,522 @@
/*
* searchtools.js
* ~~~~~~~~~~~~~~~~
*
* Sphinx JavaScript utilities for the full-text search.
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
if (!Scorer) {
/**
* Simple result scoring code.
*/
var Scorer = {
// Implement the following function to further tweak the score for each result
// The function takes a result array [filename, title, anchor, descr, score]
// and returns the new score.
/*
score: function(result) {
return result[4];
},
*/
// query matches the full name of an object
objNameMatch: 11,
// or matches in the last dotted part of the object name
objPartialMatch: 6,
// Additive scores depending on the priority of the object
objPrio: {0: 15, // used to be importantResults
1: 5, // used to be objectResults
2: -5}, // used to be unimportantResults
// Used when the priority is not in the mapping.
objPrioDefault: 0,
// query found in title
title: 15,
partialTitle: 7,
// query found in terms
term: 5,
partialTerm: 2
};
}
if (!splitQuery) {
function splitQuery(query) {
return query.split(/\s+/);
}
}
/**
* Search Module
*/
var Search = {
_index : null,
_queued_query : null,
_pulse_status : -1,
htmlToText : function(htmlString) {
var virtualDocument = document.implementation.createHTMLDocument('virtual');
var htmlElement = $(htmlString, virtualDocument);
htmlElement.find('.headerlink').remove();
docContent = htmlElement.find('[role=main]')[0];
if(docContent === undefined) {
console.warn("Content block not found. Sphinx search tries to obtain it " +
"via '[role=main]'. Could you check your theme or template.");
return "";
}
return docContent.textContent || docContent.innerText;
},
init : function() {
var params = $.getQueryParameters();
if (params.q) {
var query = params.q[0];
$('input[name="q"]')[0].value = query;
this.performSearch(query);
}
},
loadIndex : function(url) {
$.ajax({type: "GET", url: url, data: null,
dataType: "script", cache: true,
complete: function(jqxhr, textstatus) {
if (textstatus != "success") {
document.getElementById("searchindexloader").src = url;
}
}});
},
setIndex : function(index) {
var q;
this._index = index;
if ((q = this._queued_query) !== null) {
this._queued_query = null;
Search.query(q);
}
},
hasIndex : function() {
return this._index !== null;
},
deferQuery : function(query) {
this._queued_query = query;
},
stopPulse : function() {
this._pulse_status = 0;
},
startPulse : function() {
if (this._pulse_status >= 0)
return;
function pulse() {
var i;
Search._pulse_status = (Search._pulse_status + 1) % 4;
var dotString = '';
for (i = 0; i < Search._pulse_status; i++)
dotString += '.';
Search.dots.text(dotString);
if (Search._pulse_status > -1)
window.setTimeout(pulse, 500);
}
pulse();
},
/**
* perform a search for something (or wait until index is loaded)
*/
performSearch : function(query) {
// create the required interface elements
this.out = $('#search-results');
this.title = $('<h2>' + _('Searching') + '</h2>').appendTo(this.out);
this.dots = $('<span></span>').appendTo(this.title);
this.status = $('<p class="search-summary">&nbsp;</p>').appendTo(this.out);
this.output = $('<ul class="search"/>').appendTo(this.out);
$('#search-progress').text(_('Preparing search...'));
this.startPulse();
// index already loaded, the browser was quick!
if (this.hasIndex())
this.query(query);
else
this.deferQuery(query);
},
/**
* execute search (requires search index to be loaded)
*/
query : function(query) {
var i;
// stem the searchterms and add them to the correct list
var stemmer = new Stemmer();
var searchterms = [];
var excluded = [];
var hlterms = [];
var tmp = splitQuery(query);
var objectterms = [];
for (i = 0; i < tmp.length; i++) {
if (tmp[i] !== "") {
objectterms.push(tmp[i].toLowerCase());
}
if ($u.indexOf(stopwords, tmp[i].toLowerCase()) != -1 || tmp[i] === "") {
// skip this "word"
continue;
}
// stem the word
var word = stemmer.stemWord(tmp[i].toLowerCase());
// prevent stemmer from cutting word smaller than two chars
if(word.length < 3 && tmp[i].length >= 3) {
word = tmp[i];
}
var toAppend;
// select the correct list
if (word[0] == '-') {
toAppend = excluded;
word = word.substr(1);
}
else {
toAppend = searchterms;
hlterms.push(tmp[i].toLowerCase());
}
// only add if not already in the list
if (!$u.contains(toAppend, word))
toAppend.push(word);
}
var highlightstring = '?highlight=' + $.urlencode(hlterms.join(" "));
// console.debug('SEARCH: searching for:');
// console.info('required: ', searchterms);
// console.info('excluded: ', excluded);
// prepare search
var terms = this._index.terms;
var titleterms = this._index.titleterms;
// array of [filename, title, anchor, descr, score]
var results = [];
$('#search-progress').empty();
// lookup as object
for (i = 0; i < objectterms.length; i++) {
var others = [].concat(objectterms.slice(0, i),
objectterms.slice(i+1, objectterms.length));
results = results.concat(this.performObjectSearch(objectterms[i], others));
}
// lookup as search terms in fulltext
results = results.concat(this.performTermsSearch(searchterms, excluded, terms, titleterms));
// let the scorer override scores with a custom scoring function
if (Scorer.score) {
for (i = 0; i < results.length; i++)
results[i][4] = Scorer.score(results[i]);
}
// now sort the results by score (in opposite order of appearance, since the
// display function below uses pop() to retrieve items) and then
// alphabetically
results.sort(function(a, b) {
var left = a[4];
var right = b[4];
if (left > right) {
return 1;
} else if (left < right) {
return -1;
} else {
// same score: sort alphabetically
left = a[1].toLowerCase();
right = b[1].toLowerCase();
return (left > right) ? -1 : ((left < right) ? 1 : 0);
}
});
// for debugging
//Search.lastresults = results.slice(); // a copy
//console.info('search results:', Search.lastresults);
// print the results
var resultCount = results.length;
function displayNextItem() {
// results left, load the summary and display it
if (results.length) {
var item = results.pop();
var listItem = $('<li></li>');
var requestUrl = "";
var linkUrl = "";
if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') {
// dirhtml builder
var dirname = item[0] + '/';
if (dirname.match(/\/index\/$/)) {
dirname = dirname.substring(0, dirname.length-6);
} else if (dirname == 'index/') {
dirname = '';
}
requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + dirname;
linkUrl = requestUrl;
} else {
// normal html builders
requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX;
linkUrl = item[0] + DOCUMENTATION_OPTIONS.LINK_SUFFIX;
}
listItem.append($('<a/>').attr('href',
linkUrl +
highlightstring + item[2]).html(item[1]));
if (item[3]) {
listItem.append($('<span> (' + item[3] + ')</span>'));
Search.output.append(listItem);
setTimeout(function() {
displayNextItem();
}, 5);
} else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) {
$.ajax({url: requestUrl,
dataType: "text",
complete: function(jqxhr, textstatus) {
var data = jqxhr.responseText;
if (data !== '' && data !== undefined) {
listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
}
Search.output.append(listItem);
setTimeout(function() {
displayNextItem();
}, 5);
}});
} else {
// no source available, just display title
Search.output.append(listItem);
setTimeout(function() {
displayNextItem();
}, 5);
}
}
// search finished, update title and status message
else {
Search.stopPulse();
Search.title.text(_('Search Results'));
if (!resultCount)
Search.status.text(_('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.'));
else
Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
Search.status.fadeIn(500);
}
}
displayNextItem();
},
/**
* search for object names
*/
performObjectSearch : function(object, otherterms) {
var filenames = this._index.filenames;
var docnames = this._index.docnames;
var objects = this._index.objects;
var objnames = this._index.objnames;
var titles = this._index.titles;
var i;
var results = [];
for (var prefix in objects) {
for (var name in objects[prefix]) {
var fullname = (prefix ? prefix + '.' : '') + name;
var fullnameLower = fullname.toLowerCase()
if (fullnameLower.indexOf(object) > -1) {
var score = 0;
var parts = fullnameLower.split('.');
// check for different match types: exact matches of full name or
// "last name" (i.e. last dotted part)
if (fullnameLower == object || parts[parts.length - 1] == object) {
score += Scorer.objNameMatch;
// matches in last name
} else if (parts[parts.length - 1].indexOf(object) > -1) {
score += Scorer.objPartialMatch;
}
var match = objects[prefix][name];
var objname = objnames[match[1]][2];
var title = titles[match[0]];
// If more than one term searched for, we require other words to be
// found in the name/title/description
if (otherterms.length > 0) {
var haystack = (prefix + ' ' + name + ' ' +
objname + ' ' + title).toLowerCase();
var allfound = true;
for (i = 0; i < otherterms.length; i++) {
if (haystack.indexOf(otherterms[i]) == -1) {
allfound = false;
break;
}
}
if (!allfound) {
continue;
}
}
var descr = objname + _(', in ') + title;
var anchor = match[3];
if (anchor === '')
anchor = fullname;
else if (anchor == '-')
anchor = objnames[match[1]][1] + '-' + fullname;
// add custom score for some objects according to scorer
if (Scorer.objPrio.hasOwnProperty(match[2])) {
score += Scorer.objPrio[match[2]];
} else {
score += Scorer.objPrioDefault;
}
results.push([docnames[match[0]], fullname, '#'+anchor, descr, score, filenames[match[0]]]);
}
}
}
return results;
},
/**
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
*/
escapeRegExp : function(string) {
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
},
/**
* search for full-text terms in the index
*/
performTermsSearch : function(searchterms, excluded, terms, titleterms) {
var docnames = this._index.docnames;
var filenames = this._index.filenames;
var titles = this._index.titles;
var i, j, file;
var fileMap = {};
var scoreMap = {};
var results = [];
// perform the search on the required terms
for (i = 0; i < searchterms.length; i++) {
var word = searchterms[i];
var files = [];
var _o = [
{files: terms[word], score: Scorer.term},
{files: titleterms[word], score: Scorer.title}
];
// add support for partial matches
if (word.length > 2) {
var word_regex = this.escapeRegExp(word);
for (var w in terms) {
if (w.match(word_regex) && !terms[word]) {
_o.push({files: terms[w], score: Scorer.partialTerm})
}
}
for (var w in titleterms) {
if (w.match(word_regex) && !titleterms[word]) {
_o.push({files: titleterms[w], score: Scorer.partialTitle})
}
}
}
// no match but word was a required one
if ($u.every(_o, function(o){return o.files === undefined;})) {
break;
}
// found search word in contents
$u.each(_o, function(o) {
var _files = o.files;
if (_files === undefined)
return
if (_files.length === undefined)
_files = [_files];
files = files.concat(_files);
// set score for the word in each file to Scorer.term
for (j = 0; j < _files.length; j++) {
file = _files[j];
if (!(file in scoreMap))
scoreMap[file] = {};
scoreMap[file][word] = o.score;
}
});
// create the mapping
for (j = 0; j < files.length; j++) {
file = files[j];
if (file in fileMap && fileMap[file].indexOf(word) === -1)
fileMap[file].push(word);
else
fileMap[file] = [word];
}
}
// now check if the files don't contain excluded terms
for (file in fileMap) {
var valid = true;
// check if all requirements are matched
var filteredTermCount = // as search terms with length < 3 are discarded: ignore
searchterms.filter(function(term){return term.length > 2}).length
if (
fileMap[file].length != searchterms.length &&
fileMap[file].length != filteredTermCount
) continue;
// ensure that none of the excluded terms is in the search result
for (i = 0; i < excluded.length; i++) {
if (terms[excluded[i]] == file ||
titleterms[excluded[i]] == file ||
$u.contains(terms[excluded[i]] || [], file) ||
$u.contains(titleterms[excluded[i]] || [], file)) {
valid = false;
break;
}
}
// if we have still a valid result we can add it to the result list
if (valid) {
// select one (max) score for the file.
// for better ranking, we should calculate ranking by using words statistics like basic tf-idf...
var score = $u.max($u.map(fileMap[file], function(w){return scoreMap[file][w]}));
results.push([docnames[file], titles[file], '', null, score, filenames[file]]);
}
}
return results;
},
/**
* helper function to return a node containing the
* search summary for a given text. keywords is a list
* of stemmed words, hlwords is the list of normal, unstemmed
* words. the first one is used to find the occurrence, the
* latter for highlighting it.
*/
makeSearchSummary : function(htmlText, keywords, hlwords) {
var text = Search.htmlToText(htmlText);
var textLower = text.toLowerCase();
var start = 0;
$.each(keywords, function() {
var i = textLower.indexOf(this.toLowerCase());
if (i > -1)
start = i;
});
start = Math.max(start - 120, 0);
var excerpt = ((start > 0) ? '...' : '') +
$.trim(text.substr(start, 240)) +
((start + 240 - text.length) ? '...' : '');
var rv = $('<p class="context"></p>').text(excerpt);
$.each(hlwords, function() {
rv = rv.highlightText(this, 'highlighted');
});
return rv;
}
};
$(document).ready(function() {
Search.init();
});
+159
View File
@@ -0,0 +1,159 @@
/*
* sidebar.js
* ~~~~~~~~~~
*
* This script makes the Sphinx sidebar collapsible.
*
* .sphinxsidebar contains .sphinxsidebarwrapper. This script adds
* in .sphixsidebar, after .sphinxsidebarwrapper, the #sidebarbutton
* used to collapse and expand the sidebar.
*
* When the sidebar is collapsed the .sphinxsidebarwrapper is hidden
* and the width of the sidebar and the margin-left of the document
* are decreased. When the sidebar is expanded the opposite happens.
* This script saves a per-browser/per-session cookie used to
* remember the position of the sidebar among the pages.
* Once the browser is closed the cookie is deleted and the position
* reset to the default (expanded).
*
* :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
$(function() {
// global elements used by the functions.
// the 'sidebarbutton' element is defined as global after its
// creation, in the add_sidebar_button function
var bodywrapper = $('.bodywrapper');
var sidebar = $('.sphinxsidebar');
var sidebarwrapper = $('.sphinxsidebarwrapper');
// for some reason, the document has no sidebar; do not run into errors
if (!sidebar.length) return;
// original margin-left of the bodywrapper and width of the sidebar
// with the sidebar expanded
var bw_margin_expanded = bodywrapper.css('margin-left');
var ssb_width_expanded = sidebar.width();
// margin-left of the bodywrapper and width of the sidebar
// with the sidebar collapsed
var bw_margin_collapsed = '.8em';
var ssb_width_collapsed = '.8em';
// colors used by the current theme
var dark_color = $('.related').css('background-color');
var light_color = $('.document').css('background-color');
function sidebar_is_collapsed() {
return sidebarwrapper.is(':not(:visible)');
}
function toggle_sidebar() {
if (sidebar_is_collapsed())
expand_sidebar();
else
collapse_sidebar();
}
function collapse_sidebar() {
sidebarwrapper.hide();
sidebar.css('width', ssb_width_collapsed);
bodywrapper.css('margin-left', bw_margin_collapsed);
sidebarbutton.css({
'margin-left': '0',
'height': bodywrapper.height()
});
sidebarbutton.find('span').text('»');
sidebarbutton.attr('title', _('Expand sidebar'));
document.cookie = 'sidebar=collapsed';
}
function expand_sidebar() {
bodywrapper.css('margin-left', bw_margin_expanded);
sidebar.css('width', ssb_width_expanded);
sidebarwrapper.show();
sidebarbutton.css({
'margin-left': ssb_width_expanded-12,
'height': bodywrapper.height()
});
sidebarbutton.find('span').text('«');
sidebarbutton.attr('title', _('Collapse sidebar'));
document.cookie = 'sidebar=expanded';
}
function add_sidebar_button() {
sidebarwrapper.css({
'float': 'left',
'margin-right': '0',
'width': ssb_width_expanded - 28
});
// create the button
sidebar.append(
'<div id="sidebarbutton"><span>&laquo;</span></div>'
);
var sidebarbutton = $('#sidebarbutton');
light_color = sidebarbutton.css('background-color');
// find the height of the viewport to center the '<<' in the page
var viewport_height;
if (window.innerHeight)
viewport_height = window.innerHeight;
else
viewport_height = $(window).height();
sidebarbutton.find('span').css({
'display': 'block',
'margin-top': (viewport_height - sidebar.position().top - 20) / 2
});
sidebarbutton.click(toggle_sidebar);
sidebarbutton.attr('title', _('Collapse sidebar'));
sidebarbutton.css({
'color': '#FFFFFF',
'border-left': '1px solid ' + dark_color,
'font-size': '1.2em',
'cursor': 'pointer',
'height': bodywrapper.height(),
'padding-top': '1px',
'margin-left': ssb_width_expanded - 12
});
sidebarbutton.hover(
function () {
$(this).css('background-color', dark_color);
},
function () {
$(this).css('background-color', light_color);
}
);
}
function set_position_from_cookie() {
if (!document.cookie)
return;
var items = document.cookie.split(';');
for(var k=0; k<items.length; k++) {
var key_val = items[k].split('=');
var key = key_val[0].replace(/ /, ""); // strip leading spaces
if (key == 'sidebar') {
var value = key_val[1];
if ((value == 'collapsed') && (!sidebar_is_collapsed()))
collapse_sidebar();
else if ((value == 'expanded') && (sidebar_is_collapsed()))
expand_sidebar();
}
}
}
add_sidebar_button();
var sidebarbutton = $('#sidebarbutton');
set_position_from_cookie();
});
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+450
View File
@@ -0,0 +1,450 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &#8212; Reticulum Network Stack 0.3.7 beta documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/classic.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="#" />
<link rel="search" title="Search" href="search.html" />
</head><body>
<div class="related" role="navigation" aria-label="related navigation">
<h3>Navigation</h3>
<ul>
<li class="right" style="margin-right: 10px">
<a href="#" title="General Index"
accesskey="I">index</a></li>
<li class="nav-item nav-item-0"><a href="index.html">Reticulum Network Stack 0.3.7 beta documentation</a> &#187;</li>
<li class="nav-item nav-item-this"><a href="">Index</a></li>
</ul>
</div>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<h1 id="index">Index</h1>
<div class="genindex-jumpbox">
<a href="#A"><strong>A</strong></a>
| <a href="#C"><strong>C</strong></a>
| <a href="#D"><strong>D</strong></a>
| <a href="#E"><strong>E</strong></a>
| <a href="#F"><strong>F</strong></a>
| <a href="#G"><strong>G</strong></a>
| <a href="#H"><strong>H</strong></a>
| <a href="#I"><strong>I</strong></a>
| <a href="#K"><strong>K</strong></a>
| <a href="#L"><strong>L</strong></a>
| <a href="#M"><strong>M</strong></a>
| <a href="#N"><strong>N</strong></a>
| <a href="#P"><strong>P</strong></a>
| <a href="#R"><strong>R</strong></a>
| <a href="#S"><strong>S</strong></a>
| <a href="#T"><strong>T</strong></a>
| <a href="#V"><strong>V</strong></a>
</div>
<h2 id="A">A</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Resource.advertise">advertise() (RNS.Resource method)</a>
</li>
<li><a href="reference.html#RNS.Destination.announce">announce() (RNS.Destination method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Reticulum.ANNOUNCE_CAP">ANNOUNCE_CAP (RNS.Reticulum attribute)</a>
</li>
<li><a href="reference.html#RNS.Destination.app_and_aspects_from_name">app_and_aspects_from_name() (RNS.Destination static method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="C">C</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Resource.cancel">cancel() (RNS.Resource method)</a>
</li>
<li><a href="reference.html#RNS.Destination.clear_default_app_data">clear_default_app_data() (RNS.Destination method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Destination.create_keys">create_keys() (RNS.Destination method)</a>
</li>
<li><a href="reference.html#RNS.Identity.CURVE">CURVE (RNS.Identity attribute)</a>
<ul>
<li><a href="reference.html#RNS.Link.CURVE">(RNS.Link attribute)</a>
</li>
</ul></li>
</ul></td>
</tr></table>
<h2 id="D">D</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Destination.decrypt">decrypt() (RNS.Destination method)</a>
<ul>
<li><a href="reference.html#RNS.Identity.decrypt">(RNS.Identity method)</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Transport.deregister_announce_handler">deregister_announce_handler() (RNS.Transport static method)</a>
</li>
<li><a href="reference.html#RNS.Destination.deregister_request_handler">deregister_request_handler() (RNS.Destination method)</a>
</li>
<li><a href="reference.html#RNS.Destination">Destination (class in RNS)</a>
</li>
</ul></td>
</tr></table>
<h2 id="E">E</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Destination.encrypt">encrypt() (RNS.Destination method)</a>
<ul>
<li><a href="reference.html#RNS.Identity.encrypt">(RNS.Identity method)</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Packet.ENCRYPTED_MDU">ENCRYPTED_MDU (RNS.Packet attribute)</a>
</li>
<li><a href="reference.html#RNS.Link.ESTABLISHMENT_TIMEOUT_PER_HOP">ESTABLISHMENT_TIMEOUT_PER_HOP (RNS.Link attribute)</a>
</li>
</ul></td>
</tr></table>
<h2 id="F">F</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Identity.from_bytes">from_bytes() (RNS.Identity static method)</a>
</li>
<li><a href="reference.html#RNS.Identity.from_file">from_file() (RNS.Identity static method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Identity.full_hash">full_hash() (RNS.Identity static method)</a>
</li>
<li><a href="reference.html#RNS.Destination.full_name">full_name() (RNS.Destination static method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="G">G</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Resource.get_data_size">get_data_size() (RNS.Resource method)</a>
</li>
<li><a href="reference.html#RNS.Resource.get_hash">get_hash() (RNS.Resource method)</a>
</li>
<li><a href="reference.html#RNS.Resource.get_parts">get_parts() (RNS.Resource method)</a>
</li>
<li><a href="reference.html#RNS.Destination.get_private_key">get_private_key() (RNS.Destination method)</a>
<ul>
<li><a href="reference.html#RNS.Identity.get_private_key">(RNS.Identity method)</a>
</li>
</ul></li>
<li><a href="reference.html#RNS.RequestReceipt.get_progress">get_progress() (RNS.RequestReceipt method)</a>
<ul>
<li><a href="reference.html#RNS.Resource.get_progress">(RNS.Resource method)</a>
</li>
</ul></li>
<li><a href="reference.html#RNS.Identity.get_public_key">get_public_key() (RNS.Identity method)</a>
</li>
<li><a href="reference.html#RNS.Identity.get_random_hash">get_random_hash() (RNS.Identity static method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Link.get_remote_identity">get_remote_identity() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.RequestReceipt.get_request_id">get_request_id() (RNS.RequestReceipt method)</a>
</li>
<li><a href="reference.html#RNS.RequestReceipt.get_response">get_response() (RNS.RequestReceipt method)</a>
</li>
<li><a href="reference.html#RNS.RequestReceipt.get_response_time">get_response_time() (RNS.RequestReceipt method)</a>
</li>
<li><a href="reference.html#RNS.PacketReceipt.get_rtt">get_rtt() (RNS.PacketReceipt method)</a>
</li>
<li><a href="reference.html#RNS.Resource.get_segments">get_segments() (RNS.Resource method)</a>
</li>
<li><a href="reference.html#RNS.PacketReceipt.get_status">get_status() (RNS.PacketReceipt method)</a>
<ul>
<li><a href="reference.html#RNS.RequestReceipt.get_status">(RNS.RequestReceipt method)</a>
</li>
</ul></li>
<li><a href="reference.html#RNS.Resource.get_transfer_size">get_transfer_size() (RNS.Resource method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="H">H</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Transport.has_path">has_path() (RNS.Transport static method)</a>
</li>
<li><a href="reference.html#RNS.Destination.hash">hash() (RNS.Destination static method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Destination.hash_from_name_and_identity">hash_from_name_and_identity() (RNS.Destination static method)</a>
</li>
<li><a href="reference.html#RNS.Transport.hops_to">hops_to() (RNS.Transport static method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="I">I</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Link.identify">identify() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.Identity">Identity (class in RNS)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Link.inactive_for">inactive_for() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.Resource.is_compressed">is_compressed() (RNS.Resource method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="K">K</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Link.KEEPALIVE">KEEPALIVE (RNS.Link attribute)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Link.KEEPALIVE_TIMEOUT_FACTOR">KEEPALIVE_TIMEOUT_FACTOR (RNS.Link attribute)</a>
</li>
<li><a href="reference.html#RNS.Identity.KEYSIZE">KEYSIZE (RNS.Identity attribute)</a>
</li>
</ul></td>
</tr></table>
<h2 id="L">L</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Link">Link (class in RNS)</a>
</li>
<li><a href="reference.html#RNS.Destination.load_private_key">load_private_key() (RNS.Destination method)</a>
<ul>
<li><a href="reference.html#RNS.Identity.load_private_key">(RNS.Identity method)</a>
</li>
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Identity.load_public_key">load_public_key() (RNS.Identity method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="M">M</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Reticulum.MTU">MTU (RNS.Reticulum attribute)</a>
</li>
</ul></td>
</tr></table>
<h2 id="N">N</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Transport.next_hop">next_hop() (RNS.Transport static method)</a>
</li>
<li><a href="reference.html#RNS.Transport.next_hop_interface">next_hop_interface() (RNS.Transport static method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Link.no_inbound_for">no_inbound_for() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.Link.no_outbound_for">no_outbound_for() (RNS.Link method)</a>
</li>
</ul></td>
</tr></table>
<h2 id="P">P</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Packet">Packet (class in RNS)</a>
</li>
<li><a href="reference.html#RNS.PacketReceipt">PacketReceipt (class in RNS)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Transport.PATHFINDER_M">PATHFINDER_M (RNS.Transport attribute)</a>
</li>
<li><a href="reference.html#RNS.Packet.PLAIN_MDU">PLAIN_MDU (RNS.Packet attribute)</a>
</li>
</ul></td>
</tr></table>
<h2 id="R">R</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Identity.recall">recall() (RNS.Identity static method)</a>
</li>
<li><a href="reference.html#RNS.Identity.recall_app_data">recall_app_data() (RNS.Identity static method)</a>
</li>
<li><a href="reference.html#RNS.Transport.register_announce_handler">register_announce_handler() (RNS.Transport static method)</a>
</li>
<li><a href="reference.html#RNS.Destination.register_request_handler">register_request_handler() (RNS.Destination method)</a>
</li>
<li><a href="reference.html#RNS.Link.request">request() (RNS.Link method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Transport.request_path">request_path() (RNS.Transport static method)</a>
</li>
<li><a href="reference.html#RNS.RequestReceipt">RequestReceipt (class in RNS)</a>
</li>
<li><a href="reference.html#RNS.Packet.resend">resend() (RNS.Packet method)</a>
</li>
<li><a href="reference.html#RNS.Resource">Resource (class in RNS)</a>
</li>
<li><a href="reference.html#RNS.Reticulum">Reticulum (class in RNS)</a>
</li>
</ul></td>
</tr></table>
<h2 id="S">S</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Packet.send">send() (RNS.Packet method)</a>
</li>
<li><a href="reference.html#RNS.Destination.set_default_app_data">set_default_app_data() (RNS.Destination method)</a>
</li>
<li><a href="reference.html#RNS.PacketReceipt.set_delivery_callback">set_delivery_callback() (RNS.PacketReceipt method)</a>
</li>
<li><a href="reference.html#RNS.Link.set_link_closed_callback">set_link_closed_callback() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.Destination.set_link_established_callback">set_link_established_callback() (RNS.Destination method)</a>
</li>
<li><a href="reference.html#RNS.Destination.set_packet_callback">set_packet_callback() (RNS.Destination method)</a>
<ul>
<li><a href="reference.html#RNS.Link.set_packet_callback">(RNS.Link method)</a>
</li>
</ul></li>
<li><a href="reference.html#RNS.Destination.set_proof_requested_callback">set_proof_requested_callback() (RNS.Destination method)</a>
</li>
<li><a href="reference.html#RNS.Destination.set_proof_strategy">set_proof_strategy() (RNS.Destination method)</a>
</li>
<li><a href="reference.html#RNS.Link.set_remote_identified_callback">set_remote_identified_callback() (RNS.Link method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Link.set_resource_callback">set_resource_callback() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.Link.set_resource_concluded_callback">set_resource_concluded_callback() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.Link.set_resource_started_callback">set_resource_started_callback() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.Link.set_resource_strategy">set_resource_strategy() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.PacketReceipt.set_timeout">set_timeout() (RNS.PacketReceipt method)</a>
</li>
<li><a href="reference.html#RNS.PacketReceipt.set_timeout_callback">set_timeout_callback() (RNS.PacketReceipt method)</a>
</li>
<li><a href="reference.html#RNS.Reticulum.should_use_implicit_proof">should_use_implicit_proof() (RNS.Reticulum static method)</a>
</li>
<li><a href="reference.html#RNS.Destination.sign">sign() (RNS.Destination method)</a>
<ul>
<li><a href="reference.html#RNS.Identity.sign">(RNS.Identity method)</a>
</li>
</ul></li>
<li><a href="reference.html#RNS.Link.STALE_GRACE">STALE_GRACE (RNS.Link attribute)</a>
</li>
<li><a href="reference.html#RNS.Link.STALE_TIME">STALE_TIME (RNS.Link attribute)</a>
</li>
</ul></td>
</tr></table>
<h2 id="T">T</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Link.teardown">teardown() (RNS.Link method)</a>
</li>
<li><a href="reference.html#RNS.Identity.to_file">to_file() (RNS.Identity method)</a>
</li>
<li><a href="reference.html#RNS.Transport">Transport (class in RNS)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Reticulum.transport_enabled">transport_enabled() (RNS.Reticulum static method)</a>
</li>
<li><a href="reference.html#RNS.Identity.truncated_hash">truncated_hash() (RNS.Identity static method)</a>
</li>
<li><a href="reference.html#RNS.Identity.TRUNCATED_HASHLENGTH">TRUNCATED_HASHLENGTH (RNS.Identity attribute)</a>
</li>
</ul></td>
</tr></table>
<h2 id="V">V</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Identity.validate">validate() (RNS.Identity method)</a>
</li>
</ul></td>
</tr></table>
<div class="clearer"></div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<div id="searchbox" style="display: none" role="search">
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="related" role="navigation" aria-label="related navigation">
<h3>Navigation</h3>
<ul>
<li class="right" style="margin-right: 10px">
<a href="#" title="General Index"
>index</a></li>
<li class="nav-item nav-item-0"><a href="index.html">Reticulum Network Stack 0.3.7 beta documentation</a> &#187;</li>
<li class="nav-item nav-item-this"><a href="">Index</a></li>
</ul>
</div>
<div class="footer" role="contentinfo">
&#169; Copyright 2022, Mark Qvist.
Created using <a href="https://www.sphinx-doc.org/">Sphinx</a> 4.0.1.
</div>
</body>
</html>
+396
View File
@@ -0,0 +1,396 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Getting Started Fast &#8212; Reticulum Network Stack 0.3.7 beta documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/classic.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="next" title="Using Reticulum on Your System" href="using.html" />
<link rel="prev" title="What is Reticulum?" href="whatis.html" />
</head><body>
<div class="related" role="navigation" aria-label="related navigation">
<h3>Navigation</h3>
<ul>
<li class="right" style="margin-right: 10px">
<a href="genindex.html" title="General Index"
accesskey="I">index</a></li>
<li class="right" >
<a href="using.html" title="Using Reticulum on Your System"
accesskey="N">next</a> |</li>
<li class="right" >
<a href="whatis.html" title="What is Reticulum?"
accesskey="P">previous</a> |</li>
<li class="nav-item nav-item-0"><a href="index.html">Reticulum Network Stack 0.3.7 beta documentation</a> &#187;</li>
<li class="nav-item nav-item-this"><a href="">Getting Started Fast</a></li>
</ul>
</div>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<div class="section" id="getting-started-fast">
<h1>Getting Started Fast<a class="headerlink" href="#getting-started-fast" title="Permalink to this headline"></a></h1>
<p>The best way to get started with the Reticulum Network Stack depends on what
you want to do. This guide will outline sensible starting paths for different
scenarios.</p>
<div class="section" id="try-using-a-reticulum-based-program">
<h2>Try Using a Reticulum-based Program<a class="headerlink" href="#try-using-a-reticulum-based-program" title="Permalink to this headline"></a></h2>
<p>If you simply want to try using a program built with Reticulum, a few different
programs exist that allow basic communication and a range of other useful functions
over even extremely low-bandwidth Reticulum networks.</p>
<p>These programs will let you get a feel for how Reticulum works. They have been designed
to run well over networks based on LoRa or packet radio, but can also be used completely
over local WiFi, wired ethernet, the Internet, or any combination.</p>
<p>As such, it is easy to get started experimenting, without having to set up any radio
transceivers or infrastructure just to try it out. Launching the programs on separate
devices connected to the same WiFi network is enough to get started, and physical
radio interfaces can then be added later.</p>
<div class="section" id="nomad-network">
<h3>Nomad Network<a class="headerlink" href="#nomad-network" title="Permalink to this headline"></a></h3>
<p>The terminal-based program <a class="reference external" href="https://github.com/markqvist/nomadnet">Nomad Network</a>
provides a complete encrypted communications suite built with Reticulum. It features
encrypted messaging (both direct and delayed-delivery for offline users), file sharing,
and has a built-in text-browser and page server with support for dynamically rendered pages,
user authentication and more.</p>
<a class="reference external image-reference" href="_images/nomadnet_3.png"><img alt="_images/nomadnet_3.png" src="_images/nomadnet_3.png" /></a>
<p><a class="reference external" href="https://github.com/markqvist/nomadnet">Nomad Network</a> is a user-facing client
for the messaging and information-sharing protocol
<a class="reference external" href="https://github.com/markqvist/lxmf">LXMF</a>, another project built with Reticulum.</p>
<p>You can install Nomad Network via pip:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install ...</span>
<span class="n">pip3</span> <span class="n">install</span> <span class="n">nomadnet</span>
<span class="c1"># ... and run</span>
<span class="n">nomadnet</span>
</pre></div>
</div>
<p><strong>Please Note</strong>: If this is the very first time you use pip to install a program
on your system, you might need to reboot your system for your program to become
available. If you get a “command not found” error or similar when running the
program, reboot your system and try again.</p>
</div>
<div class="section" id="sideband">
<h3>Sideband<a class="headerlink" href="#sideband" title="Permalink to this headline"></a></h3>
<p>If you would rather use a program with a graphical user interface, you can take
a look at <a class="reference external" href="https://unsigned.io/sideband">Sideband</a>, which is available for Android,
Linux and macOS.</p>
<a class="reference external image-reference" href="_images/sideband_1.png"><img alt="_images/sideband_1.png" class="align-center" src="_images/sideband_1.png" style="width: 400px;" /></a>
<p>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.</p>
</div>
</div>
<div class="section" id="using-the-included-utilities">
<h2>Using the Included Utilities<a class="headerlink" href="#using-the-included-utilities" title="Permalink to this headline"></a></h2>
<p>Reticulum comes with a range of included utilities that make it easier to
manage your network, check connectivity and make Reticulum available to other
programs on your system.</p>
<p>You can use <code class="docutils literal notranslate"><span class="pre">rnsd</span></code> to run Reticulum as a background or foreground service,
and the <code class="docutils literal notranslate"><span class="pre">rnstatus</span></code>, <code class="docutils literal notranslate"><span class="pre">rnpath</span></code> and <code class="docutils literal notranslate"><span class="pre">rnprobe</span></code> utilities to view and query
network status and connectivity.</p>
<p>To learn more about these utility programs, have a look at the
<a class="reference internal" href="using.html#using-main"><span class="std std-ref">Using Reticulum on Your System</span></a> chapter of this manual.</p>
</div>
<div class="section" id="creating-a-network-with-reticulum">
<h2>Creating a Network With Reticulum<a class="headerlink" href="#creating-a-network-with-reticulum" title="Permalink to this headline"></a></h2>
<p>To create a network, you will need to specify one or more <em>interfaces</em> for
Reticulum to use. This is done in the Reticulum configuration file, which by
default is located at <code class="docutils literal notranslate"><span class="pre">~/.reticulum/config</span></code>. You can edit this file by hand,
or use the interactive <code class="docutils literal notranslate"><span class="pre">rnsconfig</span></code> utility.</p>
<p>When Reticulum is started for the first time, it will create a default
configuration file, with one active interface. This default interface uses
your existing ethernet and WiFi networks (if any), and only allows you to
communicate with other Reticulum peers within your local broadcast domains.</p>
<p>To communicate further, you will have to add one or more interfaces. The default
configuration includes a number of examples, ranging from using TCP over the
internet, to LoRa and Packet Radio interfaces.</p>
<p>With Reticulum, you only need to configure what interfaces you want to communicate
over. There is no need to configure address spaces, subnets, routing tables,
or other things you might be used to from other network types.</p>
<p>Once Reticulums knows which interfaces it should use, it will automatically
discover topography and configure transport of data to any destinations it
knows about.</p>
<p>In situations where you already have an established WiFi or ethernet network, and
many devices that want to utilise the same external Reticulum network (for example over
LoRa), it will often be sufficient to let one system act as a Reticulum gateway, by
adding any external interfaces to this systems configuration, and enabling transport. Any
other device on your local WiFi will then be able to connect to this wider Reticulum
network just using the default interface configuration.</p>
<p>Possibly, the examples in the config file are enough to get you started. If
you want more information, you can read the <a class="reference internal" href="networks.html#networks-main"><span class="std std-ref">Building Networks</span></a>
and <a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">Interfaces</span></a> chapters of this manual.</p>
</div>
<div class="section" id="connecting-reticulum-instances-over-the-internet">
<h2>Connecting Reticulum Instances Over the Internet<a class="headerlink" href="#connecting-reticulum-instances-over-the-internet" title="Permalink to this headline"></a></h2>
<p>Reticulum currently offers two interfaces suitable for connecting instances over the Internet: <a class="reference internal" href="interfaces.html#interfaces-tcps"><span class="std std-ref">TCP</span></a>
and <a class="reference internal" href="interfaces.html#interfaces-i2p"><span class="std std-ref">I2P</span></a>. Each interface offers a different set of features, and Reticulum
users should carefully choose the interface which best suites their needs.</p>
<p>The <code class="docutils literal notranslate"><span class="pre">TCPServerInterface</span></code> allows users to host an instance accessible over TCP/IP. This
method is generally faster, lower latency, and more energy efficient than using <code class="docutils literal notranslate"><span class="pre">I2PInterface</span></code>,
however it also leaks more data about the server host.</p>
<p>TCP connections reveal the IP address of both your instance and the server to anyone who can
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
inspecting your packets may be able to record packet metadata like time of transmission and packet size.
Even though Reticulum encrypts traffic, TCP does not, so an adversary may be able to use
packet inspection to learn that a system is running Reticulum, and what other IP adresses connect to it.
Hosting a publicly reachable instance over TCP also requires a publicly reachable IP address,
which most Internet connections dont offer anymore.</p>
<p>The <code class="docutils literal notranslate"><span class="pre">I2PInterface</span></code> routes messages through the <a class="reference external" href="https://geti2p.net/en/">Invisible Internet Protocol
(I2P)</a>. To properly use this interface, users must also run an I2P daemon in
parallel to <code class="docutils literal notranslate"><span class="pre">rnsd</span></code>. For always-on I2P nodes it is recommended to use <a class="reference external" href="https://i2pd.website/">i2pd</a>.</p>
<p>By default, I2P will encrypt and mix all traffic sent over the Internet, and
hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node
will also relay other I2P users encrypted packets, which will use extra
bandwidth and compute power, but also makes timing attacks and other forms of
deep-packet-inspection much more difficult.</p>
<p>I2P also allows users to host globally available Reticulum instances from non-public IPs and behind firewalls.</p>
<p>In general it is recommended to use an I2P node if you want to host a publically accessible
instance, while preserving anonymity. If you care more about performance, and a slightly
easier setup, use TCP.</p>
</div>
<div class="section" id="connect-to-the-public-testnet">
<h2>Connect to the Public Testnet<a class="headerlink" href="#connect-to-the-public-testnet" title="Permalink to this headline"></a></h2>
<p>An experimental public testnet has been made accessible over both I2P and TCP. You can join it
by adding one of the following interfaces to your <code class="docutils literal notranslate"><span class="pre">.reticulum/config</span></code> file:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># For connecting over TCP/IP:</span>
<span class="p">[[</span><span class="n">RNS</span> <span class="n">Testnet</span> <span class="n">Frankfurt</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPClientInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
<span class="n">outgoing</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">target_host</span> <span class="o">=</span> <span class="n">frankfurt</span><span class="o">.</span><span class="n">rns</span><span class="o">.</span><span class="n">unsigned</span><span class="o">.</span><span class="n">io</span>
<span class="n">target_port</span> <span class="o">=</span> <span class="mi">4965</span>
<span class="c1"># For connecting over I2P:</span>
<span class="p">[[</span><span class="n">RNS</span> <span class="n">Testnet</span> <span class="n">I2P</span> <span class="n">Node</span> <span class="n">A</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">I2PInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
<span class="n">peers</span> <span class="o">=</span> <span class="n">ykzlw5ujbaqc2xkec4cpvgyxj257wcrmmgkuxqmqcur7cq3w3lha</span><span class="o">.</span><span class="n">b32</span><span class="o">.</span><span class="n">i2p</span>
</pre></div>
</div>
<p>Many other Reticulum instances are connecting to this testnet, and you can also join it
via other entry points if you know them. There is absolutely no control over the network
topography, usage or what types of instances connect. It will also occasionally be used
to test various failure scenarios, and there are no availability or service guarantees.</p>
</div>
<div class="section" id="develop-a-program-with-reticulum">
<h2>Develop a Program with Reticulum<a class="headerlink" href="#develop-a-program-with-reticulum" title="Permalink to this headline"></a></h2>
<p>If you want to develop programs that use Reticulum, the easiest way to get
started is to install the latest release of Reticulum via pip:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">pip3</span> <span class="n">install</span> <span class="n">rns</span>
</pre></div>
</div>
<p>The above command will install Reticulum and dependencies, and you will be
ready to import and use RNS in your own programs. The next step will most
likely be to look at some <a class="reference internal" href="examples.html#examples-main"><span class="std std-ref">Example Programs</span></a>.</p>
<p>For extended functionality, you can install optional dependencies:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">pip3</span> <span class="n">install</span> <span class="n">pyserial</span> <span class="n">netifaces</span>
</pre></div>
</div>
<p>Further information can be found in the <a class="reference internal" href="reference.html#api-main"><span class="std std-ref">API Reference</span></a>.</p>
</div>
<div class="section" id="participate-in-reticulum-development">
<h2>Participate in Reticulum Development<a class="headerlink" href="#participate-in-reticulum-development" title="Permalink to this headline"></a></h2>
<p>If you want to participate in the development of Reticulum and associated
utilities, youll want to get the latest source from GitHub. In that case,
dont use pip, but try this recipe:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install dependencies</span>
<span class="n">pip3</span> <span class="n">install</span> <span class="n">cryptography</span> <span class="n">pyserial</span> <span class="n">netifaces</span>
<span class="c1"># Clone repository</span>
<span class="n">git</span> <span class="n">clone</span> <span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">github</span><span class="o">.</span><span class="n">com</span><span class="o">/</span><span class="n">markqvist</span><span class="o">/</span><span class="n">Reticulum</span><span class="o">.</span><span class="n">git</span>
<span class="c1"># Move into Reticulum folder and symlink library to examples folder</span>
<span class="n">cd</span> <span class="n">Reticulum</span>
<span class="n">ln</span> <span class="o">-</span><span class="n">s</span> <span class="o">../</span><span class="n">RNS</span> <span class="o">./</span><span class="n">Examples</span><span class="o">/</span>
<span class="c1"># Run an example</span>
<span class="n">python3</span> <span class="n">Examples</span><span class="o">/</span><span class="n">Echo</span><span class="o">.</span><span class="n">py</span> <span class="o">-</span><span class="n">s</span>
<span class="c1"># Unless you&#39;ve manually created a config file, Reticulum will do so now,</span>
<span class="c1"># and immediately exit. Make any necessary changes to the file:</span>
<span class="n">nano</span> <span class="o">~/.</span><span class="n">reticulum</span><span class="o">/</span><span class="n">config</span>
<span class="c1"># ... and launch the example again.</span>
<span class="n">python3</span> <span class="n">Examples</span><span class="o">/</span><span class="n">Echo</span><span class="o">.</span><span class="n">py</span> <span class="o">-</span><span class="n">s</span>
<span class="c1"># You can now repeat the process on another computer,</span>
<span class="c1"># and run the same example with -h to get command line options.</span>
<span class="n">python3</span> <span class="n">Examples</span><span class="o">/</span><span class="n">Echo</span><span class="o">.</span><span class="n">py</span> <span class="o">-</span><span class="n">h</span>
<span class="c1"># Run the example in client mode to &quot;ping&quot; the server.</span>
<span class="c1"># Replace the hash below with the actual destination hash of your server.</span>
<span class="n">python3</span> <span class="n">Examples</span><span class="o">/</span><span class="n">Echo</span><span class="o">.</span><span class="n">py</span> <span class="mf">3e12</span><span class="n">fc71692f8ec47bc5</span>
<span class="c1"># Have a look at another example</span>
<span class="n">python3</span> <span class="n">Examples</span><span class="o">/</span><span class="n">Filetransfer</span><span class="o">.</span><span class="n">py</span> <span class="o">-</span><span class="n">h</span>
</pre></div>
</div>
<p>When you have experimented with the basic examples, its time to go read the
<a class="reference internal" href="understanding.html#understanding-main"><span class="std std-ref">Understanding Reticulum</span></a> chapter.</p>
</div>
<div class="section" id="reticulum-on-arm64">
<h2>Reticulum on ARM64<a class="headerlink" href="#reticulum-on-arm64" title="Permalink to this headline"></a></h2>
<p>On some architectures, including ARM64, not all dependencies have precompiled
binaries. On such systems, you will need to install <code class="docutils literal notranslate"><span class="pre">python3-dev</span></code> before
installing Reticulum or programs that depend on Reticulum.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Install Python and development packages</span>
<span class="n">sudo</span> <span class="n">apt</span> <span class="n">update</span>
<span class="n">sudo</span> <span class="n">apt</span> <span class="n">install</span> <span class="n">python3</span> <span class="n">python3</span><span class="o">-</span><span class="n">pip</span> <span class="n">python3</span><span class="o">-</span><span class="n">dev</span>
<span class="c1"># Install Reticulum</span>
<span class="n">python3</span> <span class="o">-</span><span class="n">m</span> <span class="n">pip</span> <span class="n">install</span> <span class="n">rns</span>
</pre></div>
</div>
</div>
<div class="section" id="reticulum-on-android">
<h2>Reticulum on Android<a class="headerlink" href="#reticulum-on-android" title="Permalink to this headline"></a></h2>
<p>Reticulum can be used on Android in different ways. The easiest way to get
started is using an app like <a class="reference external" href="https://unsigned.io/sideband">Sideband</a>.</p>
<p>For more control and features, you can use Reticulum and related programs via
the <a class="reference external" href="https://termux.com/">Termux app</a>, at the time of writing available on
<a class="reference external" href="https://f-droid.org">F-droid</a>.</p>
<p>Termux is a terminal emulator and Linux environment for Android based devices,
which includes the ability to use many different programs and libraries,
including Reticulum.</p>
<p>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.</p>
<p>From within Termux, execute the following:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># First, make sure indexes and packages are up to date.</span>
<span class="n">pkg</span> <span class="n">update</span>
<span class="n">pkg</span> <span class="n">upgrade</span>
<span class="c1"># Then install dependencies for the cryptography library.</span>
<span class="n">pkg</span> <span class="n">install</span> <span class="n">python</span> <span class="n">build</span><span class="o">-</span><span class="n">essential</span> <span class="n">openssl</span> <span class="n">libffi</span> <span class="n">rust</span>
<span class="c1"># Make sure pip is up to date, and install the wheel module.</span>
<span class="n">pip3</span> <span class="n">install</span> <span class="n">wheel</span> <span class="n">pip</span> <span class="o">--</span><span class="n">upgrade</span>
<span class="c1"># To allow the installer to build the cryptography module,</span>
<span class="c1"># we need to let it know what platform we are compiling for:</span>
<span class="n">export</span> <span class="n">CARGO_BUILD_TARGET</span><span class="o">=</span><span class="s2">&quot;aarch64-linux-android&quot;</span>
<span class="c1"># Start the install process for the cryptography module.</span>
<span class="c1"># Depending on your device, this can take several minutes,</span>
<span class="c1"># since the module must be compiled locally on your device.</span>
<span class="n">pip3</span> <span class="n">install</span> <span class="n">cryptography</span>
<span class="c1"># If the above installation succeeds, you can now install</span>
<span class="c1"># Reticulum and any related software</span>
<span class="n">pip3</span> <span class="n">install</span> <span class="n">rns</span>
</pre></div>
</div>
<p>It is also possible to include Reticulum in apps compiled and distributed as
Android APKs. A detailed tutorial and example source code will be included
here at a later point.</p>
</div>
<div class="section" id="adding-radio-interfaces">
<h2>Adding Radio Interfaces<a class="headerlink" href="#adding-radio-interfaces" title="Permalink to this headline"></a></h2>
<p>Once you have Reticulum installed and working, you can add radio interfaces with
any compatible hardware you have available. For information on how to configure
this, see the <a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">Interfaces</span></a> section of this manual.</p>
<p>A range of common LoRa development boards and transceiver modules can be used
as interfaces with Reticulum. You can refer to the following external resources
for more information:</p>
<ul class="simple">
<li><p><a class="reference external" href="https://unsigned.io/how-to-make-your-own-rnodes/">How To Make Your Own RNodes</a></p></li>
<li><p><a class="reference external" href="https://unsigned.io/installing-rnode-firmware-on-t-beam-and-lora32-devices/">Installing RNode Firmware on Compatible LoRa Devices</a></p></li>
<li><p><a class="reference external" href="https://unsigned.io/private-messaging-over-lora/">Private, Secure and Uncensorable Messaging Over a LoRa Mesh</a></p></li>
<li><p><a class="reference external" href="https://github.com/markqvist/RNode_Firmware/">RNode Firmware</a></p></li>
</ul>
<p>If you have communications hardware that you think would be suitable for use with Reticulum,
you are welcome to head over to the <a class="reference external" href="https://github.com/markqvist/Reticulum/discussions">GitHub discussion pages</a>
and propose adding an interface for the hardware.</p>
</div>
</div>
<div class="clearer"></div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h3><a href="index.html">Table of Contents</a></h3>
<ul>
<li><a class="reference internal" href="#">Getting Started Fast</a><ul>
<li><a class="reference internal" href="#try-using-a-reticulum-based-program">Try Using a Reticulum-based Program</a><ul>
<li><a class="reference internal" href="#nomad-network">Nomad Network</a></li>
<li><a class="reference internal" href="#sideband">Sideband</a></li>
</ul>
</li>
<li><a class="reference internal" href="#using-the-included-utilities">Using the Included Utilities</a></li>
<li><a class="reference internal" href="#creating-a-network-with-reticulum">Creating a Network With Reticulum</a></li>
<li><a class="reference internal" href="#connecting-reticulum-instances-over-the-internet">Connecting Reticulum Instances Over the Internet</a></li>
<li><a class="reference internal" href="#connect-to-the-public-testnet">Connect to the Public Testnet</a></li>
<li><a class="reference internal" href="#develop-a-program-with-reticulum">Develop a Program with Reticulum</a></li>
<li><a class="reference internal" href="#participate-in-reticulum-development">Participate in Reticulum Development</a></li>
<li><a class="reference internal" href="#reticulum-on-arm64">Reticulum on ARM64</a></li>
<li><a class="reference internal" href="#reticulum-on-android">Reticulum on Android</a></li>
<li><a class="reference internal" href="#adding-radio-interfaces">Adding Radio Interfaces</a></li>
</ul>
</li>
</ul>
<h4>Previous topic</h4>
<p class="topless"><a href="whatis.html"
title="previous chapter">What is Reticulum?</a></p>
<h4>Next topic</h4>
<p class="topless"><a href="using.html"
title="next chapter">Using Reticulum on Your System</a></p>
<div role="note" aria-label="source link">
<h3>This Page</h3>
<ul class="this-page-menu">
<li><a href="_sources/gettingstartedfast.rst.txt"
rel="nofollow">Show Source</a></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="related" role="navigation" aria-label="related navigation">
<h3>Navigation</h3>
<ul>
<li class="right" style="margin-right: 10px">
<a href="genindex.html" title="General Index"
>index</a></li>
<li class="right" >
<a href="using.html" title="Using Reticulum on Your System"
>next</a> |</li>
<li class="right" >
<a href="whatis.html" title="What is Reticulum?"
>previous</a> |</li>
<li class="nav-item nav-item-0"><a href="index.html">Reticulum Network Stack 0.3.7 beta documentation</a> &#187;</li>
<li class="nav-item nav-item-this"><a href="">Getting Started Fast</a></li>
</ul>
</div>
<div class="footer" role="contentinfo">
&#169; Copyright 2022, Mark Qvist.
Created using <a href="https://www.sphinx-doc.org/">Sphinx</a> 4.0.1.
</div>
</body>
</html>
+237
View File
@@ -0,0 +1,237 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reticulum Network Stack Manual &#8212; Reticulum Network Stack 0.3.7 beta documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/classic.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="next" title="What is Reticulum?" href="whatis.html" />
</head><body>
<div class="related" role="navigation" aria-label="related navigation">
<h3>Navigation</h3>
<ul>
<li class="right" style="margin-right: 10px">
<a href="genindex.html" title="General Index"
accesskey="I">index</a></li>
<li class="right" >
<a href="whatis.html" title="What is Reticulum?"
accesskey="N">next</a> |</li>
<li class="nav-item nav-item-0"><a href="#">Reticulum Network Stack 0.3.7 beta documentation</a> &#187;</li>
<li class="nav-item nav-item-this"><a href="">Reticulum Network Stack Manual</a></li>
</ul>
</div>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<div class="section" id="reticulum-network-stack-manual">
<h1>Reticulum Network Stack Manual<a class="headerlink" href="#reticulum-network-stack-manual" title="Permalink to this headline"></a></h1>
<p>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.</p>
<div class="toctree-wrapper compound">
<ul>
<li class="toctree-l1"><a class="reference internal" href="whatis.html">What is Reticulum?</a><ul>
<li class="toctree-l2"><a class="reference internal" href="whatis.html#current-status">Current Status</a></li>
<li class="toctree-l2"><a class="reference internal" href="whatis.html#what-does-reticulum-offer">What does Reticulum Offer?</a></li>
<li class="toctree-l2"><a class="reference internal" href="whatis.html#where-can-reticulum-be-used">Where can Reticulum be Used?</a></li>
<li class="toctree-l2"><a class="reference internal" href="whatis.html#interface-types-and-devices">Interface Types and Devices</a></li>
<li class="toctree-l2"><a class="reference internal" href="whatis.html#caveat-emptor">Caveat Emptor</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="gettingstartedfast.html">Getting Started Fast</a><ul>
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#try-using-a-reticulum-based-program">Try Using a Reticulum-based Program</a><ul>
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#nomad-network">Nomad Network</a></li>
<li class="toctree-l3"><a class="reference internal" href="gettingstartedfast.html#sideband">Sideband</a></li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#using-the-included-utilities">Using the Included Utilities</a></li>
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#creating-a-network-with-reticulum">Creating a Network With Reticulum</a></li>
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#connecting-reticulum-instances-over-the-internet">Connecting Reticulum Instances Over the Internet</a></li>
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#connect-to-the-public-testnet">Connect to the Public Testnet</a></li>
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#develop-a-program-with-reticulum">Develop a Program with Reticulum</a></li>
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#participate-in-reticulum-development">Participate in Reticulum Development</a></li>
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#reticulum-on-arm64">Reticulum on ARM64</a></li>
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#reticulum-on-android">Reticulum on Android</a></li>
<li class="toctree-l2"><a class="reference internal" href="gettingstartedfast.html#adding-radio-interfaces">Adding Radio Interfaces</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="using.html">Using Reticulum on Your System</a><ul>
<li class="toctree-l2"><a class="reference internal" href="using.html#configuration-data">Configuration &amp; Data</a></li>
<li class="toctree-l2"><a class="reference internal" href="using.html#included-utility-programs">Included Utility Programs</a><ul>
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnsd-utility">The rnsd Utility</a></li>
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnstatus-utility">The rnstatus Utility</a></li>
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnpath-utility">The rnpath Utility</a></li>
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnprobe-utility">The rnprobe Utility</a></li>
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rncp-utility">The rncp Utility</a></li>
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnx-utility">The rnx Utility</a></li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="using.html#improving-system-configuration">Improving System Configuration</a><ul>
<li class="toctree-l3"><a class="reference internal" href="using.html#fixed-serial-port-names">Fixed Serial Port Names</a></li>
<li class="toctree-l3"><a class="reference internal" href="using.html#reticulum-as-a-system-service">Reticulum as a System Service</a></li>
</ul>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a><ul>
<li class="toctree-l2"><a class="reference internal" href="networks.html#concepts-overview">Concepts &amp; Overview</a></li>
<li class="toctree-l2"><a class="reference internal" href="networks.html#example-scenarios">Example Scenarios</a><ul>
<li class="toctree-l3"><a class="reference internal" href="networks.html#interconnected-lora-sites">Interconnected LoRa Sites</a></li>
<li class="toctree-l3"><a class="reference internal" href="networks.html#bridging-over-the-internet">Bridging Over the Internet</a></li>
<li class="toctree-l3"><a class="reference internal" href="networks.html#growth-and-convergence">Growth and Convergence</a></li>
</ul>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Supported Interfaces</a><ul>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#auto-interface">Auto Interface</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#i2p-interface">I2P Interface</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#tcp-server-interface">TCP Server Interface</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#tcp-client-interface">TCP Client Interface</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#udp-interface">UDP Interface</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#rnode-lora-interface">RNode LoRa Interface</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#serial-interface">Serial Interface</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#pipe-interface">Pipe Interface</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#kiss-interface">KISS Interface</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#ax-25-kiss-interface">AX.25 KISS Interface</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#common-interface-options">Common Interface Options</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#interface-modes">Interface Modes</a></li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#announce-rate-control">Announce Rate Control</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="understanding.html">Understanding Reticulum</a><ul>
<li class="toctree-l2"><a class="reference internal" href="understanding.html#motivation">Motivation</a></li>
<li class="toctree-l2"><a class="reference internal" href="understanding.html#goals">Goals</a></li>
<li class="toctree-l2"><a class="reference internal" href="understanding.html#introduction-basic-functionality">Introduction &amp; Basic Functionality</a><ul>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#destinations">Destinations</a></li>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#public-key-announcements">Public Key Announcements</a></li>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#understanding-identities">Identities</a></li>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#getting-further">Getting Further</a></li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="understanding.html#reticulum-transport">Reticulum Transport</a><ul>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#node-types">Node Types</a></li>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#the-announce-mechanism-in-detail">The Announce Mechanism in Detail</a></li>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#reaching-the-destination">Reaching the Destination</a></li>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#resources">Resources</a></li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="understanding.html#reference-setup">Reference Setup</a></li>
<li class="toctree-l2"><a class="reference internal" href="understanding.html#protocol-specifics">Protocol Specifics</a><ul>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#packet-prioritisation">Packet Prioritisation</a></li>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#interface-access-codes">Interface Access Codes</a></li>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#wire-format">Wire Format</a></li>
<li class="toctree-l3"><a class="reference internal" href="understanding.html#announce-propagation-rules">Announce Propagation Rules</a></li>
</ul>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a><ul>
<li class="toctree-l2"><a class="reference internal" href="reference.html#classes">Classes</a><ul>
<li class="toctree-l3"><a class="reference internal" href="reference.html#reticulum">Reticulum</a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#api-identity">Identity</a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#destination">Destination</a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#packet">Packet</a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#packet-receipt">Packet Receipt</a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#link">Link</a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#request-receipt">Request Receipt</a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#resource">Resource</a></li>
<li class="toctree-l3"><a class="reference internal" href="reference.html#transport">Transport</a></li>
</ul>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a><ul>
<li class="toctree-l2"><a class="reference internal" href="examples.html#minimal">Minimal</a></li>
<li class="toctree-l2"><a class="reference internal" href="examples.html#announce">Announce</a></li>
<li class="toctree-l2"><a class="reference internal" href="examples.html#broadcast">Broadcast</a></li>
<li class="toctree-l2"><a class="reference internal" href="examples.html#echo">Echo</a></li>
<li class="toctree-l2"><a class="reference internal" href="examples.html#link">Link</a></li>
<li class="toctree-l2"><a class="reference internal" href="examples.html#example-identify">Identification</a></li>
<li class="toctree-l2"><a class="reference internal" href="examples.html#requests-responses">Requests &amp; Responses</a></li>
<li class="toctree-l2"><a class="reference internal" href="examples.html#filetransfer">Filetransfer</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="indices-and-tables">
<h2>Indices and Tables<a class="headerlink" href="#indices-and-tables" title="Permalink to this headline"></a></h2>
<ul class="simple">
<li><p><a class="reference internal" href="genindex.html"><span class="std std-ref">Index</span></a></p></li>
<li><p><a class="reference internal" href="search.html"><span class="std std-ref">Search Page</span></a></p></li>
</ul>
</div>
</div>
<div class="clearer"></div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h3><a href="#">Table of Contents</a></h3>
<ul>
<li><a class="reference internal" href="#">Reticulum Network Stack Manual</a><ul>
<li><a class="reference internal" href="#indices-and-tables">Indices and Tables</a></li>
</ul>
</li>
</ul>
<h4>Next topic</h4>
<p class="topless"><a href="whatis.html"
title="next chapter">What is Reticulum?</a></p>
<div role="note" aria-label="source link">
<h3>This Page</h3>
<ul class="this-page-menu">
<li><a href="_sources/index.rst.txt"
rel="nofollow">Show Source</a></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="related" role="navigation" aria-label="related navigation">
<h3>Navigation</h3>
<ul>
<li class="right" style="margin-right: 10px">
<a href="genindex.html" title="General Index"
>index</a></li>
<li class="right" >
<a href="whatis.html" title="What is Reticulum?"
>next</a> |</li>
<li class="nav-item nav-item-0"><a href="#">Reticulum Network Stack 0.3.7 beta documentation</a> &#187;</li>
<li class="nav-item nav-item-this"><a href="">Reticulum Network Stack Manual</a></li>
</ul>
</div>
<div class="footer" role="contentinfo">
&#169; Copyright 2022, Mark Qvist.
Created using <a href="https://www.sphinx-doc.org/">Sphinx</a> 4.0.1.
</div>
</body>
</html>
+826
View File
@@ -0,0 +1,826 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Supported Interfaces &#8212; Reticulum Network Stack 0.3.7 beta documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/classic.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="next" title="Understanding Reticulum" href="understanding.html" />
<link rel="prev" title="Building Networks" href="networks.html" />
</head><body>
<div class="related" role="navigation" aria-label="related navigation">
<h3>Navigation</h3>
<ul>
<li class="right" style="margin-right: 10px">
<a href="genindex.html" title="General Index"
accesskey="I">index</a></li>
<li class="right" >
<a href="understanding.html" title="Understanding Reticulum"
accesskey="N">next</a> |</li>
<li class="right" >
<a href="networks.html" title="Building Networks"
accesskey="P">previous</a> |</li>
<li class="nav-item nav-item-0"><a href="index.html">Reticulum Network Stack 0.3.7 beta documentation</a> &#187;</li>
<li class="nav-item nav-item-this"><a href="">Supported Interfaces</a></li>
</ul>
</div>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<div class="section" id="supported-interfaces">
<span id="interfaces-main"></span><h1>Supported Interfaces<a class="headerlink" href="#supported-interfaces" title="Permalink to this headline"></a></h1>
<p>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
network topologies you can create with Reticulum is more or less endless, but
common to them all is that you will need to define one or more <em>interfaces</em>
for Reticulum to use.</p>
<p>The following sections describe the interfaces currently available in Reticulum,
and gives example configurations for the respective interface types.</p>
<p>For a high-level overview of how networks can be formed over different interface
types, have a look at the <a class="reference internal" href="networks.html#networks-main"><span class="std std-ref">Building Networks</span></a> chapter of this
manual.</p>
<div class="section" id="auto-interface">
<span id="interfaces-auto"></span><h2>Auto Interface<a class="headerlink" href="#auto-interface" title="Permalink to this headline"></a></h2>
<p>The Auto Interface enables communication with other discoverable Reticulum
nodes over autoconfigured IPv6 and UDP. It does not need any functional IP
infrastructure like routers or DHCP servers, but will require at least some
sort of switching medium between peers (a wired switch, a hub, a WiFi access
point or similar), and that link-local IPv6 is enabled in your operating
system, which should be enabled by default in almost all OSes.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># This example demonstrates a TCP server interface.</span>
<span class="c1"># It will listen for incoming connections on the</span>
<span class="c1"># specified IP address and port number.</span>
<span class="p">[[</span><span class="n">Default</span> <span class="n">Interface</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">AutoInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="c1"># You can create multiple isolated Reticulum</span>
<span class="c1"># networks on the same physical LAN by</span>
<span class="c1"># specifying different Group IDs.</span>
<span class="n">group_id</span> <span class="o">=</span> <span class="n">reticulum</span>
<span class="c1"># You can also select specifically which</span>
<span class="c1"># kernel networking devices to use.</span>
<span class="n">devices</span> <span class="o">=</span> <span class="n">wlan0</span><span class="p">,</span><span class="n">eth1</span>
<span class="c1"># Or let AutoInterface use all suitable</span>
<span class="c1"># devices except for a list of ignored ones.</span>
<span class="n">ignored_devices</span> <span class="o">=</span> <span class="n">tun0</span><span class="p">,</span><span class="n">eth0</span>
</pre></div>
</div>
<p>If you are connected to the Internet with IPv6, and your provider will route
IPv6 multicast, you can potentially configure the Auto Interface to globally
autodiscover other Reticulum nodes within your selected Group ID. You can specify
the discovery scope by setting it to one of <code class="docutils literal notranslate"><span class="pre">link</span></code>, <code class="docutils literal notranslate"><span class="pre">admin</span></code>, <code class="docutils literal notranslate"><span class="pre">site</span></code>,
<code class="docutils literal notranslate"><span class="pre">organisation</span></code> or <code class="docutils literal notranslate"><span class="pre">global</span></code>.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Default</span> <span class="n">Interface</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">AutoInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="c1"># Configure global discovery</span>
<span class="n">group_id</span> <span class="o">=</span> <span class="n">custom_network_name</span>
<span class="n">discovery_scope</span> <span class="o">=</span> <span class="k">global</span>
<span class="c1"># Other configuration options</span>
<span class="n">discovery_port</span> <span class="o">=</span> <span class="mi">48555</span>
<span class="n">data_port</span> <span class="o">=</span> <span class="mi">49555</span>
</pre></div>
</div>
</div>
<div class="section" id="i2p-interface">
<span id="interfaces-i2p"></span><h2>I2P Interface<a class="headerlink" href="#i2p-interface" title="Permalink to this headline"></a></h2>
<p>The I2P interface lets you connect Reticulum instances over the
<a class="reference external" href="https://i2pd.website">Invisible Internet Protocol</a>. This can be
especially useful in cases where you want to host a globally reachable
Reticulum instance, but do not have access to any public IP addresses,
have a frequently changing IP address, or have firewalls blocking
inbound traffic.</p>
<p>Using the I2P interface, you will get a globally reachable, portable
and persistent I2P address that your Reticulum instance can be reached
at.</p>
<p>To use the I2P interface, you must have an I2P router running
on your system. The easiest way to acheive this is to download and
install the <a class="reference external" href="https://github.com/PurpleI2P/i2pd/releases/latest">latest release</a>
of the <code class="docutils literal notranslate"><span class="pre">i2pd</span></code> package. For more details about I2P, see the
<a class="reference external" href="https://geti2p.net/en/about/intro">geti2p.net website</a>.</p>
<p>When an I2P router is running on your system, you can simply add
an I2P interface to reticulum:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">I2P</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">I2PInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
<span class="n">connectable</span> <span class="o">=</span> <span class="n">yes</span>
</pre></div>
</div>
<p>On the first start, Reticulum will generate a new I2P address for the
interface and start listening for inbound traffic on it. This can take
a while the first time, especially if your I2P router was also just
started, and is not yet well-connected to the I2P network. When ready,
you should see I2P base32 address printed to your log file. You can
also inspect the status of the interface using the <code class="docutils literal notranslate"><span class="pre">rnstatus</span></code> utility.</p>
<p>To connect to other Reticulum instances over I2P, just add a comma-separated
list of I2P base32 addresses to the <code class="docutils literal notranslate"><span class="pre">peers</span></code> option of the interface:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">I2P</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">I2PInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
<span class="n">connectable</span> <span class="o">=</span> <span class="n">yes</span>
<span class="n">peers</span> <span class="o">=</span> <span class="mi">5</span><span class="n">urvjicpzi7q3ybztsef4i5ow2aq4soktfj7zedz53s47r54jnqq</span><span class="o">.</span><span class="n">b32</span><span class="o">.</span><span class="n">i2p</span>
</pre></div>
</div>
<p>It can take anywhere from a few seconds to a few minutes to establish
I2P connections to the desired peers, so Reticulum handles the process
in the background, and will output relevant events to the log.</p>
<p><strong>Please Note!</strong> While the I2P interface is the simplest way to use
Reticulum over I2P, it is also possible to tunnel the TCP server and
client interfaces over I2P manually. This can be useful in situations
where more control is needed, but requires manual tunnel setup through
the I2P daemon configuration.</p>
<p>It is important to note that the two methods are <em>interchangably compatible</em>.
You can use the I2PInterface to connect to a TCPServerInterface that
was manually tunneled over I2P, for example. This offers a high degree
of flexibility in network setup, while retaining ease of use in simpler
use-cases.</p>
</div>
<div class="section" id="tcp-server-interface">
<span id="interfaces-tcps"></span><h2>TCP Server Interface<a class="headerlink" href="#tcp-server-interface" title="Permalink to this headline"></a></h2>
<p>The TCP Server interface is suitable for allowing other peers to connect over
the Internet or private IP networks. When a TCP server interface has been
configured, other Reticulum peers can connect to it with a TCP Client interface.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># This example demonstrates a TCP server interface.</span>
<span class="c1"># It will listen for incoming connections on the</span>
<span class="c1"># specified IP address and port number.</span>
<span class="p">[[</span><span class="n">TCP</span> <span class="n">Server</span> <span class="n">Interface</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPServerInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="c1"># This configuration will listen on all IP</span>
<span class="c1"># interfaces on port 4242</span>
<span class="n">listen_ip</span> <span class="o">=</span> <span class="mf">0.0</span><span class="o">.</span><span class="mf">0.0</span>
<span class="n">listen_port</span> <span class="o">=</span> <span class="mi">4242</span>
<span class="c1"># Alternatively you can bind to a specific IP</span>
<span class="c1"># listen_ip = 10.0.0.88</span>
<span class="c1"># listen_port = 4242</span>
<span class="c1"># Or a specific network device</span>
<span class="c1"># device = eth0</span>
<span class="c1"># port = 4242</span>
</pre></div>
</div>
<p><strong>Please Note!</strong> The TCP interfaces support tunneling over I2P, but to do so reliably,
you must use the i2p_tunneled option:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">TCP</span> <span class="n">Server</span> <span class="n">on</span> <span class="n">I2P</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPServerInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
<span class="n">listen_ip</span> <span class="o">=</span> <span class="mf">127.0</span><span class="o">.</span><span class="mf">0.1</span>
<span class="n">listen_port</span> <span class="o">=</span> <span class="mi">5001</span>
<span class="n">i2p_tunneled</span> <span class="o">=</span> <span class="n">yes</span>
</pre></div>
</div>
<p>In almost all cases, it is easier to use the dedicated <code class="docutils literal notranslate"><span class="pre">I2PInterface</span></code>, but for complete
control, and using I2P routers running on external systems, this option also exists.</p>
</div>
<div class="section" id="tcp-client-interface">
<span id="interfaces-tcpc"></span><h2>TCP Client Interface<a class="headerlink" href="#tcp-client-interface" title="Permalink to this headline"></a></h2>
<p>To connect to a TCP server interface, you would naturally use the TCP client
interface. Many TCP Client interfaces from different peers can connect to the
same TCP Server interface at the same time.</p>
<p>The TCP interface types can also tolerate intermittency in the IP link layer.
This means that Reticulum will gracefully handle IP links that go up and down,
and restore connectivity after a failure, once the other end of a TCP interface reappears.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Here&#39;s an example of a TCP Client interface. The</span>
<span class="c1"># target_host can either be an IP address or a hostname.</span>
<span class="p">[[</span><span class="n">TCP</span> <span class="n">Client</span> <span class="n">Interface</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPClientInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">target_host</span> <span class="o">=</span> <span class="mf">127.0</span><span class="o">.</span><span class="mf">0.1</span>
<span class="n">target_port</span> <span class="o">=</span> <span class="mi">4242</span>
</pre></div>
</div>
<p>It is also possible to use this interface type to connect via other programs
or hardware devices that expose a KISS interface on a TCP port, for example
software-based soundmodems. To do this, use the <code class="docutils literal notranslate"><span class="pre">kiss_framing</span></code> option:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Here&#39;s an example of a TCP Client interface that connects</span>
<span class="c1"># to a software TNC soundmodem on a KISS over TCP port.</span>
<span class="p">[[</span><span class="n">TCP</span> <span class="n">KISS</span> <span class="n">Interface</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPClientInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">kiss_framing</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">target_host</span> <span class="o">=</span> <span class="mf">127.0</span><span class="o">.</span><span class="mf">0.1</span>
<span class="n">target_port</span> <span class="o">=</span> <span class="mi">8001</span>
</pre></div>
</div>
<p><strong>Caution!</strong> Only use the KISS framing option when connecting to external devices
and programs like soundmodems and similar over TCP. When using the
<code class="docutils literal notranslate"><span class="pre">TCPClientInterface</span></code> in conjunction with the <code class="docutils literal notranslate"><span class="pre">TCPServerInterface</span></code> you should
never enable <code class="docutils literal notranslate"><span class="pre">kiss_framing</span></code>, since this will disable internal reliability and
recovery mechanisms that greatly improves performance over unreliable and
intermittent TCP links.</p>
<p><strong>Please Note!</strong> The TCP interfaces support tunneling over I2P, but to do so reliably,
you must use the i2p_tunneled option:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">TCP</span> <span class="n">Client</span> <span class="n">over</span> <span class="n">I2P</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPClientInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
<span class="n">target_host</span> <span class="o">=</span> <span class="mf">127.0</span><span class="o">.</span><span class="mf">0.1</span>
<span class="n">target_port</span> <span class="o">=</span> <span class="mi">5001</span>
<span class="n">i2p_tunneled</span> <span class="o">=</span> <span class="n">yes</span>
</pre></div>
</div>
</div>
<div class="section" id="udp-interface">
<span id="interfaces-udp"></span><h2>UDP Interface<a class="headerlink" href="#udp-interface" title="Permalink to this headline"></a></h2>
<p>A UDP interface can be useful for communicating over IP networks, both
private and the internet. It can also allow broadcast communication
over IP networks, so it can provide an easy way to enable connectivity
with all other peers on a local area network.</p>
<p><em>Please Note!</em> 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
<a class="reference internal" href="#interfaces-auto"><span class="std std-ref">Auto Interface</span></a> performs better, and is even
easier to use.</p>
<p>The below example is enabled by default on new Reticulum installations,
as it provides an easy way to get started and to test Reticulum on a
pre-existing LAN.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># This example enables communication with other</span>
<span class="c1"># local Reticulum peers over UDP.</span>
<span class="p">[[</span><span class="n">UDP</span> <span class="n">Interface</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">UDPInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">listen_ip</span> <span class="o">=</span> <span class="mf">0.0</span><span class="o">.</span><span class="mf">0.0</span>
<span class="n">listen_port</span> <span class="o">=</span> <span class="mi">4242</span>
<span class="n">forward_ip</span> <span class="o">=</span> <span class="mf">255.255</span><span class="o">.</span><span class="mf">255.255</span>
<span class="n">forward_port</span> <span class="o">=</span> <span class="mi">4242</span>
<span class="c1"># The above configuration will allow communication</span>
<span class="c1"># within the local broadcast domains of all local</span>
<span class="c1"># IP interfaces.</span>
<span class="c1"># Instead of specifying listen_ip, listen_port,</span>
<span class="c1"># forward_ip and forward_port, you can also bind</span>
<span class="c1"># to a specific network device like below.</span>
<span class="c1"># device = eth0</span>
<span class="c1"># port = 4242</span>
<span class="c1"># Assuming the eth0 device has the address</span>
<span class="c1"># 10.55.0.72/24, the above configuration would</span>
<span class="c1"># be equivalent to the following manual setup.</span>
<span class="c1"># Note that we are both listening and forwarding to</span>
<span class="c1"># the broadcast address of the network segments.</span>
<span class="c1"># listen_ip = 10.55.0.255</span>
<span class="c1"># listen_port = 4242</span>
<span class="c1"># forward_ip = 10.55.0.255</span>
<span class="c1"># forward_port = 4242</span>
<span class="c1"># You can of course also communicate only with</span>
<span class="c1"># a single IP address</span>
<span class="c1"># listen_ip = 10.55.0.15</span>
<span class="c1"># listen_port = 4242</span>
<span class="c1"># forward_ip = 10.55.0.16</span>
<span class="c1"># forward_port = 4242</span>
</pre></div>
</div>
</div>
<div class="section" id="rnode-lora-interface">
<span id="interfaces-rnode"></span><h2>RNode LoRa Interface<a class="headerlink" href="#rnode-lora-interface" title="Permalink to this headline"></a></h2>
<p>To use Reticulum over LoRa, the <a class="reference external" href="https://unsigned.io/rnode/">RNode</a> interface
can be used, and offers full control over LoRa parameters.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Here&#39;s an example of how to add a LoRa interface</span>
<span class="c1"># using the RNode LoRa transceiver.</span>
<span class="p">[[</span><span class="n">RNode</span> <span class="n">LoRa</span> <span class="n">Interface</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">RNodeInterface</span>
<span class="c1"># Enable interface if you want use it!</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="c1"># Serial port for the device</span>
<span class="n">port</span> <span class="o">=</span> <span class="o">/</span><span class="n">dev</span><span class="o">/</span><span class="n">ttyUSB0</span>
<span class="c1"># Set frequency to 867.2 MHz</span>
<span class="n">frequency</span> <span class="o">=</span> <span class="mi">867200000</span>
<span class="c1"># Set LoRa bandwidth to 125 KHz</span>
<span class="n">bandwidth</span> <span class="o">=</span> <span class="mi">125000</span>
<span class="c1"># Set TX power to 7 dBm (5 mW)</span>
<span class="n">txpower</span> <span class="o">=</span> <span class="mi">7</span>
<span class="c1"># Select spreading factor 8. Valid</span>
<span class="c1"># range is 7 through 12, with 7</span>
<span class="c1"># being the fastest and 12 having</span>
<span class="c1"># the longest range.</span>
<span class="n">spreadingfactor</span> <span class="o">=</span> <span class="mi">8</span>
<span class="c1"># Select coding rate 5. Valid range</span>
<span class="c1"># is 5 throough 8, with 5 being the</span>
<span class="c1"># fastest, and 8 the longest range.</span>
<span class="n">codingrate</span> <span class="o">=</span> <span class="mi">5</span>
<span class="c1"># You can configure the RNode to send</span>
<span class="c1"># out identification on the channel with</span>
<span class="c1"># a set interval by configuring the</span>
<span class="c1"># following two parameters.</span>
<span class="c1"># id_callsign = MYCALL-0</span>
<span class="c1"># id_interval = 600</span>
<span class="c1"># For certain homebrew RNode interfaces</span>
<span class="c1"># with low amounts of RAM, using packet</span>
<span class="c1"># flow control can be useful. By default</span>
<span class="c1"># it is disabled.</span>
<span class="n">flow_control</span> <span class="o">=</span> <span class="kc">False</span>
</pre></div>
</div>
</div>
<div class="section" id="serial-interface">
<span id="interfaces-serial"></span><h2>Serial Interface<a class="headerlink" href="#serial-interface" title="Permalink to this headline"></a></h2>
<p>Reticulum can be used over serial ports directly, or over any device with a
serial port, that will transparently pass data. Useful for communicating
directly over a wire-pair, or for using devices such as data radios and lasers.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Serial</span> <span class="n">Interface</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">SerialInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="c1"># Serial port for the device</span>
<span class="n">port</span> <span class="o">=</span> <span class="o">/</span><span class="n">dev</span><span class="o">/</span><span class="n">ttyUSB0</span>
<span class="c1"># Set the serial baud-rate and other</span>
<span class="c1"># configuration parameters.</span>
<span class="n">speed</span> <span class="o">=</span> <span class="mi">115200</span>
<span class="n">databits</span> <span class="o">=</span> <span class="mi">8</span>
<span class="n">parity</span> <span class="o">=</span> <span class="n">none</span>
<span class="n">stopbits</span> <span class="o">=</span> <span class="mi">1</span>
</pre></div>
</div>
</div>
<div class="section" id="pipe-interface">
<span id="interfaces-pipe"></span><h2>Pipe Interface<a class="headerlink" href="#pipe-interface" title="Permalink to this headline"></a></h2>
<p>Using this interface, reticulum can use any program as an interface via <cite>stdin</cite> and
<cite>stdout</cite>. This can be used to easily create virtual interfaces, or to interface with
custom hardware or other systems.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Pipe</span> <span class="n">Interface</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">PipeInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="c1"># External command to execute</span>
<span class="n">command</span> <span class="o">=</span> <span class="n">netcat</span> <span class="o">-</span><span class="n">l</span> <span class="mi">5757</span>
<span class="c1"># Optional respawn delay, in seconds</span>
<span class="n">respawn_delay</span> <span class="o">=</span> <span class="mi">5</span>
</pre></div>
</div>
<p>Reticulum will write all packets to <cite>stdin</cite> of the <code class="docutils literal notranslate"><span class="pre">command</span></code> option, and will
continously read and scan its <cite>stdout</cite> for Reticulum packets. If <code class="docutils literal notranslate"><span class="pre">EOF</span></code> is reached,
Reticulum will try to respawn the program after waiting for <code class="docutils literal notranslate"><span class="pre">respawn_interval</span></code> seconds.</p>
</div>
<div class="section" id="kiss-interface">
<span id="interfaces-kiss"></span><h2>KISS Interface<a class="headerlink" href="#kiss-interface" title="Permalink to this headline"></a></h2>
<p>With the KISS interface, you can use Reticulum over a variety of packet
radio modems and TNCs, including <a class="reference external" href="https://unsigned.io/openmodem/">OpenModem</a>.
KISS interfaces can also be configured to periodically send out beacons
for station identification purposes.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Packet</span> <span class="n">Radio</span> <span class="n">KISS</span> <span class="n">Interface</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">KISSInterface</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="c1"># Serial port for the device</span>
<span class="n">port</span> <span class="o">=</span> <span class="o">/</span><span class="n">dev</span><span class="o">/</span><span class="n">ttyUSB1</span>
<span class="c1"># Set the serial baud-rate and other</span>
<span class="c1"># configuration parameters.</span>
<span class="n">speed</span> <span class="o">=</span> <span class="mi">115200</span>
<span class="n">databits</span> <span class="o">=</span> <span class="mi">8</span>
<span class="n">parity</span> <span class="o">=</span> <span class="n">none</span>
<span class="n">stopbits</span> <span class="o">=</span> <span class="mi">1</span>
<span class="c1"># Set the modem preamble.</span>
<span class="n">preamble</span> <span class="o">=</span> <span class="mi">150</span>
<span class="c1"># Set the modem TX tail.</span>
<span class="n">txtail</span> <span class="o">=</span> <span class="mi">10</span>
<span class="c1"># Configure CDMA parameters. These</span>
<span class="c1"># settings are reasonable defaults.</span>
<span class="n">persistence</span> <span class="o">=</span> <span class="mi">200</span>
<span class="n">slottime</span> <span class="o">=</span> <span class="mi">20</span>
<span class="c1"># You can configure the interface to send</span>
<span class="c1"># out identification on the channel with</span>
<span class="c1"># a set interval by configuring the</span>
<span class="c1"># following two parameters. The KISS</span>
<span class="c1"># interface will only ID if the set</span>
<span class="c1"># interval has elapsed since it&#39;s last</span>
<span class="c1"># actual transmission. The interval is</span>
<span class="c1"># configured in seconds.</span>
<span class="c1"># This option is commented out and not</span>
<span class="c1"># used by default.</span>
<span class="c1"># id_callsign = MYCALL-0</span>
<span class="c1"># id_interval = 600</span>
<span class="c1"># Whether to use KISS flow-control.</span>
<span class="c1"># This is useful for modems that have</span>
<span class="c1"># a small internal packet buffer, but</span>
<span class="c1"># support packet flow control instead.</span>
<span class="n">flow_control</span> <span class="o">=</span> <span class="n">false</span>
</pre></div>
</div>
</div>
<div class="section" id="ax-25-kiss-interface">
<span id="interfaces-ax25"></span><h2>AX.25 KISS Interface<a class="headerlink" href="#ax-25-kiss-interface" title="Permalink to this headline"></a></h2>
<p>If youre using Reticulum on amateur radio spectrum, you might want to
use the AX.25 KISS interface. This way, Reticulum will automatically
encapsulate its traffic in AX.25 and also identify your stations
transmissions with your callsign and SSID.</p>
<p>Only do this if you really need to! Reticulum doesnt need the AX.25
layer for anything, and it incurs extra overhead on every packet to
encapsulate in AX.25.</p>
<p>A more efficient way is to use the plain KISS interface with the
beaconing functionality described above.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Packet</span> <span class="n">Radio</span> <span class="n">AX</span><span class="o">.</span><span class="mi">25</span> <span class="n">KISS</span> <span class="n">Interface</span><span class="p">]]</span>
<span class="nb">type</span> <span class="o">=</span> <span class="n">AX25KISSInterface</span>
<span class="c1"># Set the station callsign and SSID</span>
<span class="n">callsign</span> <span class="o">=</span> <span class="n">NO1CLL</span>
<span class="n">ssid</span> <span class="o">=</span> <span class="mi">0</span>
<span class="c1"># Enable interface if you want use it!</span>
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
<span class="c1"># Serial port for the device</span>
<span class="n">port</span> <span class="o">=</span> <span class="o">/</span><span class="n">dev</span><span class="o">/</span><span class="n">ttyUSB2</span>
<span class="c1"># Set the serial baud-rate and other</span>
<span class="c1"># configuration parameters.</span>
<span class="n">speed</span> <span class="o">=</span> <span class="mi">115200</span>
<span class="n">databits</span> <span class="o">=</span> <span class="mi">8</span>
<span class="n">parity</span> <span class="o">=</span> <span class="n">none</span>
<span class="n">stopbits</span> <span class="o">=</span> <span class="mi">1</span>
<span class="c1"># Set the modem preamble. A 150ms</span>
<span class="c1"># preamble should be a reasonable</span>
<span class="c1"># default, but may need to be</span>
<span class="c1"># increased for radios with slow-</span>
<span class="c1"># opening squelch and long TX/RX</span>
<span class="c1"># turnaround</span>
<span class="n">preamble</span> <span class="o">=</span> <span class="mi">150</span>
<span class="c1"># Set the modem TX tail. In most</span>
<span class="c1"># cases this should be kept as low</span>
<span class="c1"># as possible to not waste airtime.</span>
<span class="n">txtail</span> <span class="o">=</span> <span class="mi">10</span>
<span class="c1"># Configure CDMA parameters. These</span>
<span class="c1"># settings are reasonable defaults.</span>
<span class="n">persistence</span> <span class="o">=</span> <span class="mi">200</span>
<span class="n">slottime</span> <span class="o">=</span> <span class="mi">20</span>
<span class="c1"># Whether to use KISS flow-control.</span>
<span class="c1"># This is useful for modems with a</span>
<span class="c1"># small internal packet buffer.</span>
<span class="n">flow_control</span> <span class="o">=</span> <span class="n">false</span>
</pre></div>
</div>
</div>
<div class="section" id="common-interface-options">
<span id="interfaces-options"></span><h2>Common Interface Options<a class="headerlink" href="#common-interface-options" title="Permalink to this headline"></a></h2>
<p>A number of general configuration options are available on most interfaces.
These can be used to control various aspects of interface behaviour.</p>
<blockquote>
<div><ul>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">enabled</span></code> option tells Reticulum whether or not
to bring up the interface. Defaults to <code class="docutils literal notranslate"><span class="pre">False</span></code>. For any
interface to be brought up, the <code class="docutils literal notranslate"><span class="pre">enabled</span></code> option
must be set to <code class="docutils literal notranslate"><span class="pre">True</span></code> or <code class="docutils literal notranslate"><span class="pre">Yes</span></code>.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">mode</span></code> option allows selecting the high-level behaviour
of the interface from a number of options.</div>
</div>
<blockquote>
<div><ul class="simple">
<li><p>The default value is <code class="docutils literal notranslate"><span class="pre">full</span></code>. In this mode, all discovery,
meshing and transport functionality is available.</p></li>
<li><p>In the <code class="docutils literal notranslate"><span class="pre">access_point</span></code> (or shorthand <code class="docutils literal notranslate"><span class="pre">ap</span></code>) mode, the
interface will operate as a network access point. In this
mode, announces will not be automatically broadcasted on
the interface, and paths to destinations on the interface
will have a much shorter expiry time. This mode is useful
for creating interfaces that are mostly quiet, unless when
someone is actually using them. An example of this could
be a radio interface serving a wide area, where users are
expected to connect momentarily, use the network, and then
disappear again.</p></li>
</ul>
</div></blockquote>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">outgoing</span></code> option sets whether an interface is allowed
to transmit. Defaults to <code class="docutils literal notranslate"><span class="pre">True</span></code>. If set to <code class="docutils literal notranslate"><span class="pre">False</span></code> or <code class="docutils literal notranslate"><span class="pre">No</span></code>
the interface will only receive data, and never transmit.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">network_name</span></code> option sets the virtual network name for
the interface. This allows multiple separate network segments
to exist on the same physical channel or medium.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">passphrase</span></code> option sets an authentication passphrase on
the interface. This option can be used in conjunction with the
<code class="docutils literal notranslate"><span class="pre">network_name</span></code> option, or be used alone.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ifac_size</span></code> option allows customising the length of the
Interface Authentication Codes carried by each packet on named
and/or authenticated network segments. It is set by default to
a size suitable for the interface in question, but can be set
to a custom size between 8 and 512 bits by using this option.
In normal usage, this option should not be changed from the
default.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">announce_cap</span></code> option lets you configure the maximum
bandwidth to allocate, at any given time, to propagating
announces and other network upkeep traffic. It is configured at
2% by default, and should normally not need to be changed. Can
be set to any value between <code class="docutils literal notranslate"><span class="pre">1</span></code> and <code class="docutils literal notranslate"><span class="pre">100</span></code>.</div>
</div>
<blockquote>
<div><p><em>If an interface exceeds its announce cap, it will queue announces
for later transmission. Reticulum will always prioritise propagating
announces from nearby nodes first. This ensures that the local
topology is prioritised, and that slow networks are not overwhelmed
by interconnected fast networks.</em></p>
<p><em>Destinations that are rapidly re-announcing will be down-prioritised
further. Trying to get “first-in-line” by announce spamming will have
the exact opposite effect: Getting moved to the back of the queue every
time a new announce from the excessively announcing destination is received.</em></p>
<p><em>This means that it is always beneficial to select a balanced
announce rate, and not announce more often than is actually necesarry
for your application to function.</em></p>
</div></blockquote>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">bitrate</span></code> option configures the interface bitrate.
Reticulum will use interface speeds reported by hardware, or
try to guess a suitable rate when the hardware doesnt report
any. In most cases, the automatically found rate should be
sufficient, but it can be configured by using the <code class="docutils literal notranslate"><span class="pre">bitrate</span></code>
option, to set the interface speed in <em>bits per second</em>.</div>
</div>
</li>
</ul>
</div></blockquote>
</div>
<div class="section" id="interface-modes">
<span id="interfaces-modes"></span><h2>Interface Modes<a class="headerlink" href="#interface-modes" title="Permalink to this headline"></a></h2>
<p>The optional <code class="docutils literal notranslate"><span class="pre">mode</span></code> setting is available on all interfaces, and allows
selecting the high-level behaviour of the interface from a number of modes.
These modes affect how Reticulum selects paths in the network, how announces
are propagated, how long paths are valid and how paths are discovered.</p>
<p>Configuring modes on interfaces is <strong>not</strong> strictly necessary, but can be useful
when building or connecting to more complex networks. If your Reticulum
instance is not running a Transport Node, it is rarely useful to configure
interface modes, and in such cases interfaces should generally be left in
the default mode.</p>
<blockquote>
<div><ul>
<li><div class="line-block">
<div class="line">The default mode is <code class="docutils literal notranslate"><span class="pre">full</span></code>. In this mode, all discovery,
meshing and transport functionality is activated.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">gateway</span></code> mode (or shorthand <code class="docutils literal notranslate"><span class="pre">gw</span></code>) also has all
discovery, meshing and transport functionality available,
but will additionally try to discover unknown paths on
behalf of other nodes residing on the <code class="docutils literal notranslate"><span class="pre">gateway</span></code> interface.
If Reticulum receives a path request for an unknown
destination, from a node on a <code class="docutils literal notranslate"><span class="pre">gateway</span></code> interface, it
will try to discover this path via all other active interfaces,
and forward the discovered path to the requestor if one is
found.</div>
</div>
<div class="line-block">
<div class="line">If you want to allow other nodes to widely resolve paths or connect
to a network via an interface, it might be useful to put it in this
mode. By creating a chain of <code class="docutils literal notranslate"><span class="pre">gateway</span></code> interfaces, other
nodes will be able to immediately discover paths to any
destination along the chain.</div>
</div>
<div class="line-block">
<div class="line"><em>Please note!</em> It is the interface <em>facing the clients</em> that
must be put into <code class="docutils literal notranslate"><span class="pre">gateway</span></code> mode for this to work, not
the interface facing the wider network (for this, the <code class="docutils literal notranslate"><span class="pre">boundary</span></code>
mode can be useful, though).</div>
</div>
</li>
<li><div class="line-block">
<div class="line">In the <code class="docutils literal notranslate"><span class="pre">access_point</span></code> (or shorthand <code class="docutils literal notranslate"><span class="pre">ap</span></code>) mode, the
interface will operate as a network access point. In this
mode, announces will not be automatically broadcasted on
the interface, and paths to destinations on the interface
will have a much shorter expiry time. In addition, path
requests from clients on the access point interface will
be handled in the same way as the <code class="docutils literal notranslate"><span class="pre">gateway</span></code> interface.</div>
</div>
<div class="line-block">
<div class="line">This mode is useful for creating interfaces that remain
quiet, until someone actually starts using them. An example
of this could be a radio interface serving a wide area,
where users are expected to connect momentarily, use the
network, and then disappear again.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">roaming</span></code> mode should be used on interfaces that are
roaming (physically mobile), seen from the perspective of
other nodes in the network. As an example, if a vehicle is
equipped with an external LoRa interface, and an internal,
WiFi-based interface, that serves devices that are moving
_with_ the vehicle, the external LoRa interface should be
configured as <code class="docutils literal notranslate"><span class="pre">roaming</span></code>, and the internal interface can
be left in the default mode. With transport enabled, such
a setup will allow all internal devices to reach each other,
and all other devices that are available on the LoRa side
of the network, when they are in range. Devices on the LoRa
side of the network will also be able to reach devices
internal to the vehicle, when it is in range. Paths via
<code class="docutils literal notranslate"><span class="pre">roaming</span></code> interfaces also expire faster.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The purpose of the <code class="docutils literal notranslate"><span class="pre">boundary</span></code> mode is to specify interfaces
that establish connectivity with network segments that are
significantly different than the one this node exists on.
As an example, if a Reticulum instance is part of a LoRa-based
network, but also has a high-speed connection to a
public Transport Node available on the Internet, the interface
connecting over the Internet should be set to <code class="docutils literal notranslate"><span class="pre">boundary</span></code> mode.</div>
</div>
</li>
</ul>
</div></blockquote>
<p>For a table describing the impact of all modes on announce propagation,
please see the <a class="reference internal" href="understanding.html#understanding-announcepropagation"><span class="std std-ref">Announce Propagation Rules</span></a> section.</p>
</div>
<div class="section" id="announce-rate-control">
<span id="interfaces-announcerates"></span><h2>Announce Rate Control<a class="headerlink" href="#announce-rate-control" title="Permalink to this headline"></a></h2>
<p>The built-in announce control mechanisms and the default <code class="docutils literal notranslate"><span class="pre">announce_cap</span></code>
option described above are sufficient most of the time, but in some cases, especially on fast
interfaces, it may be useful to control the target announce rate. Using the
<code class="docutils literal notranslate"><span class="pre">announce_rate_target</span></code>, <code class="docutils literal notranslate"><span class="pre">announce_rate_grace</span></code> and <code class="docutils literal notranslate"><span class="pre">announce_rate_penalty</span></code>
options, this can be done on a per-interface basis, and moderates the <em>rate at
which received announces are re-broadcasted to other interfaces</em>.</p>
<blockquote>
<div><ul>
<li><div class="line-block">
<div class="line">The <code class="docutils literal notranslate"><span class="pre">announce_rate_target</span></code> option sets the minimum amount of time,
in seconds, that should pass between received announces, for any one
destination. As an example, setting this value to <code class="docutils literal notranslate"><span class="pre">3600</span></code> means that
announces <em>received</em> on this interface will only be re-transmitted and
propagated to other interfaces once every hour, no matter how often they
are received.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The optional <code class="docutils literal notranslate"><span class="pre">announce_rate_grace</span></code> defines the number of times a destination
can violate the announce rate before the target rate is enforced.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">The optional <code class="docutils literal notranslate"><span class="pre">announce_rate_penalty</span></code> configures an extra amount of
time that is added to the normal rate target. As an example, if a penalty
of <code class="docutils literal notranslate"><span class="pre">7200</span></code> seconds is defined, once the rate target is enforced, the
destination in question will only have its announces propagated every
3 hours, until it lowers its actual announce rate to within the target.</div>
</div>
</li>
</ul>
</div></blockquote>
<p>These mechanisms, in conjunction with the <code class="docutils literal notranslate"><span class="pre">annouce_cap</span></code> mechanisms mentioned
above means that it is essential to select a balanced announce strategy for
your destinations. The more balanced you can make this decision, the easier
it will be for your destinations to make it into slower networks that many hops
away. Or you can prioritise only reaching high-capacity networks with more frequent
announces.</p>
<p>Current statistics and information about announce rates can be viewed using the
<code class="docutils literal notranslate"><span class="pre">rnpath</span> <span class="pre">-r</span></code> command.</p>
<p>It is important to note that there is no one right or wrong way to set up announce
rates. Slower networks will naturally tend towards using less frequent announces to
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 <em>co-exist</em> and interconnect.</p>
</div>
</div>
<div class="clearer"></div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h3><a href="index.html">Table of Contents</a></h3>
<ul>
<li><a class="reference internal" href="#">Supported Interfaces</a><ul>
<li><a class="reference internal" href="#auto-interface">Auto Interface</a></li>
<li><a class="reference internal" href="#i2p-interface">I2P Interface</a></li>
<li><a class="reference internal" href="#tcp-server-interface">TCP Server Interface</a></li>
<li><a class="reference internal" href="#tcp-client-interface">TCP Client Interface</a></li>
<li><a class="reference internal" href="#udp-interface">UDP Interface</a></li>
<li><a class="reference internal" href="#rnode-lora-interface">RNode LoRa Interface</a></li>
<li><a class="reference internal" href="#serial-interface">Serial Interface</a></li>
<li><a class="reference internal" href="#pipe-interface">Pipe Interface</a></li>
<li><a class="reference internal" href="#kiss-interface">KISS Interface</a></li>
<li><a class="reference internal" href="#ax-25-kiss-interface">AX.25 KISS Interface</a></li>
<li><a class="reference internal" href="#common-interface-options">Common Interface Options</a></li>
<li><a class="reference internal" href="#interface-modes">Interface Modes</a></li>
<li><a class="reference internal" href="#announce-rate-control">Announce Rate Control</a></li>
</ul>
</li>
</ul>
<h4>Previous topic</h4>
<p class="topless"><a href="networks.html"
title="previous chapter">Building Networks</a></p>
<h4>Next topic</h4>
<p class="topless"><a href="understanding.html"
title="next chapter">Understanding Reticulum</a></p>
<div role="note" aria-label="source link">
<h3>This Page</h3>
<ul class="this-page-menu">
<li><a href="_sources/interfaces.rst.txt"
rel="nofollow">Show Source</a></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="related" role="navigation" aria-label="related navigation">
<h3>Navigation</h3>
<ul>
<li class="right" style="margin-right: 10px">
<a href="genindex.html" title="General Index"
>index</a></li>
<li class="right" >
<a href="understanding.html" title="Understanding Reticulum"
>next</a> |</li>
<li class="right" >
<a href="networks.html" title="Building Networks"
>previous</a> |</li>
<li class="nav-item nav-item-0"><a href="index.html">Reticulum Network Stack 0.3.7 beta documentation</a> &#187;</li>
<li class="nav-item nav-item-this"><a href="">Supported Interfaces</a></li>
</ul>
</div>
<div class="footer" role="contentinfo">
&#169; Copyright 2022, Mark Qvist.
Created using <a href="https://www.sphinx-doc.org/">Sphinx</a> 4.0.1.
</div>
</body>
</html>
+284
View File
@@ -0,0 +1,284 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Building Networks &#8212; Reticulum Network Stack 0.3.7 beta documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/classic.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
<link rel="index" title="Index" href="genindex.html" />
<link rel="search" title="Search" href="search.html" />
<link rel="next" title="Supported Interfaces" href="interfaces.html" />
<link rel="prev" title="Using Reticulum on Your System" href="using.html" />
</head><body>
<div class="related" role="navigation" aria-label="related navigation">
<h3>Navigation</h3>
<ul>
<li class="right" style="margin-right: 10px">
<a href="genindex.html" title="General Index"
accesskey="I">index</a></li>
<li class="right" >
<a href="interfaces.html" title="Supported Interfaces"
accesskey="N">next</a> |</li>
<li class="right" >
<a href="using.html" title="Using Reticulum on Your System"
accesskey="P">previous</a> |</li>
<li class="nav-item nav-item-0"><a href="index.html">Reticulum Network Stack 0.3.7 beta documentation</a> &#187;</li>
<li class="nav-item nav-item-this"><a href="">Building Networks</a></li>
</ul>
</div>
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
<div class="section" id="building-networks">
<span id="networks-main"></span><h1>Building Networks<a class="headerlink" href="#building-networks" title="Permalink to this headline"></a></h1>
<p>This chapter will provide you with the knowledge needed to build networks with
Reticulum, which can often be easier than using traditional stacks, since you
dont have to worry about coordinating addresses, subnets and routing for an
entire network that you might not know how will evolve in the future. With
Reticulum, you can simply add more segments to your network when it becomes
necesarry, and Reticulum will handle the convergence of the entire network
automatically.</p>
<div class="section" id="concepts-overview">
<h2>Concepts &amp; Overview<a class="headerlink" href="#concepts-overview" title="Permalink to this headline"></a></h2>
<p>There are important points that need to be kept in mind when building networks
with Reticulum:</p>
<blockquote>
<div><ul>
<li><div class="line-block">
<div class="line">In a Reticulum network, any node can autonomously generate as many adresses
(called <em>destinations</em> 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.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">Reticulum was designed to handle both very small, and very large networks.
While the adress space can support billions of endpoints, Reticulum is
also very useful when just a few devices needs to communicate.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">Low-bandwidth networks, like LoRa and packet radio, can interoperate and
interconnect with much larger and higher bandwidth networks without issue.
Reticulum automatically manages the flow of information to and from various
network segments, and when bandwidth is limited, local traffic is prioritised.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">Reticulum provides sender/initiator anonymity by default. There is no way
to filter traffic or discriminate it based on the source of the traffic.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">All traffic is encrypted using ephemeral keys generated by an Elliptic Curve
Diffie-Hellman key exchange on Curve25519. There is no way to inspect traffic
contents, and no way to prioritise or throttle certain kinds of traffic.
All transport and routing layers are thus completely agnostic to traffic type,
and will pass all traffic equally.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">Reticulum can function both with and without infrastructure. When <em>transport
nodes</em> are available, they can route traffic over multiple hops for other
nodes, and will function as a distributed cryptographic keystore. When there
is no transport nodes available, all nodes that are within communication range
can still communicate.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">Every node can become a transport node, simply by enabling it in its
configuration, but there is no need for every node on the network to be a
transport node. Letting every node be a transport node will in most cases
degrade the performance and reliability of the network.</div>
</div>
<blockquote>
<div><p><em>In general terms, if a node is stationary, well-connected and kept running
most of the time, it is a good candidate to be a transport node. For optimal
performance, a network should contain the amount of transport nodes that
provides connectivity to the intended area / topography, and not many more
than that.</em></p>
</div></blockquote>
</li>
<li><div class="line-block">
<div class="line">Reticulum is designed to work reliably in open, trustless environments. This
means you can use it to create open-access networks, where participants can
join and leave in an free and unorganised manner. This property allows an
entirely new, and so far, mostly unexplored class of networked applications,
where networks, and the information flow within them can form and dissolve
organically.</div>
</div>
</li>
<li><div class="line-block">
<div class="line">You can just as easily create closed networks, since Reticulum allows you to
add authentication to any interface. This means you can restrict access on
any interface type, even when using legacy devices, such as modems. You can
also mix authenticated and open interfaces on the same system. See the
<a class="reference internal" href="interfaces.html#interfaces-options"><span class="std std-ref">Common Interface Options</span></a> section of the <a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">Interfaces</span></a>
chapter of this manual for information on how to set up interface authentication.</div>
</div>
</li>
</ul>
</div></blockquote>
<p>Reticulum allows you to mix very different kinds of networking mediums into a
unified mesh, or to keep everything within one medium. You could build a “virtual
network” running entirely over the Internet, where all nodes communicate over TCP
and UDP “channels”. You could also build such a network using other already-established
communications channels as the underlying carrier for Reticulum.</p>
<p>However, most real-world networks will probably involve either some form of
wireless or direct hardline communications. To allow Reticulum to communicate
over any type of medium, you must specify it in the configuration file, by default
located at <code class="docutils literal notranslate"><span class="pre">~/.reticulum/config</span></code>. See the <a class="reference internal" href="interfaces.html#interfaces-main"><span class="std std-ref">Supported Interfaces</span></a>
chapter of this manual for interface configuration examples.</p>
<p>Any number of interfaces can be configured, and Reticulum will automatically
decide which are suitable to use in any given situation, depending on where
traffic needs to flow.</p>
</div>
<div class="section" id="example-scenarios">
<h2>Example Scenarios<a class="headerlink" href="#example-scenarios" title="Permalink to this headline"></a></h2>
<p>This section illustrates a few example scenarios, and how they would, in general
terms, be planned, implemented and configured.</p>
<div class="section" id="interconnected-lora-sites">
<h3>Interconnected LoRa Sites<a class="headerlink" href="#interconnected-lora-sites" title="Permalink to this headline"></a></h3>
<p>An organisation wants to provide communication and information services to its
members, which are located mainly in three separate areas. Three suitable hill-top
locations are found, where the organisation can install equipment: Site A, B and C.</p>
<p>Since the amount of data that needs to be exchanged between users is mainly text-
based, the bandwidth requirements are low, and LoRa radios are chosen to connect
users to the network.</p>
<p>Due to the hill-top locations found, there is radio line-of-sight between site A
and B, and also between site B and C. Because of this, the organisation does not
need to use the Internet to interconnect the sites, but purchases four Point-to-Point
WiFi based radios for interconnecting the sites.</p>
<p>At each site, a Raspberry Pi is installed to function as a gateway. A LoRa radio
is connected to the Pi with a USB cable, and the WiFi radio is connected to the
ethernet port of the Pi. At site B, two WiFi radios are needed to be able to reach
both site A and site C, so an extra ethernet adapter is connected to the Pi in
this location.</p>
<p>Once the hardware has been installed, Reticulum is installed on all the Pis, and at
site A and C, one interface is added for the LoRa radio, as well as one for the WiFi
radio. At site B, an interface for the LoRa radio, and one interface for each WiFi
radio is added to the Reticulum configuration file. The transport node option is
enabled in the configuration of all three gateways.</p>
<p>The network is now operational, and ready to serve users across all three areas.
The organisation prepares a LoRa radio that is supplied to the end users, along
with a Reticulum configuration file, that contains the right parameters for
communicating with the LoRa radios installed at the gateway sites.</p>
<p>Once users connect to the network, anyone will be able to communicate with anyone
else across all three sites.</p>
</div>
<div class="section" id="bridging-over-the-internet">
<h3>Bridging Over the Internet<a class="headerlink" href="#bridging-over-the-internet" title="Permalink to this headline"></a></h3>
<p>As the organisation grows, several new communities form in places too far away
from the core network to be reachable over WiFi links. New gateways similar to those
previously installed are set up for the new communities at the new sites D and E, but
they are islanded from the core network, and only serve the local users.</p>
<p>After investigating the options, it is found that it is possible to install an
Internet connection at site A, and an interface on the Internet connection is
configured for Reticulum on the Raspberry Pi at site A.</p>
<p>A member of the organisation at site D, named Dori, is willing to help by sharing
the Internet connection she already has in her home, and is able to leave a Raspberry
Pi running. A new Reticulum interface is configured on her Pi, connecting to the newly
enabled Internet interface on the gateway at site A. Dori is now connected to both
all the nodes at her own local site (through the hill-top LoRa gateway), and all the
combined users of sites A, B and C. She then enables transport on her node, and
traffic from site D can now reach everyone at site A, B and C, and vice versa.</p>
</div>
<div class="section" id="growth-and-convergence">
<h3>Growth and Convergence<a class="headerlink" href="#growth-and-convergence" title="Permalink to this headline"></a></h3>
<p>As the organisation grows, more gateways are added to keep up with the growing user
base. Some local gateways even add VHF radios and packet modems to reach outlying users
and communities that are out of reach for the LoRa radios and WiFi backhauls.</p>
<p>As more sites, gateways and users are connected, the amount of coordination required
is kept to a minimum. If one community wants to add connectivity to the next one
over, it can simply be done without having to involve everyone or coordinate address
space or routing tables.</p>
<p>With the added geographical coverage, the operators at site A one day find that
the original internet bridged interfaces are no longer utilised. The network has
converged to be completely self-connected, and the sites that were once poorly
connected outliers are now an integral part of the network.</p>
</div>
</div>
</div>
<div class="clearer"></div>
</div>
</div>
</div>
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
<div class="sphinxsidebarwrapper">
<h3><a href="index.html">Table of Contents</a></h3>
<ul>
<li><a class="reference internal" href="#">Building Networks</a><ul>
<li><a class="reference internal" href="#concepts-overview">Concepts &amp; Overview</a></li>
<li><a class="reference internal" href="#example-scenarios">Example Scenarios</a><ul>
<li><a class="reference internal" href="#interconnected-lora-sites">Interconnected LoRa Sites</a></li>
<li><a class="reference internal" href="#bridging-over-the-internet">Bridging Over the Internet</a></li>
<li><a class="reference internal" href="#growth-and-convergence">Growth and Convergence</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h4>Previous topic</h4>
<p class="topless"><a href="using.html"
title="previous chapter">Using Reticulum on Your System</a></p>
<h4>Next topic</h4>
<p class="topless"><a href="interfaces.html"
title="next chapter">Supported Interfaces</a></p>
<div role="note" aria-label="source link">
<h3>This Page</h3>
<ul class="this-page-menu">
<li><a href="_sources/networks.rst.txt"
rel="nofollow">Show Source</a></li>
</ul>
</div>
<div id="searchbox" style="display: none" role="search">
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="submit" value="Go" />
</form>
</div>
</div>
<script>$('#searchbox').show(0);</script>
</div>
</div>
<div class="clearer"></div>
</div>
<div class="related" role="navigation" aria-label="related navigation">
<h3>Navigation</h3>
<ul>
<li class="right" style="margin-right: 10px">
<a href="genindex.html" title="General Index"
>index</a></li>
<li class="right" >
<a href="interfaces.html" title="Supported Interfaces"
>next</a> |</li>
<li class="right" >
<a href="using.html" title="Using Reticulum on Your System"
>previous</a> |</li>
<li class="nav-item nav-item-0"><a href="index.html">Reticulum Network Stack 0.3.7 beta documentation</a> &#187;</li>
<li class="nav-item nav-item-this"><a href="">Building Networks</a></li>
</ul>
</div>
<div class="footer" role="contentinfo">
&#169; Copyright 2022, Mark Qvist.
Created using <a href="https://www.sphinx-doc.org/">Sphinx</a> 4.0.1.
</div>
</body>
</html>
Binary file not shown.

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