Compare commits
812 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5420d3be3 | |||
| 50b5ab80c4 | |||
| e6371d74b5 | |||
| 0ab38faeac | |||
| b0444104cc | |||
| 4757d6ee87 | |||
| 1780965ef8 | |||
| aaa88e9b7d | |||
| 17ce91a4a2 | |||
| 08751a762a | |||
| 77c0beecf2 | |||
| 28bcf6a8ac | |||
| 61004b4dfb | |||
| e5c22b8a3f | |||
| 001d0f30aa | |||
| fbe4bb03d1 | |||
| 3469b6beb8 | |||
| c696efe0bc | |||
| d0ca61f373 | |||
| 350687eda9 | |||
| d898641e6a | |||
| db576d73bb | |||
| 5fcdd17665 | |||
| e8f2bd9b0c | |||
| 0ff51fed44 | |||
| 6e25f96024 | |||
| ad228fb3b3 | |||
| a61b20a066 | |||
| a49b04af21 | |||
| f030cf6f22 | |||
| 9e7641d2d3 | |||
| c909871fb7 | |||
| 47f60b0320 | |||
| 6797909d90 | |||
| fd6d8ffff8 | |||
| 06de7f4a3d | |||
| 7221becd35 | |||
| a51f5f2eaf | |||
| 9e8d71ddaf | |||
| 9bc55a9047 | |||
| 3e7ab5136e | |||
| d2cf3c2a7e | |||
| 77519f1a0c | |||
| e869b3cac9 | |||
| a2878f1722 | |||
| 748a7290a9 | |||
| 6e80a553c8 | |||
| ec7aa44a17 | |||
| 4fa335639c | |||
| 67195c0b14 | |||
| ad1e6a41ee | |||
| a56d93fc1e | |||
| b8aa6a3e44 | |||
| 1709cd929a | |||
| 4f4961257c | |||
| 1b48f43a0d | |||
| e5d446a54e | |||
| 0af768e742 | |||
| 1a7d20a8d6 | |||
| ec4f4d5a83 | |||
| 8cefa4b2a9 | |||
| 2331f1ea3e | |||
| be7dafa30c | |||
| 3e20cb1b67 | |||
| 097e136662 | |||
| e3a716224d | |||
| 80dc567a53 | |||
| c6576d6504 | |||
| 89d5d9517d | |||
| dc315653c0 | |||
| 746b403890 | |||
| fc619460f0 | |||
| cd0f82d9ad | |||
| 330c2aacac | |||
| 63da084bbe | |||
| cbbd8221ee | |||
| 1d18d53052 | |||
| ceccf3153b | |||
| bde33e7d84 | |||
| 93330d96a0 | |||
| d93ce62878 | |||
| eafa4aefbb | |||
| 53df2fa5e0 | |||
| abc657806d | |||
| a0f219f7f4 | |||
| 47eba03a4b | |||
| 3289cd1299 | |||
| ab5fcd7a5b | |||
| 45494f21aa | |||
| 5d677d2fb7 | |||
| 808082e300 | |||
| 97cfdfd023 | |||
| 9b15cf2295 | |||
| eaa68c2d04 | |||
| ac5ca78c77 | |||
| 5b17dbdfd6 | |||
| d4ed20c7d5 | |||
| a5093ea8f0 | |||
| f5cf438abd | |||
| bf6e73e163 | |||
| 503f475ca5 | |||
| 8506118aee | |||
| dfa295a90a | |||
| 3ace1583da | |||
| c62b66195d | |||
| b724836d2b | |||
| 1e1b9dc79e | |||
| c668a51e39 | |||
| 09b34d34c6 | |||
| 54e18e41c5 | |||
| 5550bca040 | |||
| f7a02351d4 | |||
| 3125b99043 | |||
| 158765abb7 | |||
| 81aa9ac5b6 | |||
| 55f5842587 | |||
| 38dd63a99a | |||
| 558cd6c4a7 | |||
| 15e6a1bfde | |||
| c1087e62fd | |||
| 9d924dcd6d | |||
| 163d2ed157 | |||
| 68f07ddd38 | |||
| d956b93c13 | |||
| 3036305662 | |||
| ee603ce68e | |||
| 989513cb46 | |||
| 7e52c37580 | |||
| 0984f92fa2 | |||
| 2ab2d8e9df | |||
| b828e0e858 | |||
| d4dd706bba | |||
| ed30fa3e0a | |||
| 5e2b3df623 | |||
| ae7dffdfc0 | |||
| 32b5c7a3af | |||
| 8b08658b7f | |||
| ee79c3a732 | |||
| 0e5f4aa08a | |||
| ec0407e5c8 | |||
| db1380c413 | |||
| 7e3979dac0 | |||
| c1b6bde4a7 | |||
| 8df89cc2d0 | |||
| 19adadf4cf | |||
| c30feb3fc2 | |||
| 4c81589d5b | |||
| c014357e24 | |||
| ec41dc1a03 | |||
| 463dfa6fb4 | |||
| 0354b5969d | |||
| fc225bd55d | |||
| 67562126fc | |||
| 9319d613f5 | |||
| 014994a788 | |||
| 0f8efe3de1 | |||
| 274a8ca76a | |||
| ea3ad6b287 | |||
| f095b9cb8e | |||
| 6f8d3e882a | |||
| aabb763cea | |||
| 04d2626809 | |||
| 823bfd537c | |||
| 434ebd2954 | |||
| 44782c3429 | |||
| 890846fa8d | |||
| 36c761e8dd | |||
| 4a4b625075 | |||
| 4223203134 | |||
| e6966fe19a | |||
| e81c22cf53 | |||
| c02e59e3ab | |||
| 5d5abf352b | |||
| ec9bb33d16 | |||
| f3e836cec8 | |||
| 8a50528111 | |||
| 9523595282 | |||
| a762af035a | |||
| 760ab981d0 | |||
| 7b43ff0cef | |||
| 996161e2f4 | |||
| bf633bba5d | |||
| 8337a5945d | |||
| a736b3adfc | |||
| 25127cd3c9 | |||
| ebf084cff0 | |||
| cd8fe95d91 | |||
| e2efc61208 | |||
| 5de63d5bf2 | |||
| c9d744f88a | |||
| 18e0dbddfa | |||
| 52c816cb27 | |||
| 582d2b91f5 | |||
| 28a0dbb0e0 | |||
| 2895806541 | |||
| 5b8de73143 | |||
| 212af2f43b | |||
| 1282061701 | |||
| 49dba483a9 | |||
| ebec63487f | |||
| 9373819234 | |||
| 04925d8004 | |||
| 4284084fef | |||
| 63ad2afe3f | |||
| 61712d322a | |||
| 3599066356 | |||
| 18c2a38b97 | |||
| f55004a574 | |||
| 1768ddc459 | |||
| d002a75f34 | |||
| 0b6d239551 | |||
| 926b811a84 | |||
| 2bc8e11ad5 | |||
| f5412f5c0b | |||
| 5470f752b4 | |||
| 48c006a94c | |||
| 8445417661 | |||
| 30248854ed | |||
| f34bc75588 | |||
| 3b23e2f37d | |||
| 7417cf5947 | |||
| 60d8da843c | |||
| f9667fd684 | |||
| d9269c6047 | |||
| 6521f839cd | |||
| d63bbcdc0a | |||
| c36c7186de | |||
| 6fec76205c | |||
| 715f4d9fcb | |||
| 8d7857c4e2 | |||
| c9a2b45368 | |||
| c57d927660 | |||
| 8d98c8751a | |||
| 527f6cc906 | |||
| a0d61f6441 | |||
| c5687f190b | |||
| 44d1f6d0e5 | |||
| ac09bc3567 | |||
| a41bce012b | |||
| 83a2999d29 | |||
| 4465fa9882 | |||
| ce974db084 | |||
| e6c1dc075b | |||
| 9602f67b06 | |||
| ef798e0d54 | |||
| 5cd8d229fb | |||
| d4808b7ff1 | |||
| 3dc8729e70 | |||
| f500a063dc | |||
| eca1e53b55 | |||
| 53226d7035 | |||
| 7363c9c821 | |||
| bb8b8b4f81 | |||
| 0f0f459321 | |||
| df887f6d63 | |||
| b526e3554c | |||
| 903ab53fc9 | |||
| f461a7827b | |||
| 62091b28b0 | |||
| 48045856bf | |||
| 6ba5efcb42 | |||
| a505441b98 | |||
| 976e5543e1 | |||
| fcc7b50ac6 | |||
| 72971d1aef | |||
| 9a8d46ab21 | |||
| 8adab7ee7d | |||
| b5bde99322 | |||
| 560c8e164c | |||
| e059363f1d | |||
| 4930477b99 | |||
| 312489e4dc | |||
| 43d8fdb423 | |||
| 1c56385473 | |||
| 787af92ade | |||
| 131dbd2813 | |||
| 9df81ce365 | |||
| 490a56450a | |||
| 52a5156304 | |||
| 538e7320fd | |||
| 2d351a59e9 | |||
| 2269d6cef9 | |||
| 813edc8b17 | |||
| 099e344996 | |||
| 42319a092d | |||
| cdee3b6191 | |||
| e41d8ff296 | |||
| 946bea8825 | |||
| ba856ea1c4 | |||
| 9a97195b8c | |||
| 3e4172b697 | |||
| 66163776c2 | |||
| 3dbde726c1 | |||
| 97ae4d74b3 | |||
| c71ece6b8e | |||
| 1e45a002e1 | |||
| 68e64523b5 | |||
| d9e6145034 | |||
| a91e67129e | |||
| 76362bad4a | |||
| 421b5ef32e | |||
| 8d61ee8a81 | |||
| 2329181c88 | |||
| 8ea0dc65c4 | |||
| bba67836f0 | |||
| a666bb6e73 | |||
| 7b7ebbec90 | |||
| 8b3523dee0 | |||
| 2901ed2bae | |||
| 34010c94d1 | |||
| a4b5248a4c | |||
| 75272d77a5 | |||
| d4ad4589dd | |||
| 8d45ad36eb | |||
| 2a0d411869 | |||
| b9421347ef | |||
| ffec78d49a | |||
| 356ae378f9 | |||
| 28e3919dbd | |||
| 58a19610c4 | |||
| 50b1eae380 | |||
| c119ef4273 | |||
| b506ca94d0 | |||
| a072a5b074 | |||
| 3a580e74de | |||
| 9a20a3929a | |||
| fe054fd03c | |||
| 4524a17e67 | |||
| 8a82d6bfeb | |||
| 971f5ffadd | |||
| 6a392fdb0f | |||
| b42e075be0 | |||
| 4bc8a0b69b | |||
| 9ef10a7b3e | |||
| 320704f812 | |||
| c5e5986b89 | |||
| 5c6ee07d66 | |||
| 3eb8d92028 | |||
| ef3baf2cd9 | |||
| f2f936d846 | |||
| 6599e210de | |||
| d21dda2830 | |||
| 6ac393bbcd | |||
| 0c04663942 | |||
| bfa216de54 | |||
| a4b1606921 | |||
| ad0db9c95c | |||
| 2fdcbec860 | |||
| dd889d16d4 | |||
| a11f14e75f | |||
| c32086c6f1 | |||
| 9d744e2317 | |||
| d64064691a | |||
| 54eaff203f | |||
| 2bf75f60bc | |||
| 3f64141455 | |||
| b4ac3df2d0 | |||
| 8193f3621c | |||
| 5166596375 | |||
| 063ea2bb7a | |||
| 625db2622d | |||
| a8bc468e21 | |||
| 95c4269869 | |||
| 65a40aefb6 | |||
| a840bd4aaf | |||
| 7f2154110c | |||
| 9bc957e442 | |||
| 6d5ef3a511 | |||
| dec9145d65 | |||
| b3536f16e8 | |||
| 4e21b6f3b9 | |||
| 31e0939657 | |||
| bd9aa2954b | |||
| 3a5ee15dd8 | |||
| 166b00b6bf | |||
| 2413add00d | |||
| 169d1921be | |||
| 7be6a0e000 | |||
| d3b8c1c829 | |||
| 8ee11ac32c | |||
| cf87b1352a | |||
| 219d717afb | |||
| e8d1897edd | |||
| bce37fe8c0 | |||
| 0c95d720db | |||
| 96527380c3 | |||
| 035a44e34d | |||
| 59bb09426c | |||
| 6ac07989b0 | |||
| f1d6cda337 | |||
| 4aa60243a7 | |||
| eb4fc3362a | |||
| 849bd1bdad | |||
| cdce0c4223 | |||
| 4e16e6ac0e | |||
| 9e054ae71d | |||
| 2fad5464da | |||
| 3c4783b25e | |||
| 5feb833573 | |||
| 60e6b712d2 | |||
| a1be97bd69 | |||
| 07ff9fc663 | |||
| 2ef87a5e70 | |||
| e3948526fe | |||
| 2943d59042 | |||
| 1335ffd528 | |||
| 4e783ced31 | |||
| 228667578e | |||
| 6ded42edd7 | |||
| d1a150329a | |||
| 893dc2877c | |||
| 86224ef387 | |||
| 794cac98fe | |||
| cfdba51640 | |||
| c4ecbf29cb | |||
| c80289987c | |||
| 9371f857a8 | |||
| 4fdb9dda40 | |||
| c4705fd594 | |||
| 30228d12f7 | |||
| 1cee0a2619 | |||
| df92fb1bcf | |||
| 3a163c6f09 | |||
| 1f6560619e | |||
| b994db3745 | |||
| 173a534572 | |||
| fc7268a8ff | |||
| 0049c98684 | |||
| 3ef6c06b51 | |||
| 0bb1108771 | |||
| ba2feaa211 | |||
| 097d2b0dd9 | |||
| bb0ce4faca | |||
| 5915228f5b | |||
| 0b66649158 | |||
| e28dd6e14a | |||
| 0a15b4c6c1 | |||
| 62db09571d | |||
| 444ae0206b | |||
| 4b07e30b9d | |||
| 583e65419e | |||
| 1564930a51 | |||
| b81b1de4eb | |||
| 746a38f818 | |||
| c230eceaa6 | |||
| 09d9285104 | |||
| 3551662187 | |||
| f7f34e0ea3 | |||
| 43fc2a6c92 | |||
| b17175dfef | |||
| 1103784997 | |||
| d2feb8b136 | |||
| f595648a9b | |||
| b06f5285c5 | |||
| 8330f70a27 | |||
| 15e10b9435 | |||
| b91c852330 | |||
| 75acdf5902 | |||
| dae40f2684 | |||
| 4edacf82f3 | |||
| 4b0a0668a5 | |||
| a52af17123 | |||
| 0b0a3313c5 | |||
| 34af2e7af7 | |||
| 12bf7977d2 | |||
| b69b939d6f | |||
| b5556f664b | |||
| f804ba0263 | |||
| 84a1ab0ca3 | |||
| 465695b9ae | |||
| a999a4a250 | |||
| cbb5d99280 | |||
| 64f5192c79 | |||
| d223ebc8c0 | |||
| c28f413fe6 | |||
| 92e5f65887 | |||
| b977f33df6 | |||
| 589fcb8201 | |||
| e5427d70ac | |||
| 2f5381b307 | |||
| 11baace08d | |||
| a4d5b5cb17 | |||
| 9cb181690e | |||
| ff6604290e | |||
| 2dbd3cbc0f | |||
| 2a11097cac | |||
| c0e3181ae3 | |||
| 5a0316ae7f | |||
| 177bb62610 | |||
| 7cd3cde398 | |||
| 29bdcea616 | |||
| d9460c43ad | |||
| fb02e980db | |||
| 4947463440 | |||
| 5565349255 | |||
| 1b7b131adc | |||
| ace0d997d4 | |||
| 798c252284 | |||
| 7da22c8580 | |||
| eefbb89cde | |||
| 18f50ff1ae | |||
| 05e97ac0db | |||
| c2c3a144d2 | |||
| ea369015ee | |||
| 9745842862 | |||
| 246289c52d | |||
| ff71cb2f98 | |||
| 5ca1ef1777 | |||
| 2b764b4af8 | |||
| a62843cd75 | |||
| 633435390d | |||
| 1e207ef972 | |||
| 35e9a0b38a | |||
| 3d7f3825fb | |||
| 04b67a545d | |||
| 61c2fbd0da | |||
| 1aba4ec43a | |||
| 841a3daa26 | |||
| d98f03f245 | |||
| 878e67f69d | |||
| e582a6d6d1 | |||
| a948afb816 | |||
| 86a294388f | |||
| 429a0b1bd3 | |||
| ee8bb42633 | |||
| c659388a2c | |||
| eaa8199988 | |||
| 4f890e7e8a | |||
| a37e039424 | |||
| 8e1e2a9c54 | |||
| e4f94c9d0b | |||
| b007530123 | |||
| 4066bba303 | |||
| 8951517d01 | |||
| ae1d962b9b | |||
| a2caa47334 | |||
| 9f43da9105 | |||
| 038c696db9 | |||
| 8fa6ec144c | |||
| a8ccff7c55 | |||
| a5783da407 | |||
| bec3cee425 | |||
| b15bd19de5 | |||
| 38390fd021 | |||
| 40e0eee64f | |||
| af4cbb1baf | |||
| d3f4192fe3 | |||
| 47ef62ac11 | |||
| d15ddc7a49 | |||
| d67c8eb1cd | |||
| f4de5d5199 | |||
| 34e42988ea | |||
| 81d5d41149 | |||
| 6b3f3a37f0 | |||
| 60a604f635 | |||
| 55a2daf379 | |||
| 2dbde13321 | |||
| 6620dcde6b | |||
| 60966d5bb1 | |||
| ea22a53bf2 | |||
| 7b9526b4ed | |||
| 676074187a | |||
| 5dd2c31caf | |||
| 2db400a1a0 | |||
| b68dbaf15e | |||
| 84febcdf95 | |||
| c972ef90c8 | |||
| 19a74e3130 | |||
| 5ba789f782 | |||
| 58b5501e17 | |||
| b584832b8f | |||
| fc0cf17c4d | |||
| 001dd369ec | |||
| 9ce2ea4a5c | |||
| eec8814c22 | |||
| 7a6ed68482 | |||
| cd9e23f2de | |||
| ffa84de0bc | |||
| 89d3cdba17 | |||
| 2ba5843f22 | |||
| c4d0f08767 | |||
| db1cdec2a2 | |||
| 1eea1a6a22 | |||
| 4a69ce5a98 | |||
| 8d653cba9b | |||
| a6126a6bc5 | |||
| 957c2b3bc1 | |||
| 494bde4e79 | |||
| 5e39136dff | |||
| 4b26a86a73 | |||
| 43a6e280c0 | |||
| 237a45b2ca | |||
| b161650ced | |||
| 24975eac31 | |||
| 5d1ff36565 | |||
| 628777900e | |||
| 12e87425dc | |||
| 873f049e20 | |||
| 2ea963ed03 | |||
| 1d1276d6dd | |||
| 83741724b0 | |||
| a4143cfe6d | |||
| 3d645ae2f4 | |||
| 5ba125c801 | |||
| badb392898 | |||
| c0e1ce8d86 | |||
| 0bc248c5e4 | |||
| 798dfb1727 | |||
| a451b987aa | |||
| f01074e5b8 | |||
| 0e12442a28 | |||
| a4e8489a34 | |||
| 276b6fbd22 | |||
| 52ab08c289 | |||
| 38236366cf | |||
| af3cc3c5dd | |||
| 35ed1f950c | |||
| c050ef945e | |||
| bed71fa3f8 | |||
| cf125daf5c | |||
| 9f425c2e8d | |||
| 0dc78241ac | |||
| 01e963e891 | |||
| b3731524ac | |||
| 67c7395ea7 | |||
| fddf36a920 | |||
| 4f561a8c0c | |||
| 778d6105c1 | |||
| 60c94dc9b6 | |||
| f71395e449 | |||
| 1abacca9bf | |||
| 40281d5403 | |||
| e0da489156 | |||
| 2dcf1350e7 | |||
| 1e280611ce | |||
| f1d107846f | |||
| cc951dcb53 | |||
| b5856a3706 | |||
| ed3479da9a | |||
| 5e15f421b7 | |||
| 0a9366ba6e | |||
| cf31435f39 | |||
| 9f58860842 | |||
| 875348383d | |||
| f79f190525 | |||
| 5e27a81412 | |||
| 0dcb009579 | |||
| 943f76804b | |||
| 8bbe6ae3ae | |||
| f0d85dd078 | |||
| f85dda1829 | |||
| 91e064cdf1 | |||
| fb4e53f6e3 | |||
| 03340ed091 | |||
| ed424fa0a2 | |||
| 406ab216d1 | |||
| 00d8a2064d | |||
| 38b920e393 | |||
| 1ed000c4d9 | |||
| d360958d10 | |||
| fcdb455d73 | |||
| 575639b721 | |||
| 492573f9fe | |||
| c5d30f8ee6 | |||
| 3c4791a622 | |||
| 803a5736c9 | |||
| 267ffbdf5f | |||
| 52028aa44c | |||
| c5248d53d6 | |||
| 2d2f0947ac | |||
| 4fa616a326 | |||
| 136713eec1 | |||
| 0fd75cb819 | |||
| ea52153969 | |||
| 3854781028 | |||
| ec2805f357 | |||
| b5cb3a65dd | |||
| c79cb3aa20 | |||
| 8bff119691 | |||
| 5e0b2c5b42 | |||
| 8908022b88 | |||
| b0dda0ed86 | |||
| 6ae72d4225 | |||
| 0a188a2d39 | |||
| 036abb28fe | |||
| a732767a28 | |||
| 32a1261d98 | |||
| 27c5af3bbc | |||
| 5872108da3 | |||
| 8f6c6b76de | |||
| 99db625c62 | |||
| fdf6a31cbd | |||
| 75f353d7e2 | |||
| 82f204fb44 | |||
| 8d4492ecfd | |||
| f8a53458d6 | |||
| 4229837170 | |||
| 4be2ae6c70 | |||
| dbdeba2fe0 | |||
| 7e34b61f37 | |||
| bf726ed2c7 | |||
| fa54a2affe | |||
| 62e1d0e554 | |||
| 9c823a038b | |||
| 1e6cd50f46 | |||
| 06716e4873 | |||
| 8e4a1e3ffa | |||
| 0abb3bd4c3 | |||
| 336574daed | |||
| 07938ba111 | |||
| e699eb6d25 | |||
| 3864549752 | |||
| 0b934cd0f6 | |||
| 5bac38a752 | |||
| 72c8d4d3dd | |||
| b8c6ea015e | |||
| ffe1beb7ae | |||
| 21c6dbfce0 | |||
| 70cbb8dc79 | |||
| 334f2a364d | |||
| b477354235 | |||
| 254c966159 | |||
| 7ee9b07d9c | |||
| 839b72469c | |||
| 874d76b343 | |||
| 7497e7aa0c | |||
| efa084fb0f | |||
| 48e4a27054 | |||
| 96cf6a790e | |||
| d7b54ff397 | |||
| 90ab065073 | |||
| b6f0784311 | |||
| e37ec654ee | |||
| b237d51276 | |||
| 155ea24008 | |||
| 8c8affc800 | |||
| 481062fca1 | |||
| ffcc5560dc | |||
| 09e146ef0b | |||
| 4c6b04ff69 | |||
| 9889b479d1 | |||
| 95dec00c76 | |||
| cff268926d | |||
| 6fa88f4e4a | |||
| ab8e6791fe | |||
| 13c45cc59a | |||
| 67c468884f | |||
| f028d44609 | |||
| 18b952e612 | |||
| 25178d8f50 | |||
| 1c0b7c00fd | |||
| 2439761529 | |||
| 8803dd5b65 | |||
| d15d04eae5 | |||
| bf40f74a4a | |||
| c0339c0f46 | |||
| b64bb166c0 | |||
| 31d30030dc | |||
| 556e111a98 | |||
| 70b0dd621b | |||
| f7d3212651 | |||
| 0a29f0cfa1 | |||
| 97153ad59d | |||
| bc8378fb60 | |||
| 3320cf8da8 | |||
| bb53bd3f27 | |||
| 73eed59fab | |||
| 91ede52634 | |||
| 93f13a98b2 | |||
| c87c5c9709 | |||
| b0c6c53430 | |||
| 94a5222390 | |||
| 98bb304060 | |||
| 08bfd923ea | |||
| ae28f04ce4 | |||
| 024a742f2a | |||
| df184f3e54 | |||
| 5542410afa | |||
| 99205cdc0f | |||
| 8c936af963 | |||
| 7fe751e74f | |||
| 6d551578c3 | |||
| 40c85fb607 | |||
| 743736b376 | |||
| 7fdb431d70 | |||
| ebcc3d8912 | |||
| 32e29a54c3 | |||
| 049733c4b6 | |||
| 420d58527d | |||
| bab779a34c | |||
| 45aa71b2b7 | |||
| 6dcfe2cad6 | |||
| f206047908 | |||
| 6ce979a7de | |||
| 97f97eb063 | |||
| f3db762e9f | |||
| f9f623dfa5 | |||
| ffa6bec3b4 | |||
| 4f78973751 | |||
| a8a7af4b74 | |||
| 45295c779c | |||
| a82376d1f5 | |||
| 75c6248264 | |||
| 9294ab4f97 | |||
| f01193e854 | |||
| d7375bc4c3 | |||
| 1a860c6ffd | |||
| 800ed3af7a | |||
| 9c8e79546c | |||
| 4c272aa536 | |||
| e184861822 | |||
| d40e19f08d |
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ✨ Feature Request or Idea
|
||||
url: https://github.com/markqvist/Reticulum/discussions/new?category=ideas
|
||||
about: Propose and discuss features and ideas
|
||||
- name: 💬 Questions, Help & Discussion
|
||||
about: Ask anything, or get help
|
||||
url: https://github.com/markqvist/Reticulum/discussions/new/choose
|
||||
- name: 📖 Read the Reticulum Manual
|
||||
url: https://markqvist.github.io/Reticulum/manual/
|
||||
about: The complete documentation for Reticulum
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "\U0001F41B Bug Report"
|
||||
about: Report a reproducible bug
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Read the Contribution Guidelines**
|
||||
Before creating a bug report on this issue tracker, you **must** read the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md). Issues that do not follow the contribution guidelines **will be deleted without comment**.
|
||||
|
||||
- The issue tracker is used by developers of this project. **Do not use it to ask general questions, or for support requests**.
|
||||
- Ideas and feature requests can be made on the [Discussions](https://github.com/markqvist/Reticulum/discussions). **Only** feature requests accepted by maintainers and developers are tracked and included on the issue tracker. **Do not post feature requests here**.
|
||||
- After reading the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md), **delete this section only** (*"Read the Contribution Guidelines"*) from your bug report, **and fill in all the other sections**.
|
||||
|
||||
**Describe the Bug**
|
||||
First of all: Is this really a bug? Is it reproducible?
|
||||
|
||||
If this is a request for help because something is not working as you expected, stop right here, and go to the [discussions](https://github.com/markqvist/Reticulum/discussions) instead, where you can post your questions and get help from other users.
|
||||
|
||||
If this really is a bug or issue with the software, remove this section of the template, and provide **a clear and concise description of what the bug is**.
|
||||
|
||||
**To Reproduce**
|
||||
Describe in detail how to reproduce the bug.
|
||||
|
||||
**Expected Behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Logs & Screenshots**
|
||||
Please include any relevant log output. If applicable, also add screenshots to help explain your problem. In most cases, without any relevant log output, we will not be able to determine the cause of the bug, or reproduce it.
|
||||
|
||||
**System Information**
|
||||
- OS and version
|
||||
- Python version
|
||||
- Program version
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,96 @@
|
||||
name: Build Reticulum
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+*"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- .gitignore
|
||||
- LICENSE
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: make test
|
||||
|
||||
package:
|
||||
needs: test
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ contains(github.ref, '-') && 'development' || 'production' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: |
|
||||
python -m pip install -q build wheel setuptools
|
||||
make remove_symlinks
|
||||
make build_wheel
|
||||
make build_pure_wheel
|
||||
make create_symlinks
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: package
|
||||
path: dist/*.whl
|
||||
|
||||
# documentation:
|
||||
# needs: test
|
||||
# if: startsWith(github.ref, 'refs/tags/')
|
||||
# runs-on: ubuntu-latest
|
||||
# environment: ${{ contains(github.ref, '-') && 'development' || 'production' }}
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: 3.x
|
||||
# - run: |
|
||||
# sudo apt-get -qq update && sudo apt-get -qq install latexmk texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended
|
||||
# python -m pip -q install sphinx sphinx-copybutton
|
||||
# cd docs && make latexpdf && make epub
|
||||
# - uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: documentation
|
||||
# path: |
|
||||
# docs/build/latex/*.pdf
|
||||
# docs/build/epub/*.epub
|
||||
|
||||
release:
|
||||
needs: [package]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ contains(github.ref, '-') && 'development' || 'production' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: .artifacts
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
# .artifacts/package/**.whl
|
||||
# .artifacts/documentation/latex/reticulumnetworkstack.pdf
|
||||
# .artifacts/documentation/epub/ReticulumNetworkStack.epub
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
prerelease: ${{ contains(github.ref, '-') }}
|
||||
fail_on_unmatched_files: true
|
||||
@@ -10,6 +10,6 @@ docs/build
|
||||
rns*.egg-info
|
||||
profile.data
|
||||
tests/rnsconfig/storage
|
||||
tests/rnsconfig/logfile
|
||||
tests/rnsconfig/logfile*
|
||||
*.data
|
||||
*.result
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
compiling = False
|
||||
noticed = False
|
||||
notice_delay = 0.3
|
||||
import time
|
||||
import sys
|
||||
import threading
|
||||
from importlib.util import find_spec
|
||||
if find_spec("pyximport") and find_spec("cython"):
|
||||
import pyximport; pyxloader = pyximport.install(pyimport=True, language_level=3)[1]
|
||||
|
||||
def notice_job():
|
||||
global noticed
|
||||
started = time.time()
|
||||
while compiling:
|
||||
if time.time() > started+notice_delay and compiling:
|
||||
noticed = True
|
||||
print("Compiling RNS object code... ", end="")
|
||||
sys.stdout.flush()
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
compiling = True
|
||||
threading.Thread(target=notice_job, daemon=True).start()
|
||||
import RNS; compiling = False
|
||||
if noticed: print("Done."); sys.stdout.flush()
|
||||
@@ -1,3 +1,767 @@
|
||||
### 2025-03-13: RNS β 0.9.3
|
||||
|
||||
This maintenance release improves performance and fixes a number of bugs.
|
||||
|
||||
**Changes**
|
||||
- Enabled link MTU discovery by default
|
||||
- Added on-demand object code compilation and loader shim
|
||||
- Added link API methods
|
||||
- Added child interface spawning for AutoInterface
|
||||
- Fixed corrupt ratchet files not being removed on maintenance cleaning
|
||||
- Fixed `rnid` not waiting for announce timebase tick before announcing
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
0270c988a2b898b28348cd78138667115d4ef3f7e09c86531baaefbee35ef851 rns-0.9.3-py3-none-any.whl
|
||||
eee1a6c4c9c0f04bb17b12b8fb37b9c4cec12a99c87a046730eb7c9a6ffd999f rnspure-0.9.3-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-01-19: RNS β 0.9.2
|
||||
|
||||
This maintenance release fixes a number of bugs.
|
||||
|
||||
**Changes**
|
||||
- Fixed missing RX/TX bytes statistics assignment
|
||||
- Fixed potential daemon thread IO buffer deadlock on externally mediated shutdown signal
|
||||
- Fixed missing check for path announce emission timestamp in lower hop-count announce processing
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
068eb4408b332ea6eec1a58fb4644fba3531c9ca10dcd79ecf893aaaf40e720d rns-0.9.2-py3-none-any.whl
|
||||
1e7c123d244cc14c287568f3a99953cc11ffc1e79a72a029aa1be72fa8eff24e rnspure-0.9.2-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-01-19: RNS β 0.9.1
|
||||
|
||||
This maintenance release adds reject signalling mechanism to resource transfers, fixes inconsistencies in the code examples, and improves thread configuration in the transport core.
|
||||
|
||||
**Changes**
|
||||
- Added resource reject signalling
|
||||
- Added error reporting on configured radio parameter mismatch on Android
|
||||
- Improved thread configuration for transport core threads
|
||||
- Updated examples
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
49288a562ad6d4b5647c3afec051a6bb6497b75e3f165a972436134d4a93ad76 rns-0.9.1-py3-none-any.whl
|
||||
abd6c4bdead2fc25d0b9b2cda5708586e8cb776b088f2a901a5f262e2ed901ae rnspure-0.9.1-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-01-17: RNS β 0.9.0
|
||||
|
||||
This release lays the groundwork for future performance and resource utilisation optimisations. Most importantly, this release adds **link MTU autodiscovery**, which allow established links to use much higher MTUs than the base MTU of 500 bytes.
|
||||
|
||||
**Please note!** To actually use link MTU discovery, all transport nodes along the path must be upgraded to at least version `0.9.0`. Since this is the first release to add support for this feature, *it is currently **not** activated by default*, and no clients or applications will use it yet. Using link MTU autodiscovery by default will be enabled by default in RNS version `0.9.1`. Please upgrade your nodes!
|
||||
|
||||
Additionally, this release adds several new features, performance improvements and bug fixes, as well as support for RNodes running firmware version `1.81`.
|
||||
|
||||
**Changes**
|
||||
- Added MTU autoconfiguration on interfaces that support higher MTUs
|
||||
- Added link MTU autodiscovery and path clamping
|
||||
- Added dynamic SDU calculations based on link MTU to `Resource`, `Channel` and `Buffer`
|
||||
- Added resource EIFR continuity to split resource handling
|
||||
- Added interference status to `RNodeInterface`
|
||||
- Fixed a display bug in `rnstatus`
|
||||
- Added live traffic stats to `rnstatus`
|
||||
- Added T3S3 support to `rnodeconf`
|
||||
- Added Heltec T114 support to `rnodeconf`
|
||||
- Added LilyGO T-Echo support to `rnodeconf`
|
||||
- Added option to print device configuration to `rnodeconf`
|
||||
- Improved CPU utilisation and memory consumption
|
||||
- Improved `rnsd` restart time on systems with many interfaces
|
||||
- Improved `rncp` status output
|
||||
- Improved packet filter performance
|
||||
- Improved interface detachment handling
|
||||
- Improved resource transfer timing and performance
|
||||
- Improved Transport core efficiency
|
||||
- Improved reliability of ratchet reloads if I/O conflicts occur
|
||||
- Improved logging
|
||||
- Improved built-in profiler
|
||||
- Fixed a potential deadlock in logging
|
||||
- Fixed time formatters not handling negative times
|
||||
- Updated example code
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
1ee60634cf0627c45b93f4e6c9adaf1fcdf9c1a8dfd4dd3dcd499e029554ab4f rns-0.9.0-py3-none-any.whl
|
||||
b67eec583fdb224ba8174b317e66b8f7344e338e93760ed1a90f0bafea8cf09e rnspure-0.9.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-01-09: RNS β 0.8.9
|
||||
|
||||
This maintenance release adds a number of configuration options to `rnodeconf`.
|
||||
|
||||
**Changes**
|
||||
- Added noise floor output to `rnstatus` for supported interfaces
|
||||
- Added channel noise floor and CSMA parameter reporting to `RNodeInterface`
|
||||
- Added ability to set display rotation in `rnodeconf`
|
||||
- Added ability to configure interference avoidance to `rnodeconf`
|
||||
- Fixed missing console image install on Heltec V3 in `rnodeconf`
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
b54fe8bc296f83a3a70569c9d1e9db3096249789c18f8d0217671479fa6881a1 rns-0.8.9-py3-none-any.whl
|
||||
52fd992e5f9478d5a1f61f8f37dc0ee2d268fdd0b8a4e6656d33d632490afc5a rnspure-0.8.9-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-12-11: RNS β 0.8.8
|
||||
|
||||
This maintenance release adds a single API function and fixes a bug.
|
||||
|
||||
**Changes**
|
||||
- Allow announce handlers to receive announce packet hash
|
||||
- Fix packet RSSI/SNR/Q cache not being available on standalone instances
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
9c1755a81049c67b051ecb9fe4b2c5f7d98bf09d20ed52d6ce6a410298b0527b rns-0.8.8-py3-none-any.whl
|
||||
d8871d69cde4b0a0b99b383f324d651dc77a2f44ec9641be828902c778a8d128 rnspure-0.8.8-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-12-09: RNS β 0.8.7
|
||||
|
||||
This maintenance release adds support for OpenWRT packaging, and brings several minor improvements and bugfixes.
|
||||
|
||||
Thanks to @gretel and @jacobeva, who contributed to this release!
|
||||
|
||||
**Changes**
|
||||
- Added support for packaging RNS to OpenWRT
|
||||
- Added ability to run `rnstatus` as application-local imported module
|
||||
- Added ability to reflect RNS log output to app-internal log handler callback
|
||||
- Added display read functionality to `RNodeInterface`
|
||||
- Fixed a regression in `RNodeMultiInterface` caused by earlier refactoring
|
||||
- Imrpoved documentation
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
e76ba8feeeae2c8df27e9906deebd7c721f0f0e887ad3fbd26df0212d6ce907a rns-0.8.7-py3-none-any.whl
|
||||
046608539bc235d52c970c7f3c54e7aa01a86016ae00263f8a55fc796b6939f5 rnspure-0.8.7-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-11-24: RNS β 0.8.6
|
||||
|
||||
This release adds full interface modularity and custom interface loading to RNS. Users can now easily create and use their own custom interfaces for communicating over practically anything. Support for IPv6 has also been added to the TCP-based interfaces.
|
||||
|
||||
In addition, several bugs have been fixed, and various internal improvements to code consistency and naming conventions have been carried out.
|
||||
|
||||
Thanks to @gretel and @deavmi, who contributed to this release!
|
||||
|
||||
**Changes**
|
||||
- Added ability to load and configure custom, user-supplied interfaces
|
||||
- Added IPv6 support to `TCPClientInterface` and `TCPServerInterface`
|
||||
- Added an init option to the API for requiring an existing shared instance
|
||||
- Changed `rnstatus` behaviour to only show status if Reticulum is already running
|
||||
- Fixed `KISSInterface` beacon length for compatibility with software modems
|
||||
- Fixed interface client count sometimes reporting incorrect values on TCP and I2P interfaces
|
||||
- Refactored and improved interface initialisation and configuration handling
|
||||
- Refactored interface code to be more consistent
|
||||
- Refactored various deprecated references and names
|
||||
- Updated documentation and manual
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
60be127f003cd7838149bf8f01020206f829a7bd192706a608e39d8d7193d07b rns-0.8.6-py3-none-any.whl
|
||||
d8701e19279d292b5b8af9da7c67b6ac88a992ca65109f8182c3e5c761a9ebeb rnspure-0.8.6-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-10-20: RNS β 0.8.5
|
||||
|
||||
This maintenance release fixes a number of bugs. Thanks to @faragher for contributing to this release!
|
||||
|
||||
**Changes**
|
||||
- Fixed missing close of file handles
|
||||
- Fixed invalid values returned from `get_snr()` and `get_q()` physical layer stats API functions
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
1757e809e083585bf4c23b6fe0f29954e5a1586ce14081099e38e606a75831df rns-0.8.5-py3-none-any.whl
|
||||
44254630634f4dbb1ce3242247fe8180379d27bff15d183263b1856fd662f88d rnspure-0.8.5-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-10-11: RNS β 0.8.4
|
||||
|
||||
This release fixes a number of bugs and improves reliability of automatic reconnection when BLE-connected RNodes unexpectedly disappear or lose connection.
|
||||
|
||||
**Changes**
|
||||
- Improved RNode BLE reconnection realiability
|
||||
- Added RNode battery state to `rnstatus` output
|
||||
- Fixed resource transfer hanging for a long time over slow links if proof packet is lost
|
||||
- Fixed missing import on Android
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
d3f7a9fddc6c1e59b1e4895756fe602408ac6ef09de377ee65ec62d09fff97a3 rns-0.8.4-py3-none-any.whl
|
||||
eb3843bcab1428be0adb097988991229a4c03156ab40cc9c6e2d9c590d8b850b rnspure-0.8.4-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-10-10: RNS β 0.8.3
|
||||
|
||||
This release fixes a bug in resource transfer progress calculation, improves RNode error handling, and brings minor improvements to the `rncp` utility.
|
||||
|
||||
**Changes**
|
||||
- Fixed a bug in resource transfer progress calculations
|
||||
- Added physical layer transfer rate output option to `rncp`
|
||||
- Added save directory option to `rncp`
|
||||
- Improved path handling for the fetch-jail option of of `rncp`
|
||||
- Added error detection for modem communication timeouts on connected RNode devices
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
54ddab32769081045db5fe45b27492cc012bf2fad64bc65ed37011f3651469fb rns-0.8.3-py3-none-any.whl
|
||||
a04915111d65b05a5f2ef2687ed208813034196c0c5e711cb01e6db72faa23ef rnspure-0.8.3-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-10-06: RNS β 0.8.2
|
||||
|
||||
This release adds several new boards to `rnodeconf`, fixes a range of bugs and improves transport reliability.
|
||||
|
||||
Thanks to @jacobeva, @prusnak and @deavmi who contributed to this release!
|
||||
|
||||
**Changes**
|
||||
- Added support for T-Beam Supreme devices to `rnodeconf`
|
||||
- Added support for T3S3 devices to `rnodeconf`
|
||||
- Added support for T-Deck devices to `rnodeconf`
|
||||
- Added support for new hardware error codes from connected RNodes
|
||||
- Added the ability to control the display on nRF52-based RNodes
|
||||
- Improved resource transfers over very slow links, by adding more suitable `MAX_WINDOW` cap if link speed is continously below threshold.
|
||||
- Improved `rnodeconf` flashing so manual resets for some devices are no longer required
|
||||
- Added edge case handling for receiving a link proof after the link had timed out and been closed, but before it having been purged from active links table
|
||||
- Updated supported hardware section of the manual with new boards
|
||||
- Tuned path request timing for roaming instances
|
||||
- Fixed a bug that caused RNS to fail to initialise in Termux on Android
|
||||
- Fixed a bug in RNodeInterface firmware version comparison
|
||||
- Fixed a bug in the serial framing of RNodeMultiInterface
|
||||
- Fixed a bug in sub-interface spawning of RNodeMultiInterface
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
db720a727a09c0c9d76288dec5a995a30146e65d6a4c5c034f47fb60a78f4962 rns-0.8.2-py3-none-any.whl
|
||||
ee412535edba48817551658247fb0c843d17e1c97cad9d2a819a7fc627c5ba28 rnspure-0.8.2-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-10-02: RNS β 0.8.1
|
||||
|
||||
This release adds BLE support to RNodeInterface, and support for configuring additional options to `rnodeconf`.
|
||||
|
||||
**Changes**
|
||||
- Added Bluetooth Low Energy support to RNodeInterface
|
||||
- Added RNode battery information to `rnstatus` output
|
||||
- Added display blanking configuration to `rnodeconf`
|
||||
- Added NeoPixel intensity configuration to `rnodeconf`
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
f4b6b99b67d6b33b8a4562e5d5d5ac54c76814fff26e6c7a79950b82bd80123f rns-0.8.1-py3-none-any.whl
|
||||
c2e540b4bf0f272bb51ae3e33a02f9c07f2619746d069d7ed83d88017bf7ea30 rnspure-0.8.1-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-09-25: RNS β 0.8.0
|
||||
|
||||
This maintenance release improves the interface statistics API, and updates documentation.
|
||||
|
||||
**Changes**
|
||||
- Added additional information to interface statistics
|
||||
- Updated documentation
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
fa5ff6d98230693be6805bb9a94585a6f54ec0af9cba15b771d4e676f140dc43 rns-0.8.0-py3-none-any.whl
|
||||
ba20f688b69ae861c8aced251e10242a358fea15da6c22df10d4fc8846c9bf48 rnspure-0.8.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-09-24: RNS β 0.7.9
|
||||
|
||||
This maintenance release improves transport reliability in certain (rare) cases.
|
||||
|
||||
**Changes**
|
||||
- Added handling of a transport edge-case
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
4c20c46df021d366386d497145024396f904666b0de22a92f9e5c937886ea39d rns-0.7.9-py3-none-any.whl
|
||||
97d26282df929eca732a15523bc9d7f66387a93ffd911e8063c94c3f8f6ad73c rnspure-0.7.9-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-09-18: RNS β 0.7.8
|
||||
|
||||
This maintenance release adds support for the openCom XL to `rnodeconf`, fixes a number of bugs, and also includes a few fine-tunings of timing parameters.
|
||||
|
||||
Thanks to @liamcottle and @jacobeva for contributing to this release!
|
||||
|
||||
**Changes**
|
||||
- Added interface prioritisation according to reported bitrate
|
||||
- Added support for openCom XL to `rnodeconf`
|
||||
- Added performance profiler to built-in debugging tools
|
||||
- Tuned link traffic timeouts
|
||||
- Fixed a module import error in AX25KissInterface
|
||||
- Fixed a missing exception on erroneous destination initialisation
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
33fb9443e3b327d1a9125baa52d8ec3208a089dda62f749b819e0a94c06730f9 rns-0.7.8-py3-none-any.whl
|
||||
cdced2adef4ead146239d0510fe2b9d62f69136bcd54b22d1080686fb56f9927 rnspure-0.7.8-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-09-09: RNS β 0.7.7
|
||||
|
||||
This release adds support for automatic encryption key ratcheting for all packets, not just those sent over Reticulum links. In practical terms, this adds forward secrecy to packets sent with the raw `Packet` API.
|
||||
|
||||
In this release, the ratchets feature must be enabled on a per-destination basis by calling the `enable_ratchets` method on the relevant destination. In a future release, ratchets may become the default option, but for backwards-compatibility, it is currently optional. For more information, read the API documentation.
|
||||
|
||||
**Please note!** Versions of RNS prior to `0.7.7` will not be able to pass announces for destinations with ratchets enabled! If you use applications that can use ratchets (for example, LXMF version `0.5.0` and up), it is important that you update all transport instances on your network to `0.7.7`.
|
||||
|
||||
Thanks to @deavmi, @faragher, @jacobeva, @jeremy and @jeremybox for contributing to this release!
|
||||
|
||||
**Changes**
|
||||
- Added key ratchet rotation and signalling
|
||||
- Added ratchet API to documentation
|
||||
- Added initial support for flashing T-Echo devices to `rnodeconf`
|
||||
- Added remote management config options to example config
|
||||
- Added automtic integration tests to source repository
|
||||
- Fixed a regression that caused RNS not to work on Python versions lower than 3.10
|
||||
- Fixed missing `establishment_rate` property init on Link objects
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
0a3ab6dc82567a19adabe737358daee3002b60beda8ac0bf228f2a0c134ff6d8 rns-0.7.7-py3-none-any.whl
|
||||
89b33fe9ab923139d3f5d43726d92817642be05a8c9d328c3becfc3c409e4b4b rnspure-0.7.7-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-05-18: RNS β 0.7.6
|
||||
|
||||
This release adds support for RNodes with multiple radio transceivers, courtesy of @jacobeva. It also brings a number of functionality and performance improvements, and fixes several bugs.
|
||||
|
||||
Thanks to @jacobeva, @faragher, @nathmo, @jschulthess and @liamcottle for contributing to this release!
|
||||
|
||||
**Changes**
|
||||
- Added support for RNode Multi interfaces
|
||||
- Added initial support for remote management of Reticulum instances
|
||||
- Improved resource transfer performance for large resources
|
||||
- Improved path rediscovery in topologies with roaming transport nodes
|
||||
- Fixed incorrect TX power limit on Android RNode interfaces
|
||||
- Added ability to fetch remote files to `rncp`
|
||||
- Added fetch request jail option to `rncp`
|
||||
- Improved `rncp` status display output
|
||||
- Added link table statistics to `rnstatus`
|
||||
- Fixed `rnstatus` JSON output bug when IFAC was enabled on an interface
|
||||
- Added remote instance interface status to `rnstatus`
|
||||
- Added ability to query path- and rate-tables on remote instances with `rnpath`
|
||||
- Added JSON output option to `rnpath` utility
|
||||
- Added max hops filter to `rnpath` path-table out
|
||||
- Added link age getter to API
|
||||
- Added request concluded status to API
|
||||
- Fixed invalid resource progress reported in some cases
|
||||
- Fixed `rnodeconf` failure to set firmware hash for NRF52 boards on macOS
|
||||
- Fixed broken `--rom` command line option in `rnodeconf`
|
||||
- Fixed various typos in documentation
|
||||
- Updated documentation with new API functions and features
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
683ac87c62fe8a18d88c26bf639f4eeca550cefb11ee8e38d6e724e268cf14fc rns-0.7.6-py3-none-any.whl
|
||||
f884806624e57b799f588de9289a31d2e0460d35bc4cc5071635de5642d50ad2 rnspure-0.7.6-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-05-18: RNS β 0.7.5
|
||||
|
||||
This release adds support for AutoInterface on Windows platforms, fixes a number of bugs and adds several new supported boards to `rnodeconf`. Thanks to @faragher, @jacobeva and @liamcottle who contributed to this release!
|
||||
|
||||
**Changes**
|
||||
- Added support for AutoInterface on Windows
|
||||
- Added support for recursive path resolution for clients on roaming-mode interfaces
|
||||
- Added RAK4631 support to `rnodeconf`
|
||||
- Added LilyGO T3S3 support to `rnodeconf`
|
||||
- Added ability to get target and calculated hashes via `rnodeconf`
|
||||
- Fixed DTR timing making flashing fail on Windows in `rnodeconf`
|
||||
- Fixed various output and menu bugs in `rnodeconf`
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
99ec876966afdea45fcf164242c8e76c284f9e3edf09fb907638fba76e1324b1 rns-0.7.5-py3-none-any.whl
|
||||
11156f6301707e4d17ff2ca6d58059bc8ba6fe1bbc4dc3de165dd96dc41ee75f rnspure-0.7.5-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-05-05: RNS β 0.7.4
|
||||
|
||||
This maintenance release fixes a number of bugs, improves path requests and responses, and adds several useful features and capabilities. Thanks to @cobraPA, @jschulthess, @thiaguetz and @nothingbutlucas who contributed to this release!
|
||||
|
||||
**Changes**
|
||||
- Added support for flashing and autoinstalling Heltec V3 boards to `rnodeconf`
|
||||
- Added custom EEPROM bootstrapping capabilities to `rnodeconf`
|
||||
- Added ability to load identities from file to Echo and Link examples
|
||||
- Added ability to specify multicast address type in AutoInterface configuration
|
||||
- Added link getter to resource advertisement class
|
||||
- Improved path response logic and timing
|
||||
- Improved path request timing
|
||||
- Fixed a bug in Link Request proof delivery on unknown hop count paths
|
||||
- Fixed broken link packet routing in topologies where transport packets leak to non-intended instances in the link chain
|
||||
- Fixed typos in documentation
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
f5c35f1b8720778eb508b687d66334d01b4ab266b2d8c2bc186702220dcaae29 rns-0.7.4-py3-none-any.whl
|
||||
9eaa7170f97dad49551136965d3fcc971b56b1c2eda48c24b9ffd58d71daa016 rnspure-0.7.4-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-03-09: RNS β 0.7.3
|
||||
|
||||
This release adds the ability to specify custom firmware URLs for flashing boards with `rnodeconf`. Thanks to @attermann who contributed to this release!
|
||||
|
||||
**Changes**
|
||||
- Added ability to specify custom firmware URLs for flashing boards with `rnodeconf`
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
bb24445ae9a3a63d348e4d7fe80b750608f257851b97b38fadab929b7a774bc9 rns-0.7.3-py3-none-any.whl
|
||||
1b148d013103c35ba9a8e105082ef50686c130676d0a560ed709cb546129287e rnspure-0.7.3-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-03-02: RNS β 0.7.2
|
||||
|
||||
This maintenance release improves memory consumption, fixes a few bugs, and adds ability to flash new boards with `rnodeconf`.
|
||||
|
||||
**Changes**
|
||||
- Added ability to flash new boards with `rnodeconf`, including T3 boards with TCXOs
|
||||
- Improved memory consumption on Transport Instances with many interfaces
|
||||
- Fixed a bug that could cause the on-disk known destinations store to become corrupted
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
3ce3ba80d5ae8d19c6b55bd51f44bd4beccbcea31554cb1f0d65428e4587b3d6 rns-0.7.2-py3-none-any.whl
|
||||
83f914aaba2a8929a8cee95830a847e190197232a0cca4e7b906b15c6bbf8296 rnspure-0.7.2-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-02-14: RNS β 0.7.1
|
||||
|
||||
This release adds support for RNodes based on SX1262, SX1268 and SX1280 modems, and fixes a number of bugs. Thanks to @jacobeva, who contributed to this release!
|
||||
|
||||
**Changes**
|
||||
- Added support for SX1262, SX1268 and SX1280 based RNodes
|
||||
- Updated `rnodeconf` to allow flashing T-Beam devices with SX126x chips
|
||||
- Fixed an invalid RSSI offset reference
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
8ecfbb42b6a699fd4ac5374ab5640e4bb164e80bb9ab4401ea82da132e497877 rns-0.7.1-py3-none-any.whl
|
||||
e0ab487305ba1aee2d16044640e7eb72d332bbf51aeb0b8bf984d037a64cb493 rnspure-0.7.1-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2024-01-17: RNS β 0.7.0
|
||||
|
||||
This maintenance release fixes a number of bugs. Thanks to @jooray and @jacobeva, who contributed to this release!
|
||||
|
||||
**Changes**
|
||||
- Fixed large resource transfers failing under some conditions
|
||||
- Fixed a potential division by zero
|
||||
- Fixed a missing check on malformed advertisement packets
|
||||
- Fixed a formatting issue in `rnprobe`
|
||||
- Improved resource timeout calculations
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
0dc2abe5373b9afadfba7ec05bf7ddeff659c004aa339a94001ebed5b46f5b47 rns-0.7.0-py3-none-any.whl
|
||||
97f6e65a20b53bbdccd54b4d2bdaa36dc1712e144a55f40800c63fe7113819a5 rnspure-0.7.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-12-07: RNS β 0.6.9
|
||||
|
||||
This release adds a few convenience functions to the `rnid` utility, and improves roaming support on Android.
|
||||
|
||||
**Changes**
|
||||
- Added identity import and export in hex, base32 and base64 formats to the `rnid` utility.
|
||||
- Added better carrier change detection for AutoInterface on Android.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
258daf22cb6e72c6cd04fe94447daedf51dfd968eb2f3370eab9c71ad0898dd0 rns-0.6.9-py3-none-any.whl
|
||||
3644b64af5b4efd3969172bf0cf95ae1afba6c8ea99ce47d8e49e31a832bbaf8 rnspure-0.6.9-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-11-14: RNS β 0.6.8
|
||||
|
||||
This maintenance release fixes a single bug.
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed packet receipts not being initialised in time for arriving proofs on fast interfaces
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
3ffb01f3f45e35105ea30e60e5e493ba50528df38b4ea62672c9e1c093073b1c rns-0.6.8-py3-none-any.whl
|
||||
de372814082ef7db59f4b2745b1f22b2ef9d97815190ec16c0596ba20406e0fb rnspure-0.6.8-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-11-06: RNS β 0.6.7
|
||||
|
||||
This maintenance release improves tranport performance and fixes a logging bug.
|
||||
|
||||
**Changes**
|
||||
- Improved local and remote transport performance by approximately 6x on faster links
|
||||
- Significantly decreased latency over faster links
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed logging an error message when local clients connect while shared instance is still starting up
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
c37dd1f59e037841f69ec518deecdae6719f978947de2473f04e7d95247805ac rns-0.6.7-py3-none-any.whl
|
||||
1e2dcb44ec7271a4d26180db138fc54dce6d0d3cf3f816432d4d6a4b1cf83868 rnspure-0.6.7-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-11-04: RNS β 0.6.6
|
||||
|
||||
This maintenance release improves transfers over unreliable links and fixes a bug in requests.
|
||||
|
||||
**Changes**
|
||||
- Improved reliability of resource transfers over very slow and unreliable links
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed a bug that could cause requests to timeout prematurely
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
b1127745750a43cd7389212d31aa09ccc735ab2d69e3b80bd28874f10082c322 rns-0.6.6-py3-none-any.whl
|
||||
bf5ba5da4f37b93c14817367952cda63787ec88bbe601e41c13fcbb3fc22b6b6 rnspure-0.6.6-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-11-02: RNS β 0.6.5
|
||||
|
||||
This release fixes a bug in path rediscovery for shared instance clients.
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed a bug in path rediscovery for shared instance clients
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
5d54a5cfebe907c759351357a8f7d771670c895ff57f1325bf7fec42bcb46ba3 rns-0.6.5-py3-none-any.whl
|
||||
accd2855e18ff06455b9454957388089e293073ec7093c64dee0dc7aa46ecb46 rnspure-0.6.5-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-11-02: RNS β 0.6.4
|
||||
|
||||
This release fixes a number of bugs that had crept in while adding the new ultra low bandwidth link timing and faster path rediscovery mechanisms.
|
||||
|
||||
**Changes**
|
||||
- Adjusted link timings for better support of very slow mediums
|
||||
- Adjusted bluetooth read timeouts to account for occasional high latency in congested 2.4GHz environments
|
||||
- Added a probe count option to the `rnprobe` utility.
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed a missing timeout calculation
|
||||
- Fixed a redundant path request on path rediscovery
|
||||
- Fixed missing path state resetting on stale path rediscovery
|
||||
- Fixed a bug that could cause an attribute to be uninitialised
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
566c725f68aa154eaca0880c894a39503027bf91714f17691e51d047800444c0 rns-0.6.4-py3-none-any.whl
|
||||
a3a447fd40bf02fdb982523de0e4e9933e8e4cd4d4bd478254ea7dcac29e3fc1 rnspure-0.6.4-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-10-31: RNS β 0.6.3
|
||||
|
||||
This release brings a series of under-the-hood reliability improvements and bugfixes. But most notably, Reticulum can now establish links over even ultra low bandwidth mediums, all the way down to 5 bits per second.
|
||||
|
||||
Thanks to @jschulthess, who contributed to this release!
|
||||
|
||||
**Changes**
|
||||
- Implemented link establishment on ultra low bandwidth links
|
||||
- Added link quality calculations to RNode interfaces
|
||||
- Added physical layer link stats to Link and Packet classes
|
||||
- Added userspace service documentation to the manual
|
||||
- Improved path rediscovery in quickly changing topographies
|
||||
- Improved shared interface reconnection on service restart
|
||||
- Improved exception handling on interface detachment
|
||||
- Updated formatted print functions
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed a missing USB command definition in the RNode interface driver
|
||||
- Fixed a bug in link error handling that could cause an interface to detach
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
1f54d4c6ff7ab7721089cbee6630783765f65efd51312879c0d3e5bee3ceab2f rns-0.6.3-py3-none-any.whl
|
||||
5a90840f0fc9f1a62a3c37b514fb6222fd701a30024275dae8bcc27e29d40f25 rnspure-0.6.3-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-10-07: RNS β 0.6.2
|
||||
|
||||
This maintenance release adds the ability to specify the shared instance RPC key in the Reticulum config file, making it possible to use all Reticulum functionality in the terminal on Android.
|
||||
|
||||
**Changes**
|
||||
- Added configuration option to specify shared instance RPC key
|
||||
- Reordered airtime stats in `rnstatus`
|
||||
- Updated log levels on Android
|
||||
|
||||
**Bugfixes**
|
||||
- Adding missing superclass init on Android interfaces
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
a9958ad90f34f344003e18077f7abd3fa85666a39dc0cae8580071820dee13f9 rns-0.6.2-py3-none-any.whl
|
||||
e68e8837d35d1a07a82c4b0e9db50ceace737a650e6e7e9ce2d9a013fd28f529 rnspure-0.6.2-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-10-01: RNS β 0.6.1
|
||||
|
||||
This release brings a number of bugfixes, along with useful new mechanisms for ensuring network stability under high, non-constructive and unusual announce load situation.
|
||||
|
||||
**Changes**
|
||||
- Added announce ingress rate control for new and unknown destinations
|
||||
- Added per-interface announce frequency monitoring to the transport engine
|
||||
- Added per-interface announce burst hold queues
|
||||
- Added announce frequency statistics to `rnstatus`
|
||||
- Added option to sort `rnstatus` output according to various metrics
|
||||
- Added timeout options to `rnprobe`
|
||||
- Added ability to drop all paths via a specific transport instance to `rnpath`
|
||||
- Added new options and features to documentation and manual
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed announce queue not clearing all announces with exceeded retry limit at the same time
|
||||
- Fixed a bug that caused local packet RSSI and SNR cache to get stuck
|
||||
- Fixed output formatting in `rncp`
|
||||
- Fixed `rnid` not allowing single-aspect destination names
|
||||
- Fixed a number of typos in the documentation
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
461e5cafa7560dcd3ec047141d10f0f48f151c36e1af1d65ec6c65f732cea46a rns-0.6.1-py3-none-any.whl
|
||||
be6a4a6069f2d050e21582f2cf9d3bb59ed4040a0f07761a540bd752d90ea591 rnspure-0.6.1-py3-none-any.whl
|
||||
```
|
||||
|
||||
#### 2023-09-21: RNS β 0.6.0
|
||||
|
||||
This release brings a few performance improvements, additions to the included utilities, and fixes a number of bugs.
|
||||
|
||||
**Changes**
|
||||
- Added ability to run automatic probe responder on Transport Instances
|
||||
- Improved `rnprobe` utility
|
||||
- Improved AutoInterface peering on Android devices
|
||||
- Improved Transport performance
|
||||
- Improved path re-discovery when local nodes roam to other network segments
|
||||
- Updated various parts of the documentation
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed missing timeout check in `rncp`
|
||||
- Fixed missing link status check on `Identify()` call, which could lead to an unnecessary exception
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
88a26b1593e82a628dab96dbe8820548aea0159235f730fa992bf1833db59246 rns-0.6.0-py3-none-any.whl
|
||||
bcee416e4fb52346d01f6e0c46b1cebf84b127cc516603367fc2ae00a4149fa2 rnspure-0.6.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-09-19: RNS β 0.5.9
|
||||
|
||||
This release brings major efficiency improvements to `Channel` and `Buffer` classes, adds a range of usability improvements to the included utilities and fixes a number of bugs.
|
||||
|
||||
**Changes**
|
||||
- Improved `Channel` sequencing, retries and transfer efficiency
|
||||
- Added adaptive compression to `Buffer` class
|
||||
- Added `rnid` examples and documentation to manual
|
||||
- Added silent mode to `rncp`
|
||||
- Added remote fetch mode to `rncp`
|
||||
- Added allowed_identities file support to `rncp`
|
||||
- Added Transport Instance uptime to `rnstatus` output
|
||||
- Added channel CSMA parameter stats to RNode Interface `rnstatus` output
|
||||
- Added ability to set custom RNode OLED display address with rnodeconf
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed inadverdent AutoInterface multi-IF deque hit for resource transfer retries
|
||||
- Fixed invalid path for firmware hash generation while using extracted firmware to autoinstall in `rnodeconf`
|
||||
- Fixed various minor missing error checks
|
||||
- Fixed `rnid` status output bug
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
207ab20bd68bab16b417fbd41a4ecdbcf1e2f6fa553d48df6c8fc181b6e84dac rns-0.5.9-py3-none-any.whl
|
||||
93f0965567dfc2c43f3d703481fe1a7d7b1b8d0b3837ad41c37f28a8af5c1acc rnspure-0.5.9-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-09-14: RNS β 0.5.8
|
||||
|
||||
This maintenance release contains a number of usability improvements to Reticulum and related tools.
|
||||
|
||||
**Changes**
|
||||
- Various documentation updates
|
||||
- Improved path-resolution in mixed networks with roaming-mode nodes
|
||||
- Added channel load and airtime stats to `rnstatus` output
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
27ba5cdc4724fc8c7211c3b504f097f6adf47f7b80775e6297e4c4e621ef6348 rns-0.5.8-py3-none-any.whl
|
||||
1ea1c949763c9478ec48f064f7f7864d9f859101ab91b44400879371f490800f rnspure-0.5.8-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-08-14: RNS β 0.5.7
|
||||
|
||||
This maintenance release contains a number of bugfixes and quality improvements to Reticulum and related tools.
|
||||
|
||||
**Changes**
|
||||
- Added bytes input to destination hash convenience functions
|
||||
- Fixed possible invalid comparison in link watchdog job
|
||||
- Add option to `rnodeconf` to set baud rate when flashing
|
||||
- Added better explanation in `rnodeconf` when flashing fails
|
||||
- Fixed EEPROM dump directory in `rnodeconf`
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
867fbb5c73c2a49a75e1f8f3e9f376b507b683328e26c64d4387acd0cc1dbbc7 rns-0.5.7-py3-none-any.whl
|
||||
7bab2865264b32208e023b5c4bbe88c37f51e3176ca4a8cf332d95f59a6d7f2c rnspure-0.5.7-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-07-09: RNS β 0.5.6
|
||||
|
||||
This maintenance release contains a few bugfixes.
|
||||
|
||||
**Changes**
|
||||
- Fixed an issue in `rnodeconf` that prevented Heltec LoRa32 v2 boards from being flashed.
|
||||
- Fixed a typo in the `rnid` utility.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
255a5b4bac28326c6b2cc85f43b26dcb0606404a4abd2dfa8244937155838973 rns-0.5.6-py3-none-any.whl
|
||||
1510b6da4641ceaa4c599a142e498c7e2c1ae12035868f9db1c111e5600161e9 rnspure-0.5.6-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-06-13: RNS β 0.5.5
|
||||
|
||||
This maintenance release brings a single bugfix.
|
||||
|
||||
**Changes**
|
||||
- Fixed a race condition for link initiators on timed out link establishments.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
4ae61d28bf981a7cb853c179e9de3b56b350d2dc984fb671a21d38c4ce5b449e rns-0.5.5-py3-none-any.whl
|
||||
ed417cbd3c90e9f1b68565a3411ca5c9bc936b495300fd1ace3c4a6414aabd5a rnspure-0.5.5-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-05-19: RNS β 0.5.4
|
||||
|
||||
This maintenance release brings a single bugfix.
|
||||
|
||||
**Changes**
|
||||
- Fixed a potential race condition when timed-out link receives a late establishment proof a few milliseconds after it has timed out.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
71b42fe737da97a4b63bb227c29bb67854a7f003c9585f085b0ff68c8f460815 rns-0.5.4-py3-none-any.whl
|
||||
af6949d581445444f57cfca75756200e7c509a6fc66483d859716ce6a06064db rnspure-0.5.4-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-05-19: RNS β 0.5.3
|
||||
|
||||
This maintenance release brings a single, but important bugfix.
|
||||
|
||||
**Changes**
|
||||
- Fixed a bug that could cause data corruption to occur over when using `Buffer` instances.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
f23c8d655c9e80a12a6728495aec56f19f27184d3d8e6b6ed6184b0e89d4be35 rns-0.5.3-py3-none-any.whl
|
||||
2c692a2153bb766a9dc2391340a06f429c13a75b86b746b69c6fcd5a4fe5ee33 rnspure-0.5.3-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2023-05-12: RNS β 0.5.2
|
||||
|
||||
This maintenance release brings a number of bugfixes and improvements.
|
||||
@@ -764,4 +1528,4 @@ This was the first publicly available pre-release alpha of Reticulum.
|
||||
|
||||
### 2016-05-29: Inintial Repository Commit
|
||||
|
||||
The first commit to the Reticulum reference implementation was 9a9630cfd29e11ace3f12716ddb4dff0e5419b4b, which occurred on Sunday, the 22nd of May 2016.
|
||||
The first commit to the Reticulum reference implementation was 9a9630cfd29e11ace3f12716ddb4dff0e5419b4b, which occurred on Sunday, the 29th of May 2016.
|
||||
|
||||
@@ -2,22 +2,42 @@
|
||||
|
||||
Welcome, and thank you for your interest in contributing to Reticulum!
|
||||
|
||||
Apart from writing code, there are many ways in which you can contribute. Before getting started, please read these guidelines.
|
||||
Apart from writing code, there are many ways in which you can contribute. Before interacting with this community, read these short and simple guidelines.
|
||||
|
||||
## Expected Conduct
|
||||
|
||||
First and foremost, there is one simple requirement for taking part in this community: While we primarily interact virtually, your actions matter and have real consequences. Therefore: **Act like a responsible, civilized person** - also in the face of disputes and heated disagreements. Speak your mind here, discussions are welcome. Just do so in the spirit of being face-to-face with everyone else. Thank you.
|
||||
|
||||
## Asking Questions
|
||||
|
||||
If you want to ask a question, do not open an issue.
|
||||
If you want to ask a question, **do not open an issue**. The issue tracker is used by people *working on Reticulum* to track bugs, issues and improvements.
|
||||
|
||||
Instead, ask away on the [discussions](https://github.com/markqvist/Reticulum/discussions) or on the [Reticulum Matrix channel](https://unsigned.io/contact.html#reticulum:matrix.org) at `#reticulum:matrix.org`
|
||||
Instead, ask away on the [discussions](https://github.com/markqvist/Reticulum/discussions) or on the [Reticulum Matrix channel](https://matrix.to/#/#reticulum:matrix.org) at `#reticulum:matrix.org`
|
||||
|
||||
## Providing Feedback
|
||||
## Providing Feedback & Ideas
|
||||
|
||||
Likewise, feedback, ideas and feature requests are a very welcome way to contribute, and should also be posted on the [discussions](https://github.com/markqvist/Reticulum/discussions), or on the [Reticulum Matrix channel](https://unsigned.io/contact.html#reticulum:matrix.org) at `#reticulum:matrix.org`
|
||||
Likewise, feedback, ideas and feature requests are a very welcome way to contribute, and should also be posted on the [discussions](https://github.com/markqvist/Reticulum/discussions), or on the [Reticulum Matrix channel](https://matrix.to/#/#reticulum:matrix.org) at `#reticulum:matrix.org`.
|
||||
|
||||
Please do not post feature requests or general ideas on the issue tracker, or in direct messages to the primary developers. You are much more likely to get a response and start a constructive discussion by posting your ideas in the public channels created for these purposes.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you have found a bug or issue in Reticulum, please report it on the [issue tracker](https://github.com/markqvist/Reticulum/issues).
|
||||
If you have found a bug or issue in this project, please report it using the [issue tracker](https://github.com/markqvist/Reticulum/issues). If at all possible, be sure to include details on how to reproduce the bug.
|
||||
|
||||
Anything submitted to the issue tracker that does not follow these guidelines will be closed and removed without comments or explanation.
|
||||
|
||||
## Writing Code
|
||||
|
||||
If you are interested in contributing code, fixing open issues or adding features, please coordinate the effort with the maintainer or one of the main developers first, to ensure your efforts are in alignment with the [Roadmap](./Roadmap.md) and current development focus.
|
||||
If you are interested in contributing code, fixing open issues or adding features, please coordinate the effort with the maintainer or one of the main developers **before** submitting a pull request. Before deciding to contribute, it is also a good idea to ensure your efforts are in alignment with the [Roadmap](./Roadmap.md) and current development focus.
|
||||
|
||||
Pull requests have a high chance of being accepted if they are:
|
||||
|
||||
- In alignment with the [Roadmap](./Roadmap.md) or solve an open issue or feature request
|
||||
- Sufficiently tested to work with all API functions, and pass the standard test suite
|
||||
- Functionally and conceptually complete and well-designed
|
||||
- Not simply formatting or code style changes
|
||||
- Well-documented
|
||||
|
||||
Even new ideas and proposals that have not been approved by a maintainer, or fall outside the established roadmap, are *occasionally* accepted - if they possess the remaining of the above qualities. If not, they will be closed and removed without comments or explanation.
|
||||
|
||||
By contributing code to this project, you agree that copyright for the code is transferred to the Reticulum maintainers and that the code is irrevocably placed under the [MIT license](./LICENSE).
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import argparse
|
||||
import random
|
||||
import sys
|
||||
import RNS
|
||||
|
||||
# Let's define an app name. We'll use this for all
|
||||
@@ -32,7 +33,7 @@ def program_setup(configpath):
|
||||
# Destinations are endpoints in Reticulum, that can be addressed
|
||||
# and communicated with. Destinations can also announce their
|
||||
# existence, which will let the network know they are reachable
|
||||
# and autoomatically create paths to them, from anywhere else
|
||||
# and automatically create paths to them, from anywhere else
|
||||
# in the network.
|
||||
destination_1 = RNS.Destination(
|
||||
identity,
|
||||
@@ -53,7 +54,7 @@ def program_setup(configpath):
|
||||
)
|
||||
|
||||
# We configure the destinations to automatically prove all
|
||||
# packets adressed to it. By doing this, RNS will automatically
|
||||
# packets addressed to it. By doing this, RNS will automatically
|
||||
# generate a proof for each incoming packet and transmit it
|
||||
# back to the sender of that packet. This will let anyone that
|
||||
# tries to communicate with the destination know whether their
|
||||
@@ -168,4 +169,4 @@ if __name__ == "__main__":
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
@@ -118,4 +118,4 @@ if __name__ == "__main__":
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
@@ -157,7 +157,7 @@ def client(destination_hexhash, configpath):
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
@@ -254,9 +254,8 @@ def link_closed(link):
|
||||
else:
|
||||
RNS.log("Link closed, exiting now")
|
||||
|
||||
RNS.Reticulum.exit_handler()
|
||||
time.sleep(1.5)
|
||||
os._exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
# When the buffer has new data, read it and write it to the terminal.
|
||||
def client_buffer_ready(ready_bytes: int):
|
||||
@@ -320,4 +319,4 @@ if __name__ == "__main__":
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
@@ -124,7 +124,7 @@ def server(configpath):
|
||||
def server_loop(destination):
|
||||
# Let the user know that everything is ready
|
||||
RNS.log(
|
||||
"Link example "+
|
||||
"Channel example "+
|
||||
RNS.prettyhexrep(destination.hash)+
|
||||
" running, waiting for a connection."
|
||||
)
|
||||
@@ -212,7 +212,7 @@ def client(destination_hexhash, configpath):
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
@@ -276,7 +276,7 @@ def client_loop():
|
||||
packed_size = len(message.pack())
|
||||
channel = server_link.get_channel()
|
||||
if channel.is_ready_to_send():
|
||||
if packed_size <= channel.MDU:
|
||||
if packed_size <= channel.mdu:
|
||||
channel.send(message)
|
||||
else:
|
||||
RNS.log(
|
||||
@@ -321,9 +321,8 @@ def link_closed(link):
|
||||
else:
|
||||
RNS.log("Link closed, exiting now")
|
||||
|
||||
RNS.Reticulum.exit_handler()
|
||||
time.sleep(1.5)
|
||||
os._exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
# When a packet is received over the channel, we
|
||||
# simply print out the data.
|
||||
@@ -387,4 +386,4 @@ if __name__ == "__main__":
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
@@ -6,6 +6,7 @@
|
||||
##########################################################
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import RNS
|
||||
|
||||
# Let's define an app name. We'll use this for all
|
||||
@@ -46,7 +47,7 @@ def server(configpath):
|
||||
)
|
||||
|
||||
# We configure the destination to automatically prove all
|
||||
# packets adressed to it. By doing this, RNS will automatically
|
||||
# packets addressed to it. By doing this, RNS will automatically
|
||||
# generate a proof for each incoming packet and transmit it
|
||||
# back to the sender of that packet.
|
||||
echo_destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
|
||||
@@ -130,7 +131,7 @@ def client(destination_hexhash, configpath, timeout=None):
|
||||
except Exception as e:
|
||||
RNS.log("Invalid destination entered. Check your input!")
|
||||
RNS.log(str(e)+"\n")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
@@ -328,4 +329,4 @@ if __name__ == "__main__":
|
||||
client(args.destination, configarg, timeout=timeoutarg)
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,299 @@
|
||||
# MIT License - Copyright (c) 2024 Mark Qvist / unsigned.io
|
||||
|
||||
# This example illustrates creating a custom interface
|
||||
# definition, that can be loaded and used by Reticulum at
|
||||
# runtime. Any number of custom interfaces can be created
|
||||
# and loaded. To use the interface place it in the folder
|
||||
# ~/.reticulum/interfaces, and add an interface entry to
|
||||
# your Reticulum configuration file similar to this:
|
||||
|
||||
# [[Example Custom Interface]]
|
||||
# type = ExampleInterface
|
||||
# enabled = no
|
||||
# mode = gateway
|
||||
# port = /dev/ttyUSB0
|
||||
# speed = 115200
|
||||
# databits = 8
|
||||
# parity = none
|
||||
# stopbits = 1
|
||||
|
||||
from time import sleep
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
# This HDLC helper class is used by the interface
|
||||
# to delimit and packetize data over the physical
|
||||
# medium - in this case a serial connection.
|
||||
class HDLC():
|
||||
# This example interface packetizes data using
|
||||
# simplified HDLC framing, similar to PPP
|
||||
FLAG = 0x7E
|
||||
ESC = 0x7D
|
||||
ESC_MASK = 0x20
|
||||
|
||||
@staticmethod
|
||||
def escape(data):
|
||||
data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
|
||||
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
|
||||
return data
|
||||
|
||||
# Let's define our custom interface class. It must
|
||||
# be a sub-class of the RNS "Interface" class.
|
||||
class ExampleInterface(Interface):
|
||||
# All interface classes must define a default
|
||||
# IFAC size, used in IFAC setup when the user
|
||||
# has not specified a custom IFAC size. This
|
||||
# option is specified in bytes.
|
||||
DEFAULT_IFAC_SIZE = 8
|
||||
|
||||
# The following properties are local to this
|
||||
# particular interface implementation.
|
||||
owner = None
|
||||
port = None
|
||||
speed = None
|
||||
databits = None
|
||||
parity = None
|
||||
stopbits = None
|
||||
serial = None
|
||||
|
||||
# All Reticulum interfaces must have an __init__
|
||||
# method that takes 2 positional arguments:
|
||||
# The owner RNS Transport instance, and a dict
|
||||
# of configuration values.
|
||||
def __init__(self, owner, configuration):
|
||||
|
||||
# The following lines demonstrate handling
|
||||
# potential dependencies required for the
|
||||
# interface to function correctly.
|
||||
import importlib
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
else:
|
||||
RNS.log("Using this interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
# We start out by initialising the super-class
|
||||
super().__init__()
|
||||
|
||||
# To make sure the configuration data is in the
|
||||
# correct format, we parse it through the following
|
||||
# method on the generic Interface class. This step
|
||||
# is required to ensure compatibility on all the
|
||||
# platforms that Reticulum supports.
|
||||
ifconf = Interface.get_config_obj(configuration)
|
||||
|
||||
# Read the interface name from the configuration
|
||||
# and set it on our interface instance.
|
||||
name = ifconf["name"]
|
||||
self.name = name
|
||||
|
||||
# We read configuration parameters from the supplied
|
||||
# configuration data, and provide default values in
|
||||
# case any are missing.
|
||||
port = ifconf["port"] if "port" in ifconf else None
|
||||
speed = int(ifconf["speed"]) if "speed" in ifconf else 9600
|
||||
databits = int(ifconf["databits"]) if "databits" in ifconf else 8
|
||||
parity = ifconf["parity"] if "parity" in ifconf else "N"
|
||||
stopbits = int(ifconf["stopbits"]) if "stopbits" in ifconf else 1
|
||||
|
||||
# In case no port is specified, we abort setup by
|
||||
# raising an exception.
|
||||
if port == None:
|
||||
raise ValueError(f"No port specified for {self}")
|
||||
|
||||
# All interfaces must supply a hardware MTU value
|
||||
# to the RNS Transport instance. This value should
|
||||
# be the maximum data packet payload size that the
|
||||
# underlying medium is capable of handling in all
|
||||
# cases without any segmentation.
|
||||
self.HW_MTU = 564
|
||||
|
||||
# We initially set the "online" property to false,
|
||||
# since the interface has not actually been fully
|
||||
# initialised and connected yet.
|
||||
self.online = False
|
||||
|
||||
# In this case, we can also set the indicated bit-
|
||||
# rate of the interface to the serial port speed.
|
||||
self.bitrate = speed
|
||||
|
||||
# Configure internal properties on the interface
|
||||
# according to the supplied configuration.
|
||||
self.pyserial = serial
|
||||
self.serial = None
|
||||
self.owner = owner
|
||||
self.port = port
|
||||
self.speed = speed
|
||||
self.databits = databits
|
||||
self.parity = serial.PARITY_NONE
|
||||
self.stopbits = stopbits
|
||||
self.timeout = 100
|
||||
|
||||
if parity.lower() == "e" or parity.lower() == "even":
|
||||
self.parity = serial.PARITY_EVEN
|
||||
|
||||
if parity.lower() == "o" or parity.lower() == "odd":
|
||||
self.parity = serial.PARITY_ODD
|
||||
|
||||
# Since all required parameters are now configured,
|
||||
# we will try opening the serial port.
|
||||
try:
|
||||
self.open_port()
|
||||
except Exception as e:
|
||||
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
# If opening the port succeeded, run any post-open
|
||||
# configuration required.
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
else:
|
||||
raise IOError("Could not open serial port")
|
||||
|
||||
# Open the serial port with supplied configuration
|
||||
# parameters and store a reference to the open port.
|
||||
def open_port(self):
|
||||
RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE)
|
||||
self.serial = self.pyserial.Serial(
|
||||
port = self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = 0,
|
||||
inter_byte_timeout = None,
|
||||
write_timeout = None,
|
||||
dsrdtr = False,
|
||||
)
|
||||
|
||||
# The only thing required after opening the port
|
||||
# is to wait a small amount of time for the
|
||||
# hardware to initialise and then start a thread
|
||||
# that reads any incoming data from the device.
|
||||
def configure_device(self):
|
||||
sleep(0.5)
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
self.online = True
|
||||
RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
|
||||
|
||||
|
||||
# This method will be called from our read-loop
|
||||
# whenever a full packet has been received over
|
||||
# the underlying medium.
|
||||
def process_incoming(self, data):
|
||||
# Update our received bytes counter
|
||||
self.rxb += len(data)
|
||||
|
||||
# And send the data packet to the Transport
|
||||
# instance for processing.
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
# The running Reticulum Transport instance will
|
||||
# call this method on the interface whenever the
|
||||
# interface must transmit a packet.
|
||||
def process_outgoing(self,data):
|
||||
if self.online:
|
||||
# First, escape and packetize the data
|
||||
# according to HDLC framing.
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
|
||||
# Then write the framed data to the port
|
||||
written = self.serial.write(data)
|
||||
|
||||
# Update the transmitted bytes counter
|
||||
# and ensure that all data was written
|
||||
self.txb += len(data)
|
||||
if written != len(data):
|
||||
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
|
||||
|
||||
# This read loop runs in a thread and continously
|
||||
# receives bytes from the underlying serial port.
|
||||
# When a full packet has been received, it will
|
||||
# be sent to the process_incoming methed, which
|
||||
# will in turn pass it to the Transport instance.
|
||||
def read_loop(self):
|
||||
try:
|
||||
in_frame = False
|
||||
escape = False
|
||||
data_buffer = b""
|
||||
last_read_ms = int(time.time()*1000)
|
||||
|
||||
while self.serial.is_open:
|
||||
if self.serial.in_waiting:
|
||||
byte = ord(self.serial.read(1))
|
||||
last_read_ms = int(time.time()*1000)
|
||||
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.process_incoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (byte == HDLC.ESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.FLAG
|
||||
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.ESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
|
||||
else:
|
||||
time_since_last = int(time.time()*1000) - last_read_ms
|
||||
if len(data_buffer) > 0 and time_since_last > self.timeout:
|
||||
data_buffer = b""
|
||||
in_frame = False
|
||||
escape = False
|
||||
sleep(0.08)
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
|
||||
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
RNS.panic()
|
||||
|
||||
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
|
||||
|
||||
self.online = False
|
||||
self.serial.close()
|
||||
self.reconnect_port()
|
||||
|
||||
# This method handles serial port disconnects.
|
||||
def reconnect_port(self):
|
||||
while not self.online:
|
||||
try:
|
||||
time.sleep(5)
|
||||
RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
self.open_port()
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
except Exception as e:
|
||||
RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
# Signal to Reticulum that this interface should
|
||||
# not perform any ingress limiting.
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
# We must provide a string representation of this
|
||||
# interface, that is used whenever the interface
|
||||
# is printed in logs or external programs.
|
||||
def __str__(self):
|
||||
return "ExampleInterface["+self.name+"]"
|
||||
|
||||
# Finally, register the defined interface class as the
|
||||
# target class for Reticulum to use as an interface
|
||||
interface_class = ExampleInterface
|
||||
@@ -224,7 +224,7 @@ def client(destination_hexhash, configpath):
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
@@ -449,8 +449,7 @@ def link_established(link):
|
||||
# And set up a small job to check for
|
||||
# a potential timeout in receiving the
|
||||
# file list
|
||||
thread = threading.Thread(target=filelist_timeout_job)
|
||||
thread.setDaemon(True)
|
||||
thread = threading.Thread(target=filelist_timeout_job, daemon=True)
|
||||
thread.start()
|
||||
|
||||
# This job just sleeps for the specified
|
||||
@@ -463,7 +462,7 @@ def filelist_timeout_job():
|
||||
global server_files
|
||||
if len(server_files) == 0:
|
||||
RNS.log("Timed out waiting for filelist, exiting")
|
||||
os._exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# When a link is closed, we'll inform the
|
||||
@@ -476,9 +475,8 @@ def link_closed(link):
|
||||
else:
|
||||
RNS.log("Link closed, exiting now")
|
||||
|
||||
RNS.Reticulum.exit_handler()
|
||||
time.sleep(1.5)
|
||||
os._exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
# When RNS detects that the download has
|
||||
# started, we'll update our menu state
|
||||
@@ -602,4 +600,4 @@ if __name__ == "__main__":
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
@@ -133,7 +133,7 @@ def client(destination_hexhash, configpath):
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
@@ -245,9 +245,8 @@ def link_closed(link):
|
||||
else:
|
||||
RNS.log("Link closed, exiting now")
|
||||
|
||||
RNS.Reticulum.exit_handler()
|
||||
time.sleep(1.5)
|
||||
os._exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
# When a packet is received over the link, we
|
||||
# simply print out the data.
|
||||
@@ -311,4 +310,4 @@ if __name__ == "__main__":
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
@@ -119,7 +119,7 @@ def client(destination_hexhash, configpath):
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
@@ -222,9 +222,8 @@ def link_closed(link):
|
||||
else:
|
||||
RNS.log("Link closed, exiting now")
|
||||
|
||||
RNS.Reticulum.exit_handler()
|
||||
time.sleep(1.5)
|
||||
os._exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
# When a packet is received over the link, we
|
||||
# simply print out the data.
|
||||
@@ -288,4 +287,4 @@ if __name__ == "__main__":
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
@@ -5,6 +5,7 @@
|
||||
##########################################################
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import RNS
|
||||
|
||||
# Let's define an app name. We'll use this for all
|
||||
@@ -25,7 +26,7 @@ def program_setup(configpath):
|
||||
# Destinations are endpoints in Reticulum, that can be addressed
|
||||
# and communicated with. Destinations can also announce their
|
||||
# existence, which will let the network know they are reachable
|
||||
# and autoomatically create paths to them, from anywhere else
|
||||
# and automatically create paths to them, from anywhere else
|
||||
# in the network.
|
||||
destination = RNS.Destination(
|
||||
identity,
|
||||
@@ -36,7 +37,7 @@ def program_setup(configpath):
|
||||
)
|
||||
|
||||
# We configure the destination to automatically prove all
|
||||
# packets adressed to it. By doing this, RNS will automatically
|
||||
# packets addressed to it. By doing this, RNS will automatically
|
||||
# generate a proof for each incoming packet and transmit it
|
||||
# back to the sender of that packet. This will let anyone that
|
||||
# tries to communicate with the destination know whether their
|
||||
@@ -98,4 +99,4 @@ if __name__ == "__main__":
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,341 @@
|
||||
##########################################################
|
||||
# This RNS example demonstrates a simple client/server #
|
||||
# echo utility that uses ratchets to rotate encryption #
|
||||
# keys everytime an announce is sent. #
|
||||
##########################################################
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import RNS
|
||||
|
||||
# Let's define an app name. We'll use this for all
|
||||
# destinations we create. Since this echo example
|
||||
# is part of a range of example utilities, we'll put
|
||||
# them all within the app namespace "example_utilities"
|
||||
APP_NAME = "example_utilities"
|
||||
|
||||
|
||||
##########################################################
|
||||
#### Server Part #########################################
|
||||
##########################################################
|
||||
|
||||
# This initialisation is executed when the users chooses
|
||||
# to run as a server
|
||||
def server(configpath):
|
||||
global reticulum
|
||||
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
|
||||
# Randomly create a new identity for our echo server
|
||||
server_identity = RNS.Identity()
|
||||
|
||||
# We create a destination that clients can query. We want
|
||||
# to be able to verify echo replies to our clients, so we
|
||||
# create a "single" destination that can receive encrypted
|
||||
# messages. This way the client can send a request and be
|
||||
# certain that no-one else than this destination was able
|
||||
# to read it.
|
||||
echo_destination = RNS.Destination(
|
||||
server_identity,
|
||||
RNS.Destination.IN,
|
||||
RNS.Destination.SINGLE,
|
||||
APP_NAME,
|
||||
"ratchet",
|
||||
"echo",
|
||||
"request"
|
||||
)
|
||||
|
||||
# Enable ratchets on the destination by providing a file
|
||||
# path to store ratchets. In this example, we will just
|
||||
# use a temporary file, but in real-world applications,
|
||||
# it's extremely important to keep this file secure, since
|
||||
# it contains encryption keys for the destination.
|
||||
destination_hexhash = RNS.hexrep(echo_destination.hash, delimit=False)
|
||||
echo_destination.enable_ratchets(f"/tmp/{destination_hexhash}.ratchets")
|
||||
|
||||
# We configure the destination to automatically prove all
|
||||
# packets addressed to it. By doing this, RNS will automatically
|
||||
# generate a proof for each incoming packet and transmit it
|
||||
# back to the sender of that packet.
|
||||
echo_destination.set_proof_strategy(RNS.Destination.PROVE_ALL)
|
||||
|
||||
# Tell the destination which function in our program to
|
||||
# run when a packet is received. We do this so we can
|
||||
# print a log message when the server receives a request
|
||||
echo_destination.set_packet_callback(server_callback)
|
||||
|
||||
# Everything's ready!
|
||||
# Let's Wait for client requests or user input
|
||||
announceLoop(echo_destination)
|
||||
|
||||
|
||||
def announceLoop(destination):
|
||||
# Let the user know that everything is ready
|
||||
RNS.log(
|
||||
"Ratcheted echo server "+
|
||||
RNS.prettyhexrep(destination.hash)+
|
||||
" running, hit enter to manually send an announce (Ctrl-C to quit)"
|
||||
)
|
||||
|
||||
# We enter a loop that runs until the users exits.
|
||||
# If the user hits enter, we will announce our server
|
||||
# destination on the network, which will let clients
|
||||
# know how to create messages directed towards it.
|
||||
while True:
|
||||
entered = input()
|
||||
destination.announce()
|
||||
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
|
||||
|
||||
|
||||
def server_callback(message, packet):
|
||||
global reticulum
|
||||
|
||||
# Tell the user that we received an echo request, and
|
||||
# that we are going to send a reply to the requester.
|
||||
# Sending the proof is handled automatically, since we
|
||||
# set up the destination to prove all incoming packets.
|
||||
|
||||
reception_stats = ""
|
||||
if reticulum.is_connected_to_shared_instance:
|
||||
reception_rssi = reticulum.get_packet_rssi(packet.packet_hash)
|
||||
reception_snr = reticulum.get_packet_snr(packet.packet_hash)
|
||||
|
||||
if reception_rssi != None:
|
||||
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
|
||||
|
||||
if reception_snr != None:
|
||||
reception_stats += " [SNR "+str(reception_snr)+" dBm]"
|
||||
|
||||
else:
|
||||
if packet.rssi != None:
|
||||
reception_stats += " [RSSI "+str(packet.rssi)+" dBm]"
|
||||
|
||||
if packet.snr != None:
|
||||
reception_stats += " [SNR "+str(packet.snr)+" dB]"
|
||||
|
||||
RNS.log("Received packet from echo client, proof sent"+reception_stats)
|
||||
|
||||
|
||||
##########################################################
|
||||
#### Client Part #########################################
|
||||
##########################################################
|
||||
|
||||
# This initialisation is executed when the users chooses
|
||||
# to run as a client
|
||||
def client(destination_hexhash, configpath, timeout=None):
|
||||
global reticulum
|
||||
|
||||
# We need a binary representation of the destination
|
||||
# hash that was entered on the command line
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError(
|
||||
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
|
||||
)
|
||||
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e:
|
||||
RNS.log("Invalid destination entered. Check your input!")
|
||||
RNS.log(str(e)+"\n")
|
||||
sys.exit(0)
|
||||
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
|
||||
# We override the loglevel to provide feedback when
|
||||
# an announce is received
|
||||
if RNS.loglevel < RNS.LOG_INFO:
|
||||
RNS.loglevel = RNS.LOG_INFO
|
||||
|
||||
# Tell the user that the client is ready!
|
||||
RNS.log(
|
||||
"Echo client ready, hit enter to send echo request to "+
|
||||
destination_hexhash+
|
||||
" (Ctrl-C to quit)"
|
||||
)
|
||||
|
||||
# We enter a loop that runs until the user exits.
|
||||
# If the user hits enter, we will try to send an
|
||||
# echo request to the destination specified on the
|
||||
# command line.
|
||||
while True:
|
||||
input()
|
||||
|
||||
# Let's first check if RNS knows a path to the destination.
|
||||
# If it does, we'll load the server identity and create a packet
|
||||
if RNS.Transport.has_path(destination_hash):
|
||||
|
||||
# To address the server, we need to know it's public
|
||||
# key, so we check if Reticulum knows this destination.
|
||||
# This is done by calling the "recall" method of the
|
||||
# Identity module. If the destination is known, it will
|
||||
# return an Identity instance that can be used in
|
||||
# outgoing destinations.
|
||||
server_identity = RNS.Identity.recall(destination_hash)
|
||||
|
||||
# We got the correct identity instance from the
|
||||
# recall method, so let's create an outgoing
|
||||
# destination. We use the naming convention:
|
||||
# example_utilities.ratchet.echo.request
|
||||
# This matches the naming we specified in the
|
||||
# server part of the code.
|
||||
request_destination = RNS.Destination(
|
||||
server_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
APP_NAME,
|
||||
"ratchet",
|
||||
"echo",
|
||||
"request"
|
||||
)
|
||||
|
||||
# The destination is ready, so let's create a packet.
|
||||
# We set the destination to the request_destination
|
||||
# that was just created, and the only data we add
|
||||
# is a random hash.
|
||||
echo_request = RNS.Packet(request_destination, RNS.Identity.get_random_hash())
|
||||
|
||||
# Send the packet! If the packet is successfully
|
||||
# sent, it will return a PacketReceipt instance.
|
||||
packet_receipt = echo_request.send()
|
||||
|
||||
# If the user specified a timeout, we set this
|
||||
# timeout on the packet receipt, and configure
|
||||
# a callback function, that will get called if
|
||||
# the packet times out.
|
||||
if timeout != None:
|
||||
packet_receipt.set_timeout(timeout)
|
||||
packet_receipt.set_timeout_callback(packet_timed_out)
|
||||
|
||||
# We can then set a delivery callback on the receipt.
|
||||
# This will get automatically called when a proof for
|
||||
# this specific packet is received from the destination.
|
||||
packet_receipt.set_delivery_callback(packet_delivered)
|
||||
|
||||
# Tell the user that the echo request was sent
|
||||
RNS.log("Sent echo request to "+RNS.prettyhexrep(request_destination.hash))
|
||||
else:
|
||||
# If we do not know this destination, tell the
|
||||
# user to wait for an announce to arrive.
|
||||
RNS.log("Destination is not yet known. Requesting path...")
|
||||
RNS.log("Hit enter to manually retry once an announce is received.")
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
|
||||
# This function is called when our reply destination
|
||||
# receives a proof packet.
|
||||
def packet_delivered(receipt):
|
||||
global reticulum
|
||||
|
||||
if receipt.status == RNS.PacketReceipt.DELIVERED:
|
||||
rtt = receipt.get_rtt()
|
||||
if (rtt >= 1):
|
||||
rtt = round(rtt, 3)
|
||||
rttstring = str(rtt)+" seconds"
|
||||
else:
|
||||
rtt = round(rtt*1000, 3)
|
||||
rttstring = str(rtt)+" milliseconds"
|
||||
|
||||
reception_stats = ""
|
||||
if reticulum.is_connected_to_shared_instance:
|
||||
reception_rssi = reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
|
||||
reception_snr = reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
|
||||
|
||||
if reception_rssi != None:
|
||||
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
|
||||
|
||||
if reception_snr != None:
|
||||
reception_stats += " [SNR "+str(reception_snr)+" dB]"
|
||||
|
||||
else:
|
||||
if receipt.proof_packet != None:
|
||||
if receipt.proof_packet.rssi != None:
|
||||
reception_stats += " [RSSI "+str(receipt.proof_packet.rssi)+" dBm]"
|
||||
|
||||
if receipt.proof_packet.snr != None:
|
||||
reception_stats += " [SNR "+str(receipt.proof_packet.snr)+" dB]"
|
||||
|
||||
RNS.log(
|
||||
"Valid reply received from "+
|
||||
RNS.prettyhexrep(receipt.destination.hash)+
|
||||
", round-trip time is "+rttstring+
|
||||
reception_stats
|
||||
)
|
||||
|
||||
# This function is called if a packet times out.
|
||||
def packet_timed_out(receipt):
|
||||
if receipt.status == RNS.PacketReceipt.FAILED:
|
||||
RNS.log("Packet "+RNS.prettyhexrep(receipt.hash)+" timed out")
|
||||
|
||||
|
||||
##########################################################
|
||||
#### Program Startup #####################################
|
||||
##########################################################
|
||||
|
||||
# This part of the program gets run at startup,
|
||||
# and parses input from the user, and then starts
|
||||
# the desired program mode.
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description="Simple ratcheted echo server and client utility")
|
||||
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--server",
|
||||
action="store_true",
|
||||
help="wait for incoming packets from clients"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--timeout",
|
||||
action="store",
|
||||
metavar="s",
|
||||
default=None,
|
||||
help="set a reply timeout in seconds",
|
||||
type=float
|
||||
)
|
||||
|
||||
parser.add_argument("--config",
|
||||
action="store",
|
||||
default=None,
|
||||
help="path to alternative Reticulum config directory",
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"destination",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="hexadecimal hash of the server destination",
|
||||
type=str
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.server:
|
||||
configarg=None
|
||||
if args.config:
|
||||
configarg = args.config
|
||||
server(configarg)
|
||||
else:
|
||||
if args.config:
|
||||
configarg = args.config
|
||||
else:
|
||||
configarg = None
|
||||
|
||||
if args.timeout:
|
||||
timeoutarg = float(args.timeout)
|
||||
else:
|
||||
timeoutarg = None
|
||||
|
||||
if (args.destination == None):
|
||||
print("")
|
||||
parser.print_help()
|
||||
print("")
|
||||
else:
|
||||
client(args.destination, configarg, timeout=timeoutarg)
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
sys.exit(0)
|
||||
@@ -119,7 +119,7 @@ def client(destination_hexhash, configpath):
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
@@ -226,9 +226,8 @@ def link_closed(link):
|
||||
else:
|
||||
RNS.log("Link closed, exiting now")
|
||||
|
||||
RNS.Reticulum.exit_handler()
|
||||
time.sleep(1.5)
|
||||
os._exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
##########################################################
|
||||
@@ -284,4 +283,4 @@ if __name__ == "__main__":
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
@@ -149,8 +149,6 @@ def server_packet_received(message, packet):
|
||||
time.sleep(0.2)
|
||||
rc = 0
|
||||
received_data = 0
|
||||
# latest_client_link.teardown()
|
||||
# os._exit(0)
|
||||
|
||||
|
||||
##########################################################
|
||||
@@ -159,6 +157,7 @@ def server_packet_received(message, packet):
|
||||
|
||||
# A reference to the server link
|
||||
server_link = None
|
||||
should_quit = False
|
||||
|
||||
# This initialisation is executed when the users chooses
|
||||
# to run as a client
|
||||
@@ -175,7 +174,7 @@ def client(destination_hexhash, configpath):
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
@@ -216,7 +215,7 @@ def client(destination_hexhash, configpath):
|
||||
client_loop()
|
||||
|
||||
def client_loop():
|
||||
global server_link
|
||||
global server_link, should_quit
|
||||
|
||||
# Wait for the link to become active
|
||||
while not server_link:
|
||||
@@ -224,16 +223,7 @@ def client_loop():
|
||||
|
||||
should_quit = False
|
||||
while not should_quit:
|
||||
try:
|
||||
text = input()
|
||||
|
||||
# Check if we should quit the example
|
||||
if text == "quit" or text == "q" or text == "exit":
|
||||
should_quit = True
|
||||
server_link.teardown()
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
time.sleep(0.2)
|
||||
|
||||
# This function is called when a link
|
||||
# has been established with the server
|
||||
@@ -246,8 +236,8 @@ def link_established(link):
|
||||
|
||||
# Inform the user that the server is
|
||||
# connected
|
||||
RNS.log("Link established with server,sending...")
|
||||
rd = os.urandom(RNS.Link.MDU)
|
||||
RNS.log("Link established with server, sending...")
|
||||
rd = os.urandom(link.mdu)
|
||||
started = time.time()
|
||||
while link.status == RNS.Link.ACTIVE and data_sent < data_cap*1.25:
|
||||
RNS.Packet(server_link, rd, create_receipt=False).send()
|
||||
@@ -276,17 +266,17 @@ def link_established(link):
|
||||
# When a link is closed, we'll inform the
|
||||
# user, and exit the program
|
||||
def link_closed(link):
|
||||
global should_quit
|
||||
if link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
RNS.log("The link timed out, exiting now")
|
||||
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
|
||||
RNS.log("The link was closed by the server, exiting now")
|
||||
else:
|
||||
RNS.log("Link closed, exiting now")
|
||||
|
||||
RNS.Reticulum.exit_handler()
|
||||
|
||||
should_quit = True
|
||||
time.sleep(1.5)
|
||||
os._exit(0)
|
||||
sys.exit(0)
|
||||
|
||||
def client_packet_received(message, packet):
|
||||
pass
|
||||
@@ -344,4 +334,4 @@ if __name__ == "__main__":
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
sys.exit(0)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ko_fi: markqvist
|
||||
custom: "https://unsigned.io/donate"
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License, unless otherwise noted
|
||||
|
||||
Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
Copyright (c) 2016-2024 Mark Qvist / unsigned.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -2,7 +2,7 @@ all: release
|
||||
|
||||
test:
|
||||
@echo Running tests...
|
||||
python -m tests.all
|
||||
python3 -m tests.all
|
||||
|
||||
clean:
|
||||
@echo Cleaning...
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Reticulum Network Stack β <img align="right" src="https://static.pepy.tech/personalized-badge/rns?period=total&units=international_system&left_color=grey&right_color=blue&left_text=Installs"/>
|
||||
Reticulum Network Stack β <img align="right" src="https://static.pepy.tech/personalized-badge/rns?period=month&units=international_system&left_color=grey&right_color=blue&left_text=Installs/month" style="padding-left:10px"/><a href="https://github.com/markqvist/Reticulum/actions/workflows/build.yml"><img align="right" src="https://github.com/markqvist/Reticulum/actions/workflows/build.yml/badge.svg"/></a>
|
||||
==========
|
||||
|
||||
<p align="center"><img width="200" src="https://raw.githubusercontent.com/markqvist/Reticulum/master/docs/source/graphics/rns_logo_512.png"></p>
|
||||
@@ -35,35 +35,48 @@ userland, and can run on practically any system that runs Python 3.
|
||||
## Read The Manual
|
||||
The full documentation for Reticulum is available at [markqvist.github.io/Reticulum/manual/](https://markqvist.github.io/Reticulum/manual/).
|
||||
|
||||
You can also [download the Reticulum manual as a PDF](https://github.com/markqvist/Reticulum/raw/master/docs/Reticulum%20Manual.pdf)
|
||||
You can also download the [Reticulum manual as a PDF](https://github.com/markqvist/Reticulum/raw/master/docs/Reticulum%20Manual.pdf) or [as an e-book in EPUB format](https://github.com/markqvist/Reticulum/raw/master/docs/Reticulum%20Manual.epub).
|
||||
|
||||
For more info, see [reticulum.network](https://reticulum.network/)
|
||||
For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ section of the wiki](https://github.com/markqvist/Reticulum/wiki/Frequently-Asked-Questions).
|
||||
|
||||
## Notable Features
|
||||
- Coordination-less globally unique addressing and identification
|
||||
- Fully self-configuring multi-hop routing
|
||||
- Fully self-configuring multi-hop routing over heterogeneous carriers
|
||||
- Flexible scalability over heterogeneous topologies
|
||||
- Reticulum can carry data over any mixture of physical mediums and topologies
|
||||
- Low-bandwidth networks can co-exist and interoperate with large, high-bandwidth networks
|
||||
- Initiator anonymity, communicate without revealing your identity
|
||||
- Reticulum does not include source addresses on any packets
|
||||
- Asymmetric X25519 encryption and Ed25519 signatures as a basis for all communication
|
||||
- Forward Secrecy with ephemeral Elliptic Curve Diffie-Hellman keys on Curve25519
|
||||
- Reticulum uses the [Fernet](https://github.com/fernet/spec/blob/master/Spec.md) specification for on-the-wire / over-the-air encryption
|
||||
- Keys are ephemeral and derived from an ECDH key exchange on Curve25519
|
||||
- The foundational Reticulum Identity Keys are 512-bit Elliptic Curve keysets
|
||||
- Forward Secrecy is available for all communication types, both for single packets and over links
|
||||
- Reticulum uses the following format for encrypted tokens:
|
||||
- Ephemeral per-packet and link keys and derived from an ECDH key exchange on Curve25519
|
||||
- AES-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
|
||||
- Flexible and extensible interface system
|
||||
- Reticulum includes a large variety of built-in interface types
|
||||
- Ability to load and utilise custom user- or community-supplied interface types
|
||||
- Easily create your own custom interfaces for communicating over anything
|
||||
- Authentication and virtual network segmentation on all supported interface types
|
||||
- An intuitive and easy-to-use API
|
||||
- Simpler and easier to use than sockets APIs and simpler, but more powerful
|
||||
- Makes building distributed and decentralised applications much simpler
|
||||
- Reliable and efficient transfer of arbitrary amounts of data
|
||||
- Reticulum can handle a few bytes of data or files of many gigabytes
|
||||
- Sequencing, transfer coordination and checksumming are automatic
|
||||
- Sequencing, compression, transfer coordination and checksumming are automatic
|
||||
- The API is very easy to use, and provides transfer progress
|
||||
- Lightweight, flexible and expandable Request/Response mechanism
|
||||
- Efficient link establishment
|
||||
- Total bandwidth cost of setting up an encrypted link is 3 packets totaling 297 bytes
|
||||
- Total cost of setting up an encrypted and verified link is only 3 packets, totalling 297 bytes
|
||||
- Low cost of keeping links open at only 0.44 bits per second
|
||||
- Reliable sequential delivery with Channel and Buffer mechanisms
|
||||
|
||||
## Roadmap
|
||||
While Reticulum is already a fully featured and functional networking stack, many improvements and additions are actively being worked on, and planned for the future.
|
||||
While Reticulum is already a fully featured and functional networking stack,
|
||||
many improvements and additions are actively being worked on, and planned for the future.
|
||||
|
||||
To learn more about the direction and future of Reticulum, please see the [Development Roadmap](./Roadmap.md).
|
||||
|
||||
@@ -72,13 +85,14 @@ If you want to quickly get an idea of what Reticulum can do, take a look at the
|
||||
following resources.
|
||||
|
||||
- You can use the [rnsh](https://github.com/acehoss/rnsh) program to establish remote shell sessions over Reticulum.
|
||||
- For an off-grid, encrypted and resilient mesh communications platform, see [Nomad Network](https://github.com/markqvist/NomadNet)
|
||||
- The Android, Linux and macOS app [Sideband](https://github.com/markqvist/Sideband) has a graphical interface and focuses on ease of use.
|
||||
- [LXMF](https://github.com/markqvist/lxmf) is a distributed, delay and disruption tolerant message transfer protocol built on Reticulum
|
||||
- For an off-grid, encrypted and resilient mesh communications platform, see [Nomad Network](https://github.com/markqvist/NomadNet)
|
||||
- The Android, Linux, macOS and Windows app [Sideband](https://github.com/markqvist/Sideband) has a graphical interface and focuses on ease of use.
|
||||
- [MeshChat](https://github.com/liamcottle/reticulum-meshchat) is a user-friendly LXMF client, that also supports voice calls.
|
||||
|
||||
## Where can Reticulum be used?
|
||||
Over practically any medium that can support at least a half-duplex channel
|
||||
with 500 bits per second throughput, and an MTU of 500 bytes. Data radios,
|
||||
with greater throughput than 5 bits per second, and an MTU of 500 bytes. Data radios,
|
||||
modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes,
|
||||
WiFi and Ethernet devices, free-space optical links, and similar systems are
|
||||
all examples of the types of physical devices Reticulum can use.
|
||||
@@ -108,14 +122,28 @@ you want to do. For full details and examples, have a look at the
|
||||
[Getting Started Fast](https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html)
|
||||
section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
|
||||
|
||||
To simply install Reticulum and related utilities on your system, the easiest way is via pip:
|
||||
To simply install Reticulum and related utilities on your system, the easiest way is via `pip`.
|
||||
You can then start any program that uses Reticulum, or start Reticulum as a system service with
|
||||
[the rnsd utility](https://markqvist.github.io/Reticulum/manual/using.html#the-rnsd-utility).
|
||||
|
||||
```bash
|
||||
pip install rns
|
||||
```
|
||||
|
||||
You can then start any program that uses Reticulum, or start Reticulum as a
|
||||
system service with [the rnsd utility](https://markqvist.github.io/Reticulum/manual/using.html#the-rnsd-utility).
|
||||
If you are using an operating system that blocks normal user package installation via `pip`,
|
||||
you can return `pip` to normal behaviour by editing the `~/.config/pip/pip.conf` file,
|
||||
and adding the following directive in the `[global]` section:
|
||||
|
||||
```text
|
||||
[global]
|
||||
break-system-packages = true
|
||||
```
|
||||
|
||||
Alternatively, you can use the `pipx` tool to install Reticulum in an isolated environment:
|
||||
|
||||
```bash
|
||||
pipx install rns
|
||||
```
|
||||
|
||||
When first started, Reticulum will create a default configuration file,
|
||||
providing basic connectivity to other Reticulum peers that might be locally
|
||||
@@ -139,22 +167,26 @@ section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/)
|
||||
- An interface status utility called `rnstatus`, that displays information about interfaces
|
||||
- The path lookup and management tool `rnpath` letting you view and modify path tables
|
||||
- A diagnostics tool called `rnprobe` for checking connectivity to destinations
|
||||
- A simple file transfer program called `rncp` making it easy to copy files to remote systems
|
||||
- A simple file transfer program called `rncp` making it easy to transfer files between systems
|
||||
- The identity management and encryption utility `rnid` let's you manage Identities and encrypt/decrypt files
|
||||
- The remote command execution program `rnx` let's you run commands and
|
||||
programs and retrieve output from remote systems
|
||||
|
||||
All tools, including `rnx` and `rncp`, work reliably and well even over very
|
||||
low-bandwidth links like LoRa or Packet Radio.
|
||||
low-bandwidth links like LoRa or Packet Radio. For full-featured remote shells
|
||||
over Reticulum, also have a look at the [rnsh](https://github.com/acehoss/rnsh)
|
||||
program.
|
||||
|
||||
## Supported interface types and devices
|
||||
|
||||
Reticulum implements a range of generalised interface types that covers most of
|
||||
the communications hardware that Reticulum can run over. If your hardware is
|
||||
not supported, it's relatively simple to implement an interface class. I will
|
||||
gratefully accept pull requests for custom interfaces if they are generally
|
||||
useful.
|
||||
not supported, it's [simple to implement a custom interface module](https://markqvist.github.io/Reticulum/manual/interfaces.html#custom-interfaces).
|
||||
|
||||
Currently, the following interfaces are supported:
|
||||
Pull requests for custom interfaces are gratefully accepted, provided they are
|
||||
generally useful and well-tested in real-world usage.
|
||||
|
||||
Currently, the following built-in interfaces are supported:
|
||||
|
||||
- Any Ethernet device
|
||||
- LoRa using [RNode](https://unsigned.io/rnode/)
|
||||
@@ -172,8 +204,8 @@ functionality and performance on low-bandwidth mediums. The goal is to
|
||||
provide a dynamic performance envelope from 250 bits per second, to 1 gigabit
|
||||
per second on normal hardware.
|
||||
|
||||
Currently, the usable performance envelope is approximately 500 bits per second
|
||||
to 20 megabits per second, with physical mediums faster than that not being
|
||||
Currently, the usable performance envelope is approximately 150 bits per second
|
||||
to 40 megabits per second, with physical mediums faster than that not being
|
||||
saturated. Performance beyond the current level is intended for future
|
||||
upgrades, but not highly prioritised at this point in time.
|
||||
|
||||
@@ -214,10 +246,17 @@ Primitives](#cryptographic-primitives) section of this document.
|
||||
|
||||
## Public Testnet
|
||||
If you just want to get started experimenting without building any physical
|
||||
networks, you are welcome to join the Unsigned.io RNS Testnet. The testnet is
|
||||
just that, an informal network for testing and experimenting. It will be up
|
||||
most of the time, and anyone can join, but it also means that there's no
|
||||
guarantees for service availability.
|
||||
networks, you are welcome to join the RNS Development Testnet.
|
||||
|
||||
The testnet is just that, an informal network for testing and experimenting.
|
||||
It will be up most of the time, and anyone can join, but it also means that
|
||||
there's no guarantees for service availability.
|
||||
|
||||
It probably goes without saying, but *don't use the testnet entry-points as
|
||||
hardcoded or default interfaces in any applications you ship to users*. When
|
||||
shipping applications, the best practice is to provide your own default
|
||||
connectivity solutions, if needed and applicable, or in most cases, simply
|
||||
leave it up to the user which networks to connect to, and how.
|
||||
|
||||
The testnet runs the very latest version of Reticulum (often even a short while
|
||||
before it is publicly released). Sometimes experimental versions of Reticulum
|
||||
@@ -227,25 +266,25 @@ I2P. Just add one of the following interfaces to your Reticulum configuration
|
||||
file:
|
||||
|
||||
```
|
||||
# TCP/IP interface to the RNS Dublin Hub
|
||||
[[RNS Testnet Dublin]]
|
||||
# TCP/IP interface to the RNS Amsterdam Hub
|
||||
[[RNS Testnet Amsterdam]]
|
||||
type = TCPClientInterface
|
||||
enabled = yes
|
||||
target_host = dublin.connect.reticulum.network
|
||||
target_host = amsterdam.connect.reticulum.network
|
||||
target_port = 4965
|
||||
|
||||
# TCP/IP interface to the BetweenTheBorders Hub (community-provided)
|
||||
[[RNS Testnet BetweenTheBorders]]
|
||||
type = TCPClientInterface
|
||||
enabled = yes
|
||||
target_host = betweentheborders.com
|
||||
target_host = reticulum.betweentheborders.com
|
||||
target_port = 4242
|
||||
|
||||
# Interface to Testnet I2P Hub
|
||||
[[RNS Testnet I2P Hub]]
|
||||
type = I2PInterface
|
||||
enabled = yes
|
||||
peers = pmlm3l5rpympihoy2o5ago43kluei2jjjzsalcuiuylbve3mwi2a.b32.i2p
|
||||
peers = g3br23bvx3lq5uddcsjii74xgmn6y5q325ovrkq2zw2wbzbqgbuq.b32.i2p
|
||||
```
|
||||
|
||||
The testnet also contains a number of [Nomad Network](https://github.com/markqvist/nomadnet) nodes, and LXMF propagation nodes.
|
||||
@@ -254,16 +293,16 @@ The testnet also contains a number of [Nomad Network](https://github.com/markqvi
|
||||
You can help support the continued development of open, free and private communications systems by donating via one of the following channels:
|
||||
|
||||
- Monero:
|
||||
```
|
||||
```
|
||||
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
|
||||
```
|
||||
- Ethereum
|
||||
```
|
||||
0x81F7B979fEa6134bA9FD5c701b3501A2e61E897a
|
||||
0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73
|
||||
```
|
||||
- Bitcoin
|
||||
```
|
||||
3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq
|
||||
35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH
|
||||
```
|
||||
- Ko-Fi: https://ko-fi.com/markqvist
|
||||
|
||||
@@ -271,17 +310,20 @@ Are certain features in the development roadmap are important to you or your
|
||||
organisation? Make them a reality quickly by sponsoring their implementation.
|
||||
|
||||
## Cryptographic Primitives
|
||||
Reticulum uses a simple suite of efficient, strong and modern cryptographic
|
||||
Reticulum uses a simple suite of efficient, strong and well-tested cryptographic
|
||||
primitives, with widely available implementations that can be used both on
|
||||
general-purpose CPUs and on microcontrollers. The necessary primitives are:
|
||||
general-purpose CPUs and on microcontrollers. The utilised primitives are:
|
||||
|
||||
- Ed25519 for signatures
|
||||
- X22519 for ECDH key exchanges
|
||||
- Reticulum Identity Keys are 512-bit Curve25519 keysets
|
||||
- A 256-bit Ed25519 key for signatures
|
||||
- A 256-bit X22519 key for ECDH key exchanges
|
||||
- HKDF for key derivation
|
||||
- Modified Fernet for encrypted tokens
|
||||
- AES-128 in CBC mode
|
||||
- HMAC for message authentication
|
||||
- No Fernet version and timestamp fields
|
||||
- Encrypted tokens are based on the [Fernet spec](https://github.com/fernet/spec/)
|
||||
- Ephemeral keys derived from an ECDH key exchange on Curve25519
|
||||
- AES-128 in CBC mode with PKCS7 padding
|
||||
- HMAC using SHA256 for message authentication
|
||||
- IVs are generated through os.urandom()
|
||||
- No Fernet version and timestamp metadata fields
|
||||
- SHA-256
|
||||
- SHA-512
|
||||
|
||||
@@ -290,12 +332,12 @@ In the default installation configuration, the `X25519`, `Ed25519` and
|
||||
(via the [PyCA/cryptography](https://github.com/pyca/cryptography) package).
|
||||
The hashing functions `SHA-256` and `SHA-512` are provided by the standard
|
||||
Python [hashlib](https://docs.python.org/3/library/hashlib.html). The `HKDF`,
|
||||
`HMAC`, `Fernet` primitives, and the `PKCS7` padding function are always
|
||||
`HMAC`, `Token` primitives, and the `PKCS7` padding function are always
|
||||
provided by the following internal implementations:
|
||||
|
||||
- [HKDF.py](RNS/Cryptography/HKDF.py)
|
||||
- [HMAC.py](RNS/Cryptography/HMAC.py)
|
||||
- [Fernet.py](RNS/Cryptography/Fernet.py)
|
||||
- [Token.py](RNS/Cryptography/Token.py)
|
||||
- [PKCS7.py](RNS/Cryptography/PKCS7.py)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
import bz2
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
from threading import RLock
|
||||
import struct
|
||||
@@ -22,13 +45,14 @@ class StreamDataMessage(MessageBase):
|
||||
The stream id is limited to 2 bytes - 2 bit
|
||||
"""
|
||||
|
||||
MAX_DATA_LEN = RNS.Link.MDU - 2 - 6 # 2 for stream data message header, 6 for channel envelope
|
||||
OVERHEAD = 2 + 6 # 2 for stream data message header, 6 for channel envelope
|
||||
MAX_DATA_LEN = RNS.Link.MDU - OVERHEAD
|
||||
"""
|
||||
When the Buffer package is imported, this value is
|
||||
calculcated based on the value of OVERHEAD
|
||||
"""
|
||||
|
||||
def __init__(self, stream_id: int = None, data: bytes = None, eof: bool = False):
|
||||
def __init__(self, stream_id: int = None, data: bytes = None, eof: bool = False, compressed: bool = False):
|
||||
"""
|
||||
This class is used to encapsulate binary stream
|
||||
data to be sent over a ``Channel``.
|
||||
@@ -41,7 +65,7 @@ class StreamDataMessage(MessageBase):
|
||||
if stream_id is not None and stream_id > self.STREAM_ID_MAX:
|
||||
raise ValueError("stream_id must be 0-16383")
|
||||
self.stream_id = stream_id
|
||||
self.compressed = False
|
||||
self.compressed = compressed
|
||||
self.data = data or bytes()
|
||||
self.eof = eof
|
||||
|
||||
@@ -49,13 +73,6 @@ class StreamDataMessage(MessageBase):
|
||||
if self.stream_id is None:
|
||||
raise ValueError("stream_id")
|
||||
|
||||
compressed_data = bz2.compress(self.data)
|
||||
saved = len(self.data)-len(compressed_data)
|
||||
|
||||
if saved > 0:
|
||||
self.data = compressed_data
|
||||
self.compressed = True
|
||||
|
||||
header_val = (0x3fff & self.stream_id) | (0x8000 if self.eof else 0x0000) | (0x4000 if self.compressed > 0 else 0x0000)
|
||||
return bytes(struct.pack(">H", header_val) + (self.data if self.data else bytes()))
|
||||
|
||||
@@ -65,6 +82,7 @@ class StreamDataMessage(MessageBase):
|
||||
self.compressed = (0x4000 & self.stream_id) > 0
|
||||
self.stream_id = self.stream_id & 0x3fff
|
||||
self.data = raw[2:]
|
||||
|
||||
if self.compressed:
|
||||
self.data = bz2.decompress(self.data)
|
||||
|
||||
@@ -129,9 +147,9 @@ class RawChannelReader(RawIOBase, AbstractContextManager):
|
||||
self._eof = True
|
||||
for listener in self._listeners:
|
||||
try:
|
||||
listener(len(self._buffer))
|
||||
threading.Thread(target=listener, name="Message Callback", args=[len(self._buffer)], daemon=True).start()
|
||||
except Exception as ex:
|
||||
RNS.log("Error calling RawChannelReader(" + str(self._stream_id) + ") callback: " + str(ex))
|
||||
RNS.log("Error calling RawChannelReader(" + str(self._stream_id) + ") callback: " + str(ex), RNS.LOG_ERROR)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -184,6 +202,10 @@ class RawChannelWriter(RawIOBase, AbstractContextManager):
|
||||
object, see the Python documentation for
|
||||
``RawIOBase``.
|
||||
"""
|
||||
|
||||
MAX_CHUNK_LEN = 1024*16
|
||||
COMPRESSION_TRIES = 4
|
||||
|
||||
def __init__(self, stream_id: int, channel: Channel):
|
||||
"""
|
||||
Create a raw channel writer.
|
||||
@@ -194,19 +216,55 @@ class RawChannelWriter(RawIOBase, AbstractContextManager):
|
||||
self._stream_id = stream_id
|
||||
self._channel = channel
|
||||
self._eof = False
|
||||
self._mdu = channel.mdu - StreamDataMessage.OVERHEAD
|
||||
|
||||
def write(self, __b: bytes) -> int | None:
|
||||
try:
|
||||
chunk = bytes(__b[:StreamDataMessage.MAX_DATA_LEN])
|
||||
message = StreamDataMessage(self._stream_id, chunk, self._eof)
|
||||
comp_tries = RawChannelWriter.COMPRESSION_TRIES
|
||||
comp_try = 1
|
||||
comp_success = False
|
||||
chunk_len = len(__b)
|
||||
if chunk_len > RawChannelWriter.MAX_CHUNK_LEN:
|
||||
chunk_len = RawChannelWriter.MAX_CHUNK_LEN
|
||||
__b = __b[:RawChannelWriter.MAX_CHUNK_LEN]
|
||||
chunk_segment = None
|
||||
while chunk_len > 32 and comp_try < comp_tries:
|
||||
chunk_segment_length = int(chunk_len/comp_try)
|
||||
compressed_chunk = bz2.compress(__b[:chunk_segment_length])
|
||||
compressed_length = len(compressed_chunk)
|
||||
if compressed_length < StreamDataMessage.MAX_DATA_LEN and compressed_length < chunk_segment_length:
|
||||
comp_success = True
|
||||
break
|
||||
else:
|
||||
comp_try += 1
|
||||
|
||||
if comp_success:
|
||||
chunk = compressed_chunk
|
||||
processed_length = chunk_segment_length
|
||||
else:
|
||||
chunk = bytes(__b[:StreamDataMessage.MAX_DATA_LEN])
|
||||
processed_length = len(chunk)
|
||||
|
||||
message = StreamDataMessage(self._stream_id, chunk, self._eof, comp_success)
|
||||
|
||||
self._channel.send(message)
|
||||
return len(chunk)
|
||||
return processed_length
|
||||
|
||||
except RNS.Channel.ChannelException as cex:
|
||||
if cex.type != RNS.Channel.CEType.ME_LINK_NOT_READY:
|
||||
raise
|
||||
return 0
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
link_rtt = self._channel._outlet.link.rtt
|
||||
timeout = time.time() + (link_rtt * len(self._channel._tx_ring) * 1)
|
||||
except Exception as e:
|
||||
timeout = time.time() + 15
|
||||
|
||||
while time.time() < timeout and not self._channel.is_ready_to_send():
|
||||
time.sleep(0.05)
|
||||
|
||||
self._eof = True
|
||||
self.write(bytes())
|
||||
|
||||
|
||||
@@ -176,6 +176,9 @@ class Envelope:
|
||||
raise ChannelException(CEType.ME_NOT_REGISTERED, f"Unable to find constructor for Channel MSGTYPE {hex(msgtype)}")
|
||||
message = ctor()
|
||||
message.unpack(raw)
|
||||
self.unpacked = True
|
||||
self.message = message
|
||||
|
||||
return message
|
||||
|
||||
def pack(self) -> bytes:
|
||||
@@ -183,6 +186,7 @@ class Envelope:
|
||||
raise ChannelException(CEType.ME_NO_MSG_TYPE, f"{self.message.__class__} lacks MSGTYPE")
|
||||
data = self.message.pack()
|
||||
self.raw = struct.pack(">HHH", self.message.MSGTYPE, self.sequence, len(data)) + data
|
||||
self.packed = True
|
||||
return self.raw
|
||||
|
||||
def __init__(self, outlet: ChannelOutletBase, message: MessageBase = None, raw: bytes = None, sequence: int = None):
|
||||
@@ -194,6 +198,8 @@ class Envelope:
|
||||
self.sequence = sequence
|
||||
self.outlet = outlet
|
||||
self.tries = 0
|
||||
self.unpacked = False
|
||||
self.packed = False
|
||||
self.tracked = False
|
||||
|
||||
|
||||
@@ -228,13 +234,16 @@ class Channel(contextlib.AbstractContextManager):
|
||||
WINDOW = 2
|
||||
|
||||
# Absolute minimum window size
|
||||
WINDOW_MIN = 1
|
||||
WINDOW_MIN = 2
|
||||
WINDOW_MIN_LIMIT_SLOW = 2
|
||||
WINDOW_MIN_LIMIT_MEDIUM = 5
|
||||
WINDOW_MIN_LIMIT_FAST = 16
|
||||
|
||||
# The maximum window size for transfers on slow links
|
||||
WINDOW_MAX_SLOW = 5
|
||||
|
||||
# The maximum window size for transfers on mid-speed links
|
||||
WINDOW_MAX_MEDIUM = 16
|
||||
WINDOW_MAX_MEDIUM = 12
|
||||
|
||||
# The maximum window size for transfers on fast links
|
||||
WINDOW_MAX_FAST = 48
|
||||
@@ -249,7 +258,7 @@ class Channel(contextlib.AbstractContextManager):
|
||||
|
||||
# If the RTT rate is higher than this value,
|
||||
# the max window size for fast links will be used.
|
||||
RTT_FAST = 0.25
|
||||
RTT_FAST = 0.18
|
||||
RTT_MEDIUM = 0.75
|
||||
RTT_SLOW = 1.45
|
||||
|
||||
@@ -371,22 +380,28 @@ class Channel(contextlib.AbstractContextManager):
|
||||
def _emplace_envelope(self, envelope: Envelope, ring: collections.deque[Envelope]) -> bool:
|
||||
with self._lock:
|
||||
i = 0
|
||||
|
||||
for existing in ring:
|
||||
if existing.sequence > envelope.sequence \
|
||||
and not existing.sequence // 2 > envelope.sequence: # account for overflow
|
||||
ring.insert(i, envelope)
|
||||
return True
|
||||
if existing.sequence == envelope.sequence:
|
||||
|
||||
if envelope.sequence == existing.sequence:
|
||||
RNS.log(f"Envelope: Emplacement of duplicate envelope with sequence "+str(envelope.sequence), RNS.LOG_EXTREME)
|
||||
return False
|
||||
|
||||
if envelope.sequence < existing.sequence and not (self._next_rx_sequence - envelope.sequence) > (Channel.SEQ_MAX//2):
|
||||
ring.insert(i, envelope)
|
||||
|
||||
envelope.tracked = True
|
||||
return True
|
||||
|
||||
i += 1
|
||||
|
||||
envelope.tracked = True
|
||||
ring.append(envelope)
|
||||
|
||||
return True
|
||||
|
||||
def _run_callbacks(self, message: MessageBase):
|
||||
with self._lock:
|
||||
cbs = self._message_callbacks.copy()
|
||||
cbs = self._message_callbacks.copy()
|
||||
|
||||
for cb in cbs:
|
||||
try:
|
||||
@@ -405,30 +420,38 @@ class Channel(contextlib.AbstractContextManager):
|
||||
window_overflow = (self._next_rx_sequence+Channel.WINDOW_MAX) % Channel.SEQ_MODULUS
|
||||
if window_overflow < self._next_rx_sequence:
|
||||
if envelope.sequence > window_overflow:
|
||||
RNS.log("Invalid packet sequence ("+str(envelope.sequence)+") received on channel "+str(self), RNS.LOG_DEBUG)
|
||||
RNS.log("Invalid packet sequence ("+str(envelope.sequence)+") received on channel "+str(self), RNS.LOG_EXTREME)
|
||||
return
|
||||
else:
|
||||
if envelope.sequence < self._next_rx_sequence:
|
||||
RNS.log("Invalid packet sequence ("+str(envelope.sequence)+") received on channel "+str(self), RNS.LOG_DEBUG)
|
||||
return
|
||||
RNS.log("Invalid packet sequence ("+str(envelope.sequence)+") received on channel "+str(self), RNS.LOG_EXTREME)
|
||||
return
|
||||
|
||||
is_new = self._emplace_envelope(envelope, self._rx_ring)
|
||||
|
||||
if not is_new:
|
||||
RNS.log("Duplicate message received on channel "+str(self), RNS.LOG_EXTREME)
|
||||
return
|
||||
else:
|
||||
else:
|
||||
with self._lock:
|
||||
contigous = []
|
||||
for e in self._rx_ring:
|
||||
if e.sequence == self._next_rx_sequence:
|
||||
contigous.append(e)
|
||||
self._next_rx_sequence = (self._next_rx_sequence + 1) % Channel.SEQ_MODULUS
|
||||
if self._next_rx_sequence == 0:
|
||||
for e in self._rx_ring:
|
||||
if e.sequence == self._next_rx_sequence:
|
||||
contigous.append(e)
|
||||
self._next_rx_sequence = (self._next_rx_sequence + 1) % Channel.SEQ_MODULUS
|
||||
|
||||
for e in contigous:
|
||||
m = e.unpack(self._message_factories)
|
||||
if not e.unpacked:
|
||||
m = e.unpack(self._message_factories)
|
||||
else:
|
||||
m = e.message
|
||||
|
||||
self._rx_ring.remove(e)
|
||||
threading.Thread(target=self._run_callbacks, name="Message Callback", args=[m], daemon=True).start()
|
||||
self._run_callbacks(m)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("An error ocurred while receiving data on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -458,6 +481,7 @@ class Channel(contextlib.AbstractContextManager):
|
||||
with self._lock:
|
||||
envelope = next(filter(lambda e: self._outlet.get_packet_id(e.packet) == self._outlet.get_packet_id(packet),
|
||||
self._tx_ring), None)
|
||||
|
||||
if envelope and op(envelope):
|
||||
envelope.tracked = False
|
||||
if envelope in self._tx_ring:
|
||||
@@ -465,11 +489,9 @@ class Channel(contextlib.AbstractContextManager):
|
||||
|
||||
if self.window < self.window_max:
|
||||
self.window += 1
|
||||
if (self.window - self.window_min) > (self.window_flexibility-1):
|
||||
self.window_min += 1
|
||||
|
||||
# TODO: Remove at some point
|
||||
RNS.log("Increased "+str(self)+" window to "+str(self.window), RNS.LOG_DEBUG)
|
||||
# RNS.log("Increased "+str(self)+" window to "+str(self.window), RNS.LOG_DEBUG)
|
||||
|
||||
if self._outlet.rtt != 0:
|
||||
if self._outlet.rtt > Channel.RTT_FAST:
|
||||
@@ -482,28 +504,39 @@ class Channel(contextlib.AbstractContextManager):
|
||||
self.medium_rate_rounds += 1
|
||||
if self.window_max < Channel.WINDOW_MAX_MEDIUM and self.medium_rate_rounds == Channel.FAST_RATE_THRESHOLD:
|
||||
self.window_max = Channel.WINDOW_MAX_MEDIUM
|
||||
self.window_min = Channel.WINDOW_MIN_LIMIT_MEDIUM
|
||||
# TODO: Remove at some point
|
||||
RNS.log("Increased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_EXTREME)
|
||||
# RNS.log("Increased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
|
||||
# RNS.log("Increased "+str(self)+" min window to "+str(self.window_min), RNS.LOG_DEBUG)
|
||||
|
||||
else:
|
||||
self.fast_rate_rounds += 1
|
||||
if self.window_max < Channel.WINDOW_MAX_FAST and self.fast_rate_rounds == Channel.FAST_RATE_THRESHOLD:
|
||||
self.window_max = Channel.WINDOW_MAX_FAST
|
||||
self.window_max = Channel.WINDOW_MAX_FAST
|
||||
self.window_min = Channel.WINDOW_MIN_LIMIT_FAST
|
||||
# TODO: Remove at some point
|
||||
RNS.log("Increased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_EXTREME)
|
||||
|
||||
# RNS.log("Increased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
|
||||
# RNS.log("Increased "+str(self)+" min window to "+str(self.window_min), RNS.LOG_DEBUG)
|
||||
|
||||
|
||||
else:
|
||||
RNS.log("Envelope not found in TX ring for "+str(self), RNS.LOG_DEBUG)
|
||||
RNS.log("Envelope not found in TX ring for "+str(self), RNS.LOG_EXTREME)
|
||||
if not envelope:
|
||||
RNS.log("Spurious message received on "+str(self), RNS.LOG_EXTREME)
|
||||
|
||||
def _packet_delivered(self, packet: TPacket):
|
||||
self._packet_tx_op(packet, lambda env: True)
|
||||
|
||||
def _update_packet_timeouts(self):
|
||||
for envelope in self._tx_ring:
|
||||
updated_timeout = self._get_packet_timeout_time(envelope.tries)
|
||||
if envelope.packet and hasattr(envelope.packet, "receipt") and envelope.packet.receipt and envelope.packet.receipt.timeout:
|
||||
if updated_timeout > envelope.packet.receipt.timeout:
|
||||
envelope.packet.receipt.set_timeout(updated_timeout)
|
||||
|
||||
def _get_packet_timeout_time(self, tries: int) -> float:
|
||||
return pow(2, tries - 1) * max(self._outlet.rtt, 0.01) * 5
|
||||
to = pow(1.5, tries - 1) * max(self._outlet.rtt*2.5, 0.025) * (len(self._tx_ring)+1.5)
|
||||
return to
|
||||
|
||||
def _packet_timeout(self, packet: TPacket):
|
||||
def retry_envelope(envelope: Envelope) -> bool:
|
||||
@@ -512,20 +545,25 @@ class Channel(contextlib.AbstractContextManager):
|
||||
self._shutdown() # start on separate thread?
|
||||
self._outlet.timed_out()
|
||||
return True
|
||||
|
||||
envelope.tries += 1
|
||||
self._outlet.resend(envelope.packet)
|
||||
self._outlet.set_packet_delivered_callback(envelope.packet, self._packet_delivered)
|
||||
self._outlet.set_packet_timeout_callback(envelope.packet, self._packet_timeout, self._get_packet_timeout_time(envelope.tries))
|
||||
self._update_packet_timeouts()
|
||||
|
||||
if self.window > self.window_min:
|
||||
self.window -= 1
|
||||
if self.window_max > self.window_min:
|
||||
# TODO: Remove at some point
|
||||
# RNS.log("Decreased "+str(self)+" window to "+str(self.window), RNS.LOG_DEBUG)
|
||||
|
||||
if self.window_max > (self.window_min+self.window_flexibility):
|
||||
self.window_max -= 1
|
||||
if (self.window_max - self.window) > (self.window_flexibility-1):
|
||||
self.window_max -= 1
|
||||
# TODO: Remove at some point
|
||||
# RNS.log("Decreased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
|
||||
|
||||
# TODO: Remove at some point
|
||||
RNS.log("Decreased "+str(self)+" window to "+str(self.window), RNS.LOG_EXTREME)
|
||||
# RNS.log("Decreased "+str(self)+" window to "+str(self.window), RNS.LOG_EXTREME)
|
||||
|
||||
return False
|
||||
|
||||
@@ -543,25 +581,28 @@ class Channel(contextlib.AbstractContextManager):
|
||||
with self._lock:
|
||||
if not self.is_ready_to_send():
|
||||
raise ChannelException(CEType.ME_LINK_NOT_READY, f"Link is not ready")
|
||||
|
||||
envelope = Envelope(self._outlet, message=message, sequence=self._next_sequence)
|
||||
self._next_sequence = (self._next_sequence + 1) % Channel.SEQ_MODULUS
|
||||
|
||||
self._emplace_envelope(envelope, self._tx_ring)
|
||||
|
||||
if envelope is None:
|
||||
raise BlockingIOError()
|
||||
|
||||
envelope.pack()
|
||||
if len(envelope.raw) > self._outlet.mdu:
|
||||
raise ChannelException(CEType.ME_TOO_BIG, f"Packed message too big for packet: {len(envelope.raw)} > {self._outlet.mdu}")
|
||||
|
||||
envelope.packet = self._outlet.send(envelope.raw)
|
||||
envelope.tries += 1
|
||||
self._outlet.set_packet_delivered_callback(envelope.packet, self._packet_delivered)
|
||||
self._outlet.set_packet_timeout_callback(envelope.packet, self._packet_timeout, self._get_packet_timeout_time(envelope.tries))
|
||||
self._update_packet_timeouts()
|
||||
|
||||
return envelope
|
||||
|
||||
@property
|
||||
def MDU(self):
|
||||
def mdu(self):
|
||||
"""
|
||||
Maximum Data Unit: the number of bytes available
|
||||
for a message to consume in a single send. This
|
||||
@@ -570,7 +611,10 @@ class Channel(contextlib.AbstractContextManager):
|
||||
|
||||
:return: number of bytes available
|
||||
"""
|
||||
return self._outlet.mdu - 6 # sizeof(msgtype) + sizeof(length) + sizeof(sequence)
|
||||
mdu = self._outlet.mdu - 6 # sizeof(msgtype) + sizeof(length) + sizeof(sequence)
|
||||
if mdu > 0xFFFF:
|
||||
mdu = 0xFFFF
|
||||
return mdu
|
||||
|
||||
|
||||
class LinkChannelOutlet(ChannelOutletBase):
|
||||
@@ -591,7 +635,6 @@ class LinkChannelOutlet(ChannelOutletBase):
|
||||
return packet
|
||||
|
||||
def resend(self, packet: RNS.Packet) -> RNS.Packet:
|
||||
RNS.log("Resending packet " + RNS.prettyhexrep(packet.packet_hash), RNS.LOG_DEBUG)
|
||||
receipt = packet.resend()
|
||||
if not receipt:
|
||||
RNS.log("Failed to resend packet", RNS.LOG_ERROR)
|
||||
@@ -599,7 +642,7 @@ class LinkChannelOutlet(ChannelOutletBase):
|
||||
|
||||
@property
|
||||
def mdu(self):
|
||||
return self.link.MDU
|
||||
return self.link.mdu
|
||||
|
||||
@property
|
||||
def rtt(self):
|
||||
|
||||
@@ -48,7 +48,7 @@ def hkdf(length=None, derive_from=None, salt=None, context=None):
|
||||
derived = b""
|
||||
|
||||
for i in range(ceil(length / hash_len)):
|
||||
block = hmac_sha256(pseudorandom_key, block + context + bytes([i + 1]))
|
||||
block = hmac_sha256(pseudorandom_key, block + context + bytes([(i + 1)%(0xFF+1)]))
|
||||
derived += block
|
||||
|
||||
return derived[:length]
|
||||
|
||||
@@ -4,13 +4,14 @@ PROVIDER_NONE = 0x00
|
||||
PROVIDER_INTERNAL = 0x01
|
||||
PROVIDER_PYCA = 0x02
|
||||
|
||||
FORCE_INTERNAL = False
|
||||
PROVIDER = PROVIDER_NONE
|
||||
|
||||
pyca_v = None
|
||||
use_pyca = False
|
||||
|
||||
try:
|
||||
if importlib.util.find_spec('cryptography') != None:
|
||||
if not FORCE_INTERNAL and importlib.util.find_spec('cryptography') != None:
|
||||
import cryptography
|
||||
pyca_v = cryptography.__version__
|
||||
v = pyca_v.split(".")
|
||||
|
||||
@@ -27,7 +27,7 @@ from RNS.Cryptography import HMAC
|
||||
from RNS.Cryptography import PKCS7
|
||||
from RNS.Cryptography.AES import AES_128_CBC
|
||||
|
||||
class Fernet():
|
||||
class Token():
|
||||
"""
|
||||
This class provides a slightly modified implementation of the Fernet spec
|
||||
found at: https://github.com/fernet/spec/blob/master/Spec.md
|
||||
@@ -37,7 +37,7 @@ class Fernet():
|
||||
not relevant to Reticulum. They are therefore stripped from this
|
||||
implementation, since they incur overhead and leak initiator metadata.
|
||||
"""
|
||||
FERNET_OVERHEAD = 48 # Bytes
|
||||
TOKEN_OVERHEAD = 48 # Bytes
|
||||
|
||||
@staticmethod
|
||||
def generate_key():
|
||||
@@ -45,10 +45,10 @@ class Fernet():
|
||||
|
||||
def __init__(self, key = None):
|
||||
if key == None:
|
||||
raise ValueError("Fernet key cannot be None")
|
||||
raise ValueError("Token key cannot be None")
|
||||
|
||||
if len(key) != 32:
|
||||
raise ValueError("Fernet key must be 32 bytes, not "+str(len(key)))
|
||||
raise ValueError("Token key must be 32 bytes, not "+str(len(key)))
|
||||
|
||||
self._signing_key = key[:16]
|
||||
self._encryption_key = key[16:]
|
||||
@@ -72,7 +72,7 @@ class Fernet():
|
||||
current_time = int(time.time())
|
||||
|
||||
if not isinstance(data, bytes):
|
||||
raise TypeError("Fernet token plaintext input must be bytes")
|
||||
raise TypeError("Token plaintext input must be bytes")
|
||||
|
||||
ciphertext = AES_128_CBC.encrypt(
|
||||
plaintext = PKCS7.pad(data),
|
||||
@@ -87,10 +87,10 @@ class Fernet():
|
||||
|
||||
def decrypt(self, token = None):
|
||||
if not isinstance(token, bytes):
|
||||
raise TypeError("Fernet token must be bytes")
|
||||
raise TypeError("Token must be bytes")
|
||||
|
||||
if not self.verify_hmac(token):
|
||||
raise ValueError("Fernet token HMAC was invalid")
|
||||
raise ValueError("Token HMAC was invalid")
|
||||
|
||||
iv = token[:16]
|
||||
ciphertext = token[16:-32]
|
||||
@@ -107,4 +107,4 @@ class Fernet():
|
||||
return plaintext
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError("Could not decrypt Fernet token")
|
||||
raise ValueError("Could not decrypt token")
|
||||
@@ -82,10 +82,13 @@ def _fix_secret(n):
|
||||
n |= 64 << 8 * 31
|
||||
return n
|
||||
|
||||
def _fix_base_point(n):
|
||||
n &= ~(2**255)
|
||||
return n
|
||||
|
||||
def curve25519(base_point_raw, secret_raw):
|
||||
"""Raise the base point to a given power"""
|
||||
base_point = _unpack_number(base_point_raw)
|
||||
base_point = _fix_base_point(_unpack_number(base_point_raw))
|
||||
secret = _fix_secret(_unpack_number(secret_raw))
|
||||
return _pack_number(_raw_curve25519(base_point, secret))
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from .Hashes import sha256
|
||||
from .Hashes import sha512
|
||||
from .HKDF import hkdf
|
||||
from .PKCS7 import PKCS7
|
||||
from .Fernet import Fernet
|
||||
from .Token import Token
|
||||
from .Provider import backend
|
||||
|
||||
import RNS.Cryptography.Provider as cp
|
||||
@@ -20,5 +20,7 @@ elif cp.PROVIDER == cp.PROVIDER_PYCA:
|
||||
from RNS.Cryptography.Proxies import Ed25519PrivateKeyProxy as Ed25519PrivateKey
|
||||
from RNS.Cryptography.Proxies import Ed25519PublicKeyProxy as Ed25519PublicKey
|
||||
|
||||
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
|
||||
py_modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
|
||||
modules = py_modules+pyc_modules
|
||||
__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))]))
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -20,11 +20,14 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
import math
|
||||
import time
|
||||
import threading
|
||||
import RNS
|
||||
|
||||
from RNS.Cryptography import Fernet
|
||||
from RNS.Cryptography import Token
|
||||
from .vendor import umsgpack as umsgpack
|
||||
|
||||
class Callbacks:
|
||||
def __init__(self):
|
||||
@@ -38,14 +41,14 @@ class Destination:
|
||||
instances are used both to create outgoing and incoming endpoints. The
|
||||
destination type will decide if encryption, and what type, is used in
|
||||
communication with the endpoint. A destination can also announce its
|
||||
presence on the network, which will also distribute necessary keys for
|
||||
presence on the network, which will distribute necessary keys for
|
||||
encrypted communication with it.
|
||||
|
||||
:param identity: An instance of :ref:`RNS.Identity<api-identity>`. Can hold only public keys for an outgoing destination, or holding private keys for an ingoing.
|
||||
:param direction: ``RNS.Destination.IN`` or ``RNS.Destination.OUT``.
|
||||
:param type: ``RNS.Destination.SINGLE``, ``RNS.Destination.GROUP`` or ``RNS.Destination.PLAIN``.
|
||||
:param app_name: A string specifying the app name.
|
||||
:param \*aspects: Any non-zero number of string arguments.
|
||||
:param \\*aspects: Any non-zero number of string arguments.
|
||||
"""
|
||||
|
||||
# Constants
|
||||
@@ -71,6 +74,16 @@ class Destination:
|
||||
|
||||
PR_TAG_WINDOW = 30
|
||||
|
||||
RATCHET_COUNT = 512
|
||||
"""
|
||||
The default number of generated ratchet keys a destination will retain, if it has ratchets enabled.
|
||||
"""
|
||||
|
||||
RATCHET_INTERVAL = 30*60
|
||||
"""
|
||||
The minimum interval between rotating ratchet keys, in seconds.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def expand_name(identity, app_name, *aspects):
|
||||
"""
|
||||
@@ -99,7 +112,12 @@ class Destination:
|
||||
name_hash = RNS.Identity.full_hash(Destination.expand_name(None, app_name, *aspects).encode("utf-8"))[:(RNS.Identity.NAME_HASH_LENGTH//8)]
|
||||
addr_hash_material = name_hash
|
||||
if identity != None:
|
||||
addr_hash_material += identity.hash
|
||||
if isinstance(identity, RNS.Identity):
|
||||
addr_hash_material += identity.hash
|
||||
elif isinstance(identity, bytes) and len(identity) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
|
||||
addr_hash_material += identity
|
||||
else:
|
||||
raise TypeError("Invalid material supplied for destination hash calculation")
|
||||
|
||||
return RNS.Identity.full_hash(addr_hash_material)[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8]
|
||||
|
||||
@@ -132,6 +150,14 @@ class Destination:
|
||||
self.type = type
|
||||
self.direction = direction
|
||||
self.proof_strategy = Destination.PROVE_NONE
|
||||
self.ratchets = None
|
||||
self.ratchets_path = None
|
||||
self.ratchet_interval = Destination.RATCHET_INTERVAL
|
||||
self.ratchet_file_lock = threading.Lock()
|
||||
self.retained_ratchets = Destination.RATCHET_COUNT
|
||||
self.latest_ratchet_time = None
|
||||
self.latest_ratchet_id = None
|
||||
self.__enforce_ratchets = False
|
||||
self.mtu = 0
|
||||
|
||||
self.path_responses = {}
|
||||
@@ -141,6 +167,9 @@ class Destination:
|
||||
identity = RNS.Identity()
|
||||
aspects = aspects+(identity.hexhash,)
|
||||
|
||||
if identity == None and direction == Destination.OUT and self.type != Destination.PLAIN:
|
||||
raise ValueError("Can't create outbound SINGLE destination without an identity")
|
||||
|
||||
if identity != None and self.type == Destination.PLAIN:
|
||||
raise TypeError("Selected destination type PLAIN cannot hold an identity")
|
||||
|
||||
@@ -165,6 +194,39 @@ class Destination:
|
||||
"""
|
||||
return "<"+self.name+"/"+self.hexhash+">"
|
||||
|
||||
def _clean_ratchets(self):
|
||||
if self.ratchets != None:
|
||||
if len (self.ratchets) > self.retained_ratchets:
|
||||
self.ratchets = self.ratchets[:Destination.RATCHET_COUNT]
|
||||
|
||||
def _persist_ratchets(self):
|
||||
try:
|
||||
with self.ratchet_file_lock:
|
||||
packed_ratchets = umsgpack.packb(self.ratchets)
|
||||
persisted_data = {"signature": self.sign(packed_ratchets), "ratchets": packed_ratchets}
|
||||
ratchets_file = open(self.ratchets_path, "wb")
|
||||
ratchets_file.write(umsgpack.packb(persisted_data))
|
||||
ratchets_file.close()
|
||||
except Exception as e:
|
||||
self.ratchets = None
|
||||
self.ratchets_path = None
|
||||
raise OSError("Could not write ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def rotate_ratchets(self):
|
||||
if self.ratchets != None:
|
||||
now = time.time()
|
||||
if now > self.latest_ratchet_time+self.ratchet_interval:
|
||||
RNS.log("Rotating ratchets for "+str(self), RNS.LOG_DEBUG)
|
||||
new_ratchet = RNS.Identity._generate_ratchet()
|
||||
self.ratchets.insert(0, new_ratchet)
|
||||
self.latest_ratchet_time = now
|
||||
self._clean_ratchets()
|
||||
self._persist_ratchets()
|
||||
return True
|
||||
else:
|
||||
raise SystemError("Cannot rotate ratchet on "+str(self)+", ratchets are not enabled")
|
||||
|
||||
return False
|
||||
|
||||
def announce(self, app_data=None, path_response=False, attached_interface=None, tag=None, send=True):
|
||||
"""
|
||||
@@ -176,7 +238,11 @@ class Destination:
|
||||
"""
|
||||
if self.type != Destination.SINGLE:
|
||||
raise TypeError("Only SINGLE destination types can be announced")
|
||||
|
||||
if self.direction != Destination.IN:
|
||||
raise TypeError("Only IN destination types can be announced")
|
||||
|
||||
ratchet = b""
|
||||
now = time.time()
|
||||
stale_responses = []
|
||||
for entry_tag in self.path_responses:
|
||||
@@ -203,6 +269,11 @@ class Destination:
|
||||
destination_hash = self.hash
|
||||
random_hash = RNS.Identity.get_random_hash()[0:5]+int(time.time()).to_bytes(5, "big")
|
||||
|
||||
if self.ratchets != None:
|
||||
self.rotate_ratchets()
|
||||
ratchet = RNS.Identity._ratchet_public_bytes(self.ratchets[0])
|
||||
RNS.Identity._remember_ratchet(self.hash, ratchet)
|
||||
|
||||
if app_data == None and self.default_app_data != None:
|
||||
if isinstance(self.default_app_data, bytes):
|
||||
app_data = self.default_app_data
|
||||
@@ -211,13 +282,12 @@ class Destination:
|
||||
if isinstance(returned_app_data, bytes):
|
||||
app_data = returned_app_data
|
||||
|
||||
signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash
|
||||
signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash+ratchet
|
||||
if app_data != None:
|
||||
signed_data += app_data
|
||||
|
||||
signature = self.identity.sign(signed_data)
|
||||
|
||||
announce_data = self.identity.get_public_key()+self.name_hash+random_hash+signature
|
||||
announce_data = self.identity.get_public_key()+self.name_hash+random_hash+ratchet+signature
|
||||
|
||||
if app_data != None:
|
||||
announce_data += app_data
|
||||
@@ -229,8 +299,13 @@ class Destination:
|
||||
else:
|
||||
announce_context = RNS.Packet.NONE
|
||||
|
||||
announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context, attached_interface = attached_interface)
|
||||
if ratchet:
|
||||
context_flag = RNS.Packet.FLAG_SET
|
||||
else:
|
||||
context_flag = RNS.Packet.FLAG_UNSET
|
||||
|
||||
announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context,
|
||||
attached_interface = attached_interface, context_flag=context_flag)
|
||||
if send:
|
||||
announce_packet.send()
|
||||
else:
|
||||
@@ -290,7 +365,6 @@ class Destination:
|
||||
else:
|
||||
self.proof_strategy = proof_strategy
|
||||
|
||||
|
||||
def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None):
|
||||
"""
|
||||
Registers a request handler.
|
||||
@@ -312,7 +386,6 @@ class Destination:
|
||||
request_handler = [path, response_generator, allow, allowed_list]
|
||||
self.request_handlers[path_hash] = request_handler
|
||||
|
||||
|
||||
def deregister_request_handler(self, path):
|
||||
"""
|
||||
Deregisters a request handler.
|
||||
@@ -327,14 +400,13 @@ class Destination:
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def receive(self, packet):
|
||||
if packet.packet_type == RNS.Packet.LINKREQUEST:
|
||||
plaintext = packet.data
|
||||
self.incoming_link_request(plaintext, packet)
|
||||
else:
|
||||
plaintext = self.decrypt(packet.data)
|
||||
packet.ratchet_id = self.latest_ratchet_id
|
||||
if plaintext != None:
|
||||
if packet.packet_type == RNS.Packet.DATA:
|
||||
if self.callbacks.packet != None:
|
||||
@@ -343,13 +415,115 @@ class Destination:
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing receive callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
def incoming_link_request(self, data, packet):
|
||||
if self.accept_link_requests:
|
||||
link = RNS.Link.validate_request(self, data, packet)
|
||||
if link != None:
|
||||
self.links.append(link)
|
||||
|
||||
def _reload_ratchets(self, ratchets_path):
|
||||
if os.path.isfile(ratchets_path):
|
||||
with self.ratchet_file_lock:
|
||||
def load_attempt():
|
||||
ratchets_file = open(ratchets_path, "rb")
|
||||
persisted_data = umsgpack.unpackb(ratchets_file.read())
|
||||
if "signature" in persisted_data and "ratchets" in persisted_data:
|
||||
if self.identity.validate(persisted_data["signature"], persisted_data["ratchets"]):
|
||||
self.ratchets = umsgpack.unpackb(persisted_data["ratchets"])
|
||||
self.ratchets_path = ratchets_path
|
||||
else:
|
||||
raise KeyError("Invalid ratchet file signature")
|
||||
|
||||
try:
|
||||
try:
|
||||
load_attempt()
|
||||
|
||||
except Exception as e:
|
||||
RNS.trace_exception(e)
|
||||
RNS.log(f"First ratchet reload attempt for {self} failed. Possible I/O conflict. Retrying in 500ms.", RNS.LOG_ERROR)
|
||||
time.sleep(0.5)
|
||||
load_attempt()
|
||||
RNS.log(f"Ratchet reload retry succeeded", RNS.LOG_DEBUG)
|
||||
|
||||
except Exception as e:
|
||||
self.ratchets = None
|
||||
self.ratchets_path = None
|
||||
RNS.trace_exception(e)
|
||||
raise OSError("Could not read ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
else:
|
||||
RNS.log("No existing ratchet data found, initialising new ratchet file for "+str(self), RNS.LOG_DEBUG)
|
||||
self.ratchets = []
|
||||
self.ratchets_path = ratchets_path
|
||||
self._persist_ratchets()
|
||||
|
||||
def enable_ratchets(self, ratchets_path):
|
||||
"""
|
||||
Enables ratchets on the destination. When ratchets are enabled, Reticulum will automatically rotate
|
||||
the keys used to encrypt packets to this destination, and include the latest ratchet key in announces.
|
||||
|
||||
Enabling ratchets on a destination will provide forward secrecy for packets sent to that destination,
|
||||
even when sent outside a ``Link``. The normal Reticulum ``Link`` establishment procedure already performs
|
||||
its own ephemeral key exchange for each link establishment, which means that ratchets are not necessary
|
||||
to provide forward secrecy for links.
|
||||
|
||||
Enabling ratchets will have a small impact on announce size, adding 32 bytes to every sent announce.
|
||||
|
||||
:param ratchets_path: The path to a file to store ratchet data in.
|
||||
:returns: True if the operation succeeded, otherwise False.
|
||||
"""
|
||||
if ratchets_path != None:
|
||||
self.latest_ratchet_time = 0
|
||||
self._reload_ratchets(ratchets_path)
|
||||
|
||||
# TODO: Remove at some point
|
||||
RNS.log("Ratchets enabled on "+str(self), RNS.LOG_DEBUG)
|
||||
return True
|
||||
|
||||
else:
|
||||
raise ValueError("No ratchet file path specified for "+str(self))
|
||||
|
||||
def enforce_ratchets(self):
|
||||
"""
|
||||
When ratchet enforcement is enabled, this destination will never accept packets that use its
|
||||
base Identity key for encryption, but only accept packets encrypted with one of the retained
|
||||
ratchet keys.
|
||||
"""
|
||||
if self.ratchets != None:
|
||||
self.__enforce_ratchets = True
|
||||
RNS.log("Ratchets enforced on "+str(self), RNS.LOG_DEBUG)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_retained_ratchets(self, retained_ratchets):
|
||||
"""
|
||||
Sets the number of previously generated ratchet keys this destination will retain,
|
||||
and try to use when decrypting incoming packets. Defaults to ``Destination.RATCHET_COUNT``.
|
||||
|
||||
:param retained_ratchets: The number of generated ratchets to retain.
|
||||
:returns: True if the operation succeeded, False if not.
|
||||
"""
|
||||
if isinstance(retained_ratchets, int) and retained_ratchets > 0:
|
||||
self.retained_ratchets = retained_ratchets
|
||||
self._clean_ratchets()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_ratchet_interval(self, interval):
|
||||
"""
|
||||
Sets the minimum interval in seconds between ratchet key rotation.
|
||||
Defaults to ``Destination.RATCHET_INTERVAL``.
|
||||
|
||||
:param interval: The minimum interval in seconds.
|
||||
:returns: True if the operation succeeded, False if not.
|
||||
"""
|
||||
if isinstance(interval, int) and interval > 0:
|
||||
self.ratchet_interval = interval
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def create_keys(self):
|
||||
"""
|
||||
For a ``RNS.Destination.GROUP`` type destination, creates a new symmetric key.
|
||||
@@ -363,9 +537,8 @@ class Destination:
|
||||
raise TypeError("A single destination holds keys through an Identity instance")
|
||||
|
||||
if self.type == Destination.GROUP:
|
||||
self.prv_bytes = Fernet.generate_key()
|
||||
self.prv = Fernet(self.prv_bytes)
|
||||
|
||||
self.prv_bytes = Token.generate_key()
|
||||
self.prv = Token(self.prv_bytes)
|
||||
|
||||
def get_private_key(self):
|
||||
"""
|
||||
@@ -380,7 +553,6 @@ class Destination:
|
||||
else:
|
||||
return self.prv_bytes
|
||||
|
||||
|
||||
def load_private_key(self, key):
|
||||
"""
|
||||
For a ``RNS.Destination.GROUP`` type destination, loads a symmetric private key.
|
||||
@@ -396,7 +568,7 @@ class Destination:
|
||||
|
||||
if self.type == Destination.GROUP:
|
||||
self.prv_bytes = key
|
||||
self.prv = Fernet(self.prv_bytes)
|
||||
self.prv = Token(self.prv_bytes)
|
||||
|
||||
def load_public_key(self, key):
|
||||
if self.type != Destination.SINGLE:
|
||||
@@ -404,7 +576,6 @@ class Destination:
|
||||
else:
|
||||
raise TypeError("A single destination holds keys through an Identity instance")
|
||||
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
"""
|
||||
Encrypts information for ``RNS.Destination.SINGLE`` or ``RNS.Destination.GROUP`` type destination.
|
||||
@@ -416,7 +587,10 @@ class Destination:
|
||||
return plaintext
|
||||
|
||||
if self.type == Destination.SINGLE and self.identity != None:
|
||||
return self.identity.encrypt(plaintext)
|
||||
selected_ratchet = RNS.Identity.get_ratchet(self.hash)
|
||||
if selected_ratchet:
|
||||
self.latest_ratchet_id = RNS.Identity._get_ratchet_id(selected_ratchet)
|
||||
return self.identity.encrypt(plaintext, ratchet=selected_ratchet)
|
||||
|
||||
if self.type == Destination.GROUP:
|
||||
if hasattr(self, "prv") and self.prv != None:
|
||||
@@ -428,8 +602,6 @@ class Destination:
|
||||
else:
|
||||
raise ValueError("No private key held by GROUP destination. Did you create or load one?")
|
||||
|
||||
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
"""
|
||||
Decrypts information for ``RNS.Destination.SINGLE`` or ``RNS.Destination.GROUP`` type destination.
|
||||
@@ -441,7 +613,28 @@ class Destination:
|
||||
return ciphertext
|
||||
|
||||
if self.type == Destination.SINGLE and self.identity != None:
|
||||
return self.identity.decrypt(ciphertext)
|
||||
if self.ratchets:
|
||||
decrypted = None
|
||||
try:
|
||||
decrypted = self.identity.decrypt(ciphertext, ratchets=self.ratchets, enforce_ratchets=self.__enforce_ratchets, ratchet_id_receiver=self)
|
||||
except:
|
||||
decrypted = None
|
||||
|
||||
if not decrypted:
|
||||
try:
|
||||
RNS.log(f"Decryption with ratchets failed on {self}, reloading ratchets from storage and retrying", RNS.LOG_ERROR)
|
||||
self._reload_ratchets(self.ratchets_path)
|
||||
decrypted = self.identity.decrypt(ciphertext, ratchets=self.ratchets, enforce_ratchets=self.__enforce_ratchets, ratchet_id_receiver=self)
|
||||
except Exception as e:
|
||||
RNS.log(f"Decryption still failing after ratchet reload. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
RNS.log("Decryption succeeded after ratchet reload", RNS.LOG_NOTICE)
|
||||
|
||||
return decrypted
|
||||
|
||||
else:
|
||||
return self.identity.decrypt(ciphertext, ratchets=None, enforce_ratchets=self.__enforce_ratchets, ratchet_id_receiver=self)
|
||||
|
||||
if self.type == Destination.GROUP:
|
||||
if hasattr(self, "prv") and self.prv != None:
|
||||
@@ -453,7 +646,6 @@ class Destination:
|
||||
else:
|
||||
raise ValueError("No private key held by GROUP destination. Did you create or load one?")
|
||||
|
||||
|
||||
def sign(self, message):
|
||||
"""
|
||||
Signs information for ``RNS.Destination.SINGLE`` type destination.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -26,11 +26,12 @@ import RNS
|
||||
import time
|
||||
import atexit
|
||||
import hashlib
|
||||
import threading
|
||||
|
||||
from .vendor import umsgpack as umsgpack
|
||||
|
||||
from RNS.Cryptography import X25519PrivateKey, X25519PublicKey, Ed25519PrivateKey, Ed25519PublicKey
|
||||
from RNS.Cryptography import Fernet
|
||||
from RNS.Cryptography import Token
|
||||
|
||||
|
||||
class Identity:
|
||||
@@ -49,11 +50,23 @@ class Identity:
|
||||
|
||||
KEYSIZE = 256*2
|
||||
"""
|
||||
X25519 key size in bits. A complete key is the concatenation of a 256 bit encryption key, and a 256 bit signing key.
|
||||
"""
|
||||
X.25519 key size in bits. A complete key is the concatenation of a 256 bit encryption key, and a 256 bit signing key.
|
||||
"""
|
||||
|
||||
RATCHETSIZE = 256
|
||||
"""
|
||||
X.25519 ratchet key size in bits.
|
||||
"""
|
||||
|
||||
RATCHET_EXPIRY = 60*60*24*30
|
||||
"""
|
||||
The expiry time for received ratchets in seconds, defaults to 30 days. Reticulum will always use the most recently
|
||||
announced ratchet, and remember it for up to ``RATCHET_EXPIRY`` since receiving it, after which it will be discarded.
|
||||
If a newer ratchet is announced in the meantime, it will be replace the already known ratchet.
|
||||
"""
|
||||
|
||||
# Non-configurable constants
|
||||
FERNET_OVERHEAD = RNS.Cryptography.Fernet.FERNET_OVERHEAD
|
||||
TOKEN_OVERHEAD = RNS.Cryptography.Token.TOKEN_OVERHEAD
|
||||
AES128_BLOCKSIZE = 16 # In bytes
|
||||
HASHLENGTH = 256 # In bits
|
||||
SIGLENGTH = KEYSIZE # In bits
|
||||
@@ -67,6 +80,9 @@ class Identity:
|
||||
|
||||
# Storage
|
||||
known_destinations = {}
|
||||
known_ratchets = {}
|
||||
|
||||
ratchet_persist_lock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def remember(packet_hash, destination_hash, public_key, app_data = None):
|
||||
@@ -139,20 +155,23 @@ class Identity:
|
||||
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()
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
|
||||
storage_known_destinations = umsgpack.load(file)
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
for destination_hash in storage_known_destinations:
|
||||
if not destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
|
||||
try:
|
||||
for destination_hash in storage_known_destinations:
|
||||
if not destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
|
||||
except Exception as e:
|
||||
RNS.log("Skipped recombining known destinations from disk, since an error occurred: "+str(e), RNS.LOG_WARNING)
|
||||
|
||||
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_DEBUG)
|
||||
file = open(RNS.Reticulum.storagepath+"/known_destinations","wb")
|
||||
umsgpack.dump(Identity.known_destinations, file)
|
||||
file.close()
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","wb") as file:
|
||||
umsgpack.dump(Identity.known_destinations, file)
|
||||
|
||||
|
||||
save_time = time.time() - save_start
|
||||
if save_time < 1:
|
||||
@@ -164,6 +183,7 @@ class Identity:
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while saving known destinations to disk, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
Identity.saving_known_destinations = False
|
||||
|
||||
@@ -171,9 +191,8 @@ class Identity:
|
||||
def load_known_destinations():
|
||||
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
||||
try:
|
||||
file = open(RNS.Reticulum.storagepath+"/known_destinations","rb")
|
||||
loaded_known_destinations = umsgpack.load(file)
|
||||
file.close()
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
|
||||
loaded_known_destinations = umsgpack.load(file)
|
||||
|
||||
Identity.known_destinations = {}
|
||||
for known_destination in loaded_known_destinations:
|
||||
@@ -181,7 +200,8 @@ class Identity:
|
||||
Identity.known_destinations[known_destination] = loaded_known_destinations[known_destination]
|
||||
|
||||
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE)
|
||||
except:
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR)
|
||||
else:
|
||||
RNS.log("Destinations file does not exist, no known destinations loaded", RNS.LOG_VERBOSE)
|
||||
@@ -192,7 +212,7 @@ class Identity:
|
||||
Get a SHA-256 hash of passed data.
|
||||
|
||||
:param data: Data to be hashed as *bytes*.
|
||||
:returns: SHA-256 hash as *bytes*
|
||||
:returns: SHA-256 hash as *bytes*.
|
||||
"""
|
||||
return RNS.Cryptography.sha256(data)
|
||||
|
||||
@@ -202,7 +222,7 @@ class Identity:
|
||||
Get a truncated SHA-256 hash of passed data.
|
||||
|
||||
:param data: Data to be hashed as *bytes*.
|
||||
:returns: Truncated SHA-256 hash as *bytes*
|
||||
:returns: Truncated SHA-256 hash as *bytes*.
|
||||
"""
|
||||
return Identity.full_hash(data)[:(Identity.TRUNCATED_HASHLENGTH//8)]
|
||||
|
||||
@@ -212,24 +232,168 @@ class Identity:
|
||||
Get a random SHA-256 hash.
|
||||
|
||||
:param data: Data to be hashed as *bytes*.
|
||||
:returns: Truncated SHA-256 hash of random data as *bytes*
|
||||
:returns: Truncated SHA-256 hash of random data as *bytes*.
|
||||
"""
|
||||
return Identity.truncated_hash(os.urandom(Identity.TRUNCATED_HASHLENGTH//8))
|
||||
|
||||
@staticmethod
|
||||
def validate_announce(packet):
|
||||
def current_ratchet_id(destination_hash):
|
||||
"""
|
||||
Get the ID of the currently used ratchet key for a given destination hash
|
||||
|
||||
:param destination_hash: A destination hash as *bytes*.
|
||||
:returns: A ratchet ID as *bytes* or *None*.
|
||||
"""
|
||||
ratchet = Identity.get_ratchet(destination_hash)
|
||||
if ratchet == None:
|
||||
return None
|
||||
else:
|
||||
return Identity._get_ratchet_id(ratchet)
|
||||
|
||||
@staticmethod
|
||||
def _get_ratchet_id(ratchet_pub_bytes):
|
||||
return Identity.full_hash(ratchet_pub_bytes)[:Identity.NAME_HASH_LENGTH//8]
|
||||
|
||||
@staticmethod
|
||||
def _ratchet_public_bytes(ratchet):
|
||||
return X25519PrivateKey.from_private_bytes(ratchet).public_key().public_bytes()
|
||||
|
||||
@staticmethod
|
||||
def _generate_ratchet():
|
||||
ratchet_prv = X25519PrivateKey.generate()
|
||||
ratchet_pub = ratchet_prv.public_key()
|
||||
return ratchet_prv.private_bytes()
|
||||
|
||||
@staticmethod
|
||||
def _remember_ratchet(destination_hash, ratchet):
|
||||
try:
|
||||
if destination_hash in Identity.known_ratchets and Identity.known_ratchets[destination_hash] == ratchet:
|
||||
ratchet_exists = True
|
||||
else:
|
||||
ratchet_exists = False
|
||||
|
||||
if not ratchet_exists:
|
||||
RNS.log(f"Remembering ratchet {RNS.prettyhexrep(Identity._get_ratchet_id(ratchet))} for {RNS.prettyhexrep(destination_hash)}", RNS.LOG_EXTREME)
|
||||
Identity.known_ratchets[destination_hash] = ratchet
|
||||
if not RNS.Transport.owner.is_connected_to_shared_instance:
|
||||
def persist_job():
|
||||
with Identity.ratchet_persist_lock:
|
||||
hexhash = RNS.hexrep(destination_hash, delimit=False)
|
||||
ratchet_data = {"ratchet": ratchet, "received": time.time()}
|
||||
|
||||
ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
|
||||
|
||||
if not os.path.isdir(ratchetdir):
|
||||
os.makedirs(ratchetdir)
|
||||
|
||||
outpath = f"{ratchetdir}/{hexhash}.out"
|
||||
finalpath = f"{ratchetdir}/{hexhash}"
|
||||
with open(outpath, "wb") as ratchet_file:
|
||||
ratchet_file.write(umsgpack.packb(ratchet_data))
|
||||
os.replace(outpath, finalpath)
|
||||
|
||||
|
||||
threading.Thread(target=persist_job, daemon=True).start()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not persist ratchet for {RNS.prettyhexrep(destination_hash)} to storage.", RNS.LOG_ERROR)
|
||||
RNS.log(f"The contained exception was: {e}")
|
||||
RNS.trace_exception(e)
|
||||
|
||||
@staticmethod
|
||||
def _clean_ratchets():
|
||||
RNS.log("Cleaning ratchets...", RNS.LOG_DEBUG)
|
||||
try:
|
||||
now = time.time()
|
||||
ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
|
||||
if os.path.isdir(ratchetdir):
|
||||
for filename in os.listdir(ratchetdir):
|
||||
try:
|
||||
expired = False
|
||||
corrupted = False
|
||||
with open(f"{ratchetdir}/{filename}", "rb") as rf:
|
||||
# TODO: Remove individual ratchet file if corrupt
|
||||
try:
|
||||
ratchet_data = umsgpack.unpackb(rf.read())
|
||||
if now > ratchet_data["received"]+Identity.RATCHET_EXPIRY:
|
||||
expired = True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Corrupted ratchet data while reading {ratchetdir}/{filename}, removing file", RNS.LOG_ERROR)
|
||||
corrupted = True
|
||||
|
||||
if expired or corrupted:
|
||||
os.unlink(f"{ratchetdir}/{filename}")
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while cleaning ratchets, in the processing of {ratchetdir}/{filename}.", RNS.LOG_ERROR)
|
||||
RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while cleaning ratchets. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
@staticmethod
|
||||
def get_ratchet(destination_hash):
|
||||
if not destination_hash in Identity.known_ratchets:
|
||||
ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
|
||||
hexhash = RNS.hexrep(destination_hash, delimit=False)
|
||||
ratchet_path = f"{ratchetdir}/{hexhash}"
|
||||
if os.path.isfile(ratchet_path):
|
||||
try:
|
||||
with open(ratchet_path, "rb") as ratchet_file:
|
||||
ratchet_data = umsgpack.unpackb(ratchet_file.read())
|
||||
if time.time() < ratchet_data["received"]+Identity.RATCHET_EXPIRY and len(ratchet_data["ratchet"]) == Identity.RATCHETSIZE//8:
|
||||
Identity.known_ratchets[destination_hash] = ratchet_data["ratchet"]
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while loading ratchet data for {RNS.prettyhexrep(destination_hash)} from storage.", RNS.LOG_ERROR)
|
||||
RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
return None
|
||||
|
||||
if destination_hash in Identity.known_ratchets:
|
||||
return Identity.known_ratchets[destination_hash]
|
||||
else:
|
||||
RNS.log(f"Could not load ratchet for {RNS.prettyhexrep(destination_hash)}", RNS.LOG_DEBUG)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def validate_announce(packet, only_validate_signature=False):
|
||||
try:
|
||||
if packet.packet_type == RNS.Packet.ANNOUNCE:
|
||||
keysize = Identity.KEYSIZE//8
|
||||
ratchetsize = Identity.RATCHETSIZE//8
|
||||
name_hash_len = Identity.NAME_HASH_LENGTH//8
|
||||
sig_len = Identity.SIGLENGTH//8
|
||||
destination_hash = packet.destination_hash
|
||||
public_key = packet.data[:Identity.KEYSIZE//8]
|
||||
name_hash = packet.data[Identity.KEYSIZE//8:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8]
|
||||
random_hash = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10]
|
||||
signature = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8]
|
||||
app_data = b""
|
||||
if len(packet.data) > Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:
|
||||
app_data = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:]
|
||||
|
||||
signed_data = destination_hash+public_key+name_hash+random_hash+app_data
|
||||
# Get public key bytes from announce
|
||||
public_key = packet.data[:keysize]
|
||||
|
||||
# If the packet context flag is set,
|
||||
# this announce contains a new ratchet
|
||||
if packet.context_flag == RNS.Packet.FLAG_SET:
|
||||
name_hash = packet.data[keysize:keysize+name_hash_len ]
|
||||
random_hash = packet.data[keysize+name_hash_len:keysize+name_hash_len+10]
|
||||
ratchet = packet.data[keysize+name_hash_len+10:keysize+name_hash_len+10+ratchetsize]
|
||||
signature = packet.data[keysize+name_hash_len+10+ratchetsize:keysize+name_hash_len+10+ratchetsize+sig_len]
|
||||
app_data = b""
|
||||
if len(packet.data) > keysize+name_hash_len+10+sig_len+ratchetsize:
|
||||
app_data = packet.data[keysize+name_hash_len+10+sig_len+ratchetsize:]
|
||||
|
||||
# If the packet context flag is not set,
|
||||
# this announce does not contain a ratchet
|
||||
else:
|
||||
ratchet = b""
|
||||
name_hash = packet.data[keysize:keysize+name_hash_len]
|
||||
random_hash = packet.data[keysize+name_hash_len:keysize+name_hash_len+10]
|
||||
signature = packet.data[keysize+name_hash_len+10:keysize+name_hash_len+10+sig_len]
|
||||
app_data = b""
|
||||
if len(packet.data) > keysize+name_hash_len+10+sig_len:
|
||||
app_data = packet.data[keysize+name_hash_len+10+sig_len:]
|
||||
|
||||
signed_data = destination_hash+public_key+name_hash+random_hash+ratchet+app_data
|
||||
|
||||
if not len(packet.data) > Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:
|
||||
app_data = None
|
||||
@@ -238,6 +402,10 @@ class Identity:
|
||||
announced_identity.load_public_key(public_key)
|
||||
|
||||
if announced_identity.pub != None and announced_identity.validate(signature, signed_data):
|
||||
if only_validate_signature:
|
||||
del announced_identity
|
||||
return True
|
||||
|
||||
hash_material = name_hash+announced_identity.hash
|
||||
expected_hash = RNS.Identity.full_hash(hash_material)[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8]
|
||||
|
||||
@@ -272,6 +440,9 @@ class Identity:
|
||||
else:
|
||||
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received on "+str(packet.receiving_interface)+signal_str, RNS.LOG_EXTREME)
|
||||
|
||||
if ratchet:
|
||||
Identity._remember_ratchet(destination_hash, ratchet)
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
@@ -452,7 +623,7 @@ class Identity:
|
||||
return False
|
||||
except Exception as e:
|
||||
RNS.log("Error while loading identity from "+str(path), RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e))
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def get_salt(self):
|
||||
return self.hash
|
||||
@@ -460,7 +631,7 @@ class Identity:
|
||||
def get_context(self):
|
||||
return None
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
def encrypt(self, plaintext, ratchet=None):
|
||||
"""
|
||||
Encrypts information for the identity.
|
||||
|
||||
@@ -472,7 +643,12 @@ class Identity:
|
||||
ephemeral_key = X25519PrivateKey.generate()
|
||||
ephemeral_pub_bytes = ephemeral_key.public_key().public_bytes()
|
||||
|
||||
shared_key = ephemeral_key.exchange(self.pub)
|
||||
if ratchet != None:
|
||||
target_public_key = X25519PublicKey.from_public_bytes(ratchet)
|
||||
else:
|
||||
target_public_key = self.pub
|
||||
|
||||
shared_key = ephemeral_key.exchange(target_public_key)
|
||||
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
@@ -481,8 +657,8 @@ class Identity:
|
||||
context=self.get_context(),
|
||||
)
|
||||
|
||||
fernet = Fernet(derived_key)
|
||||
ciphertext = fernet.encrypt(plaintext)
|
||||
token = Token(derived_key)
|
||||
ciphertext = token.encrypt(plaintext)
|
||||
token = ephemeral_pub_bytes+ciphertext
|
||||
|
||||
return token
|
||||
@@ -490,7 +666,7 @@ class Identity:
|
||||
raise KeyError("Encryption failed because identity does not hold a public key")
|
||||
|
||||
|
||||
def decrypt(self, ciphertext_token):
|
||||
def decrypt(self, ciphertext_token, ratchets=None, enforce_ratchets=False, ratchet_id_receiver=None):
|
||||
"""
|
||||
Decrypts information for the identity.
|
||||
|
||||
@@ -504,22 +680,55 @@ class Identity:
|
||||
try:
|
||||
peer_pub_bytes = ciphertext_token[:Identity.KEYSIZE//8//2]
|
||||
peer_pub = X25519PublicKey.from_public_bytes(peer_pub_bytes)
|
||||
|
||||
shared_key = self.prv.exchange(peer_pub)
|
||||
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context(),
|
||||
)
|
||||
|
||||
fernet = Fernet(derived_key)
|
||||
ciphertext = ciphertext_token[Identity.KEYSIZE//8//2:]
|
||||
plaintext = fernet.decrypt(ciphertext)
|
||||
|
||||
if ratchets:
|
||||
for ratchet in ratchets:
|
||||
try:
|
||||
ratchet_prv = X25519PrivateKey.from_private_bytes(ratchet)
|
||||
ratchet_id = Identity._get_ratchet_id(ratchet_prv.public_key().public_bytes())
|
||||
shared_key = ratchet_prv.exchange(peer_pub)
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context(),
|
||||
)
|
||||
|
||||
token = Token(derived_key)
|
||||
plaintext = token.decrypt(ciphertext)
|
||||
if ratchet_id_receiver:
|
||||
ratchet_id_receiver.latest_ratchet_id = ratchet_id
|
||||
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if enforce_ratchets and plaintext == None:
|
||||
RNS.log("Decryption with ratchet enforcement by "+RNS.prettyhexrep(self.hash)+" failed. Dropping packet.", RNS.LOG_DEBUG)
|
||||
if ratchet_id_receiver:
|
||||
ratchet_id_receiver.latest_ratchet_id = None
|
||||
return None
|
||||
|
||||
if plaintext == None:
|
||||
shared_key = self.prv.exchange(peer_pub)
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context(),
|
||||
)
|
||||
|
||||
token = Token(derived_key)
|
||||
plaintext = token.decrypt(ciphertext)
|
||||
if ratchet_id_receiver:
|
||||
ratchet_id_receiver.latest_ratchet_id = None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
if ratchet_id_receiver:
|
||||
ratchet_id_receiver.latest_ratchet_id = None
|
||||
|
||||
return plaintext;
|
||||
else:
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import threading
|
||||
@@ -59,6 +59,7 @@ class AX25():
|
||||
class AX25KISSInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
BITRATE_GUESS = 1200
|
||||
DEFAULT_IFAC_SIZE = 8
|
||||
|
||||
owner = None
|
||||
port = None
|
||||
@@ -68,7 +69,7 @@ class AX25KISSInterface(Interface):
|
||||
stopbits = None
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, name, callsign, ssid, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control):
|
||||
def __init__(self, owner, configuration):
|
||||
import importlib
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
@@ -77,8 +78,26 @@ class AX25KISSInterface(Interface):
|
||||
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
preamble = int(c["preamble"]) if "preamble" in c else None
|
||||
txtail = int(c["txtail"]) if "txtail" in c else None
|
||||
persistence = int(c["persistence"]) if "persistence" in c else None
|
||||
slottime = int(c["slottime"]) if "slottime" in c else None
|
||||
flow_control = c.as_bool("flow_control") if "flow_control" in c else False
|
||||
port = c["port"] if "port" in c else None
|
||||
speed = int(c["speed"]) if "speed" in c else 9600
|
||||
databits = int(c["databits"]) if "databits" in c else 8
|
||||
parity = c["parity"] if "parity" in c else "N"
|
||||
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
|
||||
|
||||
callsign = c["callsign"] if "callsign" in c else ""
|
||||
ssid = int(c["ssid"]) if "ssid" in c else -1
|
||||
|
||||
if port == None:
|
||||
raise ValueError("No port specified for serial interface")
|
||||
|
||||
self.HW_MTU = 564
|
||||
|
||||
@@ -97,7 +116,7 @@ class AX25KISSInterface(Interface):
|
||||
self.stopbits = stopbits
|
||||
self.timeout = 100
|
||||
self.online = False
|
||||
self.bitrate = KISSInterface.BITRATE_GUESS
|
||||
self.bitrate = AX25KISSInterface.BITRATE_GUESS
|
||||
|
||||
self.packet_queue = []
|
||||
self.flow_control = flow_control
|
||||
@@ -226,13 +245,13 @@ class AX25KISSInterface(Interface):
|
||||
raise IOError("Could not enable AX.25 KISS interface flow control")
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
def process_incoming(self, data):
|
||||
if (len(data) > AX25.HEADER_SIZE):
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data[AX25.HEADER_SIZE:], self)
|
||||
|
||||
|
||||
def processOutgoing(self,data):
|
||||
def process_outgoing(self,data):
|
||||
datalen = len(data)
|
||||
if self.online:
|
||||
if self.interface_ready:
|
||||
@@ -282,7 +301,7 @@ class AX25KISSInterface(Interface):
|
||||
if len(self.packet_queue) > 0:
|
||||
data = self.packet_queue.pop(0)
|
||||
self.interface_ready = True
|
||||
self.processOutgoing(data)
|
||||
self.process_outgoing(data)
|
||||
elif len(self.packet_queue) == 0:
|
||||
self.interface_ready = True
|
||||
|
||||
@@ -301,7 +320,7 @@ class AX25KISSInterface(Interface):
|
||||
|
||||
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
self.process_incoming(data_buffer)
|
||||
elif (byte == KISS.FEND):
|
||||
in_frame = True
|
||||
command = KISS.CMD_UNKNOWN
|
||||
@@ -367,5 +386,8 @@ class AX25KISSInterface(Interface):
|
||||
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return "AX25KISSInterface["+self.name+"]"
|
||||
@@ -52,6 +52,7 @@ class KISS():
|
||||
class KISSInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
BITRATE_GUESS = 1200
|
||||
DEFAULT_IFAC_SIZE = 8
|
||||
|
||||
owner = None
|
||||
port = None
|
||||
@@ -61,7 +62,7 @@ class KISSInterface(Interface):
|
||||
stopbits = None
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control, beacon_interval, beacon_data):
|
||||
def __init__(self, owner, configuration):
|
||||
import importlib
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
self.on_android = True
|
||||
@@ -82,8 +83,22 @@ class KISSInterface(Interface):
|
||||
else:
|
||||
raise SystemError("Android-specific interface was used on non-Android OS")
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
preamble = int(c["preamble"]) if "preamble" in c else None
|
||||
txtail = int(c["txtail"]) if "txtail" in c else None
|
||||
persistence = int(c["persistence"]) if "persistence" in c else None
|
||||
slottime = int(c["slottime"]) if "slottime" in c else None
|
||||
flow_control = c.as_bool("flow_control") if "flow_control" in c else False
|
||||
port = c["port"] if "port" in c else None
|
||||
speed = int(c["speed"]) if "speed" in c else 9600
|
||||
databits = int(c["databits"]) if "databits" in c else 8
|
||||
parity = c["parity"] if "parity" in c else "N"
|
||||
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
|
||||
beacon_interval = int(c["beacon_interval"]) if "beacon_interval" in c and c["beacon_interval"] != None else None
|
||||
beacon_data = c["beacon_data"] if "beacon_data" in c else None
|
||||
|
||||
self.HW_MTU = 564
|
||||
|
||||
@@ -268,13 +283,13 @@ class KISSInterface(Interface):
|
||||
raise IOError("Could not enable KISS interface flow control")
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
def af():
|
||||
self.owner.inbound(data, self)
|
||||
threading.Thread(target=af, daemon=True).start()
|
||||
|
||||
def processOutgoing(self,data):
|
||||
def process_outgoing(self,data):
|
||||
datalen = len(data)
|
||||
if self.online:
|
||||
if self.interface_ready:
|
||||
@@ -308,7 +323,7 @@ class KISSInterface(Interface):
|
||||
if len(self.packet_queue) > 0:
|
||||
data = self.packet_queue.pop(0)
|
||||
self.interface_ready = True
|
||||
self.processOutgoing(data)
|
||||
self.process_outgoing(data)
|
||||
elif len(self.packet_queue) == 0:
|
||||
self.interface_ready = True
|
||||
|
||||
@@ -329,7 +344,7 @@ class KISSInterface(Interface):
|
||||
|
||||
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
self.process_incoming(data_buffer)
|
||||
elif (byte == KISS.FEND):
|
||||
in_frame = True
|
||||
command = KISS.CMD_UNKNOWN
|
||||
@@ -374,7 +389,13 @@ class KISSInterface(Interface):
|
||||
if time.time() > self.first_tx + self.beacon_i:
|
||||
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.beacon_d.decode("utf-8")), RNS.LOG_DEBUG)
|
||||
self.first_tx = None
|
||||
self.processOutgoing(self.beacon_d)
|
||||
|
||||
# Pad to minimum length
|
||||
frame = bytearray(self.beacon_d)
|
||||
while len(frame) < 15:
|
||||
frame.append(0x00)
|
||||
|
||||
self.process_outgoing(bytes(frame))
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
@@ -403,5 +424,8 @@ class KISSInterface(Interface):
|
||||
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return "KISSInterface["+self.name+"]"
|
||||
@@ -42,6 +42,7 @@ class HDLC():
|
||||
|
||||
class SerialInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
DEFAULT_IFAC_SIZE = 8
|
||||
|
||||
owner = None
|
||||
port = None
|
||||
@@ -51,7 +52,7 @@ class SerialInterface(Interface):
|
||||
stopbits = None
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, name, port, speed, databits, parity, stopbits):
|
||||
def __init__(self, owner, configuration):
|
||||
import importlib
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
self.on_android = True
|
||||
@@ -72,8 +73,18 @@ class SerialInterface(Interface):
|
||||
else:
|
||||
raise SystemError("Android-specific interface was used on non-Android OS")
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
port = c["port"] if "port" in c else None
|
||||
speed = int(c["speed"]) if "speed" in c else 9600
|
||||
databits = int(c["databits"]) if "databits" in c else 8
|
||||
parity = c["parity"] if "parity" in c else "N"
|
||||
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
|
||||
|
||||
if port == None:
|
||||
raise ValueError("No port specified for serial interface")
|
||||
|
||||
self.HW_MTU = 564
|
||||
|
||||
@@ -173,13 +184,13 @@ class SerialInterface(Interface):
|
||||
RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
def af():
|
||||
self.owner.inbound(data, self)
|
||||
threading.Thread(target=af, daemon=True).start()
|
||||
|
||||
def processOutgoing(self,data):
|
||||
def process_outgoing(self,data):
|
||||
if self.online:
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
written = self.serial.write(data)
|
||||
@@ -203,7 +214,7 @@ class SerialInterface(Interface):
|
||||
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
self.process_incoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
@@ -254,5 +265,8 @@ class SerialInterface(Interface):
|
||||
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return "SerialInterface["+self.name+"]"
|
||||
|
||||
@@ -23,5 +23,7 @@
|
||||
import os
|
||||
import glob
|
||||
|
||||
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
|
||||
py_modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
|
||||
modules = py_modules+pyc_modules
|
||||
__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))]))
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -20,7 +20,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
from collections import deque
|
||||
import socketserver
|
||||
import threading
|
||||
@@ -33,9 +33,13 @@ import RNS
|
||||
|
||||
|
||||
class AutoInterface(Interface):
|
||||
HW_MTU = 1196
|
||||
FIXED_MTU = True
|
||||
|
||||
DEFAULT_DISCOVERY_PORT = 29716
|
||||
DEFAULT_DATA_PORT = 42671
|
||||
DEFAULT_GROUP_ID = "reticulum".encode("utf-8")
|
||||
DEFAULT_IFAC_SIZE = 16
|
||||
|
||||
SCOPE_LINK = "2"
|
||||
SCOPE_ADMIN = "4"
|
||||
@@ -43,7 +47,10 @@ class AutoInterface(Interface):
|
||||
SCOPE_ORGANISATION = "8"
|
||||
SCOPE_GLOBAL = "e"
|
||||
|
||||
PEERING_TIMEOUT = 7.5
|
||||
MULTICAST_PERMANENT_ADDRESS_TYPE = "0"
|
||||
MULTICAST_TEMPORARY_ADDRESS_TYPE = "1"
|
||||
|
||||
PEERING_TIMEOUT = 10.0
|
||||
|
||||
ALL_IGNORE_IFS = ["lo0"]
|
||||
DARWIN_IGNORE_IFS = ["awdl0", "llw0", "lo0", "en5"]
|
||||
@@ -51,7 +58,8 @@ class AutoInterface(Interface):
|
||||
|
||||
BITRATE_GUESS = 10*1000*1000
|
||||
|
||||
MULTI_IF_DEQUE_LEN = 64
|
||||
MULTI_IF_DEQUE_LEN = 48
|
||||
MULTI_IF_DEQUE_TTL = 0.75
|
||||
|
||||
def handler_factory(self, callback):
|
||||
def create_handler(*args, **keys):
|
||||
@@ -73,18 +81,36 @@ class AutoInterface(Interface):
|
||||
ifas = self.netinfo.ifaddresses(ifname)
|
||||
return ifas
|
||||
|
||||
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):
|
||||
def interface_name_to_index(self, ifname):
|
||||
|
||||
# socket.if_nametoindex doesn't work with uuid interface names on windows, it wants the ethernet_0 style
|
||||
# we will just get the index from netinfo instead as it seems to work
|
||||
if RNS.vendor.platformutils.is_windows():
|
||||
return self.netinfo.interface_names_to_indexes()[ifname]
|
||||
|
||||
return socket.if_nametoindex(ifname)
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
group_id = c["group_id"] if "group_id" in c else None
|
||||
discovery_scope = c["discovery_scope"] if "discovery_scope" in c else None
|
||||
discovery_port = int(c["discovery_port"]) if "discovery_port" in c else None
|
||||
multicast_address_type = c["multicast_address_type"] if "multicast_address_type" in c else None
|
||||
data_port = int(c["data_port"]) if "data_port" in c else None
|
||||
allowed_interfaces = c.as_list("devices") if "devices" in c else None
|
||||
ignored_interfaces = c.as_list("ignored_devices") if "ignored_devices" in c else None
|
||||
configured_bitrate = c["configured_bitrate"] if "configured_bitrate" in c else None
|
||||
|
||||
from RNS.vendor.ifaddr import niwrapper
|
||||
super().__init__()
|
||||
self.netinfo = niwrapper
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
self.HW_MTU = AutoInterface.HW_MTU
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.name = name
|
||||
self.owner = owner
|
||||
self.online = False
|
||||
self.peers = {}
|
||||
self.link_local_addresses = []
|
||||
@@ -92,7 +118,10 @@ class AutoInterface(Interface):
|
||||
self.interface_servers = {}
|
||||
self.multicast_echoes = {}
|
||||
self.timed_out_interfaces = {}
|
||||
self.spawned_interfaces = {}
|
||||
self.write_lock = threading.Lock()
|
||||
self.mif_deque = deque(maxlen=AutoInterface.MULTI_IF_DEQUE_LEN)
|
||||
self.mif_deque_times = deque(maxlen=AutoInterface.MULTI_IF_DEQUE_LEN)
|
||||
self.carrier_changed = False
|
||||
|
||||
self.outbound_udp_socket = None
|
||||
@@ -103,6 +132,11 @@ class AutoInterface(Interface):
|
||||
self.peering_timeout = AutoInterface.PEERING_TIMEOUT
|
||||
self.multicast_echo_timeout = AutoInterface.PEERING_TIMEOUT/2
|
||||
|
||||
# Increase peering timeout on Android, due to potential
|
||||
# low-power modes implemented on many chipsets.
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
self.peering_timeout *= 2.5
|
||||
|
||||
if allowed_interfaces == None:
|
||||
self.allowed_interfaces = []
|
||||
else:
|
||||
@@ -123,6 +157,15 @@ class AutoInterface(Interface):
|
||||
else:
|
||||
self.discovery_port = discovery_port
|
||||
|
||||
if multicast_address_type == None:
|
||||
self.multicast_address_type = AutoInterface.MULTICAST_TEMPORARY_ADDRESS_TYPE
|
||||
elif str(multicast_address_type).lower() == "temporary":
|
||||
self.multicast_address_type = AutoInterface.MULTICAST_TEMPORARY_ADDRESS_TYPE
|
||||
elif str(multicast_address_type).lower() == "permanent":
|
||||
self.multicast_address_type = AutoInterface.MULTICAST_PERMANENT_ADDRESS_TYPE
|
||||
else:
|
||||
self.multicast_address_type = AutoInterface.MULTICAST_TEMPORARY_ADDRESS_TYPE
|
||||
|
||||
if data_port == None:
|
||||
self.data_port = AutoInterface.DEFAULT_DATA_PORT
|
||||
else:
|
||||
@@ -151,73 +194,94 @@ class AutoInterface(Interface):
|
||||
gt += ":"+"{:02x}".format(g[9]+(g[8]<<8))
|
||||
gt += ":"+"{:02x}".format(g[11]+(g[10]<<8))
|
||||
gt += ":"+"{:02x}".format(g[13]+(g[12]<<8))
|
||||
self.mcast_discovery_address = "ff1"+self.discovery_scope+":"+gt
|
||||
self.mcast_discovery_address = "ff"+self.multicast_address_type+self.discovery_scope+":"+gt
|
||||
|
||||
suitable_interfaces = 0
|
||||
for ifname in self.list_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)
|
||||
elif ifname in AutoInterface.ALL_IGNORE_IFS:
|
||||
RNS.log(str(self)+" skipping 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)
|
||||
try:
|
||||
if RNS.vendor.platformutils.is_darwin() and ifname in AutoInterface.DARWIN_IGNORE_IFS and not ifname in self.allowed_interfaces:
|
||||
RNS.log(str(self)+" skipping Darwin AWDL or tethering interface "+str(ifname), RNS.LOG_EXTREME)
|
||||
elif RNS.vendor.platformutils.is_darwin() and ifname == "lo0":
|
||||
RNS.log(str(self)+" skipping Darwin loopback interface "+str(ifname), RNS.LOG_EXTREME)
|
||||
elif RNS.vendor.platformutils.is_android() and ifname in AutoInterface.ANDROID_IGNORE_IFS and not ifname in self.allowed_interfaces:
|
||||
RNS.log(str(self)+" skipping Android system interface "+str(ifname), RNS.LOG_EXTREME)
|
||||
elif ifname in self.ignored_interfaces:
|
||||
RNS.log(str(self)+" ignoring disallowed interface "+str(ifname), RNS.LOG_EXTREME)
|
||||
elif ifname in AutoInterface.ALL_IGNORE_IFS:
|
||||
RNS.log(str(self)+" skipping interface "+str(ifname), RNS.LOG_EXTREME)
|
||||
else:
|
||||
addresses = self.list_addresses(ifname)
|
||||
if self.netinfo.AF_INET6 in addresses:
|
||||
link_local_addr = None
|
||||
for address in addresses[self.netinfo.AF_INET6]:
|
||||
if "addr" in address:
|
||||
if address["addr"].startswith("fe80:"):
|
||||
link_local_addr = self.descope_linklocal(address["addr"])
|
||||
self.link_local_addresses.append(link_local_addr)
|
||||
self.adopted_interfaces[ifname] = link_local_addr
|
||||
self.multicast_echoes[ifname] = time.time()
|
||||
RNS.log(str(self)+" Selecting link-local address "+str(link_local_addr)+" for interface "+str(ifname), RNS.LOG_EXTREME)
|
||||
if len(self.allowed_interfaces) > 0 and not ifname in self.allowed_interfaces:
|
||||
RNS.log(str(self)+" ignoring interface "+str(ifname)+" since it was not allowed", RNS.LOG_EXTREME)
|
||||
else:
|
||||
addresses = self.list_addresses(ifname)
|
||||
if self.netinfo.AF_INET6 in addresses:
|
||||
link_local_addr = None
|
||||
for address in addresses[self.netinfo.AF_INET6]:
|
||||
if "addr" in address:
|
||||
if address["addr"].startswith("fe80:"):
|
||||
link_local_addr = self.descope_linklocal(address["addr"])
|
||||
self.link_local_addresses.append(link_local_addr)
|
||||
self.adopted_interfaces[ifname] = link_local_addr
|
||||
self.multicast_echoes[ifname] = time.time()
|
||||
nice_name = self.netinfo.interface_name_to_nice_name(ifname)
|
||||
if nice_name != None and nice_name != ifname:
|
||||
RNS.log(f"{self} Selecting link-local address {link_local_addr} for interface {nice_name} / {ifname}", RNS.LOG_EXTREME)
|
||||
else:
|
||||
RNS.log(f"{self} Selecting link-local address {link_local_addr} for interface {ifname}", RNS.LOG_EXTREME)
|
||||
|
||||
if link_local_addr == None:
|
||||
RNS.log(str(self)+" No link-local IPv6 address configured for "+str(ifname)+", skipping interface", RNS.LOG_EXTREME)
|
||||
else:
|
||||
mcast_addr = self.mcast_discovery_address
|
||||
RNS.log(str(self)+" Creating multicast discovery listener on "+str(ifname)+" with address "+str(mcast_addr), RNS.LOG_EXTREME)
|
||||
|
||||
# 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)
|
||||
if hasattr(socket, "SO_REUSEPORT"):
|
||||
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, if_struct)
|
||||
|
||||
# Join multicast group
|
||||
mcast_group = socket.inet_pton(socket.AF_INET6, mcast_addr) + if_struct
|
||||
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mcast_group)
|
||||
|
||||
# Bind socket
|
||||
if self.discovery_scope == AutoInterface.SCOPE_LINK:
|
||||
addr_info = socket.getaddrinfo(mcast_addr+"%"+ifname, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
if link_local_addr == None:
|
||||
RNS.log(str(self)+" No link-local IPv6 address configured for "+str(ifname)+", skipping interface", RNS.LOG_EXTREME)
|
||||
else:
|
||||
addr_info = socket.getaddrinfo(mcast_addr, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
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)
|
||||
|
||||
discovery_socket.bind(addr_info[0][4])
|
||||
# Struct with interface index
|
||||
if_struct = struct.pack("I", self.interface_name_to_index(ifname))
|
||||
|
||||
# Set up thread for discovery packets
|
||||
def discovery_loop():
|
||||
self.discovery_handler(discovery_socket, ifname)
|
||||
# Set up multicast socket
|
||||
discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
if hasattr(socket, "SO_REUSEPORT"):
|
||||
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, if_struct)
|
||||
|
||||
thread = threading.Thread(target=discovery_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
# 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)
|
||||
|
||||
suitable_interfaces += 1
|
||||
# Bind socket
|
||||
if RNS.vendor.platformutils.is_windows():
|
||||
|
||||
# window throws "[WinError 10049] The requested address is not valid in its context"
|
||||
# when trying to use the multicast address as host, or when providing interface index
|
||||
# passing an empty host appears to work, but probably not exactly how we want it to...
|
||||
discovery_socket.bind(('', self.discovery_port))
|
||||
|
||||
else:
|
||||
|
||||
if self.discovery_scope == AutoInterface.SCOPE_LINK:
|
||||
addr_info = socket.getaddrinfo(mcast_addr+"%"+ifname, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
else:
|
||||
addr_info = socket.getaddrinfo(mcast_addr, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
|
||||
discovery_socket.bind(addr_info[0][4])
|
||||
|
||||
# Set up thread for discovery packets
|
||||
def discovery_loop():
|
||||
self.discovery_handler(discovery_socket, ifname)
|
||||
|
||||
thread = threading.Thread(target=discovery_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
suitable_interfaces += 1
|
||||
|
||||
except Exception as e:
|
||||
nice_name = self.netinfo.interface_name_to_nice_name(ifname)
|
||||
if nice_name != None and nice_name != ifname:
|
||||
RNS.log(f"Could not configure the system interface {nice_name} / {ifname} for use with {self}, skipping it. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
else:
|
||||
RNS.log(f"Could not configure the system interface {ifname} for use with {self}, skipping it. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
if suitable_interfaces == 0:
|
||||
RNS.log(str(self)+" could not autoconfigure. This interface currently provides no connectivity.", RNS.LOG_WARNING)
|
||||
@@ -229,32 +293,31 @@ class AutoInterface(Interface):
|
||||
else:
|
||||
self.bitrate = AutoInterface.BITRATE_GUESS
|
||||
|
||||
peering_wait = self.announce_interval*1.2
|
||||
RNS.log(str(self)+" discovering peers for "+str(round(peering_wait, 2))+" seconds...", RNS.LOG_VERBOSE)
|
||||
def final_init(self):
|
||||
peering_wait = self.announce_interval*1.2
|
||||
RNS.log(str(self)+" discovering peers for "+str(round(peering_wait, 2))+" seconds...", RNS.LOG_VERBOSE)
|
||||
|
||||
self.owner = owner
|
||||
socketserver.UDPServer.address_family = socket.AF_INET6
|
||||
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]
|
||||
for ifname in self.adopted_interfaces:
|
||||
local_addr = self.adopted_interfaces[ifname]+"%"+str(self.interface_name_to_index(ifname))
|
||||
addr_info = socket.getaddrinfo(local_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
address = addr_info[0][4]
|
||||
|
||||
udp_server = socketserver.UDPServer(address, self.handler_factory(self.processIncoming))
|
||||
self.interface_servers[ifname] = udp_server
|
||||
|
||||
thread = threading.Thread(target=udp_server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
udp_server = socketserver.UDPServer(address, self.handler_factory(self.process_incoming))
|
||||
self.interface_servers[ifname] = udp_server
|
||||
|
||||
thread = threading.Thread(target=udp_server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
job_thread = threading.Thread(target=self.peer_jobs)
|
||||
job_thread.daemon = True
|
||||
job_thread.start()
|
||||
job_thread = threading.Thread(target=self.peer_jobs)
|
||||
job_thread.daemon = True
|
||||
job_thread.start()
|
||||
|
||||
time.sleep(peering_wait)
|
||||
|
||||
self.online = True
|
||||
time.sleep(peering_wait)
|
||||
|
||||
self.online = True
|
||||
|
||||
def discovery_handler(self, socket, ifname):
|
||||
def announce_loop():
|
||||
@@ -266,8 +329,9 @@ class AutoInterface(Interface):
|
||||
|
||||
while True:
|
||||
data, ipv6_src = socket.recvfrom(1024)
|
||||
peering_hash = data[:RNS.Identity.HASHLENGTH//8]
|
||||
expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
|
||||
if data == expected_hash:
|
||||
if peering_hash == expected_hash:
|
||||
self.add_peer(ipv6_src[0], ifname)
|
||||
else:
|
||||
RNS.log(str(self)+" received peering packet on "+str(ifname)+" from "+str(ipv6_src[0])+", but authentication hash was incorrect.", RNS.LOG_DEBUG)
|
||||
@@ -288,6 +352,10 @@ class AutoInterface(Interface):
|
||||
# Remove any timed out peers
|
||||
for peer_addr in timed_out_peers:
|
||||
removed_peer = self.peers.pop(peer_addr)
|
||||
if peer_addr in self.spawned_interfaces:
|
||||
spawned_interface = self.spawned_interfaces[peer_addr]
|
||||
spawned_interface.detach()
|
||||
spawned_interface.teardown()
|
||||
RNS.log(str(self)+" removed peer "+str(peer_addr)+" on "+str(removed_peer[0]), RNS.LOG_DEBUG)
|
||||
|
||||
for ifname in self.adopted_interfaces:
|
||||
@@ -322,13 +390,15 @@ class AutoInterface(Interface):
|
||||
|
||||
RNS.log("Starting new UDP listener for "+str(self)+" "+str(ifname), RNS.LOG_DEBUG)
|
||||
|
||||
udp_server = socketserver.UDPServer(listen_address, self.handler_factory(self.processIncoming))
|
||||
udp_server = socketserver.UDPServer(listen_address, self.handler_factory(self.process_incoming))
|
||||
self.interface_servers[ifname] = udp_server
|
||||
|
||||
thread = threading.Thread(target=udp_server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
self.carrier_changed = True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not get device information while updating link-local addresses for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
@@ -361,7 +431,7 @@ class AutoInterface(Interface):
|
||||
announce_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
addr_info = socket.getaddrinfo(self.mcast_discovery_address, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
|
||||
ifis = struct.pack("I", socket.if_nametoindex(ifname))
|
||||
ifis = struct.pack("I", self.interface_name_to_index(ifname))
|
||||
announce_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifis)
|
||||
announce_socket.sendto(discovery_token, addr_info[0][4])
|
||||
announce_socket.close()
|
||||
@@ -372,6 +442,10 @@ class AutoInterface(Interface):
|
||||
else:
|
||||
pass
|
||||
|
||||
@property
|
||||
def peer_count(self):
|
||||
return len(self.spawned_interfaces)
|
||||
|
||||
def add_peer(self, addr, ifname):
|
||||
if addr in self.link_local_addresses:
|
||||
ifname = None
|
||||
@@ -387,40 +461,151 @@ class AutoInterface(Interface):
|
||||
else:
|
||||
if not addr in self.peers:
|
||||
self.peers[addr] = [ifname, time.time()]
|
||||
|
||||
spawned_interface = AutoInterfacePeer(self, addr, ifname)
|
||||
spawned_interface.OUT = self.OUT
|
||||
spawned_interface.IN = self.IN
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
|
||||
spawned_interface.ifac_size = self.ifac_size
|
||||
spawned_interface.ifac_netname = self.ifac_netname
|
||||
spawned_interface.ifac_netkey = self.ifac_netkey
|
||||
if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None:
|
||||
ifac_origin = b""
|
||||
if spawned_interface.ifac_netname != None:
|
||||
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
|
||||
if spawned_interface.ifac_netkey != None:
|
||||
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
|
||||
|
||||
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
|
||||
spawned_interface.ifac_key = RNS.Cryptography.hkdf(
|
||||
length=64,
|
||||
derive_from=ifac_origin_hash,
|
||||
salt=RNS.Reticulum.IFAC_SALT,
|
||||
context=None
|
||||
)
|
||||
spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key)
|
||||
spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key))
|
||||
|
||||
spawned_interface.announce_rate_target = self.announce_rate_target
|
||||
spawned_interface.announce_rate_grace = self.announce_rate_grace
|
||||
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
if addr in self.spawned_interfaces:
|
||||
self.spawned_interfaces[addr].detach()
|
||||
self.spawned_interfaces[addr].teardown()
|
||||
self.spawned_interfaces.pop(spawned_interface)
|
||||
self.spawned_interfaces[addr] = spawned_interface
|
||||
|
||||
RNS.log(str(self)+" added peer "+str(addr)+" on "+str(ifname), RNS.LOG_DEBUG)
|
||||
else:
|
||||
self.refresh_peer(addr)
|
||||
|
||||
def refresh_peer(self, addr):
|
||||
self.peers[addr][1] = time.time()
|
||||
try:
|
||||
self.peers[addr][1] = time.time()
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while refreshing peer {addr} on {self}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def processIncoming(self, data):
|
||||
data_hash = RNS.Identity.full_hash(data)
|
||||
if not data_hash in self.mif_deque:
|
||||
self.mif_deque.append(data_hash)
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
def process_incoming(self, data, addr=None):
|
||||
if self.online and addr in self.spawned_interfaces:
|
||||
self.spawned_interfaces[addr].process_incoming(data, addr)
|
||||
|
||||
def processOutgoing(self,data):
|
||||
for peer in self.peers:
|
||||
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])
|
||||
def process_outgoing(self,data):
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not transmit on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
# Until per-device sub-interfacing is implemented,
|
||||
# ingress limiting should be disabled on AutoInterface
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
|
||||
self.txb += len(data)
|
||||
|
||||
def detach(self):
|
||||
self.online = False
|
||||
|
||||
def __str__(self):
|
||||
return "AutoInterface["+self.name+"]"
|
||||
|
||||
class AutoInterfacePeer(Interface):
|
||||
|
||||
def __init__(self, owner, addr, ifname):
|
||||
super().__init__()
|
||||
self.owner = owner
|
||||
self.parent_interface = owner
|
||||
self.addr = addr
|
||||
self.ifname = ifname
|
||||
self.peer_addr = None
|
||||
self.addr_info = None
|
||||
self.HW_MTU = self.owner.HW_MTU
|
||||
self.FIXED_MTU = self.owner.FIXED_MTU
|
||||
|
||||
def __str__(self):
|
||||
return f"AutoInterfacePeer[{self.ifname}/{self.addr}]"
|
||||
|
||||
def process_incoming(self, data, addr=None):
|
||||
if self.online and self.owner.online:
|
||||
data_hash = RNS.Identity.full_hash(data)
|
||||
deque_hit = False
|
||||
if data_hash in self.owner.mif_deque:
|
||||
for te in self.owner.mif_deque_times:
|
||||
if te[0] == data_hash and time.time() < te[1]+AutoInterface.MULTI_IF_DEQUE_TTL:
|
||||
deque_hit = True
|
||||
break
|
||||
|
||||
if not deque_hit:
|
||||
self.owner.refresh_peer(self.addr)
|
||||
self.owner.mif_deque.append(data_hash)
|
||||
self.owner.mif_deque_times.append([data_hash, time.time()])
|
||||
self.rxb += len(data)
|
||||
self.owner.rxb += len(data)
|
||||
self.owner.owner.inbound(data, self)
|
||||
|
||||
def process_outgoing(self, data):
|
||||
if self.online:
|
||||
with self.owner.write_lock:
|
||||
try:
|
||||
if self.owner.outbound_udp_socket == None: self.owner.outbound_udp_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
if self.peer_addr == None: self.peer_addr = str(self.addr)+"%"+str(self.owner.interface_name_to_index(self.ifname))
|
||||
if self.addr_info == None: addr_info = socket.getaddrinfo(self.peer_addr, self.owner.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
self.owner.outbound_udp_socket.sendto(data, addr_info[0][4])
|
||||
self.txb += len(data)
|
||||
self.owner.txb += len(data)
|
||||
except Exception as e:
|
||||
RNS.log("Could not transmit on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def detach(self):
|
||||
self.online = False
|
||||
self.detached = True
|
||||
|
||||
def teardown(self):
|
||||
if not self.detached:
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down.", RNS.LOG_ERROR)
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
RNS.panic()
|
||||
|
||||
else:
|
||||
RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_VERBOSE)
|
||||
|
||||
self.online = False
|
||||
self.OUT = False
|
||||
self.IN = False
|
||||
|
||||
if self.addr in self.owner.spawned_interfaces:
|
||||
try: self.owner.spawned_interfaces.pop(self.addr)
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not remove {self} from parent interface on detach. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
if self in RNS.Transport.interfaces:
|
||||
RNS.Transport.interfaces.remove(self)
|
||||
|
||||
# Until per-device sub-interfacing is implemented,
|
||||
# ingress limiting should be disabled on AutoInterface
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
class AutoInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
def __init__(self, callback, *args, **keys):
|
||||
self.callback = callback
|
||||
@@ -428,4 +613,5 @@ class AutoInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
|
||||
def handle(self):
|
||||
data = self.request[0]
|
||||
self.callback(data)
|
||||
addr = self.client_address[0]
|
||||
self.callback(data, addr)
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -20,7 +20,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
import socketserver
|
||||
import threading
|
||||
import platform
|
||||
@@ -390,8 +390,7 @@ class I2PInterfacePeer(Interface):
|
||||
TUNNEL_STATE_STALE = 0x02
|
||||
|
||||
def __init__(self, parent_interface, owner, name, target_i2p_dest=None, connected_socket=None, max_reconnect_tries=None):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
super().__init__()
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
@@ -628,14 +627,14 @@ class I2PInterfacePeer(Interface):
|
||||
RNS.log("Attempt to reconnect on a non-initiator I2P interface. This should not happen.", RNS.LOG_ERROR)
|
||||
raise IOError("Attempt to reconnect on a non-initiator I2P interface")
|
||||
|
||||
def processIncoming(self, data):
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None and self.parent_count:
|
||||
self.parent_interface.rxb += len(data)
|
||||
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
def processOutgoing(self, data):
|
||||
def process_outgoing(self, data):
|
||||
if self.online:
|
||||
while self.writing:
|
||||
time.sleep(0.001)
|
||||
@@ -733,7 +732,7 @@ class I2PInterfacePeer(Interface):
|
||||
# Read loop for KISS framing
|
||||
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
self.process_incoming(data_buffer)
|
||||
elif (byte == KISS.FEND):
|
||||
in_frame = True
|
||||
command = KISS.CMD_UNKNOWN
|
||||
@@ -760,7 +759,7 @@ class I2PInterfacePeer(Interface):
|
||||
# Read loop for HDLC framing
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
self.process_incoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
@@ -816,8 +815,8 @@ class I2PInterfacePeer(Interface):
|
||||
self.IN = False
|
||||
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
if self.parent_interface.clients > 0:
|
||||
self.parent_interface.clients -= 1
|
||||
while self in self.parent_interface.spawned_interfaces:
|
||||
self.parent_interface.spawned_interfaces.remove(self)
|
||||
|
||||
if self in RNS.Transport.interfaces:
|
||||
if not self.initiator:
|
||||
@@ -830,15 +829,28 @@ class I2PInterfacePeer(Interface):
|
||||
|
||||
class I2PInterface(Interface):
|
||||
BITRATE_GUESS = 256*1000
|
||||
DEFAULT_IFAC_SIZE = 16
|
||||
|
||||
def __init__(self, owner, name, rns_storagepath, peers, connectable = False, ifac_size = 16, ifac_netname = None, ifac_netkey = None):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
@property
|
||||
def clients(self):
|
||||
return len(self.spawned_interfaces)
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
rns_storagepath = c["storagepath"]
|
||||
peers = c.as_list("peers") if "peers" in c else None
|
||||
connectable = c.as_bool("connectable") if "connectable" in c else False
|
||||
ifac_size = c["ifac_size"] if "ifac_size" in c else None
|
||||
ifac_netname = c["ifac_netname"] if "ifac_netname" in c else None
|
||||
ifac_netkey = c["ifac_netkey"] if "ifac_netkey" in c else None
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
self.online = False
|
||||
self.clients = 0
|
||||
self.spawned_interfaces = []
|
||||
self.owner = owner
|
||||
self.connectable = connectable
|
||||
self.i2p_tunneled = True
|
||||
@@ -958,12 +970,20 @@ class I2PInterface(Interface):
|
||||
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
|
||||
while spawned_interface in self.spawned_interfaces:
|
||||
self.spawned_interfaces.remove(spawned_interface)
|
||||
self.spawned_interfaces.append(spawned_interface)
|
||||
spawned_interface.read_loop()
|
||||
|
||||
def processOutgoing(self, data):
|
||||
def process_outgoing(self, data):
|
||||
pass
|
||||
|
||||
def received_announce(self, from_spawned=False):
|
||||
if from_spawned: self.ia_freq_deque.append(time.time())
|
||||
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def detach(self):
|
||||
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
|
||||
self.i2p.stop()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -23,6 +23,8 @@
|
||||
import RNS
|
||||
import time
|
||||
import threading
|
||||
from collections import deque
|
||||
from RNS.vendor.configobj import ConfigObj
|
||||
|
||||
class Interface:
|
||||
IN = False
|
||||
@@ -39,18 +41,191 @@ class Interface:
|
||||
MODE_BOUNDARY = 0x05
|
||||
MODE_GATEWAY = 0x06
|
||||
|
||||
# Which interface modes a Transport Node
|
||||
# should actively discover paths for.
|
||||
DISCOVER_PATHS_FOR = [MODE_ACCESS_POINT, MODE_GATEWAY]
|
||||
# Which interface modes a Transport Node should
|
||||
# actively discover paths for.
|
||||
DISCOVER_PATHS_FOR = [MODE_ACCESS_POINT, MODE_GATEWAY, MODE_ROAMING]
|
||||
|
||||
# How many samples to use for announce
|
||||
# frequency calculations
|
||||
IA_FREQ_SAMPLES = 6
|
||||
OA_FREQ_SAMPLES = 6
|
||||
|
||||
# Maximum amount of ingress limited announces
|
||||
# to hold at any given time.
|
||||
MAX_HELD_ANNOUNCES = 256
|
||||
|
||||
# How long a spawned interface will be
|
||||
# considered to be newly created. Two
|
||||
# hours by default.
|
||||
IC_NEW_TIME = 2*60*60
|
||||
IC_BURST_FREQ_NEW = 3.5
|
||||
IC_BURST_FREQ = 12
|
||||
IC_BURST_HOLD = 1*60
|
||||
IC_BURST_PENALTY = 5*60
|
||||
IC_HELD_RELEASE_INTERVAL = 30
|
||||
|
||||
AUTOCONFIGURE_MTU = False
|
||||
FIXED_MTU = False
|
||||
|
||||
def __init__(self):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
self.online = False
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
self.created = time.time()
|
||||
self.detached = False
|
||||
self.online = False
|
||||
self.bitrate = 62500
|
||||
self.HW_MTU = None
|
||||
|
||||
self.ingress_control = True
|
||||
self.ic_max_held_announces = Interface.MAX_HELD_ANNOUNCES
|
||||
self.ic_burst_hold = Interface.IC_BURST_HOLD
|
||||
self.ic_burst_active = False
|
||||
self.ic_burst_activated = 0
|
||||
self.ic_held_release = 0
|
||||
self.ic_burst_freq_new = Interface.IC_BURST_FREQ_NEW
|
||||
self.ic_burst_freq = Interface.IC_BURST_FREQ
|
||||
self.ic_new_time = Interface.IC_NEW_TIME
|
||||
self.ic_burst_penalty = Interface.IC_BURST_PENALTY
|
||||
self.ic_held_release_interval = Interface.IC_HELD_RELEASE_INTERVAL
|
||||
self.held_announces = {}
|
||||
|
||||
self.ia_freq_deque = deque(maxlen=Interface.IA_FREQ_SAMPLES)
|
||||
self.oa_freq_deque = deque(maxlen=Interface.OA_FREQ_SAMPLES)
|
||||
|
||||
def get_hash(self):
|
||||
return RNS.Identity.full_hash(str(self).encode("utf-8"))
|
||||
|
||||
# This is a generic function for determining when an interface
|
||||
# should activate ingress limiting. Since this can vary for
|
||||
# different interface types, this function should be overwritten
|
||||
# in case a particular interface requires a different approach.
|
||||
def should_ingress_limit(self):
|
||||
if self.ingress_control:
|
||||
freq_threshold = self.ic_burst_freq_new if self.age() < self.ic_new_time else self.ic_burst_freq
|
||||
ia_freq = self.incoming_announce_frequency()
|
||||
|
||||
if self.ic_burst_active:
|
||||
if ia_freq < freq_threshold and time.time() > self.ic_burst_activated+self.ic_burst_hold:
|
||||
self.ic_burst_active = False
|
||||
self.ic_held_release = time.time() + self.ic_burst_penalty
|
||||
return True
|
||||
|
||||
else:
|
||||
if ia_freq > freq_threshold:
|
||||
self.ic_burst_active = True
|
||||
self.ic_burst_activated = time.time()
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
def optimise_mtu(self):
|
||||
if self.AUTOCONFIGURE_MTU:
|
||||
if self.bitrate > 16_000_000:
|
||||
self.HW_MTU = 262144
|
||||
elif self.bitrate > 8_000_000:
|
||||
self.HW_MTU = 131072
|
||||
elif self.bitrate > 4_000_000:
|
||||
self.HW_MTU = 65536
|
||||
elif self.bitrate > 2_000_000:
|
||||
self.HW_MTU = 32768
|
||||
elif self.bitrate > 1_000_000:
|
||||
self.HW_MTU = 16384
|
||||
elif self.bitrate > 500_000:
|
||||
self.HW_MTU = 8192
|
||||
elif self.bitrate > 250_000:
|
||||
self.HW_MTU = 4096
|
||||
elif self.bitrate > 125_000:
|
||||
self.HW_MTU = 2048
|
||||
elif self.bitrate > 62_500:
|
||||
self.HW_MTU = 1024
|
||||
else:
|
||||
self.HW_MTU = None
|
||||
|
||||
RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_DEBUG) # TODO: Remove debug
|
||||
|
||||
def age(self):
|
||||
return time.time()-self.created
|
||||
|
||||
def hold_announce(self, announce_packet):
|
||||
if announce_packet.destination_hash in self.held_announces:
|
||||
self.held_announces[announce_packet.destination_hash] = announce_packet
|
||||
elif not len(self.held_announces) >= self.ic_max_held_announces:
|
||||
self.held_announces[announce_packet.destination_hash] = announce_packet
|
||||
|
||||
def process_held_announces(self):
|
||||
try:
|
||||
if not self.should_ingress_limit() and len(self.held_announces) > 0 and time.time() > self.ic_held_release:
|
||||
freq_threshold = self.ic_burst_freq_new if self.age() < self.ic_new_time else self.ic_burst_freq
|
||||
ia_freq = self.incoming_announce_frequency()
|
||||
if ia_freq < freq_threshold:
|
||||
selected_announce_packet = None
|
||||
min_hops = RNS.Transport.PATHFINDER_M
|
||||
for destination_hash in self.held_announces:
|
||||
announce_packet = self.held_announces[destination_hash]
|
||||
if announce_packet.hops < min_hops:
|
||||
min_hops = announce_packet.hops
|
||||
selected_announce_packet = announce_packet
|
||||
|
||||
if selected_announce_packet != None:
|
||||
RNS.log("Releasing held announce packet "+str(selected_announce_packet)+" from "+str(self), RNS.LOG_EXTREME)
|
||||
self.ic_held_release = time.time() + self.ic_held_release_interval
|
||||
self.held_announces.pop(selected_announce_packet.destination_hash)
|
||||
def release():
|
||||
RNS.Transport.inbound(selected_announce_packet.raw, selected_announce_packet.receiving_interface)
|
||||
threading.Thread(target=release, daemon=True).start()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("An error occurred while processing held announces for "+str(self), RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def received_announce(self, from_spawned=False):
|
||||
self.ia_freq_deque.append(time.time())
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.received_announce(from_spawned=True)
|
||||
|
||||
def sent_announce(self, from_spawned=False):
|
||||
self.oa_freq_deque.append(time.time())
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.sent_announce(from_spawned=True)
|
||||
|
||||
def incoming_announce_frequency(self):
|
||||
if not len(self.ia_freq_deque) > 1:
|
||||
return 0
|
||||
else:
|
||||
dq_len = len(self.ia_freq_deque)
|
||||
delta_sum = 0
|
||||
for i in range(1,dq_len):
|
||||
delta_sum += self.ia_freq_deque[i]-self.ia_freq_deque[i-1]
|
||||
delta_sum += time.time() - self.ia_freq_deque[dq_len-1]
|
||||
|
||||
if delta_sum == 0:
|
||||
avg = 0
|
||||
else:
|
||||
avg = 1/(delta_sum/(dq_len))
|
||||
|
||||
return avg
|
||||
|
||||
def outgoing_announce_frequency(self):
|
||||
if not len(self.oa_freq_deque) > 1:
|
||||
return 0
|
||||
else:
|
||||
dq_len = len(self.oa_freq_deque)
|
||||
delta_sum = 0
|
||||
for i in range(1,dq_len):
|
||||
delta_sum += self.oa_freq_deque[i]-self.oa_freq_deque[i-1]
|
||||
delta_sum += time.time() - self.oa_freq_deque[dq_len-1]
|
||||
|
||||
if delta_sum == 0:
|
||||
avg = 0
|
||||
else:
|
||||
avg = 1/(delta_sum/(dq_len))
|
||||
|
||||
return avg
|
||||
|
||||
def process_announce_queue(self):
|
||||
if not hasattr(self, "announce_cap"):
|
||||
self.announce_cap = RNS.Reticulum.ANNOUNCE_CAP
|
||||
@@ -78,7 +253,8 @@ class Interface:
|
||||
wait_time = (tx_time / self.announce_cap)
|
||||
self.announce_allowed_at = now + wait_time
|
||||
|
||||
self.processOutgoing(selected["raw"])
|
||||
self.process_outgoing(selected["raw"])
|
||||
self.sent_announce()
|
||||
|
||||
if selected in self.announce_queue:
|
||||
self.announce_queue.remove(selected)
|
||||
@@ -92,5 +268,19 @@ class Interface:
|
||||
RNS.log("Error while processing announce queue on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("The announce queue for this interface has been cleared.", RNS.LOG_ERROR)
|
||||
|
||||
def final_init(self):
|
||||
pass
|
||||
|
||||
def detach(self):
|
||||
pass
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_config_obj(config_in):
|
||||
if type(config_in) == ConfigObj:
|
||||
return config_in
|
||||
else:
|
||||
try:
|
||||
return ConfigObj(config_in)
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not parse supplied configuration data. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
raise SystemError("Invalid configuration data supplied")
|
||||
@@ -20,7 +20,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import threading
|
||||
@@ -52,6 +52,7 @@ class KISS():
|
||||
class KISSInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
BITRATE_GUESS = 1200
|
||||
DEFAULT_IFAC_SIZE = 8
|
||||
|
||||
owner = None
|
||||
port = None
|
||||
@@ -61,7 +62,7 @@ class KISSInterface(Interface):
|
||||
stopbits = None
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control, beacon_interval, beacon_data):
|
||||
def __init__(self, owner, configuration):
|
||||
import importlib
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
@@ -70,8 +71,25 @@ class KISSInterface(Interface):
|
||||
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
preamble = int(c["preamble"]) if "preamble" in c else None
|
||||
txtail = int(c["txtail"]) if "txtail" in c else None
|
||||
persistence = int(c["persistence"]) if "persistence" in c else None
|
||||
slottime = int(c["slottime"]) if "slottime" in c else None
|
||||
flow_control = c.as_bool("flow_control") if "flow_control" in c else False
|
||||
port = c["port"] if "port" in c else None
|
||||
speed = int(c["speed"]) if "speed" in c else 9600
|
||||
databits = int(c["databits"]) if "databits" in c else 8
|
||||
parity = c["parity"] if "parity" in c else "N"
|
||||
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
|
||||
beacon_interval = int(c["id_interval"]) if "id_interval" in c else None
|
||||
beacon_data = c["id_callsign"] if "id_callsign" in c else None
|
||||
|
||||
if port == None:
|
||||
raise ValueError("No port specified for serial interface")
|
||||
|
||||
self.HW_MTU = 564
|
||||
|
||||
@@ -218,12 +236,12 @@ class KISSInterface(Interface):
|
||||
raise IOError("Could not enable KISS interface flow control")
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
|
||||
def processOutgoing(self,data):
|
||||
def process_outgoing(self,data):
|
||||
datalen = len(data)
|
||||
if self.online:
|
||||
if self.interface_ready:
|
||||
@@ -257,7 +275,7 @@ class KISSInterface(Interface):
|
||||
if len(self.packet_queue) > 0:
|
||||
data = self.packet_queue.pop(0)
|
||||
self.interface_ready = True
|
||||
self.processOutgoing(data)
|
||||
self.process_outgoing(data)
|
||||
elif len(self.packet_queue) == 0:
|
||||
self.interface_ready = True
|
||||
|
||||
@@ -276,7 +294,7 @@ class KISSInterface(Interface):
|
||||
|
||||
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
self.process_incoming(data_buffer)
|
||||
elif (byte == KISS.FEND):
|
||||
in_frame = True
|
||||
command = KISS.CMD_UNKNOWN
|
||||
@@ -320,7 +338,13 @@ class KISSInterface(Interface):
|
||||
if time.time() > self.first_tx + self.beacon_i:
|
||||
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.beacon_d.decode("utf-8")), RNS.LOG_DEBUG)
|
||||
self.first_tx = None
|
||||
self.processOutgoing(self.beacon_d)
|
||||
|
||||
# Pad to minimum length
|
||||
frame = bytearray(self.beacon_d)
|
||||
while len(frame) < 15:
|
||||
frame.append(0x00)
|
||||
|
||||
self.process_outgoing(bytes(frame))
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
@@ -349,5 +373,8 @@ class KISSInterface(Interface):
|
||||
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return "KISSInterface["+self.name+"]"
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -20,7 +20,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
import socketserver
|
||||
import threading
|
||||
import socket
|
||||
@@ -28,6 +28,7 @@ import time
|
||||
import sys
|
||||
import os
|
||||
import RNS
|
||||
from threading import Lock
|
||||
|
||||
class HDLC():
|
||||
FLAG = 0x7E
|
||||
@@ -50,16 +51,13 @@ class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
self.server_address = self.socket.getsockname()
|
||||
|
||||
class LocalClientInterface(Interface):
|
||||
RECONNECT_WAIT = 3
|
||||
RECONNECT_WAIT = 8
|
||||
AUTOCONFIGURE_MTU = True
|
||||
|
||||
def __init__(self, owner, name, target_port = None, connected_socket=None):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
super().__init__()
|
||||
|
||||
# TODO: Remove at some point
|
||||
# self.rxptime = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
self.HW_MTU = 262144
|
||||
|
||||
self.online = False
|
||||
|
||||
@@ -78,6 +76,7 @@ class LocalClientInterface(Interface):
|
||||
self.target_ip = None
|
||||
self.target_port = None
|
||||
self.socket = connected_socket
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
self.is_connected_to_shared_instance = False
|
||||
|
||||
@@ -88,7 +87,7 @@ class LocalClientInterface(Interface):
|
||||
self.connect()
|
||||
|
||||
self.owner = owner
|
||||
self.bitrate = 1000*1000*1000
|
||||
self.bitrate = 1_000_000_000
|
||||
self.online = True
|
||||
self.writing = False
|
||||
|
||||
@@ -103,8 +102,12 @@ class LocalClientInterface(Interface):
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
def connect(self):
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.socket.connect((self.target_ip, self.target_port))
|
||||
|
||||
self.online = True
|
||||
@@ -137,36 +140,41 @@ class LocalClientInterface(Interface):
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
RNS.Transport.shared_connection_reappeared()
|
||||
def job():
|
||||
time.sleep(LocalClientInterface.RECONNECT_WAIT+2)
|
||||
RNS.Transport.shared_connection_reappeared()
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
|
||||
else:
|
||||
RNS.log("Attempt to reconnect on a non-initiator shared local interface. This should not happen.", RNS.LOG_ERROR)
|
||||
raise IOError("Attempt to reconnect on a non-initiator local interface")
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
if self._force_bitrate:
|
||||
time.sleep(len(data) / self.bitrate * 8)
|
||||
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.rxb += len(data)
|
||||
|
||||
# TODO: Remove at some point
|
||||
# processing_start = time.time()
|
||||
|
||||
self.owner.inbound(data, self)
|
||||
try:
|
||||
self.owner.inbound(data, self)
|
||||
except Exception as e:
|
||||
RNS.log(f"An error in the processing of an incoming frame for {self}: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
# TODO: Remove at some point
|
||||
# duration = time.time() - processing_start
|
||||
# self.rxptime += duration
|
||||
|
||||
def processOutgoing(self, data):
|
||||
def process_outgoing(self, data):
|
||||
if self.online:
|
||||
try:
|
||||
self.writing = True
|
||||
|
||||
if self._force_bitrate:
|
||||
time.sleep(len(data) / self.bitrate * 8)
|
||||
if not hasattr(self, "send_lock"):
|
||||
self.send_lock = Lock()
|
||||
|
||||
with self.send_lock:
|
||||
# RNS.log(f"Simulating latency of {RNS.prettytime(s)} for {len(data)} bytes", RNS.LOG_EXTREME)
|
||||
s = len(data) / self.bitrate * 8
|
||||
time.sleep(s)
|
||||
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
self.socket.sendall(data)
|
||||
self.writing = False
|
||||
@@ -177,6 +185,7 @@ class LocalClientInterface(Interface):
|
||||
except Exception as e:
|
||||
RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
self.teardown()
|
||||
|
||||
|
||||
@@ -184,32 +193,31 @@ class LocalClientInterface(Interface):
|
||||
try:
|
||||
in_frame = False
|
||||
escape = False
|
||||
frame_buffer = b""
|
||||
data_in = b""
|
||||
data_buffer = b""
|
||||
|
||||
while True:
|
||||
data_in = self.socket.recv(4096)
|
||||
if len(data_in) > 0:
|
||||
pointer = 0
|
||||
while pointer < len(data_in):
|
||||
byte = data_in[pointer]
|
||||
pointer += 1
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (byte == HDLC.ESC):
|
||||
escape = True
|
||||
frame_buffer += data_in
|
||||
flags_remaining = True
|
||||
while flags_remaining:
|
||||
frame_start = frame_buffer.find(HDLC.FLAG)
|
||||
if frame_start != -1:
|
||||
frame_end = frame_buffer.find(HDLC.FLAG, frame_start+1)
|
||||
if frame_end != -1:
|
||||
frame = frame_buffer[frame_start+1:frame_end]
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG]))
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
|
||||
if len(frame) > RNS.Reticulum.HEADER_MINSIZE:
|
||||
self.process_incoming(frame)
|
||||
frame_buffer = frame_buffer[frame_end:]
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.FLAG
|
||||
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.ESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
flags_remaining = False
|
||||
else:
|
||||
flags_remaining = False
|
||||
|
||||
else:
|
||||
self.online = False
|
||||
if self.is_connected_to_shared_instance and not self.detached:
|
||||
@@ -236,12 +244,14 @@ class LocalClientInterface(Interface):
|
||||
self.detached = True
|
||||
|
||||
try:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
if self.socket != None:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
|
||||
|
||||
try:
|
||||
self.socket.close()
|
||||
if self.socket != None:
|
||||
self.socket.close()
|
||||
except Exception as e:
|
||||
RNS.log("Error while closing socket for "+str(self)+": "+str(e))
|
||||
|
||||
@@ -259,7 +269,8 @@ class LocalClientInterface(Interface):
|
||||
RNS.Transport.local_client_interfaces.remove(self)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.clients -= 1
|
||||
RNS.Transport.owner._should_persist_data()
|
||||
if hasattr(RNS.Transport, "owner") and RNS.Transport.owner != None:
|
||||
RNS.Transport.owner._should_persist_data()
|
||||
|
||||
if nowarning == False:
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
|
||||
@@ -278,10 +289,10 @@ class LocalClientInterface(Interface):
|
||||
|
||||
|
||||
class LocalServerInterface(Interface):
|
||||
AUTOCONFIGURE_MTU = True
|
||||
|
||||
def __init__(self, owner, bindport=None):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
super().__init__()
|
||||
self.online = False
|
||||
self.clients = 0
|
||||
|
||||
@@ -306,6 +317,7 @@ class LocalServerInterface(Interface):
|
||||
address = (self.bind_ip, self.bind_port)
|
||||
|
||||
self.server = ThreadingTCPServer(address, handlerFactory(self.incoming_connection))
|
||||
self.server.daemon_threads = True
|
||||
|
||||
thread = threading.Thread(target=self.server.serve_forever)
|
||||
thread.daemon = True
|
||||
@@ -329,15 +341,23 @@ class LocalServerInterface(Interface):
|
||||
spawned_interface.target_port = str(handler.client_address[1])
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
RNS.log("Accepting new connection to shared instance: "+str(spawned_interface), RNS.LOG_EXTREME)
|
||||
if hasattr(self, "_force_bitrate"):
|
||||
spawned_interface._force_bitrate = self._force_bitrate
|
||||
# RNS.log("Accepting new connection to shared instance: "+str(spawned_interface), RNS.LOG_EXTREME)
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.local_client_interfaces.append(spawned_interface)
|
||||
self.clients += 1
|
||||
spawned_interface.read_loop()
|
||||
|
||||
def processOutgoing(self, data):
|
||||
def process_outgoing(self, data):
|
||||
pass
|
||||
|
||||
def received_announce(self, from_spawned=False):
|
||||
if from_spawned: self.ia_freq_deque.append(time.time())
|
||||
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def __str__(self):
|
||||
return "Shared Instance["+str(self.bind_port)+"]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import threading
|
||||
@@ -46,17 +46,25 @@ class HDLC():
|
||||
class PipeInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
BITRATE_GUESS = 1*1000*1000
|
||||
DEFAULT_IFAC_SIZE = 8
|
||||
|
||||
owner = None
|
||||
command = None
|
||||
|
||||
def __init__(self, owner, name, command, respawn_delay):
|
||||
def __init__(self, owner, configuration):
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
command = c["command"] if "command" in c else None
|
||||
respawn_delay = c.as_float("respawn_delay") if "respawn_delay" in c else None
|
||||
|
||||
if command == None:
|
||||
raise ValueError("No command specified for PipeInterface")
|
||||
|
||||
if respawn_delay == None:
|
||||
respawn_delay = 5
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
self.owner = owner
|
||||
@@ -102,12 +110,12 @@ class PipeInterface(Interface):
|
||||
RNS.log("Subprocess pipe for "+str(self)+" is now connected", RNS.LOG_VERBOSE)
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
|
||||
def processOutgoing(self,data):
|
||||
def process_outgoing(self,data):
|
||||
if self.online:
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
written = self.process.stdin.write(data)
|
||||
@@ -135,7 +143,7 @@ class PipeInterface(Interface):
|
||||
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
self.process_incoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -20,7 +20,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import threading
|
||||
@@ -43,6 +43,8 @@ class KISS():
|
||||
CMD_CR = 0x05
|
||||
CMD_RADIO_STATE = 0x06
|
||||
CMD_RADIO_LOCK = 0x07
|
||||
CMD_ST_ALOCK = 0x0B
|
||||
CMD_LT_ALOCK = 0x0C
|
||||
CMD_DETECT = 0x08
|
||||
CMD_LEAVE = 0x0A
|
||||
CMD_READY = 0x0F
|
||||
@@ -50,11 +52,17 @@ class KISS():
|
||||
CMD_STAT_TX = 0x22
|
||||
CMD_STAT_RSSI = 0x23
|
||||
CMD_STAT_SNR = 0x24
|
||||
CMD_STAT_CHTM = 0x25
|
||||
CMD_STAT_PHYPRM = 0x26
|
||||
CMD_STAT_BAT = 0x27
|
||||
CMD_STAT_CSMA = 0x28
|
||||
CMD_BLINK = 0x30
|
||||
CMD_RANDOM = 0x40
|
||||
CMD_FB_EXT = 0x41
|
||||
CMD_FB_READ = 0x42
|
||||
CMD_DISP_READ = 0x66
|
||||
CMD_FB_WRITE = 0x43
|
||||
CMD_BT_CTRL = 0x46
|
||||
CMD_PLATFORM = 0x48
|
||||
CMD_MCU = 0x49
|
||||
CMD_FW_VERSION = 0x50
|
||||
@@ -72,9 +80,13 @@ class KISS():
|
||||
ERROR_INITRADIO = 0x01
|
||||
ERROR_TXFAILED = 0x02
|
||||
ERROR_EEPROM_LOCKED = 0x03
|
||||
ERROR_QUEUE_FULL = 0x04
|
||||
ERROR_MEMORY_LOW = 0x05
|
||||
ERROR_MODEM_TIMEOUT = 0x06
|
||||
|
||||
PLATFORM_AVR = 0x90
|
||||
PLATFORM_ESP32 = 0x80
|
||||
PLATFORM_NRF52 = 0x70
|
||||
|
||||
@staticmethod
|
||||
def escape(data):
|
||||
@@ -85,9 +97,10 @@ class KISS():
|
||||
|
||||
class RNodeInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
DEFAULT_IFAC_SIZE = 8
|
||||
|
||||
FREQ_MIN = 137000000
|
||||
FREQ_MAX = 1020000000
|
||||
FREQ_MAX = 3000000000
|
||||
|
||||
RSSI_OFFSET = 157
|
||||
|
||||
@@ -98,9 +111,20 @@ class RNodeInterface(Interface):
|
||||
|
||||
RECONNECT_WAIT = 5
|
||||
|
||||
def __init__(self, owner, name, port, frequency = None, bandwidth = None, txpower = None, sf = None, cr = None, flow_control = False, id_interval = None, id_callsign = None):
|
||||
Q_SNR_MIN_BASE = -9
|
||||
Q_SNR_MAX = 6
|
||||
Q_SNR_STEP = 2
|
||||
|
||||
BATTERY_STATE_UNKNOWN = 0x00
|
||||
BATTERY_STATE_DISCHARGING = 0x01
|
||||
BATTERY_STATE_CHARGING = 0x02
|
||||
BATTERY_STATE_CHARGED = 0x03
|
||||
|
||||
DISPLAY_READ_INTERVAL = 1.0
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
raise SystemError("Invlaid interface type. The Android-specific RNode interface must be used on Android")
|
||||
raise SystemError("Invalid interface type. The Android-specific RNode interface must be used on Android")
|
||||
|
||||
import importlib
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
@@ -110,8 +134,42 @@ class RNodeInterface(Interface):
|
||||
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
frequency = int(c["frequency"]) if "frequency" in c else 0
|
||||
bandwidth = int(c["bandwidth"]) if "bandwidth" in c else 0
|
||||
txpower = int(c["txpower"]) if "txpower" in c else 0
|
||||
sf = int(c["spreadingfactor"]) if "spreadingfactor" in c else 0
|
||||
cr = int(c["codingrate"]) if "codingrate" in c else 0
|
||||
flow_control = c.as_bool("flow_control") if "flow_control" in c else False
|
||||
id_interval = int(c["id_interval"]) if "id_interval" in c else None
|
||||
id_callsign = c["id_callsign"] if "id_callsign" in c else None
|
||||
st_alock = float(c["airtime_limit_short"]) if "airtime_limit_short" in c else None
|
||||
lt_alock = float(c["airtime_limit_long"]) if "airtime_limit_long" in c else None
|
||||
|
||||
force_ble = False
|
||||
ble_name = None
|
||||
ble_addr = None
|
||||
|
||||
port = c["port"] if "port" in c else None
|
||||
|
||||
if port == None:
|
||||
raise ValueError("No port specified for RNode interface")
|
||||
|
||||
if port != None:
|
||||
ble_uri_scheme = "ble://"
|
||||
if port.lower().startswith(ble_uri_scheme):
|
||||
force_ble = True
|
||||
ble_string = port[len(ble_uri_scheme):]
|
||||
port = None
|
||||
if len(ble_string) == 0:
|
||||
pass
|
||||
elif len(ble_string.split(":")) == 6 and len(ble_string) == 17:
|
||||
ble_addr = ble_string
|
||||
else:
|
||||
ble_name = ble_string
|
||||
|
||||
self.HW_MTU = 508
|
||||
|
||||
@@ -127,6 +185,16 @@ class RNodeInterface(Interface):
|
||||
self.online = False
|
||||
self.detached = False
|
||||
self.reconnecting= False
|
||||
self.hw_errors = []
|
||||
|
||||
self.use_ble = False
|
||||
self.ble_name = ble_name
|
||||
self.ble_addr = ble_addr
|
||||
self.ble = None
|
||||
self.ble_rx_lock = threading.Lock()
|
||||
self.ble_tx_lock = threading.Lock()
|
||||
self.ble_rx_queue= b""
|
||||
self.ble_tx_queue= b""
|
||||
|
||||
self.frequency = frequency
|
||||
self.bandwidth = bandwidth
|
||||
@@ -135,6 +203,8 @@ class RNodeInterface(Interface):
|
||||
self.cr = cr
|
||||
self.state = KISS.RADIO_STATE_OFF
|
||||
self.bitrate = 0
|
||||
self.st_alock = st_alock
|
||||
self.lt_alock = lt_alock
|
||||
self.platform = None
|
||||
self.display = None
|
||||
self.mcu = None
|
||||
@@ -157,27 +227,60 @@ class RNodeInterface(Interface):
|
||||
self.r_stat_rx = None
|
||||
self.r_stat_tx = None
|
||||
self.r_stat_rssi = None
|
||||
self.r_stat_snr = None
|
||||
self.r_st_alock = None
|
||||
self.r_lt_alock = None
|
||||
self.r_random = None
|
||||
self.r_airtime_short = 0.0
|
||||
self.r_airtime_long = 0.0
|
||||
self.r_channel_load_short = 0.0
|
||||
self.r_channel_load_long = 0.0
|
||||
self.r_symbol_time_ms = None
|
||||
self.r_symbol_rate = None
|
||||
self.r_preamble_symbols = None
|
||||
self.r_premable_time_ms = None
|
||||
self.r_csma_slot_time_ms = None
|
||||
self.r_csma_difs_ms = None
|
||||
self.r_csma_cw_band = None
|
||||
self.r_csma_cw_min = None
|
||||
self.r_csma_cw_max = None
|
||||
self.r_current_rssi = None
|
||||
self.r_noise_floor = None
|
||||
|
||||
self.r_battery_state = RNodeInterface.BATTERY_STATE_UNKNOWN
|
||||
self.r_battery_percent = 0
|
||||
self.r_framebuffer = b""
|
||||
self.r_framebuffer_readtime = 0
|
||||
self.r_framebuffer_latency = 0
|
||||
self.r_disp = b""
|
||||
self.r_disp_readtime = 0
|
||||
self.r_disp_latency = 0
|
||||
|
||||
self.should_read_display = False
|
||||
self.read_display_interval = RNodeInterface.DISPLAY_READ_INTERVAL
|
||||
|
||||
self.packet_queue = []
|
||||
self.flow_control = flow_control
|
||||
self.interface_ready = False
|
||||
self.announce_rate_target = None
|
||||
|
||||
if force_ble or self.ble_addr != None or self.ble_name != None:
|
||||
self.use_ble = True
|
||||
|
||||
self.validcfg = True
|
||||
if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
|
||||
RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
if (self.txpower < 0 or self.txpower > 17):
|
||||
if (self.txpower < 0 or self.txpower > 22):
|
||||
RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
if (self.bandwidth < 7800 or self.bandwidth > 500000):
|
||||
if (self.bandwidth < 7800 or self.bandwidth > 1625000):
|
||||
RNS.log("Invalid bandwidth configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
if (self.sf < 7 or self.sf > 12):
|
||||
if (self.sf < 5 or self.sf > 12):
|
||||
RNS.log("Invalid spreading factor configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
@@ -185,6 +288,14 @@ class RNodeInterface(Interface):
|
||||
RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
if (self.st_alock and (self.st_alock < 0.0 or self.st_alock > 100.0)):
|
||||
RNS.log("Invalid short-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
if (self.lt_alock and (self.lt_alock < 0.0 or self.lt_alock > 100.0)):
|
||||
RNS.log("Invalid long-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
if id_interval != None and id_callsign != None:
|
||||
if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN):
|
||||
self.should_id = True
|
||||
@@ -219,23 +330,38 @@ class RNodeInterface(Interface):
|
||||
|
||||
|
||||
def open_port(self):
|
||||
RNS.log("Opening serial port "+self.port+"...")
|
||||
self.serial = self.pyserial.Serial(
|
||||
port = self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.pyserial.PARITY_NONE,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = 0,
|
||||
inter_byte_timeout = None,
|
||||
write_timeout = None,
|
||||
dsrdtr = False,
|
||||
)
|
||||
if not self.use_ble:
|
||||
RNS.log("Opening serial port "+self.port+"...")
|
||||
self.serial = self.pyserial.Serial(
|
||||
port = self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.pyserial.PARITY_NONE,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = 0,
|
||||
inter_byte_timeout = None,
|
||||
write_timeout = None,
|
||||
dsrdtr = False,
|
||||
)
|
||||
|
||||
else:
|
||||
RNS.log(f"Opening BLE connection for {self}...")
|
||||
if self.ble != None and self.ble.running == False:
|
||||
self.ble.close()
|
||||
self.ble.cleanup()
|
||||
self.ble = None
|
||||
|
||||
if self.ble == None:
|
||||
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
|
||||
self.serial = self.ble
|
||||
|
||||
def configure_device(self):
|
||||
open_time = time.time()
|
||||
while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
|
||||
time.sleep(1)
|
||||
|
||||
def reset_radio_state(self):
|
||||
self.r_frequency = None
|
||||
self.r_bandwidth = None
|
||||
self.r_txpower = None
|
||||
@@ -243,6 +369,10 @@ class RNodeInterface(Interface):
|
||||
self.r_cr = None
|
||||
self.r_state = None
|
||||
self.r_lock = None
|
||||
self.detected = False
|
||||
|
||||
def configure_device(self):
|
||||
self.reset_radio_state()
|
||||
sleep(2.0)
|
||||
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
@@ -250,13 +380,23 @@ class RNodeInterface(Interface):
|
||||
thread.start()
|
||||
|
||||
self.detect()
|
||||
sleep(0.2)
|
||||
if not self.use_ble:
|
||||
sleep(0.2)
|
||||
else:
|
||||
ble_detect_timeout = 5
|
||||
detect_time = time.time()
|
||||
while not self.detected and time.time() < detect_time + ble_detect_timeout:
|
||||
time.sleep(0.1)
|
||||
if self.detected:
|
||||
detect_time = RNS.prettytime(time.time()-detect_time)
|
||||
else:
|
||||
RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
|
||||
|
||||
if not self.detected:
|
||||
RNS.log("Could not detect device for "+str(self), RNS.LOG_ERROR)
|
||||
self.serial.close()
|
||||
else:
|
||||
if self.platform == KISS.PLATFORM_ESP32:
|
||||
if self.platform == KISS.PLATFORM_ESP32 or self.platform == KISS.PLATFORM_NRF52:
|
||||
self.display = True
|
||||
|
||||
RNS.log("Serial port "+self.port+" is now open")
|
||||
@@ -280,8 +420,13 @@ class RNodeInterface(Interface):
|
||||
self.setTXPower()
|
||||
self.setSpreadingFactor()
|
||||
self.setCodingRate()
|
||||
self.setSTALock()
|
||||
self.setLTALock()
|
||||
self.setRadioState(KISS.RADIO_STATE_ON)
|
||||
|
||||
if self.use_ble:
|
||||
time.sleep(2)
|
||||
|
||||
def detect(self):
|
||||
kiss_command = bytes([KISS.FEND, KISS.CMD_DETECT, KISS.DETECT_REQ, KISS.FEND, KISS.CMD_FW_VERSION, 0x00, KISS.FEND, KISS.CMD_PLATFORM, 0x00, KISS.FEND, KISS.CMD_MCU, 0x00, KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
@@ -330,7 +475,34 @@ class RNodeInterface(Interface):
|
||||
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while writing framebuffer data device")
|
||||
raise IOError("An IO error occurred while writing framebuffer data to device")
|
||||
|
||||
def read_framebuffer(self):
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FB_READ])+bytes([0x01])+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
self.r_framebuffer_readtime = time.time()
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while sending framebuffer read command")
|
||||
|
||||
def read_display(self):
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_DISP_READ])+bytes([0x01])+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
self.r_disp_readtime = time.time()
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while sending display read command")
|
||||
|
||||
def _read_display_job(self):
|
||||
while self.should_read_display:
|
||||
self.read_display()
|
||||
time.sleep(self.read_display_interval)
|
||||
|
||||
def start_display_updates(self):
|
||||
if not self.should_read_display:
|
||||
self.should_read_display = True
|
||||
threading.Thread(target=self._read_display_job, daemon=True).start()
|
||||
|
||||
def stop_display_updates(self):
|
||||
self.should_read_display = False
|
||||
|
||||
def hard_reset(self):
|
||||
kiss_command = bytes([KISS.FEND, KISS.CMD_RESET, 0xf8, KISS.FEND])
|
||||
@@ -384,6 +556,30 @@ class RNodeInterface(Interface):
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring coding rate for "+str(self))
|
||||
|
||||
def setSTALock(self):
|
||||
if self.st_alock != None:
|
||||
at = int(self.st_alock*100)
|
||||
c1 = at >> 8 & 0xFF
|
||||
c2 = at & 0xFF
|
||||
data = KISS.escape(bytes([c1])+bytes([c2]))
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_ST_ALOCK])+data+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring short-term airtime limit for "+str(self))
|
||||
|
||||
def setLTALock(self):
|
||||
if self.lt_alock != None:
|
||||
at = int(self.lt_alock*100)
|
||||
c1 = at >> 8 & 0xFF
|
||||
c2 = at & 0xFF
|
||||
data = KISS.escape(bytes([c1])+bytes([c2]))
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_LT_ALOCK])+data+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring long-term airtime limit for "+str(self))
|
||||
|
||||
def setRadioState(self, state):
|
||||
self.state = state
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
|
||||
@@ -392,9 +588,12 @@ class RNodeInterface(Interface):
|
||||
raise IOError("An IO error occurred while configuring radio state for "+str(self))
|
||||
|
||||
def validate_firmware(self):
|
||||
if (self.maj_version >= RNodeInterface.REQUIRED_FW_VER_MAJ):
|
||||
if (self.min_version >= RNodeInterface.REQUIRED_FW_VER_MIN):
|
||||
self.firmware_ok = True
|
||||
if (self.maj_version > RNodeInterface.REQUIRED_FW_VER_MAJ):
|
||||
self.firmware_ok = True
|
||||
else:
|
||||
if (self.maj_version >= RNodeInterface.REQUIRED_FW_VER_MAJ):
|
||||
if (self.min_version >= RNodeInterface.REQUIRED_FW_VER_MIN):
|
||||
self.firmware_ok = True
|
||||
|
||||
if self.firmware_ok:
|
||||
return
|
||||
@@ -407,7 +606,14 @@ class RNodeInterface(Interface):
|
||||
|
||||
def validateRadioState(self):
|
||||
RNS.log("Waiting for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
sleep(0.25);
|
||||
if self.use_ble:
|
||||
sleep(1.00)
|
||||
else:
|
||||
sleep(0.25)
|
||||
|
||||
if self.use_ble and self.ble != None and self.ble.device_disappeared:
|
||||
RNS.log(f"Device disappeared during radio state validation for {self}", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
self.validcfg = True
|
||||
if (self.r_frequency != None and abs(self.frequency - int(self.r_frequency)) > 100):
|
||||
@@ -440,14 +646,14 @@ class RNodeInterface(Interface):
|
||||
except:
|
||||
self.bitrate = 0
|
||||
|
||||
def processIncoming(self, data):
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
self.r_stat_rssi = None
|
||||
self.r_stat_snr = None
|
||||
|
||||
|
||||
def processOutgoing(self,data):
|
||||
def process_outgoing(self,data):
|
||||
datalen = len(data)
|
||||
if self.online:
|
||||
if self.interface_ready:
|
||||
@@ -478,7 +684,7 @@ class RNodeInterface(Interface):
|
||||
if len(self.packet_queue) > 0:
|
||||
data = self.packet_queue.pop(0)
|
||||
self.interface_ready = True
|
||||
self.processOutgoing(data)
|
||||
self.process_outgoing(data)
|
||||
elif len(self.packet_queue) == 0:
|
||||
self.interface_ready = True
|
||||
|
||||
@@ -498,7 +704,7 @@ class RNodeInterface(Interface):
|
||||
|
||||
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
self.process_incoming(data_buffer)
|
||||
data_buffer = b""
|
||||
command_buffer = b""
|
||||
elif (byte == KISS.FEND):
|
||||
@@ -567,7 +773,6 @@ class RNodeInterface(Interface):
|
||||
self.r_state = byte
|
||||
if self.r_state:
|
||||
pass
|
||||
#RNS.log(str(self)+" Radio reporting state is online", RNS.LOG_DEBUG)
|
||||
else:
|
||||
RNS.log(str(self)+" Radio reporting state is offline", RNS.LOG_DEBUG)
|
||||
|
||||
@@ -621,6 +826,157 @@ class RNodeInterface(Interface):
|
||||
self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET
|
||||
elif (command == KISS.CMD_STAT_SNR):
|
||||
self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25
|
||||
try:
|
||||
sfs = self.r_sf-7
|
||||
snr = self.r_stat_snr
|
||||
q_snr_min = RNodeInterface.Q_SNR_MIN_BASE-sfs*RNodeInterface.Q_SNR_STEP
|
||||
q_snr_max = RNodeInterface.Q_SNR_MAX
|
||||
q_snr_span = q_snr_max-q_snr_min
|
||||
quality = round(((snr-q_snr_min)/(q_snr_span))*100,1)
|
||||
if quality > 100.0: quality = 100.0
|
||||
if quality < 0.0: quality = 0.0
|
||||
self.r_stat_q = quality
|
||||
except:
|
||||
pass
|
||||
elif (command == KISS.CMD_ST_ALOCK):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
command_buffer = command_buffer+bytes([byte])
|
||||
if (len(command_buffer) == 2):
|
||||
at = command_buffer[0] << 8 | command_buffer[1]
|
||||
self.r_st_alock = at/100.0
|
||||
RNS.log(str(self)+" Radio reporting short-term airtime limit is "+str(self.r_st_alock)+"%", RNS.LOG_DEBUG)
|
||||
elif (command == KISS.CMD_LT_ALOCK):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
command_buffer = command_buffer+bytes([byte])
|
||||
if (len(command_buffer) == 2):
|
||||
at = command_buffer[0] << 8 | command_buffer[1]
|
||||
self.r_lt_alock = at/100.0
|
||||
RNS.log(str(self)+" Radio reporting long-term airtime limit is "+str(self.r_lt_alock)+"%", RNS.LOG_DEBUG)
|
||||
elif (command == KISS.CMD_STAT_CHTM):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
command_buffer = command_buffer+bytes([byte])
|
||||
if (len(command_buffer) == 11):
|
||||
ats = command_buffer[0] << 8 | command_buffer[1]
|
||||
atl = command_buffer[2] << 8 | command_buffer[3]
|
||||
cus = command_buffer[4] << 8 | command_buffer[5]
|
||||
cul = command_buffer[6] << 8 | command_buffer[7]
|
||||
crs = command_buffer[8]
|
||||
nfl = command_buffer[9]
|
||||
ntf = command_buffer[10]
|
||||
|
||||
self.r_airtime_short = ats/100.0
|
||||
self.r_airtime_long = atl/100.0
|
||||
self.r_channel_load_short = cus/100.0
|
||||
self.r_channel_load_long = cul/100.0
|
||||
self.r_current_rssi = crs-RNodeInterface.RSSI_OFFSET
|
||||
self.r_noise_floor = nfl-RNodeInterface.RSSI_OFFSET
|
||||
if ntf == 0xFF:
|
||||
self.r_interference = None
|
||||
else:
|
||||
self.r_interference = ntf-RNodeInterface.RSSI_OFFSET
|
||||
|
||||
if self.r_interference != None:
|
||||
RNS.log(f"{self} Radio detected interference at {self.r_interference} dBm", RNS.LOG_DEBUG)
|
||||
|
||||
# TODO: Remove debug
|
||||
# RNS.log(f"RSSI: {self.r_current_rssi}, Noise floor: {self.r_noise_floor}, Interference: {self.r_interference}", RNS.LOG_EXTREME)
|
||||
elif (command == KISS.CMD_STAT_PHYPRM):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
command_buffer = command_buffer+bytes([byte])
|
||||
if (len(command_buffer) == 12):
|
||||
lst = (command_buffer[0] << 8 | command_buffer[1])/1000.0
|
||||
lsr = command_buffer[2] << 8 | command_buffer[3]
|
||||
prs = command_buffer[4] << 8 | command_buffer[5]
|
||||
prt = command_buffer[6] << 8 | command_buffer[7]
|
||||
cst = command_buffer[8] << 8 | command_buffer[9]
|
||||
dft = command_buffer[10] << 8 | command_buffer[11]
|
||||
|
||||
if lst != self.r_symbol_time_ms or lsr != self.r_symbol_rate or prs != self.r_preamble_symbols or prt != self.r_premable_time_ms or cst != self.r_csma_slot_time_ms or dft != self.r_csma_difs_ms:
|
||||
self.r_symbol_time_ms = lst
|
||||
self.r_symbol_rate = lsr
|
||||
self.r_preamble_symbols = prs
|
||||
self.r_premable_time_ms = prt
|
||||
self.r_csma_slot_time_ms = cst
|
||||
self.r_csma_difs_ms = dft
|
||||
RNS.log(f"{self} Radio reporting symbol time is "+str(round(self.r_symbol_time_ms,2))+"ms ("+str(self.r_symbol_rate)+" baud)", RNS.LOG_DEBUG)
|
||||
RNS.log(f"{self} Radio reporting preamble is "+str(self.r_preamble_symbols)+" symbols ("+str(self.r_premable_time_ms)+"ms)", RNS.LOG_DEBUG)
|
||||
RNS.log(f"{self} Radio reporting CSMA slot time is "+str(self.r_csma_slot_time_ms)+"ms", RNS.LOG_DEBUG)
|
||||
RNS.log(f"{self} Radio reporting DIFS time is "+str(self.r_csma_difs_ms)+"ms", RNS.LOG_DEBUG)
|
||||
elif (command == KISS.CMD_STAT_CSMA):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
command_buffer = command_buffer+bytes([byte])
|
||||
if (len(command_buffer) == 3):
|
||||
cbw = command_buffer[0]
|
||||
cbl = command_buffer[1]
|
||||
cbh = command_buffer[2]
|
||||
|
||||
if cbw != self.r_csma_cw_band or cbl != self.r_csma_cw_min or cbh != self.r_csma_cw_max:
|
||||
self.r_csma_cw_band = cbw
|
||||
self.r_csma_cw_min = cbl
|
||||
self.r_csma_cw_max = cbh
|
||||
# TODO: Remove debug
|
||||
# RNS.log(f"{self} Radio reporting contention window band is {self.r_csma_cw_band}", RNS.LOG_EXTREME)
|
||||
# RNS.log(f"{self} Radio reporting minimum contention window is {self.r_csma_cw_min}", RNS.LOG_EXTREME)
|
||||
# RNS.log(f"{self} Radio reporting maximum contention window is {self.r_csma_cw_max}", RNS.LOG_EXTREME)
|
||||
elif (command == KISS.CMD_STAT_BAT):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
command_buffer = command_buffer+bytes([byte])
|
||||
if (len(command_buffer) == 2):
|
||||
bat_percent = command_buffer[1]
|
||||
if bat_percent > 100:
|
||||
bat_percent = 100
|
||||
if bat_percent < 0:
|
||||
bat_percent = 0
|
||||
self.r_battery_state = command_buffer[0]
|
||||
self.r_battery_percent = bat_percent
|
||||
elif (command == KISS.CMD_RANDOM):
|
||||
self.r_random = byte
|
||||
elif (command == KISS.CMD_PLATFORM):
|
||||
@@ -634,6 +990,12 @@ class RNodeInterface(Interface):
|
||||
elif (byte == KISS.ERROR_TXFAILED):
|
||||
RNS.log(str(self)+" hardware TX error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
|
||||
raise IOError("Hardware transmit failure")
|
||||
elif (byte == KISS.ERROR_MEMORY_LOW):
|
||||
RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+"): Memory exhausted", RNS.LOG_ERROR)
|
||||
self.hw_errors.append({"error": KISS.ERROR_MEMORY_LOW, "description": "Memory exhausted on connected device"})
|
||||
elif (byte == KISS.ERROR_MODEM_TIMEOUT):
|
||||
RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+"): Modem communication timed out", RNS.LOG_ERROR)
|
||||
self.hw_errors.append({"error": KISS.ERROR_MODEM_TIMEOUT, "description": "Modem communication timed out on connected device"})
|
||||
else:
|
||||
RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
|
||||
raise IOError("Unknown hardware failure")
|
||||
@@ -645,6 +1007,36 @@ class RNodeInterface(Interface):
|
||||
raise IOError("ESP32 reset")
|
||||
elif (command == KISS.CMD_READY):
|
||||
self.process_queue()
|
||||
elif (command == KISS.CMD_FB_READ):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
command_buffer = command_buffer+bytes([byte])
|
||||
if (len(command_buffer) == 512):
|
||||
self.r_framebuffer_latency = time.time() - self.r_framebuffer_readtime
|
||||
self.r_framebuffer = command_buffer
|
||||
|
||||
elif (command == KISS.CMD_DISP_READ):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
command_buffer = command_buffer+bytes([byte])
|
||||
if (len(command_buffer) == 1024):
|
||||
self.r_disp_latency = time.time() - self.r_disp_readtime
|
||||
self.r_disp = command_buffer
|
||||
|
||||
elif (command == KISS.CMD_DETECT):
|
||||
if byte == KISS.DETECT_RESP:
|
||||
self.detected = True
|
||||
@@ -654,7 +1046,7 @@ class RNodeInterface(Interface):
|
||||
else:
|
||||
time_since_last = int(time.time()*1000) - last_read_ms
|
||||
if len(data_buffer) > 0 and time_since_last > self.timeout:
|
||||
RNS.log(str(self)+" serial read timeout", RNS.LOG_DEBUG)
|
||||
RNS.log(str(self)+" serial read timeout in command "+str(command), RNS.LOG_WARNING)
|
||||
data_buffer = b""
|
||||
in_frame = False
|
||||
command = KISS.CMD_UNKNOWN
|
||||
@@ -664,7 +1056,7 @@ class RNodeInterface(Interface):
|
||||
if self.first_tx != None:
|
||||
if time.time() > self.first_tx + self.id_interval:
|
||||
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.id_callsign.decode("utf-8")), RNS.LOG_DEBUG)
|
||||
self.processOutgoing(self.id_callsign)
|
||||
self.process_outgoing(self.id_callsign)
|
||||
|
||||
sleep(0.08)
|
||||
|
||||
@@ -708,6 +1100,219 @@ class RNodeInterface(Interface):
|
||||
self.disable_external_framebuffer()
|
||||
self.setRadioState(KISS.RADIO_STATE_OFF)
|
||||
self.leave()
|
||||
|
||||
if self.use_ble:
|
||||
self.ble.close()
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
def get_battery_state(self):
|
||||
return self.r_battery_state
|
||||
|
||||
def get_battery_state_string(self):
|
||||
if self.r_battery_state == RNodeInterface.BATTERY_STATE_CHARGED:
|
||||
return "charged"
|
||||
elif self.r_battery_state == RNodeInterface.BATTERY_STATE_CHARGING:
|
||||
return "charging"
|
||||
elif self.r_battery_state == RNodeInterface.BATTERY_STATE_DISCHARGING:
|
||||
return "discharging"
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
def get_battery_percent(self):
|
||||
return self.r_battery_percent
|
||||
|
||||
def ble_receive(self, data):
|
||||
with self.ble_rx_lock:
|
||||
self.ble_rx_queue += data
|
||||
|
||||
def ble_waiting(self):
|
||||
return len(self.ble_tx_queue) > 0
|
||||
|
||||
def get_ble_waiting(self, n):
|
||||
with self.ble_tx_lock:
|
||||
data = self.ble_tx_queue[:n]
|
||||
self.ble_tx_queue = self.ble_tx_queue[n:]
|
||||
return data
|
||||
|
||||
def __str__(self):
|
||||
return "RNodeInterface["+str(self.name)+"]"
|
||||
|
||||
class BLEConnection():
|
||||
UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
bleak = None
|
||||
|
||||
SCAN_TIMEOUT = 2.0
|
||||
CONNECT_TIMEOUT = 5.0
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
return self.connected
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
buflen = len(self.owner.ble_rx_queue)
|
||||
return buflen > 0
|
||||
|
||||
def write(self, data_bytes):
|
||||
with self.owner.ble_tx_lock:
|
||||
self.owner.ble_tx_queue += data_bytes
|
||||
return len(data_bytes)
|
||||
|
||||
def read(self, n):
|
||||
with self.owner.ble_rx_lock:
|
||||
data = self.owner.ble_rx_queue[:n]
|
||||
self.owner.ble_rx_queue = self.owner.ble_rx_queue[n:]
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
if self.connected and self.ble_device:
|
||||
RNS.log(f"Disconnecting BLE device from {self.owner}", RNS.LOG_DEBUG)
|
||||
self.must_disconnect = True
|
||||
|
||||
while self.connect_job_running:
|
||||
time.sleep(0.1)
|
||||
|
||||
def __init__(self, owner=None, target_name=None, target_bt_addr=None):
|
||||
self.owner = owner
|
||||
self.target_name = target_name
|
||||
self.target_bt_addr = target_bt_addr
|
||||
self.scan_timeout = BLEConnection.SCAN_TIMEOUT
|
||||
self.ble_device = None
|
||||
self.last_client = None
|
||||
self.connected = False
|
||||
self.running = False
|
||||
self.should_run = False
|
||||
self.must_disconnect = False
|
||||
self.connect_job_running = False
|
||||
self.device_disappeared = False
|
||||
|
||||
import importlib
|
||||
if BLEConnection.bleak == None:
|
||||
if importlib.util.find_spec("bleak") != None:
|
||||
import bleak
|
||||
BLEConnection.bleak = bleak
|
||||
|
||||
import asyncio
|
||||
BLEConnection.asyncio = asyncio
|
||||
else:
|
||||
RNS.log("Using the RNode interface over BLE requires a the \"bleak\" module to be installed.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install one with the command: python3 -m pip install bleak", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.should_run = True
|
||||
self.connection_thread = threading.Thread(target=self.connection_job, daemon=True).start()
|
||||
|
||||
def cleanup(self):
|
||||
try:
|
||||
if self.last_client != None:
|
||||
self.asyncio.run(self.last_client.disconnect())
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while disconnecting BLE device on cleanup for {self.owner}", RNS.LOG_ERROR)
|
||||
|
||||
self.should_run = False
|
||||
|
||||
def connection_job(self):
|
||||
while self.should_run:
|
||||
if self.ble_device == None:
|
||||
self.ble_device = self.find_target_device()
|
||||
|
||||
if type(self.ble_device) == self.bleak.backends.device.BLEDevice:
|
||||
if not self.connected:
|
||||
self.connect_device()
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
self.cleanup()
|
||||
self.running = False
|
||||
RNS.log(f"BLE connection job for {self.owner} ended", RNS.LOG_DEBUG)
|
||||
|
||||
def connect_device(self):
|
||||
if self.ble_device != None and type(self.ble_device) == self.bleak.backends.device.BLEDevice:
|
||||
RNS.log(f"Connecting BLE device {self.ble_device} for {self.owner}...", RNS.LOG_DEBUG)
|
||||
|
||||
async def connect_job():
|
||||
self.connect_job_running = True
|
||||
async with self.bleak.BleakClient(self.ble_device, disconnected_callback=self.device_disconnected) as ble_client:
|
||||
def handle_rx(device, data):
|
||||
if self.owner != None:
|
||||
self.owner.ble_receive(data)
|
||||
|
||||
self.connected = True
|
||||
self.ble_device = ble_client
|
||||
self.last_client = ble_client
|
||||
self.owner.port = str(f"ble://{ble_client.address}")
|
||||
|
||||
loop = self.asyncio.get_running_loop()
|
||||
uart_service = ble_client.services.get_service(BLEConnection.UART_SERVICE_UUID)
|
||||
rx_characteristic = uart_service.get_characteristic(BLEConnection.UART_RX_CHAR_UUID)
|
||||
await ble_client.start_notify(BLEConnection.UART_TX_CHAR_UUID, handle_rx)
|
||||
|
||||
while self.connected:
|
||||
if self.owner != None and self.owner.ble_waiting():
|
||||
outbound_data = self.owner.get_ble_waiting(rx_characteristic.max_write_without_response_size)
|
||||
await ble_client.write_gatt_char(rx_characteristic, outbound_data, response=False)
|
||||
elif self.must_disconnect:
|
||||
await ble_client.disconnect()
|
||||
else:
|
||||
await self.asyncio.sleep(0.1)
|
||||
|
||||
|
||||
try:
|
||||
self.asyncio.run(connect_job())
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not connect BLE device {self.ble_device} for {self.owner}. Possibly missing authentication.", RNS.LOG_ERROR)
|
||||
|
||||
self.connect_job_running = False
|
||||
|
||||
def device_disconnected(self, device):
|
||||
RNS.log(f"BLE device for {self.owner} disconnected", RNS.LOG_NOTICE)
|
||||
self.connected = False
|
||||
self.ble_device = None
|
||||
self.device_disappeared = True
|
||||
|
||||
def find_target_device(self):
|
||||
RNS.log(f"Searching for attachable BLE device for {self.owner}...", RNS.LOG_EXTREME)
|
||||
def device_filter(device: self.bleak.backends.device.BLEDevice, adv: self.bleak.backends.scanner.AdvertisementData):
|
||||
if BLEConnection.UART_SERVICE_UUID.lower() in adv.service_uuids:
|
||||
if self.device_bonded(device):
|
||||
if self.target_bt_addr == None and self.target_name == None:
|
||||
if device.name.startswith("RNode "):
|
||||
return True
|
||||
|
||||
if self.target_bt_addr == None or (device.address != None and device.address == self.target_bt_addr):
|
||||
if self.target_name == None or (device.name != None and device.name == self.target_name):
|
||||
return True
|
||||
|
||||
else:
|
||||
if self.target_bt_addr != None and device.address == self.target_bt_addr:
|
||||
RNS.log(f"Can't connect to target device {self.target_bt_addr} over BLE, device is not bonded", RNS.LOG_ERROR)
|
||||
|
||||
elif self.target_name != None and device.name == self.target_name:
|
||||
RNS.log(f"Can't connect to target device {self.target_name} over BLE, device is not bonded", RNS.LOG_ERROR)
|
||||
|
||||
return False
|
||||
|
||||
device = None
|
||||
try:
|
||||
device = self.asyncio.run(self.bleak.BleakScanner.find_device_by_filter(device_filter, timeout=self.scan_timeout))
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while finding BLE device for {self.owner}: {e}", RNS.LOG_ERROR)
|
||||
self.should_run = False
|
||||
|
||||
return device
|
||||
|
||||
def device_bonded(self, device):
|
||||
try:
|
||||
if hasattr(device, "details"):
|
||||
if "props" in device.details and "Bonded" in device.details["props"]:
|
||||
if device.details["props"]["Bonded"] == True:
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while determining device bond status for {device}, the contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
return False
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import threading
|
||||
@@ -42,6 +42,7 @@ class HDLC():
|
||||
|
||||
class SerialInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
DEFAULT_IFAC_SIZE = 8
|
||||
|
||||
owner = None
|
||||
port = None
|
||||
@@ -51,7 +52,7 @@ class SerialInterface(Interface):
|
||||
stopbits = None
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, name, port, speed, databits, parity, stopbits):
|
||||
def __init__(self, owner, configuration):
|
||||
import importlib
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
@@ -60,8 +61,18 @@ class SerialInterface(Interface):
|
||||
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
port = c["port"] if "port" in c else None
|
||||
speed = int(c["speed"]) if "speed" in c else 9600
|
||||
databits = int(c["databits"]) if "databits" in c else 8
|
||||
parity = c["parity"] if "parity" in c else "N"
|
||||
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
|
||||
|
||||
if port == None:
|
||||
raise ValueError("No port specified for serial interface")
|
||||
|
||||
self.HW_MTU = 564
|
||||
|
||||
@@ -122,12 +133,12 @@ class SerialInterface(Interface):
|
||||
RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
|
||||
def processOutgoing(self,data):
|
||||
def process_outgoing(self,data):
|
||||
if self.online:
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
written = self.serial.write(data)
|
||||
@@ -150,7 +161,7 @@ class SerialInterface(Interface):
|
||||
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
self.process_incoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
@@ -201,5 +212,8 @@ class SerialInterface(Interface):
|
||||
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return "SerialInterface["+self.name+"]"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -20,7 +20,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
import socketserver
|
||||
import threading
|
||||
import platform
|
||||
@@ -30,6 +30,9 @@ import sys
|
||||
import os
|
||||
import RNS
|
||||
|
||||
class TCPInterface():
|
||||
HW_MTU = 262144
|
||||
|
||||
class HDLC():
|
||||
FLAG = 0x7E
|
||||
ESC = 0x7D
|
||||
@@ -58,8 +61,13 @@ class KISS():
|
||||
class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
pass
|
||||
|
||||
class ThreadingTCP6Server(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
address_family = socket.AF_INET6
|
||||
|
||||
class TCPClientInterface(Interface):
|
||||
BITRATE_GUESS = 10*1000*1000
|
||||
DEFAULT_IFAC_SIZE = 16
|
||||
AUTOCONFIGURE_MTU = True
|
||||
|
||||
RECONNECT_WAIT = 5
|
||||
RECONNECT_MAX_TRIES = None
|
||||
@@ -78,12 +86,21 @@ class TCPClientInterface(Interface):
|
||||
I2P_PROBE_INTERVAL = 9
|
||||
I2P_PROBES = 5
|
||||
|
||||
def __init__(self, owner, name, target_ip=None, target_port=None, connected_socket=None, max_reconnect_tries=None, kiss_framing=False, i2p_tunneled = False, connect_timeout = None):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
def __init__(self, owner, configuration, connected_socket=None):
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
target_ip = c["target_host"] if "target_host" in c and c["target_host"] != None else None
|
||||
target_port = int(c["target_port"]) if "target_port" in c and c["target_host"] != None else None
|
||||
kiss_framing = False
|
||||
if "kiss_framing" in c and c.as_bool("kiss_framing") == True:
|
||||
kiss_framing = True
|
||||
i2p_tunneled = c.as_bool("i2p_tunneled") if "i2p_tunneled" in c else False
|
||||
connect_timeout = c.as_int("connect_timeout") if "connect_timeout" in c else None
|
||||
max_reconnect_tries = c.as_int("max_reconnect_tries") if "max_reconnect_tries" in c else None
|
||||
|
||||
self.HW_MTU = TCPInterface.HW_MTU
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.socket = None
|
||||
@@ -117,6 +134,8 @@ class TCPClientInterface(Interface):
|
||||
elif platform.system() == "Darwin":
|
||||
self.set_timeouts_osx()
|
||||
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
elif target_ip != None and target_port != None:
|
||||
self.receives = True
|
||||
self.target_ip = target_ip
|
||||
@@ -176,19 +195,21 @@ class TCPClientInterface(Interface):
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(TCPClientInterface.I2P_PROBE_AFTER))
|
||||
|
||||
def detach(self):
|
||||
self.online = False
|
||||
if self.socket != None:
|
||||
if hasattr(self.socket, "close"):
|
||||
if callable(self.socket.close):
|
||||
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
|
||||
self.detached = True
|
||||
|
||||
try:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
if self.socket != None:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
|
||||
|
||||
try:
|
||||
self.socket.close()
|
||||
if self.socket != None:
|
||||
self.socket.close()
|
||||
except Exception as e:
|
||||
RNS.log("Error while closing socket for "+str(self)+": "+str(e))
|
||||
|
||||
@@ -199,9 +220,14 @@ class TCPClientInterface(Interface):
|
||||
if initial:
|
||||
RNS.log("Establishing TCP connection for "+str(self)+"...", RNS.LOG_DEBUG)
|
||||
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
address_info = socket.getaddrinfo(self.target_ip, self.target_port, proto=socket.IPPROTO_TCP)[0]
|
||||
address_family = address_info[0]
|
||||
target_address = address_info[4]
|
||||
|
||||
self.socket = socket.socket(address_family, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(TCPClientInterface.INITIAL_CONNECT_TIMEOUT)
|
||||
self.socket.connect((self.target_ip, self.target_port))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.socket.connect(target_address)
|
||||
self.socket.settimeout(None)
|
||||
self.online = True
|
||||
|
||||
@@ -263,15 +289,16 @@ class TCPClientInterface(Interface):
|
||||
RNS.log("Attempt to reconnect on a non-initiator TCP interface. This should not happen.", RNS.LOG_ERROR)
|
||||
raise IOError("Attempt to reconnect on a non-initiator TCP interface")
|
||||
|
||||
def processIncoming(self, data):
|
||||
self.rxb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.rxb += len(data)
|
||||
|
||||
self.owner.inbound(data, self)
|
||||
def process_incoming(self, data):
|
||||
if self.online and not self.detached:
|
||||
self.rxb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.rxb += len(data)
|
||||
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
def processOutgoing(self, data):
|
||||
if self.online:
|
||||
def process_outgoing(self, data):
|
||||
if self.online and not self.detached:
|
||||
# while self.writing:
|
||||
# time.sleep(0.01)
|
||||
|
||||
@@ -299,22 +326,22 @@ class TCPClientInterface(Interface):
|
||||
try:
|
||||
in_frame = False
|
||||
escape = False
|
||||
frame_buffer = b""
|
||||
data_in = b""
|
||||
data_buffer = b""
|
||||
command = KISS.CMD_UNKNOWN
|
||||
|
||||
while True:
|
||||
data_in = self.socket.recv(4096)
|
||||
if len(data_in) > 0:
|
||||
pointer = 0
|
||||
while pointer < len(data_in):
|
||||
byte = data_in[pointer]
|
||||
pointer += 1
|
||||
|
||||
if self.kiss_framing:
|
||||
# Read loop for KISS framing
|
||||
if self.kiss_framing:
|
||||
# Read loop for KISS framing
|
||||
pointer = 0
|
||||
while pointer < len(data_in):
|
||||
byte = data_in[pointer]
|
||||
pointer += 1
|
||||
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
self.process_incoming(data_buffer)
|
||||
elif (byte == KISS.FEND):
|
||||
in_frame = True
|
||||
command = KISS.CMD_UNKNOWN
|
||||
@@ -337,25 +364,26 @@ class TCPClientInterface(Interface):
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
|
||||
else:
|
||||
# Read loop for HDLC framing
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (byte == HDLC.ESC):
|
||||
escape = True
|
||||
else:
|
||||
# Read loop for standard HDLC framing
|
||||
frame_buffer += data_in
|
||||
flags_remaining = True
|
||||
while flags_remaining:
|
||||
frame_start = frame_buffer.find(HDLC.FLAG)
|
||||
if frame_start != -1:
|
||||
frame_end = frame_buffer.find(HDLC.FLAG, frame_start+1)
|
||||
if frame_end != -1:
|
||||
frame = frame_buffer[frame_start+1:frame_end]
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG]))
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
|
||||
if len(frame) > RNS.Reticulum.HEADER_MINSIZE:
|
||||
self.process_incoming(frame)
|
||||
frame_buffer = frame_buffer[frame_end:]
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.FLAG
|
||||
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.ESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
flags_remaining = False
|
||||
else:
|
||||
flags_remaining = False
|
||||
|
||||
else:
|
||||
self.online = False
|
||||
if self.initiator and not self.detached:
|
||||
@@ -392,7 +420,8 @@ class TCPClientInterface(Interface):
|
||||
self.IN = False
|
||||
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.clients -= 1
|
||||
while self in self.parent_interface.spawned_interfaces:
|
||||
self.parent_interface.spawned_interfaces.remove(self)
|
||||
|
||||
if self in RNS.Transport.interfaces:
|
||||
if not self.initiator:
|
||||
@@ -400,32 +429,73 @@ class TCPClientInterface(Interface):
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "TCPInterface["+str(self.name)+"/"+str(self.target_ip)+":"+str(self.target_port)+"]"
|
||||
if ":" in self.target_ip:
|
||||
ip_str = f"[{self.target_ip}]"
|
||||
else:
|
||||
ip_str = f"{self.target_ip}"
|
||||
|
||||
return "TCPInterface["+str(self.name)+"/"+ip_str+":"+str(self.target_port)+"]"
|
||||
|
||||
|
||||
class TCPServerInterface(Interface):
|
||||
BITRATE_GUESS = 10*1000*1000
|
||||
BITRATE_GUESS = 10_000_000
|
||||
DEFAULT_IFAC_SIZE = 16
|
||||
AUTOCONFIGURE_MTU = True
|
||||
|
||||
@staticmethod
|
||||
def get_address_for_if(name):
|
||||
def get_address_for_if(name, bind_port, prefer_ipv6=False):
|
||||
import RNS.vendor.ifaddr.niwrapper as netinfo
|
||||
ifaddr = netinfo.ifaddresses(name)
|
||||
return ifaddr[netinfo.AF_INET][0]["addr"]
|
||||
if len(ifaddr) < 1:
|
||||
raise SystemError(f"No addresses available on specified kernel interface \"{name}\" for TCPServerInterface to bind to")
|
||||
|
||||
if (prefer_ipv6 or not netinfo.AF_INET in ifaddr) and netinfo.AF_INET6 in ifaddr:
|
||||
bind_ip = ifaddr[netinfo.AF_INET6][0]["addr"]
|
||||
if bind_ip.lower().startswith("fe80::"):
|
||||
# We'll need to add the interface as scope for link-local addresses
|
||||
return TCPServerInterface.get_address_for_host(f"{bind_ip}%{name}", bind_port)
|
||||
else:
|
||||
return TCPServerInterface.get_address_for_host(bind_ip, bind_port)
|
||||
elif netinfo.AF_INET in ifaddr:
|
||||
bind_ip = ifaddr[netinfo.AF_INET][0]["addr"]
|
||||
return (bind_ip, bind_port)
|
||||
else:
|
||||
raise SystemError(f"No addresses available on specified kernel interface \"{name}\" for TCPServerInterface to bind to")
|
||||
|
||||
@staticmethod
|
||||
def get_broadcast_for_if(name):
|
||||
import RNS.vendor.ifaddr.niwrapper as netinfo
|
||||
ifaddr = netinfo.ifaddresses(name)
|
||||
return ifaddr[netinfo.AF_INET][0]["broadcast"]
|
||||
def get_address_for_host(name, bind_port):
|
||||
address_info = socket.getaddrinfo(name, bind_port, proto=socket.IPPROTO_TCP)[0]
|
||||
if address_info[0] == socket.AF_INET6:
|
||||
return (name, bind_port, address_info[4][2], address_info[4][3])
|
||||
elif address_info[0] == socket.AF_INET:
|
||||
return (name, bind_port)
|
||||
else:
|
||||
raise SystemError(f"No suitable kernel interface available for address \"{name}\" for TCPServerInterface to bind to")
|
||||
|
||||
def __init__(self, owner, name, device=None, bindip=None, bindport=None, i2p_tunneled=False):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
@property
|
||||
def clients(self):
|
||||
return len(self.spawned_interfaces)
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
device = c["device"] if "device" in c else None
|
||||
port = int(c["port"]) if "port" in c else None
|
||||
bindip = c["listen_ip"] if "listen_ip" in c else None
|
||||
bindport = int(c["listen_port"]) if "listen_port" in c else None
|
||||
i2p_tunneled = c.as_bool("i2p_tunneled") if "i2p_tunneled" in c else False
|
||||
prefer_ipv6 = c.as_bool("prefer_ipv6") if "prefer_ipv6" in c else False
|
||||
|
||||
if port != None:
|
||||
bindport = port
|
||||
|
||||
self.HW_MTU = TCPInterface.HW_MTU
|
||||
|
||||
self.online = False
|
||||
self.clients = 0
|
||||
self.spawned_interfaces = []
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
@@ -435,24 +505,41 @@ class TCPServerInterface(Interface):
|
||||
self.i2p_tunneled = i2p_tunneled
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
|
||||
if device != None:
|
||||
bindip = TCPServerInterface.get_address_for_if(device)
|
||||
|
||||
if (bindip != None and bindport != None):
|
||||
self.receives = True
|
||||
self.bind_ip = bindip
|
||||
if bindport == None:
|
||||
raise SystemError(f"No TCP port configured for interface \"{name}\"")
|
||||
else:
|
||||
self.bind_port = bindport
|
||||
|
||||
bind_address = None
|
||||
if device != None:
|
||||
bind_address = TCPServerInterface.get_address_for_if(device, self.bind_port, prefer_ipv6)
|
||||
else:
|
||||
if bindip == None:
|
||||
raise SystemError(f"No TCP bind IP configured for interface \"{name}\"")
|
||||
bind_address = TCPServerInterface.get_address_for_host(bindip, self.bind_port)
|
||||
|
||||
if bind_address != None:
|
||||
self.receives = True
|
||||
self.bind_ip = bind_address[0]
|
||||
|
||||
def handlerFactory(callback):
|
||||
def createHandler(*args, **keys):
|
||||
return TCPInterfaceHandler(callback, *args, **keys)
|
||||
return createHandler
|
||||
|
||||
self.owner = owner
|
||||
address = (self.bind_ip, self.bind_port)
|
||||
|
||||
ThreadingTCPServer.allow_reuse_address = True
|
||||
self.server = ThreadingTCPServer(address, handlerFactory(self.incoming_connection))
|
||||
if len(bind_address) == 4:
|
||||
try:
|
||||
ThreadingTCP6Server.allow_reuse_address = True
|
||||
self.server = ThreadingTCP6Server(bind_address, handlerFactory(self.incoming_connection))
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while binding IPv6 socket for interface, the contained exception was: {e}", RNS.LOG_ERROR)
|
||||
raise SystemError("Could not bind IPv6 socket for interface. Please check the specified \"listen_ip\" configuration option")
|
||||
else:
|
||||
ThreadingTCPServer.allow_reuse_address = True
|
||||
self.server = ThreadingTCPServer(bind_address, handlerFactory(self.incoming_connection))
|
||||
self.server.daemon_threads = True
|
||||
|
||||
self.bitrate = TCPServerInterface.BITRATE_GUESS
|
||||
|
||||
@@ -462,17 +549,20 @@ class TCPServerInterface(Interface):
|
||||
|
||||
self.online = True
|
||||
|
||||
else:
|
||||
raise SystemError("Insufficient parameters to create TCP listener")
|
||||
|
||||
def incoming_connection(self, handler):
|
||||
RNS.log("Accepting incoming TCP connection", RNS.LOG_VERBOSE)
|
||||
interface_name = "Client on "+self.name
|
||||
spawned_interface = TCPClientInterface(self.owner, interface_name, target_ip=None, target_port=None, connected_socket=handler.request, i2p_tunneled=self.i2p_tunneled)
|
||||
spawned_configuration = {"name": "Client on "+self.name, "target_host": None, "target_port": None, "i2p_tunneled": self.i2p_tunneled}
|
||||
spawned_interface = TCPClientInterface(self.owner, spawned_configuration, connected_socket=handler.request)
|
||||
spawned_interface.OUT = self.OUT
|
||||
spawned_interface.IN = self.IN
|
||||
spawned_interface.target_ip = handler.client_address[0]
|
||||
spawned_interface.target_port = str(handler.client_address[1])
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
spawned_interface.optimise_mtu()
|
||||
|
||||
spawned_interface.ifac_size = self.ifac_size
|
||||
spawned_interface.ifac_netname = self.ifac_netname
|
||||
@@ -502,21 +592,30 @@ class TCPServerInterface(Interface):
|
||||
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
|
||||
while spawned_interface in self.spawned_interfaces:
|
||||
self.spawned_interfaces.remove(spawned_interface)
|
||||
self.spawned_interfaces.append(spawned_interface)
|
||||
spawned_interface.read_loop()
|
||||
|
||||
def processOutgoing(self, data):
|
||||
def received_announce(self, from_spawned=False):
|
||||
if from_spawned: self.ia_freq_deque.append(time.time())
|
||||
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def process_outgoing(self, data):
|
||||
pass
|
||||
|
||||
|
||||
def detach(self):
|
||||
self.detached = True
|
||||
self.online = False
|
||||
if self.server != None:
|
||||
if hasattr(self.server, "shutdown"):
|
||||
if callable(self.server.shutdown):
|
||||
try:
|
||||
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
|
||||
self.server.shutdown()
|
||||
self.detached = True
|
||||
self.server.server_close()
|
||||
self.server = None
|
||||
|
||||
except Exception as e:
|
||||
@@ -524,7 +623,12 @@ class TCPServerInterface(Interface):
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "TCPServerInterface["+self.name+"/"+self.bind_ip+":"+str(self.bind_port)+"]"
|
||||
if ":" in self.bind_ip:
|
||||
ip_str = f"[{self.bind_ip}]"
|
||||
else:
|
||||
ip_str = f"{self.bind_ip}"
|
||||
|
||||
return "TCPServerInterface["+self.name+"/"+ip_str+":"+str(self.bind_port)+"]"
|
||||
|
||||
|
||||
class TCPInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
import socketserver
|
||||
import threading
|
||||
import socket
|
||||
@@ -31,6 +31,7 @@ import RNS
|
||||
|
||||
class UDPInterface(Interface):
|
||||
BITRATE_GUESS = 10*1000*1000
|
||||
DEFAULT_IFAC_SIZE = 16
|
||||
|
||||
@staticmethod
|
||||
def get_address_for_if(name):
|
||||
@@ -44,9 +45,23 @@ class UDPInterface(Interface):
|
||||
ifaddr = netinfo.ifaddresses(name)
|
||||
return ifaddr[netinfo.AF_INET][0]["broadcast"]
|
||||
|
||||
def __init__(self, owner, name, device=None, bindip=None, bindport=None, forwardip=None, forwardport=None):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
def __init__(self, owner, configuration):
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
device = c["device"] if "device" in c else None
|
||||
port = int(c["port"]) if "port" in c else None
|
||||
bindip = c["listen_ip"] if "listen_ip" in c else None
|
||||
bindport = int(c["listen_port"]) if "listen_port" in c else None
|
||||
forwardip = c["forward_ip"] if "forward_ip" in c else None
|
||||
forwardport = int(c["forward_port"]) if "forward_port" in c else None
|
||||
|
||||
if port != None:
|
||||
if bindport == None:
|
||||
bindport = port
|
||||
if forwardport == None:
|
||||
forwardport = port
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
@@ -76,7 +91,7 @@ class UDPInterface(Interface):
|
||||
self.owner = owner
|
||||
address = (self.bind_ip, self.bind_port)
|
||||
socketserver.UDPServer.address_family = socket.AF_INET
|
||||
self.server = socketserver.UDPServer(address, handlerFactory(self.processIncoming))
|
||||
self.server = socketserver.UDPServer(address, handlerFactory(self.process_incoming))
|
||||
|
||||
thread = threading.Thread(target=self.server.serve_forever)
|
||||
thread.daemon = True
|
||||
@@ -90,11 +105,11 @@ class UDPInterface(Interface):
|
||||
self.forward_port = forwardport
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
def processOutgoing(self,data):
|
||||
def process_outgoing(self,data):
|
||||
try:
|
||||
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
|
||||
@@ -24,5 +24,7 @@ import os
|
||||
import glob
|
||||
import RNS.Interfaces.Android
|
||||
|
||||
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
|
||||
py_modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
|
||||
modules = py_modules+pyc_modules
|
||||
__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))]))
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -30,7 +30,7 @@ class Packet:
|
||||
"""
|
||||
The Packet class is used to create packet instances that can be sent
|
||||
over a Reticulum network. Packets will automatically be encrypted if
|
||||
they are adressed to a ``RNS.Destination.SINGLE`` destination,
|
||||
they are addressed to a ``RNS.Destination.SINGLE`` destination,
|
||||
``RNS.Destination.GROUP`` destination or a :ref:`RNS.Link<api-link>`.
|
||||
|
||||
For ``RNS.Destination.GROUP`` destinations, Reticulum will use the
|
||||
@@ -83,6 +83,10 @@ class Packet:
|
||||
LRRTT = 0xFE # Packet is a link request round-trip time measurement
|
||||
LRPROOF = 0xFF # Packet is a link request proof
|
||||
|
||||
# Context flag values
|
||||
FLAG_SET = 0x01
|
||||
FLAG_UNSET = 0x00
|
||||
|
||||
# This is used to calculate allowable
|
||||
# payload sizes
|
||||
HEADER_MAXSIZE = RNS.Reticulum.HEADER_MAXSIZE
|
||||
@@ -91,7 +95,7 @@ class Packet:
|
||||
# With an MTU of 500, the maximum of data we can
|
||||
# send in a single encrypted packet is given by
|
||||
# the below calculation; 383 bytes.
|
||||
ENCRYPTED_MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.FERNET_OVERHEAD-RNS.Identity.KEYSIZE//16)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
|
||||
ENCRYPTED_MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.TOKEN_OVERHEAD-RNS.Identity.KEYSIZE//16)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
|
||||
"""
|
||||
The maximum size of the payload data in a single encrypted packet
|
||||
"""
|
||||
@@ -102,7 +106,9 @@ class Packet:
|
||||
|
||||
TIMEOUT_PER_HOP = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT
|
||||
|
||||
def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True):
|
||||
def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST,
|
||||
header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True, context_flag=FLAG_UNSET):
|
||||
|
||||
if destination != None:
|
||||
if transport_type == None:
|
||||
transport_type = RNS.Transport.BROADCAST
|
||||
@@ -111,6 +117,7 @@ class Packet:
|
||||
self.packet_type = packet_type
|
||||
self.transport_type = transport_type
|
||||
self.context = context
|
||||
self.context_flag = context_flag
|
||||
|
||||
self.hops = 0;
|
||||
self.destination = destination
|
||||
@@ -130,20 +137,27 @@ class Packet:
|
||||
self.fromPacked = True
|
||||
self.create_receipt = False
|
||||
|
||||
self.MTU = RNS.Reticulum.MTU
|
||||
if destination and destination.type == RNS.Destination.LINK:
|
||||
self.MTU = destination.mtu
|
||||
else:
|
||||
self.MTU = RNS.Reticulum.MTU
|
||||
|
||||
self.sent_at = None
|
||||
self.packet_hash = None
|
||||
self.ratchet_id = None
|
||||
|
||||
self.attached_interface = attached_interface
|
||||
self.receiving_interface = None
|
||||
self.rssi = None
|
||||
self.snr = None
|
||||
self.q = None
|
||||
|
||||
def get_packed_flags(self):
|
||||
if self.context == Packet.LRPROOF:
|
||||
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (RNS.Destination.LINK << 2) | self.packet_type
|
||||
packed_flags = (self.header_type << 6) | (self.context_flag << 5) | (self.transport_type << 4) | (RNS.Destination.LINK << 2) | self.packet_type
|
||||
else:
|
||||
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
|
||||
packed_flags = (self.header_type << 6) | (self.context_flag << 5) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
|
||||
|
||||
return packed_flags
|
||||
|
||||
def pack(self):
|
||||
@@ -186,6 +200,8 @@ class Packet:
|
||||
# In all other cases, we encrypt the packet
|
||||
# with the destination's encryption method
|
||||
self.ciphertext = self.destination.encrypt(self.data)
|
||||
if hasattr(self.destination, "latest_ratchet_id"):
|
||||
self.ratchet_id = self.destination.latest_ratchet_id
|
||||
|
||||
if self.header_type == Packet.HEADER_2:
|
||||
if self.transport_id != None:
|
||||
@@ -215,7 +231,8 @@ class Packet:
|
||||
self.hops = self.raw[1]
|
||||
|
||||
self.header_type = (self.flags & 0b01000000) >> 6
|
||||
self.transport_type = (self.flags & 0b00110000) >> 4
|
||||
self.context_flag = (self.flags & 0b00100000) >> 5
|
||||
self.transport_type = (self.flags & 0b00010000) >> 4
|
||||
self.destination_type = (self.flags & 0b00001100) >> 2
|
||||
self.packet_type = (self.flags & 0b00000011)
|
||||
|
||||
@@ -328,6 +345,33 @@ class Packet:
|
||||
|
||||
return hashable_part
|
||||
|
||||
def get_rssi(self):
|
||||
"""
|
||||
:returns: The physical layer *Received Signal Strength Indication* if available, otherwise ``None``.
|
||||
"""
|
||||
if self.rssi != None:
|
||||
return self.rssi
|
||||
else:
|
||||
return reticulum.get_packet_rssi(self.packet_hash)
|
||||
|
||||
def get_snr(self):
|
||||
"""
|
||||
:returns: The physical layer *Signal-to-Noise Ratio* if available, otherwise ``None``.
|
||||
"""
|
||||
if self.snr != None:
|
||||
return self.snr
|
||||
else:
|
||||
return reticulum.get_packet_snr(self.packet_hash)
|
||||
|
||||
def get_q(self):
|
||||
"""
|
||||
:returns: The physical layer *Link Quality* if available, otherwise ``None``.
|
||||
"""
|
||||
if self.q != None:
|
||||
return self.q
|
||||
else:
|
||||
return reticulum.get_packet_q(self.packet_hash)
|
||||
|
||||
class ProofDestination:
|
||||
def __init__(self, packet):
|
||||
self.hash = packet.get_hash()[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8];
|
||||
@@ -368,10 +412,10 @@ class PacketReceipt:
|
||||
self.proof_packet = None
|
||||
|
||||
if packet.destination.type == RNS.Destination.LINK:
|
||||
self.timeout = packet.destination.rtt * packet.destination.traffic_timeout_factor
|
||||
self.timeout = max(packet.destination.rtt * packet.destination.traffic_timeout_factor, RNS.Link.TRAFFIC_TIMEOUT_MIN_MS/1000)
|
||||
else:
|
||||
self.timeout = Packet.TIMEOUT_PER_HOP * RNS.Transport.hops_to(self.destination.hash)
|
||||
|
||||
self.timeout = RNS.Reticulum.get_instance().get_first_hop_timeout(self.destination.hash)
|
||||
self.timeout += Packet.TIMEOUT_PER_HOP * RNS.Transport.hops_to(self.destination.hash)
|
||||
|
||||
def get_status(self):
|
||||
"""
|
||||
@@ -408,6 +452,7 @@ class PacketReceipt:
|
||||
except Exception as e:
|
||||
RNS.log("An error occurred while evaluating external delivery callback for "+str(link), RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
return True
|
||||
else:
|
||||
@@ -439,7 +484,7 @@ class PacketReceipt:
|
||||
# This is an explicit proof
|
||||
proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
|
||||
signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
|
||||
if proof_hash == self.hash:
|
||||
if proof_hash == self.hash and hasattr(self.destination, "identity") and self.destination.identity != None:
|
||||
proof_valid = self.destination.identity.validate(signature, self.hash)
|
||||
if proof_valid:
|
||||
self.status = PacketReceipt.DELIVERED
|
||||
@@ -460,6 +505,10 @@ class PacketReceipt:
|
||||
return False
|
||||
elif len(proof) == PacketReceipt.IMPL_LENGTH:
|
||||
# This is an implicit proof
|
||||
|
||||
if not hasattr(self.destination, "identity"):
|
||||
return False
|
||||
|
||||
if self.destination.identity == None:
|
||||
return False
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
class Resolver:
|
||||
|
||||
@staticmethod
|
||||
def resolve_identity(full_name):
|
||||
pass
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -25,7 +25,9 @@ import os
|
||||
import bz2
|
||||
import math
|
||||
import time
|
||||
import tempfile
|
||||
import threading
|
||||
from threading import Lock
|
||||
from .vendor import umsgpack as umsgpack
|
||||
from time import sleep
|
||||
|
||||
@@ -47,11 +49,14 @@ class Resource:
|
||||
WINDOW = 4
|
||||
|
||||
# Absolute minimum window size during transfer
|
||||
WINDOW_MIN = 1
|
||||
WINDOW_MIN = 2
|
||||
|
||||
# The maximum window size for transfers on slow links
|
||||
WINDOW_MAX_SLOW = 10
|
||||
|
||||
# The maximum window size for transfers on very slow links
|
||||
WINDOW_MAX_VERY_SLOW = 4
|
||||
|
||||
# The maximum window size for transfers on fast links
|
||||
WINDOW_MAX_FAST = 75
|
||||
|
||||
@@ -63,12 +68,22 @@ class Resource:
|
||||
# rounds, the fast link window size will be allowed.
|
||||
FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 2
|
||||
|
||||
# If the very slow rate is sustained for this many request
|
||||
# rounds, window will be capped to the very slow limit.
|
||||
VERY_SLOW_RATE_THRESHOLD = 2
|
||||
|
||||
# If the RTT rate is higher than this value,
|
||||
# the max window size for fast links will be used.
|
||||
# The default is 50 Kbps (the value is stored in
|
||||
# bytes per second, hence the "/ 8").
|
||||
RATE_FAST = (50*1000) / 8
|
||||
|
||||
# If the RTT rate is lower than this value,
|
||||
# the window size will be capped at .
|
||||
# The default is 50 Kbps (the value is stored in
|
||||
# bytes per second, hence the "/ 8").
|
||||
RATE_VERY_SLOW = (2*1000) / 8
|
||||
|
||||
# The minimum allowed flexibility of the window size.
|
||||
# The difference between window_max and window_min
|
||||
# will never be smaller than this value.
|
||||
@@ -103,9 +118,11 @@ class Resource:
|
||||
|
||||
PART_TIMEOUT_FACTOR = 4
|
||||
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
|
||||
MAX_RETRIES = 8
|
||||
PROOF_TIMEOUT_FACTOR = 3
|
||||
MAX_RETRIES = 16
|
||||
MAX_ADV_RETRIES = 4
|
||||
SENDER_GRACE_TIME = 10
|
||||
SENDER_GRACE_TIME = 10.0
|
||||
PROCESSING_GRACE = 1.0
|
||||
RETRY_GRACE_TIME = 0.25
|
||||
PER_RETRY_DELAY = 0.5
|
||||
|
||||
@@ -124,6 +141,19 @@ class Resource:
|
||||
COMPLETE = 0x06
|
||||
FAILED = 0x07
|
||||
CORRUPT = 0x08
|
||||
REJECTED = 0x00
|
||||
|
||||
@staticmethod
|
||||
def reject(advertisement_packet):
|
||||
try:
|
||||
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
|
||||
resource_hash = adv.h
|
||||
reject_packet = RNS.Packet(advertisement_packet.link, resource_hash, context=RNS.Packet.RESOURCE_RCL)
|
||||
reject_packet.send()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error ocurred while rejecting advertised resource: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
@staticmethod
|
||||
def accept(advertisement_packet, callback=None, progress_callback = None, request_id = None):
|
||||
@@ -133,32 +163,33 @@ class Resource:
|
||||
resource = Resource(None, advertisement_packet.link, request_id = request_id)
|
||||
resource.status = Resource.TRANSFERRING
|
||||
|
||||
resource.flags = adv.f
|
||||
resource.size = adv.t
|
||||
resource.total_size = adv.d
|
||||
resource.uncompressed_size = adv.d
|
||||
resource.hash = adv.h
|
||||
resource.original_hash = adv.o
|
||||
resource.random_hash = adv.r
|
||||
resource.hashmap_raw = adv.m
|
||||
resource.encrypted = True if resource.flags & 0x01 else False
|
||||
resource.compressed = True if resource.flags >> 1 & 0x01 else False
|
||||
resource.initiator = False
|
||||
resource.flags = adv.f
|
||||
resource.size = adv.t
|
||||
resource.total_size = adv.d
|
||||
resource.uncompressed_size = adv.d
|
||||
resource.hash = adv.h
|
||||
resource.original_hash = adv.o
|
||||
resource.random_hash = adv.r
|
||||
resource.hashmap_raw = adv.m
|
||||
resource.encrypted = True if resource.flags & 0x01 else False
|
||||
resource.compressed = True if resource.flags >> 1 & 0x01 else False
|
||||
resource.initiator = False
|
||||
resource.callback = callback
|
||||
resource.__progress_callback = progress_callback
|
||||
resource.total_parts = int(math.ceil(resource.size/float(Resource.SDU)))
|
||||
resource.received_count = 0
|
||||
resource.outstanding_parts = 0
|
||||
resource.parts = [None] * resource.total_parts
|
||||
resource.window = Resource.WINDOW
|
||||
resource.window_max = Resource.WINDOW_MAX_SLOW
|
||||
resource.window_min = Resource.WINDOW_MIN
|
||||
resource.window_flexibility = Resource.WINDOW_FLEXIBILITY
|
||||
resource.last_activity = time.time()
|
||||
resource.__progress_callback = progress_callback
|
||||
resource.total_parts = int(math.ceil(resource.size/float(resource.sdu)))
|
||||
resource.received_count = 0
|
||||
resource.outstanding_parts = 0
|
||||
resource.parts = [None] * resource.total_parts
|
||||
resource.window = Resource.WINDOW
|
||||
resource.window_max = Resource.WINDOW_MAX_SLOW
|
||||
resource.window_min = Resource.WINDOW_MIN
|
||||
resource.window_flexibility = Resource.WINDOW_FLEXIBILITY
|
||||
resource.last_activity = time.time()
|
||||
resource.started_transferring = resource.last_activity
|
||||
|
||||
resource.storagepath = RNS.Reticulum.resourcepath+"/"+resource.original_hash.hex()
|
||||
resource.segment_index = adv.i
|
||||
resource.total_segments = adv.l
|
||||
resource.storagepath = RNS.Reticulum.resourcepath+"/"+resource.original_hash.hex()
|
||||
resource.segment_index = adv.i
|
||||
resource.total_segments = adv.l
|
||||
if adv.l > 1:
|
||||
resource.split = True
|
||||
else:
|
||||
@@ -167,15 +198,20 @@ class Resource:
|
||||
resource.hashmap = [None] * resource.total_parts
|
||||
resource.hashmap_height = 0
|
||||
resource.waiting_for_hmu = False
|
||||
|
||||
resource.receiving_part = False
|
||||
resource.consecutive_completed_height = -1
|
||||
|
||||
resource.consecutive_completed_height = 0
|
||||
previous_window = resource.link.get_last_resource_window()
|
||||
previous_eifr = resource.link.get_last_resource_eifr()
|
||||
if previous_window:
|
||||
resource.window = previous_window
|
||||
if previous_eifr:
|
||||
resource.previous_eifr = previous_eifr
|
||||
|
||||
if not resource.link.has_incoming_resource(resource):
|
||||
resource.link.register_incoming_resource(resource)
|
||||
|
||||
RNS.log("Accepting resource advertisement for "+RNS.prettyhexrep(resource.hash), RNS.LOG_DEBUG)
|
||||
RNS.log(f"Accepting resource advertisement for {RNS.prettyhexrep(resource.hash)}. Transfer size is {RNS.prettysize(resource.size)} in {resource.total_parts} parts.", RNS.LOG_DEBUG)
|
||||
if resource.link.callbacks.resource_started != None:
|
||||
try:
|
||||
resource.link.callbacks.resource_started(resource)
|
||||
@@ -203,11 +239,22 @@ class Resource:
|
||||
data_size = None
|
||||
resource_data = None
|
||||
self.assembly_lock = False
|
||||
self.preparing_next_segment = False
|
||||
self.next_segment = None
|
||||
|
||||
if data != None:
|
||||
if not hasattr(data, "read") and len(data) > Resource.MAX_EFFICIENT_SIZE:
|
||||
original_data = data
|
||||
data_size = len(original_data)
|
||||
data = tempfile.TemporaryFile()
|
||||
data.write(original_data)
|
||||
del original_data
|
||||
|
||||
if hasattr(data, "read"):
|
||||
data_size = os.stat(data.name).st_size
|
||||
self.total_size = data_size
|
||||
self.grand_total_parts = math.ceil(data_size/Resource.SDU)
|
||||
if data_size == None:
|
||||
data_size = os.stat(data.name).st_size
|
||||
|
||||
self.total_size = data_size
|
||||
|
||||
if data_size <= Resource.MAX_EFFICIENT_SIZE:
|
||||
self.total_segments = 1
|
||||
@@ -228,7 +275,6 @@ class Resource:
|
||||
|
||||
elif isinstance(data, bytes):
|
||||
data_size = len(data)
|
||||
self.grand_total_parts = math.ceil(data_size/Resource.SDU)
|
||||
self.total_size = data_size
|
||||
|
||||
resource_data = data
|
||||
@@ -246,6 +292,10 @@ class Resource:
|
||||
|
||||
self.status = Resource.NONE
|
||||
self.link = link
|
||||
if self.link.mtu:
|
||||
self.sdu = self.link.mtu - RNS.Reticulum.HEADER_MAXSIZE - RNS.Reticulum.IFAC_MIN_SIZE
|
||||
else:
|
||||
self.sdu = link.mdu or Resource.SDU
|
||||
self.max_retries = Resource.MAX_RETRIES
|
||||
self.max_adv_retries = Resource.MAX_ADV_RETRIES
|
||||
self.retries_left = self.max_retries
|
||||
@@ -261,9 +311,15 @@ class Resource:
|
||||
self.req_sent = 0
|
||||
self.req_resp_rtt_rate = 0
|
||||
self.rtt_rxd_bytes_at_part_req = 0
|
||||
self.req_data_rtt_rate = 0
|
||||
self.eifr = None
|
||||
self.previous_eifr = None
|
||||
self.fast_rate_rounds = 0
|
||||
self.very_slow_rate_rounds = 0
|
||||
self.request_id = request_id
|
||||
self.started_transferring = None
|
||||
self.is_response = is_response
|
||||
self.auto_compress = auto_compress
|
||||
|
||||
self.req_hashlist = []
|
||||
self.receiver_min_consecutive_height = 0
|
||||
@@ -279,10 +335,10 @@ class Resource:
|
||||
self.uncompressed_data = data
|
||||
|
||||
compression_began = time.time()
|
||||
if (auto_compress and len(self.uncompressed_data) < Resource.AUTO_COMPRESS_MAX_SIZE):
|
||||
RNS.log("Compressing resource data...", RNS.LOG_DEBUG)
|
||||
if (auto_compress and len(self.uncompressed_data) <= Resource.AUTO_COMPRESS_MAX_SIZE):
|
||||
RNS.log("Compressing resource data...", RNS.LOG_EXTREME)
|
||||
self.compressed_data = bz2.compress(self.uncompressed_data)
|
||||
RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_DEBUG)
|
||||
RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_EXTREME)
|
||||
else:
|
||||
self.compressed_data = self.uncompressed_data
|
||||
|
||||
@@ -291,7 +347,7 @@ class Resource:
|
||||
|
||||
if (self.compressed_size < self.uncompressed_size and auto_compress):
|
||||
saved_bytes = len(self.uncompressed_data) - len(self.compressed_data)
|
||||
RNS.log("Compression saved "+str(saved_bytes)+" bytes, sending compressed", RNS.LOG_DEBUG)
|
||||
RNS.log("Compression saved "+str(saved_bytes)+" bytes, sending compressed", RNS.LOG_EXTREME)
|
||||
|
||||
self.data = b""
|
||||
self.data += RNS.Identity.get_random_hash()[:Resource.RANDOM_HASH_SIZE]
|
||||
@@ -309,7 +365,7 @@ class Resource:
|
||||
self.compressed = False
|
||||
self.compressed_data = None
|
||||
if auto_compress:
|
||||
RNS.log("Compression did not decrease size, sending uncompressed", RNS.LOG_DEBUG)
|
||||
RNS.log("Compression did not decrease size, sending uncompressed", RNS.LOG_EXTREME)
|
||||
|
||||
# Resources handle encryption directly to
|
||||
# make optimal use of packet MTU on an entire
|
||||
@@ -320,12 +376,13 @@ class Resource:
|
||||
|
||||
self.size = len(self.data)
|
||||
self.sent_parts = 0
|
||||
hashmap_entries = int(math.ceil(self.size/float(Resource.SDU)))
|
||||
hashmap_entries = int(math.ceil(self.size/float(self.sdu)))
|
||||
self.total_parts = hashmap_entries
|
||||
|
||||
hashmap_ok = False
|
||||
while not hashmap_ok:
|
||||
hashmap_computation_began = time.time()
|
||||
RNS.log("Starting resource hashmap computation with "+str(hashmap_entries)+" entries...", RNS.LOG_DEBUG)
|
||||
RNS.log("Starting resource hashmap computation with "+str(hashmap_entries)+" entries...", RNS.LOG_EXTREME)
|
||||
|
||||
self.random_hash = RNS.Identity.get_random_hash()[:Resource.RANDOM_HASH_SIZE]
|
||||
self.hash = RNS.Identity.full_hash(data+self.random_hash)
|
||||
@@ -341,11 +398,11 @@ class Resource:
|
||||
self.hashmap = b""
|
||||
collision_guard_list = []
|
||||
for i in range(0,hashmap_entries):
|
||||
data = self.data[i*Resource.SDU:(i+1)*Resource.SDU]
|
||||
data = self.data[i*self.sdu:(i+1)*self.sdu]
|
||||
map_hash = self.get_map_hash(data)
|
||||
|
||||
if map_hash in collision_guard_list:
|
||||
RNS.log("Found hash collision in resource map, remapping...", RNS.LOG_VERBOSE)
|
||||
RNS.log("Found hash collision in resource map, remapping...", RNS.LOG_DEBUG)
|
||||
hashmap_ok = False
|
||||
break
|
||||
else:
|
||||
@@ -361,12 +418,13 @@ class Resource:
|
||||
self.hashmap += part.map_hash
|
||||
self.parts.append(part)
|
||||
|
||||
RNS.log("Hashmap computation concluded in "+str(round(time.time()-hashmap_computation_began, 3))+" seconds", RNS.LOG_DEBUG)
|
||||
RNS.log("Hashmap computation concluded in "+str(round(time.time()-hashmap_computation_began, 3))+" seconds", RNS.LOG_EXTREME)
|
||||
|
||||
if advertise:
|
||||
self.advertise()
|
||||
else:
|
||||
pass
|
||||
self.receive_lock = Lock()
|
||||
|
||||
|
||||
def hashmap_update_packet(self, plaintext):
|
||||
if not self.status == Resource.FAILED:
|
||||
@@ -398,10 +456,13 @@ class Resource:
|
||||
Advertise the resource. If the other end of the link accepts
|
||||
the resource advertisement it will begin transferring.
|
||||
"""
|
||||
thread = threading.Thread(target=self.__advertise_job)
|
||||
thread.daemon = True
|
||||
thread = threading.Thread(target=self.__advertise_job, daemon=True)
|
||||
thread.start()
|
||||
|
||||
if self.segment_index < self.total_segments:
|
||||
prepare_thread = threading.Thread(target=self.__prepare_next_segment, daemon=True)
|
||||
prepare_thread.start()
|
||||
|
||||
def __advertise_job(self):
|
||||
self.advertisement_packet = RNS.Packet(self.link, ResourceAdvertisement(self).pack(), context=RNS.Packet.RESOURCE_ADV)
|
||||
while not self.link.ready_for_new_resource():
|
||||
@@ -411,12 +472,13 @@ class Resource:
|
||||
try:
|
||||
self.advertisement_packet.send()
|
||||
self.last_activity = time.time()
|
||||
self.started_transferring = self.last_activity
|
||||
self.adv_sent = self.last_activity
|
||||
self.rtt = None
|
||||
self.status = Resource.ADVERTISED
|
||||
self.retries_left = self.max_adv_retries
|
||||
self.link.register_outgoing_resource(self)
|
||||
RNS.log("Sent resource advertisement for "+RNS.prettyhexrep(self.hash), RNS.LOG_DEBUG)
|
||||
RNS.log("Sent resource advertisement for "+RNS.prettyhexrep(self.hash), RNS.LOG_EXTREME)
|
||||
except Exception as e:
|
||||
RNS.log("Could not advertise resource, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
self.cancel()
|
||||
@@ -424,6 +486,23 @@ class Resource:
|
||||
|
||||
self.watchdog_job()
|
||||
|
||||
def update_eifr(self):
|
||||
if self.rtt == None:
|
||||
rtt = self.link.rtt
|
||||
else:
|
||||
rtt = self.rtt
|
||||
|
||||
if self.req_data_rtt_rate != 0:
|
||||
expected_inflight_rate = self.req_data_rtt_rate*8
|
||||
else:
|
||||
if self.previous_eifr != None:
|
||||
expected_inflight_rate = self.previous_eifr
|
||||
else:
|
||||
expected_inflight_rate = self.link.establishment_cost*8 / rtt
|
||||
|
||||
self.eifr = expected_inflight_rate
|
||||
if self.link: self.link.expected_rate = self.eifr
|
||||
|
||||
def watchdog_job(self):
|
||||
thread = threading.Thread(target=self.__watchdog_job)
|
||||
thread.daemon = True
|
||||
@@ -438,9 +517,8 @@ class Resource:
|
||||
sleep(0.025)
|
||||
|
||||
sleep_time = None
|
||||
|
||||
if self.status == Resource.ADVERTISED:
|
||||
sleep_time = (self.adv_sent+self.timeout)-time.time()
|
||||
sleep_time = (self.adv_sent+self.timeout+Resource.PROCESSING_GRACE)-time.time()
|
||||
if sleep_time < 0:
|
||||
if self.retries_left <= 0:
|
||||
RNS.log("Resource transfer timeout after sending advertisement", RNS.LOG_DEBUG)
|
||||
@@ -456,27 +534,30 @@ class Resource:
|
||||
self.adv_sent = self.last_activity
|
||||
sleep_time = 0.001
|
||||
except Exception as e:
|
||||
RNS.log("Could not resend advertisement packet, cancelling resource", RNS.LOG_VERBOSE)
|
||||
RNS.log("Could not resend advertisement packet, cancelling resource. The contained exception was: "+str(e), RNS.LOG_VERBOSE)
|
||||
self.cancel()
|
||||
|
||||
|
||||
elif self.status == Resource.TRANSFERRING:
|
||||
if not self.initiator:
|
||||
|
||||
if self.rtt == None:
|
||||
rtt = self.link.rtt
|
||||
else:
|
||||
rtt = self.rtt
|
||||
|
||||
window_remaining = self.outstanding_parts
|
||||
|
||||
retries_used = self.max_retries - self.retries_left
|
||||
extra_wait = retries_used * Resource.PER_RETRY_DELAY
|
||||
sleep_time = self.last_activity + (rtt*(self.part_timeout_factor+window_remaining)) + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
|
||||
|
||||
self.update_eifr()
|
||||
expected_tof_remaining = (self.outstanding_parts*self.sdu*8)/self.eifr
|
||||
|
||||
if self.req_resp_rtt_rate != 0:
|
||||
sleep_time = self.last_activity + self.part_timeout_factor*expected_tof_remaining + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
|
||||
else:
|
||||
sleep_time = self.last_activity + self.part_timeout_factor*((3*self.sdu)/self.eifr) + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
|
||||
|
||||
# RNS.log(f"EIFR {RNS.prettyspeed(self.eifr)}, ETOF {RNS.prettyshorttime(expected_tof_remaining)} ", RNS.LOG_DEBUG, pt=True)
|
||||
# RNS.log(f"Resource ST {RNS.prettyshorttime(sleep_time)}, RTT {RNS.prettyshorttime(self.rtt or self.link.rtt)}, {self.outstanding_parts} left", RNS.LOG_DEBUG, pt=True)
|
||||
|
||||
if sleep_time < 0:
|
||||
if self.retries_left > 0:
|
||||
RNS.log("Timed out waiting for parts, requesting retry", RNS.LOG_DEBUG)
|
||||
ms = "" if self.outstanding_parts == 1 else "s"
|
||||
RNS.log("Timed out waiting for "+str(self.outstanding_parts)+" part"+ms+", requesting retry", RNS.LOG_DEBUG)
|
||||
if self.window > self.window_min:
|
||||
self.window -= 1
|
||||
if self.window_max > self.window_min:
|
||||
@@ -501,6 +582,10 @@ class Resource:
|
||||
sleep_time = 0.001
|
||||
|
||||
elif self.status == Resource.AWAITING_PROOF:
|
||||
# Decrease timeout factor since proof packets are
|
||||
# significantly smaller than full req/resp roundtrip
|
||||
self.timeout_factor = Resource.PROOF_TIMEOUT_FACTOR
|
||||
|
||||
sleep_time = self.last_part_sent + (self.rtt*self.timeout_factor+self.sender_grace_time) - time.time()
|
||||
if sleep_time < 0:
|
||||
if self.retries_left <= 0:
|
||||
@@ -517,8 +602,11 @@ class Resource:
|
||||
self.last_part_sent = time.time()
|
||||
sleep_time = 0.001
|
||||
|
||||
elif self.status == Resource.REJECTED:
|
||||
sleep_time = 0.001
|
||||
|
||||
if sleep_time == 0:
|
||||
RNS.log("Warning! Link watchdog sleep time of 0!", RNS.LOG_WARNING)
|
||||
RNS.log("Warning! Link watchdog sleep time of 0!", RNS.LOG_DEBUG)
|
||||
if sleep_time == None or sleep_time < 0:
|
||||
RNS.log("Timing error, cancelling resource transfer.", RNS.LOG_ERROR)
|
||||
self.cancel()
|
||||
@@ -592,11 +680,28 @@ class Resource:
|
||||
proof_data = self.hash+proof
|
||||
proof_packet = RNS.Packet(self.link, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.RESOURCE_PRF)
|
||||
proof_packet.send()
|
||||
RNS.Transport.cache(proof_packet, force_cache=True)
|
||||
except Exception as e:
|
||||
RNS.log("Could not send proof packet, cancelling resource", RNS.LOG_DEBUG)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
|
||||
self.cancel()
|
||||
|
||||
def __prepare_next_segment(self):
|
||||
# Prepare the next segment for advertisement
|
||||
RNS.log(f"Preparing segment {self.segment_index+1} of {self.total_segments} for resource {self}", RNS.LOG_DEBUG)
|
||||
self.preparing_next_segment = True
|
||||
self.next_segment = Resource(
|
||||
self.input_file, self.link,
|
||||
callback = self.callback,
|
||||
segment_index = self.segment_index+1,
|
||||
original_hash=self.original_hash,
|
||||
progress_callback = self.__progress_callback,
|
||||
request_id = self.request_id,
|
||||
is_response = self.is_response,
|
||||
advertise = False,
|
||||
auto_compress = self.auto_compress,
|
||||
)
|
||||
|
||||
def validate_proof(self, proof_data):
|
||||
if not self.status == Resource.FAILED:
|
||||
if len(proof_data) == RNS.Identity.HASHLENGTH//8*2:
|
||||
@@ -611,10 +716,24 @@ class Resource:
|
||||
self.callback(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing resource concluded callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
finally:
|
||||
try:
|
||||
if hasattr(self, "input_file"):
|
||||
if hasattr(self.input_file, "close") and callable(self.input_file.close):
|
||||
self.input_file.close()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while closing resource input file: "+str(e), RNS.LOG_ERROR)
|
||||
else:
|
||||
# Otherwise we'll recursively create the
|
||||
# next segment of the resource
|
||||
Resource(self.input_file, self.link, callback = self.callback, segment_index = self.segment_index+1, original_hash=self.original_hash, progress_callback = self.__progress_callback)
|
||||
if not self.preparing_next_segment:
|
||||
RNS.log(f"Next segment preparation for resource {self} was not started yet, manually preparing now. This will cause transfer slowdown.", RNS.LOG_WARNING)
|
||||
self.__prepare_next_segment()
|
||||
|
||||
while self.next_segment == None:
|
||||
time.sleep(0.05)
|
||||
self.next_segment.advertise()
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
@@ -622,98 +741,106 @@ class Resource:
|
||||
|
||||
|
||||
def receive_part(self, packet):
|
||||
while self.receiving_part:
|
||||
sleep(0.001)
|
||||
with self.receive_lock:
|
||||
|
||||
self.receiving_part = True
|
||||
self.last_activity = time.time()
|
||||
self.retries_left = self.max_retries
|
||||
self.receiving_part = True
|
||||
self.last_activity = time.time()
|
||||
self.retries_left = self.max_retries
|
||||
|
||||
if self.req_resp == None:
|
||||
self.req_resp = self.last_activity
|
||||
rtt = self.req_resp-self.req_sent
|
||||
|
||||
self.part_timeout_factor = Resource.PART_TIMEOUT_FACTOR_AFTER_RTT
|
||||
if self.rtt == None:
|
||||
self.rtt = self.link.rtt
|
||||
self.watchdog_job()
|
||||
elif rtt < self.rtt:
|
||||
self.rtt = max(self.rtt - self.rtt*0.05, rtt)
|
||||
elif rtt > self.rtt:
|
||||
self.rtt = min(self.rtt + self.rtt*0.05, rtt)
|
||||
if self.req_resp == None:
|
||||
self.req_resp = self.last_activity
|
||||
rtt = self.req_resp-self.req_sent
|
||||
|
||||
self.part_timeout_factor = Resource.PART_TIMEOUT_FACTOR_AFTER_RTT
|
||||
if self.rtt == None:
|
||||
self.rtt = self.link.rtt
|
||||
self.watchdog_job()
|
||||
elif rtt < self.rtt:
|
||||
self.rtt = max(self.rtt - self.rtt*0.05, rtt)
|
||||
elif rtt > self.rtt:
|
||||
self.rtt = min(self.rtt + self.rtt*0.05, rtt)
|
||||
|
||||
if rtt > 0:
|
||||
req_resp_cost = len(packet.raw)+self.req_sent_bytes
|
||||
self.req_resp_rtt_rate = req_resp_cost / rtt
|
||||
if rtt > 0:
|
||||
req_resp_cost = len(packet.raw)+self.req_sent_bytes
|
||||
self.req_resp_rtt_rate = req_resp_cost / rtt
|
||||
|
||||
if self.req_resp_rtt_rate > Resource.RATE_FAST and self.fast_rate_rounds < Resource.FAST_RATE_THRESHOLD:
|
||||
self.fast_rate_rounds += 1
|
||||
if self.req_resp_rtt_rate > Resource.RATE_FAST and self.fast_rate_rounds < Resource.FAST_RATE_THRESHOLD:
|
||||
self.fast_rate_rounds += 1
|
||||
|
||||
if self.fast_rate_rounds == Resource.FAST_RATE_THRESHOLD:
|
||||
self.window_max = Resource.WINDOW_MAX_FAST
|
||||
if self.fast_rate_rounds == Resource.FAST_RATE_THRESHOLD:
|
||||
self.window_max = Resource.WINDOW_MAX_FAST
|
||||
|
||||
if not self.status == Resource.FAILED:
|
||||
self.status = Resource.TRANSFERRING
|
||||
part_data = packet.data
|
||||
part_hash = self.get_map_hash(part_data)
|
||||
if not self.status == Resource.FAILED:
|
||||
self.status = Resource.TRANSFERRING
|
||||
part_data = packet.data
|
||||
part_hash = self.get_map_hash(part_data)
|
||||
|
||||
i = self.consecutive_completed_height
|
||||
for map_hash in self.hashmap[self.consecutive_completed_height:self.consecutive_completed_height+self.window]:
|
||||
if map_hash == part_hash:
|
||||
if self.parts[i] == None:
|
||||
# Insert data into parts list
|
||||
self.parts[i] = part_data
|
||||
self.rtt_rxd_bytes += len(part_data)
|
||||
self.received_count += 1
|
||||
self.outstanding_parts -= 1
|
||||
consecutive_index = self.consecutive_completed_height if self.consecutive_completed_height >= 0 else 0
|
||||
i = consecutive_index
|
||||
for map_hash in self.hashmap[consecutive_index:consecutive_index+self.window]:
|
||||
if map_hash == part_hash:
|
||||
if self.parts[i] == None:
|
||||
|
||||
# Update consecutive completed pointer
|
||||
if i == self.consecutive_completed_height + 1:
|
||||
self.consecutive_completed_height = i
|
||||
|
||||
cp = self.consecutive_completed_height + 1
|
||||
while cp < len(self.parts) and self.parts[cp] != None:
|
||||
self.consecutive_completed_height = cp
|
||||
cp += 1
|
||||
# Insert data into parts list
|
||||
self.parts[i] = part_data
|
||||
self.rtt_rxd_bytes += len(part_data)
|
||||
self.received_count += 1
|
||||
self.outstanding_parts -= 1
|
||||
|
||||
if self.__progress_callback != None:
|
||||
try:
|
||||
self.__progress_callback(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
# Update consecutive completed pointer
|
||||
if i == self.consecutive_completed_height + 1:
|
||||
self.consecutive_completed_height = i
|
||||
|
||||
cp = self.consecutive_completed_height + 1
|
||||
while cp < len(self.parts) and self.parts[cp] != None:
|
||||
self.consecutive_completed_height = cp
|
||||
cp += 1
|
||||
|
||||
i += 1
|
||||
if self.__progress_callback != None:
|
||||
try:
|
||||
self.__progress_callback(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
self.receiving_part = False
|
||||
i += 1
|
||||
|
||||
if self.received_count == self.total_parts and not self.assembly_lock:
|
||||
self.assembly_lock = True
|
||||
self.assemble()
|
||||
elif self.outstanding_parts == 0:
|
||||
# TODO: Figure out if there is a mathematically
|
||||
# optimal way to adjust windows
|
||||
if self.window < self.window_max:
|
||||
self.window += 1
|
||||
if (self.window - self.window_min) > (self.window_flexibility-1):
|
||||
self.window_min += 1
|
||||
self.receiving_part = False
|
||||
|
||||
if self.req_sent != 0:
|
||||
rtt = time.time()-self.req_sent
|
||||
req_transferred = self.rtt_rxd_bytes - self.rtt_rxd_bytes_at_part_req
|
||||
if self.received_count == self.total_parts and not self.assembly_lock:
|
||||
self.assembly_lock = True
|
||||
self.assemble()
|
||||
elif self.outstanding_parts == 0:
|
||||
# TODO: Figure out if there is a mathematically
|
||||
# optimal way to adjust windows
|
||||
if self.window < self.window_max:
|
||||
self.window += 1
|
||||
if (self.window - self.window_min) > (self.window_flexibility-1):
|
||||
self.window_min += 1
|
||||
|
||||
if rtt != 0:
|
||||
self.req_data_rtt_rate = req_transferred/rtt
|
||||
self.rtt_rxd_bytes_at_part_req = self.rtt_rxd_bytes
|
||||
if self.req_sent != 0:
|
||||
rtt = time.time()-self.req_sent
|
||||
req_transferred = self.rtt_rxd_bytes - self.rtt_rxd_bytes_at_part_req
|
||||
|
||||
if self.req_data_rtt_rate > Resource.RATE_FAST and self.fast_rate_rounds < Resource.FAST_RATE_THRESHOLD:
|
||||
self.fast_rate_rounds += 1
|
||||
if rtt != 0:
|
||||
self.req_data_rtt_rate = req_transferred/rtt
|
||||
self.update_eifr()
|
||||
self.rtt_rxd_bytes_at_part_req = self.rtt_rxd_bytes
|
||||
|
||||
if self.fast_rate_rounds == Resource.FAST_RATE_THRESHOLD:
|
||||
self.window_max = Resource.WINDOW_MAX_FAST
|
||||
if self.req_data_rtt_rate > Resource.RATE_FAST and self.fast_rate_rounds < Resource.FAST_RATE_THRESHOLD:
|
||||
self.fast_rate_rounds += 1
|
||||
|
||||
self.request_next()
|
||||
else:
|
||||
self.receiving_part = False
|
||||
if self.fast_rate_rounds == Resource.FAST_RATE_THRESHOLD:
|
||||
self.window_max = Resource.WINDOW_MAX_FAST
|
||||
|
||||
if self.fast_rate_rounds == 0 and self.req_data_rtt_rate < Resource.RATE_VERY_SLOW and self.very_slow_rate_rounds < Resource.VERY_SLOW_RATE_THRESHOLD:
|
||||
self.very_slow_rate_rounds += 1
|
||||
|
||||
if self.very_slow_rate_rounds == Resource.VERY_SLOW_RATE_THRESHOLD:
|
||||
self.window_max = Resource.WINDOW_MAX_VERY_SLOW
|
||||
|
||||
self.request_next()
|
||||
else:
|
||||
self.receiving_part = False
|
||||
|
||||
# Called on incoming resource to send a request for more data
|
||||
def request_next(self):
|
||||
@@ -726,11 +853,11 @@ class Resource:
|
||||
hashmap_exhausted = Resource.HASHMAP_IS_NOT_EXHAUSTED
|
||||
requested_hashes = b""
|
||||
|
||||
offset = (1 if self.consecutive_completed_height > 0 else 0)
|
||||
i = 0; pn = self.consecutive_completed_height+offset
|
||||
i = 0; pn = self.consecutive_completed_height+1
|
||||
search_start = pn
|
||||
|
||||
for part in self.parts[search_start:search_start+self.window]:
|
||||
search_size = self.window
|
||||
|
||||
for part in self.parts[search_start:search_start+search_size]:
|
||||
if part == None:
|
||||
part_hash = self.hashmap[pn]
|
||||
if part_hash != None:
|
||||
@@ -750,7 +877,6 @@ class Resource:
|
||||
hmu_part += last_map_hash
|
||||
self.waiting_for_hmu = True
|
||||
|
||||
requested_data = b""
|
||||
request_data = hmu_part + self.hash + requested_hashes
|
||||
request_packet = RNS.Packet(self.link, request_data, context = RNS.Packet.RESOURCE_REQ)
|
||||
|
||||
@@ -760,6 +886,7 @@ class Resource:
|
||||
self.req_sent = self.last_activity
|
||||
self.req_sent_bytes = len(request_packet.raw)
|
||||
self.req_resp = None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not send resource request packet, cancelling resource", RNS.LOG_DEBUG)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
|
||||
@@ -852,6 +979,7 @@ class Resource:
|
||||
|
||||
if self.sent_parts == len(self.parts):
|
||||
self.status = Resource.AWAITING_PROOF
|
||||
self.retries_left = 3
|
||||
|
||||
if self.__progress_callback != None:
|
||||
try:
|
||||
@@ -883,6 +1011,18 @@ class Resource:
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing callbacks on resource cancel from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def _rejected(self):
|
||||
if self.status < Resource.COMPLETE:
|
||||
if self.initiator:
|
||||
self.status = Resource.REJECTED
|
||||
self.link.cancel_outgoing_resource(self)
|
||||
if self.callback != None:
|
||||
try:
|
||||
self.link.resource_concluded(self)
|
||||
self.callback(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing callbacks on resource reject from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def set_callback(self, callback):
|
||||
self.callback = callback
|
||||
|
||||
@@ -893,20 +1033,68 @@ class Resource:
|
||||
"""
|
||||
:returns: The current progress of the resource transfer as a *float* between 0.0 and 1.0.
|
||||
"""
|
||||
if self.initiator:
|
||||
self.processed_parts = (self.segment_index-1)*math.ceil(Resource.MAX_EFFICIENT_SIZE/Resource.SDU)
|
||||
self.processed_parts += self.sent_parts
|
||||
self.progress_total_parts = float(self.grand_total_parts)
|
||||
else:
|
||||
self.processed_parts = (self.segment_index-1)*math.ceil(Resource.MAX_EFFICIENT_SIZE/Resource.SDU)
|
||||
self.processed_parts += self.received_count
|
||||
if self.split:
|
||||
self.progress_total_parts = float(math.ceil(self.total_size/Resource.SDU))
|
||||
else:
|
||||
if self.status == RNS.Resource.COMPLETE and self.segment_index == self.total_segments:
|
||||
return 1.0
|
||||
|
||||
elif self.initiator:
|
||||
if not self.split:
|
||||
self.processed_parts = self.sent_parts
|
||||
self.progress_total_parts = float(self.total_parts)
|
||||
|
||||
|
||||
progress = self.processed_parts / self.progress_total_parts
|
||||
else:
|
||||
is_last_segment = self.segment_index != self.total_segments
|
||||
total_segments = self.total_segments
|
||||
processed_segments = self.segment_index-1
|
||||
|
||||
current_segment_parts = self.total_parts
|
||||
max_parts_per_segment = math.ceil(Resource.MAX_EFFICIENT_SIZE/self.sdu)
|
||||
|
||||
previously_processed_parts = processed_segments*max_parts_per_segment
|
||||
|
||||
if current_segment_parts < max_parts_per_segment:
|
||||
current_segment_factor = max_parts_per_segment / current_segment_parts
|
||||
else:
|
||||
current_segment_factor = 1
|
||||
|
||||
self.processed_parts = previously_processed_parts + self.sent_parts*current_segment_factor
|
||||
self.progress_total_parts = self.total_segments*max_parts_per_segment
|
||||
|
||||
else:
|
||||
if not self.split:
|
||||
self.processed_parts = self.received_count
|
||||
self.progress_total_parts = float(self.total_parts)
|
||||
|
||||
else:
|
||||
is_last_segment = self.segment_index != self.total_segments
|
||||
total_segments = self.total_segments
|
||||
processed_segments = self.segment_index-1
|
||||
|
||||
current_segment_parts = self.total_parts
|
||||
max_parts_per_segment = math.ceil(Resource.MAX_EFFICIENT_SIZE/self.sdu)
|
||||
|
||||
previously_processed_parts = processed_segments*max_parts_per_segment
|
||||
|
||||
if current_segment_parts < max_parts_per_segment:
|
||||
current_segment_factor = max_parts_per_segment / current_segment_parts
|
||||
else:
|
||||
current_segment_factor = 1
|
||||
|
||||
self.processed_parts = previously_processed_parts + self.received_count*current_segment_factor
|
||||
self.progress_total_parts = self.total_segments*max_parts_per_segment
|
||||
|
||||
|
||||
progress = min(1.0, self.processed_parts / self.progress_total_parts)
|
||||
return progress
|
||||
|
||||
def get_segment_progress(self):
|
||||
if self.status == RNS.Resource.COMPLETE and self.segment_index == self.total_segments:
|
||||
return 1.0
|
||||
elif self.initiator:
|
||||
processed_parts = self.sent_parts
|
||||
else:
|
||||
processed_parts = self.received_count
|
||||
|
||||
progress = min(1.0, processed_parts / self.total_parts)
|
||||
return progress
|
||||
|
||||
def get_transfer_size(self):
|
||||
@@ -994,6 +1182,7 @@ class ResourceAdvertisement:
|
||||
|
||||
|
||||
def __init__(self, resource=None, request_id=None, is_response=False):
|
||||
self.link = None
|
||||
if resource != None:
|
||||
self.t = resource.size # Transfer size
|
||||
self.d = resource.total_size # Total uncompressed data size
|
||||
@@ -1040,6 +1229,9 @@ class ResourceAdvertisement:
|
||||
def is_compressed(self):
|
||||
return self.c
|
||||
|
||||
def get_link(self):
|
||||
return self.link
|
||||
|
||||
def pack(self, segment=0):
|
||||
hashmap_start = segment*ResourceAdvertisement.HASHMAP_MAX_LEN
|
||||
hashmap_end = min((segment+1)*(ResourceAdvertisement.HASHMAP_MAX_LEN), self.n)
|
||||
|
||||
@@ -23,5 +23,7 @@
|
||||
import os
|
||||
import glob
|
||||
|
||||
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
|
||||
py_modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
|
||||
modules = py_modules+pyc_modules
|
||||
__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))]))
|
||||
@@ -22,8 +22,9 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
from CRNS import RNS
|
||||
import argparse
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
@@ -32,15 +33,48 @@ from RNS._version import __version__
|
||||
|
||||
APP_NAME = "rncp"
|
||||
allow_all = False
|
||||
allow_fetch = False
|
||||
fetch_jail = None
|
||||
save_path = None
|
||||
show_phy_rates = 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
|
||||
REQ_FETCH_NOT_ALLOWED = 0xF0
|
||||
|
||||
es = " "
|
||||
erase_str = "\33[2K\r"
|
||||
|
||||
def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identity = False,
|
||||
limit = None, disable_auth = None, fetch_allowed = False, jail = None, save = None, announce = False):
|
||||
global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
allow_fetch = fetch_allowed
|
||||
identity = None
|
||||
if announce < 0:
|
||||
announce = False
|
||||
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
|
||||
|
||||
if jail != None:
|
||||
fetch_jail = os.path.abspath(os.path.expanduser(jail))
|
||||
RNS.log("Restricting fetch requests to paths under \""+fetch_jail+"\"", RNS.LOG_VERBOSE)
|
||||
|
||||
if save != None:
|
||||
sp = os.path.abspath(os.path.expanduser(save))
|
||||
if os.path.isdir(sp):
|
||||
if os.access(sp, os.W_OK):
|
||||
save_path = sp
|
||||
else:
|
||||
RNS.log("Output directory not writable", RNS.LOG_ERROR)
|
||||
RNS.exit(4)
|
||||
else:
|
||||
RNS.log("Output directory not found", RNS.LOG_ERROR)
|
||||
RNS.exit(3)
|
||||
|
||||
RNS.log("Saving received files in \""+save_path+"\"", RNS.LOG_VERBOSE)
|
||||
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
@@ -54,16 +88,48 @@ def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_ident
|
||||
|
||||
if display_identity:
|
||||
print("Identity : "+str(identity))
|
||||
print("Receiving on : "+RNS.prettyhexrep(destination.hash))
|
||||
exit(0)
|
||||
print("Listening on : "+RNS.prettyhexrep(destination.hash))
|
||||
RNS.exit(0)
|
||||
|
||||
if disable_auth:
|
||||
allow_all = True
|
||||
else:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
try:
|
||||
allowed_file_name = "allowed_identities"
|
||||
allowed_file = None
|
||||
if os.path.isfile(os.path.expanduser("/etc/rncp/"+allowed_file_name)):
|
||||
allowed_file = os.path.expanduser("/etc/rncp/"+allowed_file_name)
|
||||
elif os.path.isfile(os.path.expanduser("~/.config/rncp/"+allowed_file_name)):
|
||||
allowed_file = os.path.expanduser("~/.config/rncp/"+allowed_file_name)
|
||||
elif os.path.isfile(os.path.expanduser("~/.rncp/"+allowed_file_name)):
|
||||
allowed_file = os.path.expanduser("~/.rncp/"+allowed_file_name)
|
||||
if allowed_file != None:
|
||||
af = open(allowed_file, "r")
|
||||
al = af.read().replace("\r", "").split("\n")
|
||||
ali = []
|
||||
for a in al:
|
||||
if len(a) == dest_len:
|
||||
ali.append(a)
|
||||
|
||||
if len(ali) > 0:
|
||||
if not allowed:
|
||||
allowed = ali
|
||||
else:
|
||||
allowed.extend(ali)
|
||||
if len(ali) == 1:
|
||||
ms = "y"
|
||||
else:
|
||||
ms = "ies"
|
||||
|
||||
RNS.log("Loaded "+str(len(ali))+" allowed identit"+ms+" from "+str(allowed_file), RNS.LOG_VERBOSE)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while parsing allowed_identities file. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
if allowed != None:
|
||||
for a in allowed:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(a) != dest_len:
|
||||
raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
@@ -73,21 +139,82 @@ def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_ident
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
|
||||
if len(allowed_identity_hashes) < 1 and not disable_auth:
|
||||
print("Warning: No allowed identities configured, rncp will not accept any files!")
|
||||
|
||||
destination.set_link_established_callback(receive_link_established)
|
||||
print("rncp ready to receive on "+RNS.prettyhexrep(destination.hash))
|
||||
def fetch_request(path, data, request_id, link_id, remote_identity, requested_at):
|
||||
global allow_fetch, fetch_jail
|
||||
if not allow_fetch:
|
||||
return REQ_FETCH_NOT_ALLOWED
|
||||
|
||||
if not disable_announce:
|
||||
destination.announce()
|
||||
if fetch_jail:
|
||||
if data.startswith(fetch_jail+"/"):
|
||||
data = data.replace(fetch_jail+"/", "")
|
||||
file_path = os.path.abspath(os.path.expanduser(f"{fetch_jail}/{data}"))
|
||||
if not file_path.startswith(fetch_jail+"/"):
|
||||
RNS.log(f"Disallowing fetch request for {file_path} outside of fetch jail {fetch_jail}", RNS.LOG_WARNING)
|
||||
return REQ_FETCH_NOT_ALLOWED
|
||||
else:
|
||||
file_path = os.path.abspath(os.path.expanduser(f"{data}"))
|
||||
|
||||
target_link = None
|
||||
for link in RNS.Transport.active_links:
|
||||
if link.link_id == link_id:
|
||||
target_link = link
|
||||
|
||||
if not os.path.isfile(file_path):
|
||||
RNS.log("Client-requested file not found: "+str(file_path), RNS.LOG_VERBOSE)
|
||||
return False
|
||||
else:
|
||||
if target_link != None:
|
||||
RNS.log("Sending file "+str(file_path)+" to client", RNS.LOG_VERBOSE)
|
||||
|
||||
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")
|
||||
RNS.exit(1)
|
||||
|
||||
temp_file.write(filename_len.to_bytes(2, "big"))
|
||||
temp_file.write(filename_bytes)
|
||||
temp_file.write(real_file.read())
|
||||
temp_file.seek(0)
|
||||
|
||||
fetch_resource = RNS.Resource(temp_file, target_link)
|
||||
return True
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
destination.set_link_established_callback(client_link_established)
|
||||
if allow_fetch:
|
||||
if allow_all:
|
||||
RNS.log("Allowing unauthenticated fetch requests", RNS.LOG_WARNING)
|
||||
destination.register_request_handler("fetch_file", response_generator=fetch_request, allow=RNS.Destination.ALLOW_ALL)
|
||||
else:
|
||||
destination.register_request_handler("fetch_file", response_generator=fetch_request, allow=RNS.Destination.ALLOW_LIST, allowed_list=allowed_identity_hashes)
|
||||
|
||||
print("rncp listening on "+RNS.prettyhexrep(destination.hash))
|
||||
|
||||
if announce >= 0:
|
||||
def job():
|
||||
destination.announce()
|
||||
if announce > 0:
|
||||
while True:
|
||||
time.sleep(announce)
|
||||
destination.announce()
|
||||
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
def receive_link_established(link):
|
||||
def client_link_established(link):
|
||||
RNS.log("Incoming link established", RNS.LOG_VERBOSE)
|
||||
link.set_remote_identified_callback(receive_sender_identified)
|
||||
link.set_resource_strategy(RNS.Link.ACCEPT_APP)
|
||||
@@ -130,6 +257,7 @@ def receive_resource_started(resource):
|
||||
print("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str)
|
||||
|
||||
def receive_resource_concluded(resource):
|
||||
global save_path
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
print(str(resource)+" completed")
|
||||
|
||||
@@ -138,12 +266,20 @@ def receive_resource_concluded(resource):
|
||||
filename = resource.data.read(filename_len).decode("utf-8")
|
||||
|
||||
counter = 0
|
||||
saved_filename = filename
|
||||
while os.path.isfile(saved_filename):
|
||||
if save_path:
|
||||
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
|
||||
if not saved_filename.startswith(save_path+"/"):
|
||||
RNS.log(f"Invalid save path {saved_filename}, ignoring", RNS.LOG_ERROR)
|
||||
return
|
||||
else:
|
||||
saved_filename = filename
|
||||
|
||||
full_save_path = saved_filename
|
||||
while os.path.isfile(full_save_path):
|
||||
counter += 1
|
||||
saved_filename = filename+"."+str(counter)
|
||||
full_save_path = saved_filename+"."+str(counter)
|
||||
|
||||
file = open(saved_filename, "wb")
|
||||
file = open(full_save_path, "wb")
|
||||
file.write(resource.data.read())
|
||||
file.close()
|
||||
|
||||
@@ -157,34 +293,59 @@ resource_done = False
|
||||
current_resource = None
|
||||
stats = []
|
||||
speed = 0.0
|
||||
phy_speed = 0.0
|
||||
phy_got_total = 0
|
||||
def sender_progress(resource):
|
||||
stats_max = 32
|
||||
global current_resource, stats, speed, resource_done
|
||||
global current_resource, stats, speed, phy_speed, phy_got_total, resource_done
|
||||
current_resource = resource
|
||||
|
||||
now = time.time()
|
||||
got = current_resource.get_progress()*current_resource.total_size
|
||||
entry = [now, got]
|
||||
got = current_resource.get_progress()*current_resource.get_data_size()
|
||||
phy_got = current_resource.get_segment_progress()*current_resource.get_transfer_size()
|
||||
|
||||
entry = [now, got, phy_got]
|
||||
stats.append(entry)
|
||||
|
||||
while len(stats) > stats_max:
|
||||
stats.pop(0)
|
||||
|
||||
span = now - stats[0][0]
|
||||
if span == 0:
|
||||
speed = 0
|
||||
phy_speed = 0
|
||||
|
||||
else:
|
||||
diff = got - stats[0][1]
|
||||
speed = diff/span
|
||||
|
||||
phy_diff = phy_got - stats[0][2]
|
||||
if phy_diff > 0:
|
||||
phy_speed = phy_diff/span
|
||||
# phy_got_total += phy_diff
|
||||
|
||||
if resource.status < RNS.Resource.COMPLETE:
|
||||
resource_done = False
|
||||
else:
|
||||
resource_done = True
|
||||
|
||||
link = None
|
||||
def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT):
|
||||
global current_resource, resource_done, link, speed
|
||||
from tempfile import TemporaryFile
|
||||
def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, save=None):
|
||||
global current_resource, resource_done, link, speed, show_phy_rates, save_path
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
show_phy_rates = phy_rates
|
||||
|
||||
if save:
|
||||
sp = os.path.abspath(os.path.expanduser(save))
|
||||
if os.path.isdir(sp):
|
||||
if os.access(sp, os.W_OK):
|
||||
save_path = sp
|
||||
else:
|
||||
RNS.log("Output directory not writable", RNS.LOG_ERROR)
|
||||
RNS.exit(4)
|
||||
else:
|
||||
RNS.log("Output directory not found", RNS.LOG_ERROR)
|
||||
RNS.exit(3)
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
@@ -196,37 +357,18 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(1)
|
||||
|
||||
|
||||
file_path = os.path.expanduser(file)
|
||||
if not os.path.isfile(file_path):
|
||||
print("File not found")
|
||||
exit(1)
|
||||
|
||||
temp_file = TemporaryFile()
|
||||
real_file = open(file_path, "rb")
|
||||
filename_bytes = os.path.basename(file_path).encode("utf-8")
|
||||
filename_len = len(filename_bytes)
|
||||
|
||||
if filename_len > 0xFFFF:
|
||||
print("Filename exceeds max size, cannot send")
|
||||
exit(1)
|
||||
else:
|
||||
print("Preparing file...", end=" ")
|
||||
|
||||
temp_file.write(filename_len.to_bytes(2, "big"))
|
||||
temp_file.write(filename_bytes)
|
||||
temp_file.write(real_file.read())
|
||||
temp_file.seek(0)
|
||||
|
||||
print("\r \r", end="")
|
||||
RNS.exit(1)
|
||||
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
|
||||
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
if identity == None:
|
||||
RNS.log("Could not load identity for rncp. The identity file at \""+str(identity_path)+"\" may be corrupt or unreadable.", RNS.LOG_ERROR)
|
||||
RNS.exit(2)
|
||||
else:
|
||||
identity = None
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
@@ -235,23 +377,298 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
|
||||
if silent:
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested")
|
||||
else:
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=es)
|
||||
sys.stdout.flush()
|
||||
|
||||
i = 0
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
estab_timeout = time.time()+timeout
|
||||
while not RNS.Transport.has_path(destination_hash) and time.time() < estab_timeout:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
if not silent:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
print("\r \rPath not found")
|
||||
exit(1)
|
||||
if silent:
|
||||
print("Path not found")
|
||||
else:
|
||||
print(f"{erase_str}Path not found")
|
||||
RNS.exit(1)
|
||||
else:
|
||||
print("\r \rEstablishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=" ")
|
||||
if silent:
|
||||
print("Establishing link with "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print(f"{erase_str}Establishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=es)
|
||||
|
||||
listener_identity = RNS.Identity.recall(destination_hash)
|
||||
listener_destination = RNS.Destination(
|
||||
listener_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
APP_NAME,
|
||||
"receive"
|
||||
)
|
||||
|
||||
link = RNS.Link(listener_destination)
|
||||
while link.status != RNS.Link.ACTIVE and time.time() < estab_timeout:
|
||||
if not silent:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
if silent:
|
||||
print("Could not establish link with "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print(f"{erase_str}Could not establish link with "+RNS.prettyhexrep(destination_hash))
|
||||
RNS.exit(1)
|
||||
else:
|
||||
if silent:
|
||||
print("Requesting file from remote...")
|
||||
else:
|
||||
print(f"{erase_str}Requesting file from remote ", end=es)
|
||||
|
||||
link.identify(identity)
|
||||
|
||||
request_resolved = False
|
||||
request_status = "unknown"
|
||||
resource_resolved = False
|
||||
resource_status = "unrequested"
|
||||
current_resource = None
|
||||
def request_response(request_receipt):
|
||||
nonlocal request_resolved, request_status
|
||||
if request_receipt.response == False:
|
||||
request_status = "not_found"
|
||||
elif request_receipt.response == None:
|
||||
request_status = "remote_error"
|
||||
elif request_receipt.response == REQ_FETCH_NOT_ALLOWED:
|
||||
request_status = "fetch_not_allowed"
|
||||
else:
|
||||
request_status = "found"
|
||||
|
||||
request_resolved = True
|
||||
|
||||
def request_failed(request_receipt):
|
||||
nonlocal request_resolved, request_status
|
||||
request_status = "unknown"
|
||||
request_resolved = True
|
||||
|
||||
def fetch_resource_started(resource):
|
||||
nonlocal resource_status
|
||||
current_resource = resource
|
||||
current_resource.progress_callback(sender_progress)
|
||||
resource_status = "started"
|
||||
|
||||
def fetch_resource_concluded(resource):
|
||||
nonlocal resource_resolved, resource_status
|
||||
global save_path
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
if resource.total_size > 4:
|
||||
filename_len = int.from_bytes(resource.data.read(2), "big")
|
||||
filename = resource.data.read(filename_len).decode("utf-8")
|
||||
|
||||
counter = 0
|
||||
if save_path:
|
||||
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
|
||||
|
||||
else:
|
||||
saved_filename = filename
|
||||
|
||||
full_save_path = saved_filename
|
||||
while os.path.isfile(full_save_path):
|
||||
counter += 1
|
||||
full_save_path = saved_filename+"."+str(counter)
|
||||
|
||||
file = open(full_save_path, "wb")
|
||||
file.write(resource.data.read())
|
||||
file.close()
|
||||
resource_status = "completed"
|
||||
|
||||
else:
|
||||
print("Invalid data received, ignoring resource")
|
||||
resource_status = "invalid_data"
|
||||
|
||||
else:
|
||||
print("Resource failed")
|
||||
resource_status = "failed"
|
||||
|
||||
resource_resolved = True
|
||||
|
||||
link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
|
||||
link.set_resource_started_callback(fetch_resource_started)
|
||||
link.set_resource_concluded_callback(fetch_resource_concluded)
|
||||
link.request("fetch_file", data=file, response_callback=request_response, failed_callback=request_failed)
|
||||
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
while not request_resolved:
|
||||
if not silent:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if request_status == "fetch_not_allowed":
|
||||
if not silent: print(f"{erase_str}", end="")
|
||||
print("Fetch request failed, fetching the file "+str(file)+" was not allowed by the remote")
|
||||
link.teardown()
|
||||
time.sleep(0.15)
|
||||
RNS.exit(0)
|
||||
elif request_status == "not_found":
|
||||
if not silent: print(f"{erase_str}", end="")
|
||||
print("Fetch request failed, the file "+str(file)+" was not found on the remote")
|
||||
link.teardown()
|
||||
time.sleep(0.15)
|
||||
RNS.exit(0)
|
||||
elif request_status == "remote_error":
|
||||
if not silent: print(f"{erase_str}", end="")
|
||||
print("Fetch request failed due to an error on the remote system")
|
||||
link.teardown()
|
||||
time.sleep(0.15)
|
||||
RNS.exit(0)
|
||||
elif request_status == "unknown":
|
||||
if not silent: print(f"{erase_str}", end="")
|
||||
print("Fetch request failed due to an unknown error (probably not authorised)")
|
||||
link.teardown()
|
||||
time.sleep(0.15)
|
||||
RNS.exit(0)
|
||||
elif request_status == "found":
|
||||
if not silent: print(f"{erase_str}", end="")
|
||||
|
||||
while not resource_resolved:
|
||||
if not silent:
|
||||
time.sleep(0.1)
|
||||
if current_resource:
|
||||
prg = current_resource.get_progress()
|
||||
percent = round(prg * 100.0, 1)
|
||||
if show_phy_rates:
|
||||
pss = size_str(phy_speed, "b")
|
||||
phy_str = f" ({pss}ps at physical layer)"
|
||||
else:
|
||||
phy_str = ""
|
||||
ps = size_str(int(prg*current_resource.total_size))
|
||||
ts = size_str(current_resource.total_size)
|
||||
ss = size_str(speed, "b")
|
||||
stat_str = f"{percent}% - {ps} of {ts} - {ss}ps{phy_str}"
|
||||
if prg != 1.0:
|
||||
print(f"{erase_str}Transferring file {syms[i]} {stat_str}", end=es)
|
||||
else:
|
||||
print(f"{erase_str}Transfer complete {stat_str}", end=es)
|
||||
else:
|
||||
print(f"{erase_str}Waiting for transfer to start {syms[i]} ", end=es)
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if not current_resource or current_resource.status != RNS.Resource.COMPLETE:
|
||||
if silent:
|
||||
print("The transfer failed")
|
||||
else:
|
||||
print(f"{erase_str}The transfer failed")
|
||||
RNS.exit(1)
|
||||
else:
|
||||
if silent:
|
||||
print(str(file)+" fetched from "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print("\n"+str(file)+" fetched from "+RNS.prettyhexrep(destination_hash))
|
||||
link.teardown()
|
||||
RNS.exit(0)
|
||||
|
||||
link.teardown()
|
||||
RNS.exit(0)
|
||||
|
||||
|
||||
def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, no_compress=False):
|
||||
global current_resource, resource_done, link, speed, show_phy_rates, phy_got_total, phy_speed
|
||||
from tempfile import TemporaryFile
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
show_phy_rates = phy_rates
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination) != dest_len:
|
||||
raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
RNS.exit(1)
|
||||
|
||||
|
||||
file_path = os.path.expanduser(file)
|
||||
if not os.path.isfile(file_path):
|
||||
print("File not found")
|
||||
RNS.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")
|
||||
RNS.exit(1)
|
||||
else:
|
||||
print("Preparing file...", end=es)
|
||||
|
||||
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(f"{erase_str}", 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("Could not load identity for rncp. The identity file at \""+str(identity_path)+"\" may be corrupt or unreadable.", RNS.LOG_ERROR)
|
||||
RNS.exit(2)
|
||||
else:
|
||||
identity = None
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
if silent:
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested")
|
||||
else:
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=es)
|
||||
sys.stdout.flush()
|
||||
|
||||
i = 0
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
estab_timeout = time.time()+timeout
|
||||
while not RNS.Transport.has_path(destination_hash) and time.time() < estab_timeout:
|
||||
if not silent:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
if silent:
|
||||
print("Path not found")
|
||||
else:
|
||||
print(f"{erase_str}Path not found")
|
||||
RNS.exit(1)
|
||||
else:
|
||||
if silent:
|
||||
print("Establishing link with "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print(f"{erase_str}Establishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=es)
|
||||
|
||||
receiver_identity = RNS.Identity.recall(destination_hash)
|
||||
receiver_destination = RNS.Destination(
|
||||
@@ -264,53 +681,108 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
|
||||
link = RNS.Link(receiver_destination)
|
||||
while link.status != RNS.Link.ACTIVE and time.time() < estab_timeout:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
if not silent:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
print("\r \rCould not establish link with "+RNS.prettyhexrep(destination_hash))
|
||||
exit(1)
|
||||
if time.time() > estab_timeout:
|
||||
if silent:
|
||||
print("Link establishment with "+RNS.prettyhexrep(destination_hash)+" timed out")
|
||||
else:
|
||||
print(f"{erase_str}Link establishment with "+RNS.prettyhexrep(destination_hash)+" timed out")
|
||||
RNS.exit(1)
|
||||
elif not RNS.Transport.has_path(destination_hash):
|
||||
if silent:
|
||||
print("No path found to "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print(f"{erase_str}No path found to "+RNS.prettyhexrep(destination_hash))
|
||||
RNS.exit(1)
|
||||
else:
|
||||
print("\r \rAdvertising file resource ", end=" ")
|
||||
if silent:
|
||||
print("Advertising file resource...")
|
||||
else:
|
||||
print(f"{erase_str}Advertising file resource ", end=es)
|
||||
|
||||
link.identify(identity)
|
||||
resource = RNS.Resource(temp_file, link, callback = sender_progress, progress_callback = sender_progress)
|
||||
auto_compress = True
|
||||
if no_compress:
|
||||
auto_compress = False
|
||||
resource = RNS.Resource(temp_file, link, callback = sender_progress, progress_callback = sender_progress, auto_compress = auto_compress)
|
||||
current_resource = resource
|
||||
|
||||
while resource.status < RNS.Resource.TRANSFERRING:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
if not silent:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
resource_started_at = time.time()
|
||||
|
||||
if resource.status > RNS.Resource.COMPLETE:
|
||||
print("\r \rFile was not accepted by "+RNS.prettyhexrep(destination_hash))
|
||||
exit(1)
|
||||
if silent:
|
||||
print("File was not accepted by "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print(f"{erase_str}File was not accepted by "+RNS.prettyhexrep(destination_hash))
|
||||
RNS.exit(1)
|
||||
else:
|
||||
print("\r \rTransferring file ", end=" ")
|
||||
if silent:
|
||||
print("Transferring file...")
|
||||
else:
|
||||
print(f"{erase_str}Transferring file ", end=es)
|
||||
|
||||
while not resource_done:
|
||||
def progress_update(i, done=False):
|
||||
time.sleep(0.1)
|
||||
prg = current_resource.get_progress()
|
||||
percent = round(prg * 100.0, 1)
|
||||
stat_str = str(percent)+"% - " + size_str(int(prg*current_resource.total_size)) + " of " + size_str(current_resource.total_size) + " - " +size_str(speed, "b")+"ps"
|
||||
print("\r \rTransferring file "+syms[i]+" "+stat_str, end=" ")
|
||||
if show_phy_rates and not resource_done:
|
||||
pss = size_str(phy_speed, "b")
|
||||
phy_str = f" ({pss}ps at physical layer)"
|
||||
else:
|
||||
phy_str = ""
|
||||
es = " "
|
||||
cs = size_str(int(prg*current_resource.total_size))
|
||||
ts = size_str(current_resource.total_size)
|
||||
ss = size_str(speed, "b")
|
||||
stat_str = f"{percent}% - {cs} of {ts} - {ss}ps{phy_str}"
|
||||
if not done:
|
||||
print(f"{erase_str}Transferring file "+syms[i]+" "+stat_str, end=es)
|
||||
else:
|
||||
print(f"{erase_str}Transfer complete "+stat_str, end=es)
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
return i
|
||||
|
||||
while not resource_done:
|
||||
if not silent:
|
||||
i = progress_update(i)
|
||||
|
||||
resource_concluded_at = time.time()
|
||||
transfer_time = resource_concluded_at - resource_started_at
|
||||
speed = current_resource.total_size/transfer_time
|
||||
# phy_speed = phy_got_total/transfer_time
|
||||
|
||||
if not silent:
|
||||
i = progress_update(i, done=True)
|
||||
|
||||
if current_resource.status != RNS.Resource.COMPLETE:
|
||||
print("\r \rThe transfer failed")
|
||||
exit(1)
|
||||
if silent:
|
||||
print("The transfer failed")
|
||||
else:
|
||||
print(f"{erase_str}The transfer failed")
|
||||
RNS.exit(1)
|
||||
else:
|
||||
print("\r \r"+str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
|
||||
if silent:
|
||||
print(str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print("\n"+str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
|
||||
link.teardown()
|
||||
time.sleep(0.25)
|
||||
real_file.close()
|
||||
temp_file.close()
|
||||
exit(0)
|
||||
RNS.exit(0)
|
||||
|
||||
def main():
|
||||
try:
|
||||
@@ -320,29 +792,57 @@ def main():
|
||||
parser.add_argument("--config", metavar="path", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0, help="increase verbosity")
|
||||
parser.add_argument('-q', '--quiet', action='count', default=0, help="decrease verbosity")
|
||||
parser.add_argument("-S", '--silent', action='store_true', default=False, help="disable transfer progress output")
|
||||
parser.add_argument("-l", '--listen', action='store_true', default=False, help="listen for incoming transfer requests")
|
||||
parser.add_argument("-C", '--no-compress', action='store_true', default=False, help="disable automatic compression")
|
||||
parser.add_argument("-F", '--allow-fetch', action='store_true', default=False, help="allow authenticated clients to fetch files")
|
||||
parser.add_argument("-f", '--fetch', action='store_true', default=False, help="fetch file from remote listener instead of sending")
|
||||
parser.add_argument("-j", "--jail", metavar="path", action="store", default=None, help="restrict fetch requests to specified path", type=str)
|
||||
parser.add_argument("-s", "--save", metavar="path", action="store", default=None, help="save received files in specified path", type=str)
|
||||
parser.add_argument("-b", action='store', metavar="seconds", default=-1, help="announce interval, 0 to only announce at startup", type=int)
|
||||
parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="allow this identity (or add in ~/.rncp/allowed_identities)", type=str)
|
||||
parser.add_argument('-n', '--no-auth', action='store_true', default=False, help="accept requests from anyone")
|
||||
parser.add_argument('-p', '--print-identity', action='store_true', default=False, help="print identity and destination info and exit")
|
||||
parser.add_argument("-r", '--receive', action='store_true', default=False, help="wait for incoming files")
|
||||
parser.add_argument("-b", '--no-announce', action='store_true', default=False, help="don't announce at program start")
|
||||
parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="accept from this identity", type=str)
|
||||
parser.add_argument('-n', '--no-auth', action='store_true', default=False, help="accept files from anyone")
|
||||
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="sender timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
|
||||
parser.add_argument('-P', '--phy-rates', action='store_true', default=False, help="display physical layer transfer rates")
|
||||
# parser.add_argument("--limit", action="store", metavar="files", type=float, help="maximum number of files to accept", default=None)
|
||||
parser.add_argument("--version", action="version", version="rncp {version}".format(version=__version__))
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.receive or args.print_identity:
|
||||
receive(
|
||||
if args.listen or args.print_identity:
|
||||
listen(
|
||||
configdir = args.config,
|
||||
verbosity=args.verbose,
|
||||
quietness=args.quiet,
|
||||
allowed = args.allowed,
|
||||
fetch_allowed = args.allow_fetch,
|
||||
jail = args.jail,
|
||||
save = args.save,
|
||||
display_identity=args.print_identity,
|
||||
# limit=args.limit,
|
||||
disable_auth=args.no_auth,
|
||||
disable_announce=args.no_announce,
|
||||
announce=args.b,
|
||||
)
|
||||
|
||||
elif args.fetch:
|
||||
if args.destination != None and args.file != None:
|
||||
fetch(
|
||||
configdir = args.config,
|
||||
verbosity = args.verbose,
|
||||
quietness = args.quiet,
|
||||
destination = args.destination,
|
||||
file = args.file,
|
||||
timeout = args.w,
|
||||
silent = args.silent,
|
||||
phy_rates = args.phy_rates,
|
||||
save = args.save,
|
||||
)
|
||||
else:
|
||||
print("")
|
||||
parser.print_help()
|
||||
print("")
|
||||
|
||||
elif args.destination != None and args.file != None:
|
||||
send(
|
||||
configdir = args.config,
|
||||
@@ -351,6 +851,9 @@ def main():
|
||||
destination = args.destination,
|
||||
file = args.file,
|
||||
timeout = args.w,
|
||||
silent = args.silent,
|
||||
phy_rates = args.phy_rates,
|
||||
no_compress = args.no_compress,
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -364,7 +867,7 @@ def main():
|
||||
resource.cancel()
|
||||
if link != None:
|
||||
link.teardown()
|
||||
exit()
|
||||
RNS.exit()
|
||||
|
||||
def size_str(num, suffix='B'):
|
||||
units = ['','K','M','G','T','P','E','Z']
|
||||
|
||||
@@ -64,19 +64,22 @@ def main():
|
||||
|
||||
parser.add_argument("--config", metavar="path", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument("-i", "--identity", metavar="identity", action="store", default=None, help="hexadecimal Reticulum Destination hash or path to Identity file", type=str)
|
||||
parser.add_argument("-g", "--generate", metavar="path", action="store", default=None, help="generate a new Identity")
|
||||
parser.add_argument("-g", "--generate", metavar="file", action="store", default=None, help="generate a new Identity")
|
||||
parser.add_argument("-m", "--import", dest="import_str", metavar="identity_data", action="store", default=None, help="import Reticulum identity in hex, base32 or base64 format", type=str)
|
||||
parser.add_argument("-x", "--export", action="store_true", default=None, help="export identity to hex, base32 or base64 format")
|
||||
|
||||
parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity")
|
||||
parser.add_argument("-q", "--quiet", action="count", default=0, help="decrease verbosity")
|
||||
|
||||
parser.add_argument("-a", "--announce", metavar="aspects", action="store", default=None, help="announce a destination based on this Identity")
|
||||
parser.add_argument("-H", "--hash", metavar="aspects", action="store", default=None, help="show destination hash5s for other aspects for this Identity")
|
||||
parser.add_argument("-e", "--encrypt", metavar="path", action="store", default=None, help="encrypt file")
|
||||
parser.add_argument("-d", "--decrypt", metavar="path", action="store", default=None, help="decrypt file")
|
||||
parser.add_argument("-H", "--hash", metavar="aspects", action="store", default=None, help="show destination hashes for other aspects for this Identity")
|
||||
parser.add_argument("-e", "--encrypt", metavar="file", action="store", default=None, help="encrypt file")
|
||||
parser.add_argument("-d", "--decrypt", metavar="file", action="store", default=None, help="decrypt file")
|
||||
parser.add_argument("-s", "--sign", metavar="path", action="store", default=None, help="sign file")
|
||||
parser.add_argument("-V", "--validate", metavar="path", action="store", default=None, help="validate signature")
|
||||
|
||||
parser.add_argument("-r", "--read", metavar="path", action="store", default=None, help="input file path", type=str)
|
||||
parser.add_argument("-w", "--write", metavar="path", action="store", default=None, help="output file path", type=str)
|
||||
parser.add_argument("-r", "--read", metavar="file", action="store", default=None, help="input file path", type=str)
|
||||
parser.add_argument("-w", "--write", metavar="file", action="store", default=None, help="output file path", type=str)
|
||||
parser.add_argument("-f", "--force", action="store_true", default=None, help="write output even if it overwrites existing files")
|
||||
parser.add_argument("-I", "--stdin", action="store_true", default=False, help=argparse.SUPPRESS) # "read input from STDIN instead of file"
|
||||
parser.add_argument("-O", "--stdout", action="store_true", default=False, help=argparse.SUPPRESS) # help="write output to STDOUT instead of file",
|
||||
@@ -86,7 +89,8 @@ def main():
|
||||
parser.add_argument("-p", "--print-identity", action="store_true", default=False, help="print identity info and exit")
|
||||
parser.add_argument("-P", "--print-private", action="store_true", default=False, help="allow displaying private keys")
|
||||
|
||||
parser.add_argument("-b", "--base64", action="store_true", default=False, help=argparse.SUPPRESS) # help="Use base64-encoded input and output")
|
||||
parser.add_argument("-b", "--base64", action="store_true", default=False, help="Use base64-encoded input and output")
|
||||
parser.add_argument("-B", "--base32", action="store_true", default=False, help="Use base32-encoded input and output")
|
||||
|
||||
parser.add_argument("--version", action="version", version="rnid {version}".format(version=__version__))
|
||||
|
||||
@@ -110,6 +114,59 @@ def main():
|
||||
args.read = args.sign
|
||||
|
||||
identity_str = args.identity
|
||||
if args.import_str:
|
||||
identity_bytes = None
|
||||
try:
|
||||
if args.base64:
|
||||
identity_bytes = base64.urlsafe_b64decode(args.import_str)
|
||||
elif args.base32:
|
||||
identity_bytes = base64.b32decode(args.import_str)
|
||||
else:
|
||||
identity_bytes = bytes.fromhex(args.import_str)
|
||||
except Exception as e:
|
||||
print("Invalid identity data specified for import: "+str(e))
|
||||
exit(41)
|
||||
|
||||
try:
|
||||
identity = RNS.Identity.from_bytes(identity_bytes)
|
||||
except Exception as e:
|
||||
print("Could not create Reticulum identity from specified data: "+str(e))
|
||||
exit(42)
|
||||
|
||||
RNS.log("Identity imported")
|
||||
if args.base64:
|
||||
RNS.log("Public Key : "+base64.urlsafe_b64encode(identity.get_public_key()).decode("utf-8"))
|
||||
elif args.base32:
|
||||
RNS.log("Public Key : "+base64.b32encode(identity.get_public_key()).decode("utf-8"))
|
||||
else:
|
||||
RNS.log("Public Key : "+RNS.hexrep(identity.get_public_key(), delimit=False))
|
||||
if identity.prv:
|
||||
if args.print_private:
|
||||
if args.base64:
|
||||
RNS.log("Private Key : "+base64.urlsafe_b64encode(identity.get_private_key()).decode("utf-8"))
|
||||
elif args.base32:
|
||||
RNS.log("Private Key : "+base64.b32encode(identity.get_private_key()).decode("utf-8"))
|
||||
else:
|
||||
RNS.log("Private Key : "+RNS.hexrep(identity.get_private_key(), delimit=False))
|
||||
else:
|
||||
RNS.log("Private Key : Hidden")
|
||||
|
||||
if args.write:
|
||||
try:
|
||||
wp = os.path.expanduser(args.write)
|
||||
if not os.path.isfile(wp) or args.force:
|
||||
identity.to_file(wp)
|
||||
RNS.log("Wrote imported identity to "+str(args.write))
|
||||
else:
|
||||
print("File "+str(wp)+" already exists, not overwriting")
|
||||
exit(43)
|
||||
|
||||
except Exception as e:
|
||||
print("Error while writing imported identity to file: "+str(e))
|
||||
exit(44)
|
||||
|
||||
exit(0)
|
||||
|
||||
if not args.generate and not identity_str:
|
||||
print("\nNo identity provided, cannot continue\n")
|
||||
parser.print_help()
|
||||
@@ -166,8 +223,8 @@ def main():
|
||||
RNS.log("Identity request timed out", RNS.LOG_ERROR)
|
||||
exit(6)
|
||||
else:
|
||||
RNS.log("Received Identity "+str(identity)+" for destination "+RNS.prettyhexrep(destination_hash)+" from the network")
|
||||
identity = RNS.Identity.recall(destination_hash)
|
||||
RNS.log("Received Identity "+str(identity)+" for destination "+RNS.prettyhexrep(destination_hash)+" from the network")
|
||||
|
||||
else:
|
||||
RNS.log("Recalled Identity "+str(identity)+" for destination "+RNS.prettyhexrep(destination_hash))
|
||||
@@ -196,7 +253,7 @@ def main():
|
||||
if args.hash:
|
||||
try:
|
||||
aspects = args.hash.split(".")
|
||||
if not len(aspects) > 1:
|
||||
if not len(aspects) > 0:
|
||||
RNS.log("Invalid destination aspects specified", RNS.LOG_ERROR)
|
||||
exit(32)
|
||||
else:
|
||||
@@ -229,6 +286,7 @@ def main():
|
||||
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, app_name, *aspects)
|
||||
RNS.log("Created destination "+str(destination))
|
||||
RNS.log("Announcing destination "+RNS.prettyhexrep(destination.hash))
|
||||
time.sleep(1.1)
|
||||
destination.announce()
|
||||
time.sleep(0.25)
|
||||
exit(0)
|
||||
@@ -246,14 +304,38 @@ def main():
|
||||
exit(0)
|
||||
|
||||
if args.print_identity:
|
||||
RNS.log("Public Key : "+RNS.hexrep(identity.pub_bytes, delimit=False))
|
||||
if args.base64:
|
||||
RNS.log("Public Key : "+base64.urlsafe_b64encode(identity.get_public_key()).decode("utf-8"))
|
||||
elif args.base32:
|
||||
RNS.log("Public Key : "+base64.b32encode(identity.get_public_key()).decode("utf-8"))
|
||||
else:
|
||||
RNS.log("Public Key : "+RNS.hexrep(identity.get_public_key(), delimit=False))
|
||||
if identity.prv:
|
||||
if args.print_private:
|
||||
RNS.log("Private Key : "+RNS.hexrep(identity.prv_bytes, delimit=False))
|
||||
if args.base64:
|
||||
RNS.log("Private Key : "+base64.urlsafe_b64encode(identity.get_private_key()).decode("utf-8"))
|
||||
elif args.base32:
|
||||
RNS.log("Private Key : "+base64.b32encode(identity.get_private_key()).decode("utf-8"))
|
||||
else:
|
||||
RNS.log("Private Key : "+RNS.hexrep(identity.get_private_key(), delimit=False))
|
||||
else:
|
||||
RNS.log("Private Key : Hidden")
|
||||
exit(0)
|
||||
|
||||
if args.export:
|
||||
if identity.prv:
|
||||
if args.base64:
|
||||
RNS.log("Exported Identity : "+base64.urlsafe_b64encode(identity.get_private_key()).decode("utf-8"))
|
||||
elif args.base32:
|
||||
RNS.log("Exported Identity : "+base64.b32encode(identity.get_private_key()).decode("utf-8"))
|
||||
else:
|
||||
RNS.log("Exported Identity : "+RNS.hexrep(identity.get_private_key(), delimit=False))
|
||||
else:
|
||||
RNS.log("Identity doesn't hold a private key, cannot export")
|
||||
exit(50)
|
||||
|
||||
exit(0)
|
||||
|
||||
if args.validate:
|
||||
if not args.read and args.validate.lower().endswith("."+SIG_EXT):
|
||||
args.read = str(args.validate).replace("."+SIG_EXT, "")
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2023 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import argparse
|
||||
import time
|
||||
|
||||
from RNS._version import __version__
|
||||
|
||||
|
||||
def program_setup(configdir, verbosity = 0, quietness = 0, service = False):
|
||||
targetverbosity = verbosity-quietness
|
||||
|
||||
if service:
|
||||
targetlogdest = RNS.LOG_FILE
|
||||
targetverbosity = None
|
||||
else:
|
||||
targetlogdest = RNS.LOG_STDOUT
|
||||
|
||||
reticulum = RNS.Reticulum(configdir=configdir, verbosity=targetverbosity, logdest=targetlogdest)
|
||||
exit(0)
|
||||
|
||||
def main():
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description="Reticulum Distributed Identity Resolver")
|
||||
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0)
|
||||
parser.add_argument('-q', '--quiet', action='count', default=0)
|
||||
parser.add_argument("--exampleconfig", action='store_true', default=False, help="print verbose configuration example to stdout and exit")
|
||||
parser.add_argument("--version", action="version", version="ir {version}".format(version=__version__))
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.exampleconfig:
|
||||
print(__example_rns_config__)
|
||||
exit()
|
||||
|
||||
if args.config:
|
||||
configarg = args.config
|
||||
else:
|
||||
configarg = None
|
||||
|
||||
program_setup(configdir = configarg, verbosity=args.verbose, quietness=args.quiet)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
|
||||
__example_rns_config__ = '''# This is an example Identity Resolver file.
|
||||
'''
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -23,14 +23,95 @@
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
|
||||
from RNS._version import __version__
|
||||
|
||||
remote_link = None
|
||||
def connect_remote(destination_hash, auth_identity, timeout, no_output = False):
|
||||
global remote_link, reticulum
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
if not no_output:
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested", end=" ")
|
||||
sys.stdout.flush()
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
pr_time = time.time()
|
||||
while not RNS.Transport.has_path(destination_hash):
|
||||
time.sleep(0.1)
|
||||
if time.time() - pr_time > timeout:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Path request timed out")
|
||||
exit(12)
|
||||
|
||||
remote_identity = RNS.Identity.recall(destination_hash)
|
||||
|
||||
def remote_link_closed(link):
|
||||
if link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("The link timed out, exiting now")
|
||||
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("The link was closed by the server, exiting now")
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Link closed unexpectedly, exiting now")
|
||||
exit(10)
|
||||
|
||||
def remote_link_established(link):
|
||||
global remote_link
|
||||
link.identify(auth_identity)
|
||||
remote_link = link
|
||||
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Establishing link with remote transport instance...", end=" ")
|
||||
sys.stdout.flush()
|
||||
|
||||
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
|
||||
link = RNS.Link(remote_destination)
|
||||
link.set_link_established_callback(remote_link_established)
|
||||
link.set_link_closed_callback(remote_link_closed)
|
||||
|
||||
def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity, timeout, drop_queues,
|
||||
drop_via, max_hops, remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT,
|
||||
no_output=False, json=False):
|
||||
global remote_link, reticulum
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
if remote:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(remote) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
identity_hash = bytes.fromhex(remote)
|
||||
remote_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.remote.management", identity_hash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
|
||||
identity = RNS.Identity.from_file(os.path.expanduser(management_identity))
|
||||
if identity == None:
|
||||
raise ValueError("Could not load management identity from "+str(management_identity))
|
||||
|
||||
try:
|
||||
connect_remote(remote_hash, identity, remote_timeout, no_output)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(20)
|
||||
|
||||
while remote_link == None:
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity, timeout, drop_queues):
|
||||
if table:
|
||||
destination_hash = None
|
||||
if destination_hexhash != None:
|
||||
@@ -46,23 +127,50 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
table = sorted(reticulum.get_path_table(), key=lambda e: (e["interface"], e["hops"]) )
|
||||
if not remote_link:
|
||||
table = sorted(reticulum.get_path_table(max_hops=max_hops), key=lambda e: (e["interface"], e["hops"]) )
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Sending request...", end=" ")
|
||||
sys.stdout.flush()
|
||||
receipt = remote_link.request("/path", data = ["table", destination_hash, max_hops])
|
||||
while not receipt.concluded():
|
||||
time.sleep(0.1)
|
||||
response = receipt.get_response()
|
||||
if response:
|
||||
table = response
|
||||
print("\r \r", end="")
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("The remote request failed. Likely authentication failure.")
|
||||
exit(10)
|
||||
|
||||
displayed = 0
|
||||
for path in table:
|
||||
if destination_hash == None or destination_hash == path["hash"]:
|
||||
displayed += 1
|
||||
exp_str = RNS.timestamp_str(path["expires"])
|
||||
if path["hops"] == 1:
|
||||
m_str = " "
|
||||
else:
|
||||
m_str = "s"
|
||||
print(RNS.prettyhexrep(path["hash"])+" is "+str(path["hops"])+" hop"+m_str+" away via "+RNS.prettyhexrep(path["via"])+" on "+path["interface"]+" expires "+RNS.timestamp_str(path["expires"]))
|
||||
if json:
|
||||
import json
|
||||
for p in table:
|
||||
for k in p:
|
||||
if isinstance(p[k], bytes):
|
||||
p[k] = RNS.hexrep(p[k], delimit=False)
|
||||
|
||||
if destination_hash != None and displayed == 0:
|
||||
print("No path known")
|
||||
sys.exit(1)
|
||||
print(json.dumps(table))
|
||||
exit()
|
||||
else:
|
||||
for path in table:
|
||||
if destination_hash == None or destination_hash == path["hash"]:
|
||||
displayed += 1
|
||||
exp_str = RNS.timestamp_str(path["expires"])
|
||||
if path["hops"] == 1:
|
||||
m_str = " "
|
||||
else:
|
||||
m_str = "s"
|
||||
print(RNS.prettyhexrep(path["hash"])+" is "+str(path["hops"])+" hop"+m_str+" away via "+RNS.prettyhexrep(path["via"])+" on "+path["interface"]+" expires "+RNS.timestamp_str(path["expires"]))
|
||||
|
||||
if destination_hash != None and displayed == 0:
|
||||
print("No path known")
|
||||
sys.exit(1)
|
||||
|
||||
elif rates:
|
||||
destination_hash = None
|
||||
@@ -79,60 +187,99 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
table = sorted(reticulum.get_rate_table(), key=lambda e: e["last"] )
|
||||
|
||||
if len(table) == 0:
|
||||
print("No information available")
|
||||
|
||||
if not remote_link:
|
||||
table = reticulum.get_rate_table()
|
||||
else:
|
||||
displayed = 0
|
||||
for entry in table:
|
||||
if destination_hash == None or destination_hash == entry["hash"]:
|
||||
displayed += 1
|
||||
try:
|
||||
last_str = pretty_date(int(entry["last"]))
|
||||
start_ts = entry["timestamps"][0]
|
||||
span = max(time.time() - start_ts, 3600.0)
|
||||
span_hours = span/3600.0
|
||||
span_str = pretty_date(int(entry["timestamps"][0]))
|
||||
hour_rate = round(len(entry["timestamps"])/span_hours, 3)
|
||||
if hour_rate-int(hour_rate) == 0:
|
||||
hour_rate = int(hour_rate)
|
||||
|
||||
if entry["rate_violations"] > 0:
|
||||
if entry["rate_violations"] == 1:
|
||||
s_str = ""
|
||||
else:
|
||||
s_str = "s"
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Sending request...", end=" ")
|
||||
sys.stdout.flush()
|
||||
receipt = remote_link.request("/path", data = ["rates", destination_hash])
|
||||
while not receipt.concluded():
|
||||
time.sleep(0.1)
|
||||
response = receipt.get_response()
|
||||
if response:
|
||||
table = response
|
||||
print("\r \r", end="")
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("The remote request failed. Likely authentication failure.")
|
||||
exit(10)
|
||||
|
||||
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 = ""
|
||||
table = sorted(table, key=lambda e: e["last"])
|
||||
if json:
|
||||
import json
|
||||
for p in table:
|
||||
for k in p:
|
||||
if isinstance(p[k], bytes):
|
||||
p[k] = RNS.hexrep(p[k], delimit=False)
|
||||
|
||||
|
||||
print(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(json.dumps(table))
|
||||
exit()
|
||||
else:
|
||||
if len(table) == 0:
|
||||
print("No information available")
|
||||
sys.exit(1)
|
||||
|
||||
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...")
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Dropping announce queues on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
print("Dropping announce queues on all interfaces...")
|
||||
reticulum.drop_announce_queues()
|
||||
|
||||
elif drop:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Dropping path on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
@@ -145,17 +292,44 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
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)
|
||||
|
||||
elif drop_via:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Dropping all paths via specific transport instance on remote instances yet not implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
if reticulum.drop_all_via(destination_hash):
|
||||
print("Dropped all paths via "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print("Unable to drop paths via "+RNS.prettyhexrep(destination_hash)+". Does the transport instance exist?")
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Requesting paths on remote instances not implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
@@ -168,9 +342,6 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
|
||||
@@ -187,15 +358,20 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
|
||||
if RNS.Transport.has_path(destination_hash):
|
||||
hops = RNS.Transport.hops_to(destination_hash)
|
||||
next_hop = RNS.prettyhexrep(reticulum.get_next_hop(destination_hash))
|
||||
next_hop_interface = reticulum.get_next_hop_if_name(destination_hash)
|
||||
|
||||
if hops != 1:
|
||||
ms = "s"
|
||||
next_hop_bytes = reticulum.get_next_hop(destination_hash)
|
||||
if next_hop_bytes == None:
|
||||
print("\r \rError: Invalid path data returned")
|
||||
sys.exit(1)
|
||||
else:
|
||||
ms = ""
|
||||
next_hop = RNS.prettyhexrep(next_hop_bytes)
|
||||
next_hop_interface = reticulum.get_next_hop_if_name(destination_hash)
|
||||
|
||||
print("\rPath found, destination "+RNS.prettyhexrep(destination_hash)+" is "+str(hops)+" hop"+ms+" away via "+next_hop+" on "+next_hop_interface)
|
||||
if hops != 1:
|
||||
ms = "s"
|
||||
else:
|
||||
ms = ""
|
||||
|
||||
print("\rPath found, destination "+RNS.prettyhexrep(destination_hash)+" is "+str(hops)+" hop"+ms+" away via "+next_hop+" on "+next_hop_interface)
|
||||
else:
|
||||
print("\r \rPath not found")
|
||||
sys.exit(1)
|
||||
@@ -227,6 +403,16 @@ def main():
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--max",
|
||||
action="store",
|
||||
metavar="hops",
|
||||
type=int,
|
||||
help="maximum hops to filter path table by",
|
||||
default=None
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--rates",
|
||||
@@ -251,6 +437,13 @@ def main():
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-x", "--drop-via",
|
||||
action="store_true",
|
||||
help="drop all paths via specified transport instance",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
action="store",
|
||||
@@ -260,6 +453,41 @@ def main():
|
||||
default=RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-R",
|
||||
action="store",
|
||||
metavar="hash",
|
||||
help="transport identity hash of remote instance to manage",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
action="store",
|
||||
metavar="path",
|
||||
help="path to identity used for remote management",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-W",
|
||||
action="store",
|
||||
metavar="seconds",
|
||||
type=float,
|
||||
help="timeout before giving up on remote queries",
|
||||
default=RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="output in JSON format",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"destination",
|
||||
nargs="?",
|
||||
@@ -277,7 +505,7 @@ def main():
|
||||
else:
|
||||
configarg = None
|
||||
|
||||
if not args.drop_announces and not args.table and not args.rates and not args.destination:
|
||||
if not args.drop_announces and not args.table and not args.rates and not args.destination and not args.drop_via:
|
||||
print("")
|
||||
parser.print_help()
|
||||
print("")
|
||||
@@ -291,6 +519,12 @@ def main():
|
||||
verbosity = args.verbose,
|
||||
timeout = args.w,
|
||||
drop_queues = args.drop_announces,
|
||||
drop_via = args.drop_via,
|
||||
max_hops = args.max,
|
||||
remote=args.R,
|
||||
management_identity=args.i,
|
||||
remote_timeout=args.W,
|
||||
json=args.json,
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@@ -31,8 +31,10 @@ import argparse
|
||||
from RNS._version import __version__
|
||||
|
||||
DEFAULT_PROBE_SIZE = 16
|
||||
DEFAULT_TIMEOUT = 12
|
||||
|
||||
def program_setup(configdir, destination_hexhash, size=DEFAULT_PROBE_SIZE, full_name = None, verbosity = 0):
|
||||
def program_setup(configdir, destination_hexhash, size=None, full_name = None, verbosity = 0, timeout=None, wait=0, probes=1):
|
||||
if size == None: size = DEFAULT_PROBE_SIZE
|
||||
if full_name == None:
|
||||
print("The full destination name including application name aspects must be specified for the destination")
|
||||
exit()
|
||||
@@ -71,14 +73,19 @@ def program_setup(configdir, destination_hexhash, size=DEFAULT_PROBE_SIZE, full_
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
|
||||
sys.stdout.flush()
|
||||
|
||||
_timeout = time.time() + (timeout or DEFAULT_TIMEOUT+reticulum.get_first_hop_timeout(destination_hash))
|
||||
i = 0
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
while not RNS.Transport.has_path(destination_hash):
|
||||
while not RNS.Transport.has_path(destination_hash) and not time.time() > _timeout:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if time.time() > _timeout:
|
||||
print("\r \rPath request timed out")
|
||||
exit(1)
|
||||
|
||||
server_identity = RNS.Identity.recall(destination_hash)
|
||||
|
||||
request_destination = RNS.Destination(
|
||||
@@ -89,101 +96,120 @@ def program_setup(configdir, destination_hexhash, size=DEFAULT_PROBE_SIZE, full_
|
||||
*aspects
|
||||
)
|
||||
|
||||
probe = RNS.Packet(request_destination, os.urandom(size))
|
||||
receipt = probe.send()
|
||||
sent = 0
|
||||
replies = 0
|
||||
while probes:
|
||||
|
||||
if more_output:
|
||||
more = " via "+RNS.prettyhexrep(reticulum.get_next_hop(destination_hash))+" on "+str(reticulum.get_next_hop_if_name(destination_hash))
|
||||
else:
|
||||
more = ""
|
||||
if sent > 0:
|
||||
time.sleep(wait)
|
||||
|
||||
print("\rSent "+str(size)+" byte probe to "+RNS.prettyhexrep(destination_hash)+more+" ", end=" ")
|
||||
try:
|
||||
probe = RNS.Packet(request_destination, os.urandom(size))
|
||||
probe.pack()
|
||||
except OSError:
|
||||
print("Error: Probe packet size of "+str(len(probe.raw))+" bytes exceed MTU of "+str(RNS.Reticulum.MTU)+" bytes")
|
||||
exit(3)
|
||||
|
||||
i = 0
|
||||
while not receipt.status == RNS.PacketReceipt.DELIVERED:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
receipt = probe.send()
|
||||
sent += 1
|
||||
|
||||
print("\b\b ")
|
||||
sys.stdout.flush()
|
||||
if more_output:
|
||||
nhd = reticulum.get_next_hop(destination_hash)
|
||||
via_str = " via "+RNS.prettyhexrep(nhd) if nhd != None else ""
|
||||
if_str = " on "+str(reticulum.get_next_hop_if_name(destination_hash)) if reticulum.get_next_hop_if_name(destination_hash) != "None" else ""
|
||||
more = via_str+if_str
|
||||
else:
|
||||
more = ""
|
||||
|
||||
hops = RNS.Transport.hops_to(destination_hash)
|
||||
if hops != 1:
|
||||
ms = "s"
|
||||
else:
|
||||
ms = ""
|
||||
print("\rSent probe "+str(sent)+" ("+str(size)+" bytes) to "+RNS.prettyhexrep(destination_hash)+more+" ", end=" ")
|
||||
|
||||
rtt = receipt.get_rtt()
|
||||
if (rtt >= 1):
|
||||
rtt = round(rtt, 3)
|
||||
rttstring = str(rtt)+" seconds"
|
||||
else:
|
||||
rtt = round(rtt*1000, 3)
|
||||
rttstring = str(rtt)+" milliseconds"
|
||||
_timeout = time.time() + (timeout or DEFAULT_TIMEOUT+reticulum.get_first_hop_timeout(destination_hash))
|
||||
i = 0
|
||||
while receipt.status == RNS.PacketReceipt.SENT and not time.time() > _timeout:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
reception_stats = ""
|
||||
if reticulum.is_connected_to_shared_instance:
|
||||
reception_rssi = reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
|
||||
reception_snr = reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
|
||||
|
||||
if reception_rssi != None:
|
||||
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
|
||||
if time.time() > _timeout:
|
||||
print("\r \rProbe timed out")
|
||||
|
||||
if reception_snr != None:
|
||||
reception_stats += " [SNR "+str(reception_snr)+" dB]"
|
||||
else:
|
||||
print("\b\b ")
|
||||
sys.stdout.flush()
|
||||
|
||||
if receipt.status == RNS.PacketReceipt.DELIVERED:
|
||||
replies += 1
|
||||
hops = RNS.Transport.hops_to(destination_hash)
|
||||
if hops != 1:
|
||||
ms = "s"
|
||||
else:
|
||||
ms = ""
|
||||
|
||||
rtt = receipt.get_rtt()
|
||||
if (rtt >= 1):
|
||||
rtt = round(rtt, 3)
|
||||
rttstring = str(rtt)+" seconds"
|
||||
else:
|
||||
rtt = round(rtt*1000, 3)
|
||||
rttstring = str(rtt)+" milliseconds"
|
||||
|
||||
reception_stats = ""
|
||||
if reticulum.is_connected_to_shared_instance:
|
||||
reception_rssi = reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
|
||||
reception_snr = reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
|
||||
reception_q = reticulum.get_packet_q(receipt.proof_packet.packet_hash)
|
||||
|
||||
if reception_rssi != None:
|
||||
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
|
||||
|
||||
if reception_snr != None:
|
||||
reception_stats += " [SNR "+str(reception_snr)+" dB]"
|
||||
|
||||
if reception_q != None:
|
||||
reception_stats += " [Link Quality "+str(reception_q)+"%]"
|
||||
|
||||
else:
|
||||
if receipt.proof_packet != None:
|
||||
if receipt.proof_packet.rssi != None:
|
||||
reception_stats += " [RSSI "+str(receipt.proof_packet.rssi)+" dBm]"
|
||||
|
||||
if receipt.proof_packet.snr != None:
|
||||
reception_stats += " [SNR "+str(receipt.proof_packet.snr)+" dB]"
|
||||
|
||||
print(
|
||||
"Valid reply from "+
|
||||
RNS.prettyhexrep(receipt.destination.hash)+
|
||||
"\nRound-trip time is "+rttstring+
|
||||
" over "+str(hops)+" hop"+ms+
|
||||
reception_stats+"\n"
|
||||
)
|
||||
|
||||
else:
|
||||
print("\r \rProbe timed out")
|
||||
|
||||
probes -= 1
|
||||
|
||||
loss = round((1-(replies/sent))*100, 2)
|
||||
print(f"Sent {sent}, received {replies}, packet loss {loss}%")
|
||||
if loss > 0:
|
||||
exit(2)
|
||||
else:
|
||||
if receipt.proof_packet != None:
|
||||
if receipt.proof_packet.rssi != None:
|
||||
reception_stats += " [RSSI "+str(receipt.proof_packet.rssi)+" dBm]"
|
||||
|
||||
if receipt.proof_packet.snr != None:
|
||||
reception_stats += " [SNR "+str(receipt.proof_packet.snr)+" dB]"
|
||||
|
||||
print(
|
||||
"Valid reply received from "+
|
||||
RNS.prettyhexrep(receipt.destination.hash)+
|
||||
"\nRound-trip time is "+rttstring+
|
||||
" over "+str(hops)+" hop"+ms+
|
||||
reception_stats
|
||||
)
|
||||
|
||||
exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description="Reticulum Probe Utility")
|
||||
|
||||
parser.add_argument("--config",
|
||||
action="store",
|
||||
default=None,
|
||||
help="path to alternative Reticulum config directory",
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version="rnprobe {version}".format(version=__version__)
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"full_name",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="full destination name in dotted notation",
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"destination_hash",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="hexadecimal hash of the destination",
|
||||
type=str
|
||||
)
|
||||
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument("-s", "--size", action="store", default=None, help="size of probe packet payload in bytes", type=int)
|
||||
parser.add_argument("-n", "--probes", action="store", default=1, help="number of probes to send", type=int)
|
||||
parser.add_argument("-t", "--timeout", metavar="seconds", action="store", default=None, help="timeout before giving up", type=float)
|
||||
parser.add_argument("-w", "--wait", metavar="seconds", action="store", default=0, help="time between each probe", type=float)
|
||||
parser.add_argument("--version", action="version", version="rnprobe {version}".format(version=__version__))
|
||||
parser.add_argument("full_name", nargs="?", default=None, help="full destination name in dotted notation", type=str)
|
||||
parser.add_argument("destination_hash", nargs="?", default=None, help="hexadecimal hash of the destination", type=str)
|
||||
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0)
|
||||
|
||||
@@ -202,8 +228,12 @@ def main():
|
||||
program_setup(
|
||||
configdir = configarg,
|
||||
destination_hexhash = args.destination_hash,
|
||||
size = args.size,
|
||||
full_name = args.full_name,
|
||||
verbosity = args.verbose
|
||||
verbosity = args.verbose,
|
||||
probes = args.probes,
|
||||
wait = args.wait,
|
||||
timeout = args.timeout,
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
from CRNS import RNS
|
||||
import argparse
|
||||
import time
|
||||
|
||||
@@ -42,6 +42,8 @@ def program_setup(configdir, verbosity = 0, quietness = 0, service = False):
|
||||
if reticulum.is_connected_to_shared_instance:
|
||||
RNS.log("Started rnsd version {version} connected to another shared local instance, this is probably NOT what you want!".format(version=__version__), RNS.LOG_WARNING)
|
||||
else:
|
||||
if RNS.Reticulum.get_instance().shared_instance_interface:
|
||||
RNS.Reticulum.get_instance().shared_instance_interface.server.daemon_threads = True
|
||||
RNS.log("Started rnsd version {version}".format(version=__version__), RNS.LOG_NOTICE)
|
||||
|
||||
while True:
|
||||
@@ -87,7 +89,7 @@ __example_rns_config__ = '''# This is an example Reticulum config file.
|
||||
# always-on. This directive is optional and can be removed
|
||||
# for brevity.
|
||||
|
||||
enable_transport = False
|
||||
enable_transport = No
|
||||
|
||||
|
||||
# By default, the first program to launch the Reticulum
|
||||
@@ -111,6 +113,30 @@ share_instance = Yes
|
||||
shared_instance_port = 37428
|
||||
instance_control_port = 37429
|
||||
|
||||
|
||||
# On systems where running instances may not have access
|
||||
# to the same shared Reticulum configuration directory,
|
||||
# it is still possible to allow full interactivity for
|
||||
# running instances, by manually specifying a shared RPC
|
||||
# key. In almost all cases, this option is not needed, but
|
||||
# it can be useful on operating systems such as Android.
|
||||
# The key must be specified as bytes in hexadecimal.
|
||||
|
||||
# rpc_key = e5c032d3ec4e64a6aca9927ba8ab73336780f6d71790
|
||||
|
||||
|
||||
# It is possible to allow remote management of Reticulum
|
||||
# systems using the various built-in utilities, such as
|
||||
# rnstatus and rnpath. You will need to specify one or
|
||||
# more Reticulum Identity hashes for authenticating the
|
||||
# queries from client programs. For this purpose, you can
|
||||
# use existing identity files, or generate new ones with
|
||||
# the rnid utility.
|
||||
|
||||
# enable_remote_management = yes
|
||||
# remote_management_allowed = 9fb6d773498fb3feda407ed8ef2c3229, 2d882c5586e548d79b5af27bca1776dc
|
||||
|
||||
|
||||
# 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
|
||||
@@ -120,6 +146,17 @@ instance_control_port = 37429
|
||||
panic_on_interface_error = No
|
||||
|
||||
|
||||
# When Transport is enabled, it is possible to allow the
|
||||
# Transport Instance to respond to probe requests from
|
||||
# the rnprobe utility. This can be a useful tool to test
|
||||
# connectivity. When this option is enabled, the probe
|
||||
# destination will be generated from the Identity of the
|
||||
# Transport Instance, and printed to the log at startup.
|
||||
# Optional, and disabled by default.
|
||||
|
||||
respond_to_probes = No
|
||||
|
||||
|
||||
[logging]
|
||||
# Valid log levels are 0 through 7:
|
||||
# 0: Log only critical information
|
||||
@@ -259,6 +296,23 @@ loglevel = 4
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB0
|
||||
|
||||
# It is also possible to use BLE devices
|
||||
# instead of wired serial ports. The
|
||||
# target RNode must be paired with the
|
||||
# host device before connecting. BLE
|
||||
# devices can be connected by name,
|
||||
# BLE MAC address or by any available.
|
||||
|
||||
# Connect to specific device by name
|
||||
# port = ble://RNode 3B87
|
||||
|
||||
# Or by BLE MAC address
|
||||
# port = ble://F4:12:73:29:4E:89
|
||||
|
||||
# Or connect to the first available,
|
||||
# paired device
|
||||
# port = ble://
|
||||
|
||||
# Set frequency to 867.2 MHz
|
||||
frequency = 867200000
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
|
||||
from RNS._version import __version__
|
||||
@@ -46,14 +49,155 @@ def size_str(num, suffix='B'):
|
||||
|
||||
return "%.2f%s%s" % (num, last_unit, suffix)
|
||||
|
||||
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None,json=False):
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
request_result = None
|
||||
request_concluded = False
|
||||
def get_remote_status(destination_hash, include_lstats, identity, no_output=False, timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
|
||||
global request_result, request_concluded
|
||||
link_count = None
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
if not no_output:
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested", end=" ")
|
||||
sys.stdout.flush()
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
pr_time = time.time()
|
||||
while not RNS.Transport.has_path(destination_hash):
|
||||
time.sleep(0.1)
|
||||
if time.time() - pr_time > timeout:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Path request timed out")
|
||||
exit(12)
|
||||
|
||||
remote_identity = RNS.Identity.recall(destination_hash)
|
||||
|
||||
def remote_link_closed(link):
|
||||
if link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("The link timed out, exiting now")
|
||||
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("The link was closed by the server, exiting now")
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Link closed unexpectedly, exiting now")
|
||||
exit(10)
|
||||
|
||||
def request_failed(request_receipt):
|
||||
global request_result, request_concluded
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("The remote status request failed. Likely authentication failure.")
|
||||
request_concluded = True
|
||||
|
||||
def got_response(request_receipt):
|
||||
global request_result, request_concluded
|
||||
response = request_receipt.response
|
||||
if isinstance(response, list):
|
||||
status = response[0]
|
||||
if len(response) > 1:
|
||||
link_count = response[1]
|
||||
else:
|
||||
link_count = None
|
||||
|
||||
request_result = (status, link_count)
|
||||
|
||||
request_concluded = True
|
||||
|
||||
def remote_link_established(link):
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Sending request...", end=" ")
|
||||
sys.stdout.flush()
|
||||
link.identify(identity)
|
||||
link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed)
|
||||
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Establishing link with remote transport instance...", end=" ")
|
||||
sys.stdout.flush()
|
||||
|
||||
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
|
||||
link = RNS.Link(remote_destination)
|
||||
link.set_link_established_callback(remote_link_established)
|
||||
link.set_link_closed_callback(remote_link_closed)
|
||||
|
||||
while not request_concluded:
|
||||
time.sleep(0.1)
|
||||
|
||||
if request_result != None:
|
||||
print("\r \r", end="")
|
||||
|
||||
return request_result
|
||||
|
||||
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False,
|
||||
lstats=False, sorting=None, sort_reverse=False, remote=None, management_identity=None,
|
||||
remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, must_exit=True, rns_instance=None, traffic_totals=False):
|
||||
|
||||
if remote:
|
||||
require_shared = False
|
||||
else:
|
||||
require_shared = True
|
||||
|
||||
stats = None
|
||||
try:
|
||||
stats = reticulum.get_interface_stats()
|
||||
if rns_instance:
|
||||
reticulum = rns_instance
|
||||
must_exit = False
|
||||
else:
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=3+verbosity, require_shared_instance=require_shared)
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
print("No shared RNS instance available to get status from")
|
||||
if must_exit:
|
||||
exit(1)
|
||||
else:
|
||||
return
|
||||
|
||||
link_count = None
|
||||
stats = None
|
||||
if remote:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(remote) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
identity_hash = bytes.fromhex(remote)
|
||||
destination_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.remote.management", identity_hash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
|
||||
identity = RNS.Identity.from_file(os.path.expanduser(management_identity))
|
||||
if identity == None:
|
||||
raise ValueError("Could not load management identity from "+str(management_identity))
|
||||
|
||||
try:
|
||||
remote_status = get_remote_status(destination_hash, lstats, identity, no_output=json, timeout=remote_timeout)
|
||||
if remote_status != None:
|
||||
stats, link_count = remote_status
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
if must_exit:
|
||||
exit(20)
|
||||
else:
|
||||
return
|
||||
|
||||
else:
|
||||
if lstats:
|
||||
try:
|
||||
link_count = reticulum.get_link_count()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
stats = reticulum.get_interface_stats()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if stats != None:
|
||||
if json:
|
||||
@@ -62,21 +206,51 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None,json=F
|
||||
if isinstance(stats[s], bytes):
|
||||
stats[s] = RNS.hexrep(stats[s], delimit=False)
|
||||
|
||||
for i in stats[s]:
|
||||
if isinstance(i, dict):
|
||||
for k in i:
|
||||
if isinstance(i[k], bytes):
|
||||
i[k] = RNS.hexrep(i[k], delimit=False)
|
||||
if isinstance(stats[s], dict) or isinstance(stats[s], list):
|
||||
for i in stats[s]:
|
||||
if isinstance(i, dict):
|
||||
for k in i:
|
||||
if isinstance(i[k], bytes):
|
||||
i[k] = RNS.hexrep(i[k], delimit=False)
|
||||
|
||||
print(json.dumps(stats))
|
||||
exit()
|
||||
if must_exit:
|
||||
exit()
|
||||
else:
|
||||
return
|
||||
|
||||
interfaces = stats["interfaces"]
|
||||
if sorting != None and isinstance(sorting, str):
|
||||
sorting = sorting.lower()
|
||||
if sorting == "rate" or sorting == "bitrate":
|
||||
interfaces.sort(key=lambda i: i["bitrate"], reverse=not sort_reverse)
|
||||
if sorting == "rx":
|
||||
interfaces.sort(key=lambda i: i["rxb"], reverse=not sort_reverse)
|
||||
if sorting == "tx":
|
||||
interfaces.sort(key=lambda i: i["txb"], reverse=not sort_reverse)
|
||||
if sorting == "rxs":
|
||||
interfaces.sort(key=lambda i: i["rxs"], reverse=not sort_reverse)
|
||||
if sorting == "txs":
|
||||
interfaces.sort(key=lambda i: i["txs"], reverse=not sort_reverse)
|
||||
if sorting == "traffic":
|
||||
interfaces.sort(key=lambda i: i["rxb"]+i["txb"], reverse=not sort_reverse)
|
||||
if sorting == "announces" or sorting == "announce":
|
||||
interfaces.sort(key=lambda i: i["incoming_announce_frequency"]+i["outgoing_announce_frequency"], reverse=not sort_reverse)
|
||||
if sorting == "arx":
|
||||
interfaces.sort(key=lambda i: i["incoming_announce_frequency"], reverse=not sort_reverse)
|
||||
if sorting == "atx":
|
||||
interfaces.sort(key=lambda i: i["outgoing_announce_frequency"], reverse=not sort_reverse)
|
||||
if sorting == "held":
|
||||
interfaces.sort(key=lambda i: i["held_announces"], reverse=not sort_reverse)
|
||||
|
||||
|
||||
for ifstat in stats["interfaces"]:
|
||||
for ifstat in interfaces:
|
||||
name = ifstat["name"]
|
||||
|
||||
if dispall or not (
|
||||
name.startswith("LocalInterface[") or
|
||||
name.startswith("TCPInterface[Client") or
|
||||
name.startswith("AutoInterfacePeer[") or
|
||||
name.startswith("I2PInterfacePeer[Connected peer") or
|
||||
(name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False))
|
||||
):
|
||||
@@ -113,7 +287,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None,json=F
|
||||
else:
|
||||
spec_str = " programs"
|
||||
|
||||
clients_string = "Serving : "+str(cnum)+spec_str
|
||||
clients_string = "Serving : "+str(cnum)+spec_str
|
||||
elif name.startswith("I2PInterface["):
|
||||
if "i2p_connectable" in ifstat and ifstat["i2p_connectable"] == True:
|
||||
cnum = clients
|
||||
@@ -122,11 +296,11 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None,json=F
|
||||
else:
|
||||
spec_str = " connected I2P endpoints"
|
||||
|
||||
clients_string = "Peers : "+str(cnum)+spec_str
|
||||
clients_string = "Peers : "+str(cnum)+spec_str
|
||||
else:
|
||||
clients_string = ""
|
||||
else:
|
||||
clients_string = "Clients : "+str(clients)
|
||||
clients_string = "Clients : "+str(clients)
|
||||
|
||||
else:
|
||||
clients = None
|
||||
@@ -134,50 +308,130 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None,json=F
|
||||
print(" {n}".format(n=ifstat["name"]))
|
||||
|
||||
if "ifac_netname" in ifstat and ifstat["ifac_netname"] != None:
|
||||
print(" Network : {nn}".format(nn=ifstat["ifac_netname"]))
|
||||
print(" Network : {nn}".format(nn=ifstat["ifac_netname"]))
|
||||
|
||||
print(" Status : {ss}".format(ss=ss))
|
||||
print(" Status : {ss}".format(ss=ss))
|
||||
|
||||
if clients != None and clients_string != "":
|
||||
print(" "+clients_string)
|
||||
|
||||
if not (name.startswith("Shared Instance[") or name.startswith("TCPInterface[Client") or name.startswith("LocalInterface[")):
|
||||
print(" Mode : {mode}".format(mode=modestr))
|
||||
print(" Mode : {mode}".format(mode=modestr))
|
||||
|
||||
if "bitrate" in ifstat and ifstat["bitrate"] != None:
|
||||
print(" Rate : {ss}".format(ss=speed_str(ifstat["bitrate"])))
|
||||
print(" Rate : {ss}".format(ss=speed_str(ifstat["bitrate"])))
|
||||
|
||||
if "noise_floor" in ifstat:
|
||||
if ifstat["noise_floor"] != None:
|
||||
print(" Noise Fl. : {nfl} dBm".format(nfl=str(ifstat["noise_floor"])))
|
||||
else:
|
||||
print(" Noise Fl. : Unknown")
|
||||
|
||||
if "battery_percent" in ifstat and ifstat["battery_percent"] != None:
|
||||
try:
|
||||
bpi = int(ifstat["battery_percent"])
|
||||
bss = ifstat["battery_state"]
|
||||
print(f" Battery : {bpi}% ({bss})")
|
||||
except:
|
||||
pass
|
||||
|
||||
if "airtime_short" in ifstat and "airtime_long" in ifstat:
|
||||
print(" Airtime : {ats}% (15s), {atl}% (1h)".format(ats=str(ifstat["airtime_short"]),atl=str(ifstat["airtime_long"])))
|
||||
|
||||
if "channel_load_short" in ifstat and "channel_load_long" in ifstat:
|
||||
print(" Ch. Load : {ats}% (15s), {atl}% (1h)".format(ats=str(ifstat["channel_load_short"]),atl=str(ifstat["channel_load_long"])))
|
||||
|
||||
if "peers" in ifstat and ifstat["peers"] != None:
|
||||
print(" Peers : {np} reachable".format(np=ifstat["peers"]))
|
||||
print(" Peers : {np} reachable".format(np=ifstat["peers"]))
|
||||
|
||||
if "tunnelstate" in ifstat and ifstat["tunnelstate"] != None:
|
||||
print(" I2P : {ts}".format(ts=ifstat["tunnelstate"]))
|
||||
print(" I2P : {ts}".format(ts=ifstat["tunnelstate"]))
|
||||
|
||||
if "ifac_signature" in ifstat and ifstat["ifac_signature"] != None:
|
||||
sigstr = "<…"+RNS.hexrep(ifstat["ifac_signature"][-5:], delimit=False)+">"
|
||||
print(" Access : {nb}-bit IFAC by {sig}".format(nb=ifstat["ifac_size"]*8, sig=sigstr))
|
||||
print(" Access : {nb}-bit IFAC by {sig}".format(nb=ifstat["ifac_size"]*8, sig=sigstr))
|
||||
|
||||
if "i2p_b32" in ifstat and ifstat["i2p_b32"] != None:
|
||||
print(" I2P B32 : {ep}".format(ep=str(ifstat["i2p_b32"])))
|
||||
print(" I2P B32 : {ep}".format(ep=str(ifstat["i2p_b32"])))
|
||||
|
||||
if "announce_queue" in ifstat and ifstat["announce_queue"] != None and ifstat["announce_queue"] > 0:
|
||||
if astats and "announce_queue" in ifstat and ifstat["announce_queue"] != None and ifstat["announce_queue"] > 0:
|
||||
aqn = ifstat["announce_queue"]
|
||||
if aqn == 1:
|
||||
print(" Queued : {np} announce".format(np=aqn))
|
||||
print(" Queued : {np} announce".format(np=aqn))
|
||||
else:
|
||||
print(" Queued : {np} announces".format(np=aqn))
|
||||
print(" Queued : {np} announces".format(np=aqn))
|
||||
|
||||
print(" Traffic : {txb}↑\n {rxb}↓".format(rxb=size_str(ifstat["rxb"]), txb=size_str(ifstat["txb"])))
|
||||
if astats and "held_announces" in ifstat and ifstat["held_announces"] != None and ifstat["held_announces"] > 0:
|
||||
aqn = ifstat["held_announces"]
|
||||
if aqn == 1:
|
||||
print(" Held : {np} announce".format(np=aqn))
|
||||
else:
|
||||
print(" Held : {np} announces".format(np=aqn))
|
||||
|
||||
if astats and "incoming_announce_frequency" in ifstat and ifstat["incoming_announce_frequency"] != None:
|
||||
print(" Announces : {iaf}↑".format(iaf=RNS.prettyfrequency(ifstat["outgoing_announce_frequency"])))
|
||||
print(" {iaf}↓".format(iaf=RNS.prettyfrequency(ifstat["incoming_announce_frequency"])))
|
||||
|
||||
rxb_str = "↓"+RNS.prettysize(ifstat["rxb"])
|
||||
txb_str = "↑"+RNS.prettysize(ifstat["txb"])
|
||||
strdiff = len(rxb_str)-len(txb_str)
|
||||
if strdiff > 0:
|
||||
txb_str += " "*strdiff
|
||||
elif strdiff < 0:
|
||||
rxb_str += " "*-strdiff
|
||||
|
||||
rxstat = rxb_str
|
||||
txstat = txb_str
|
||||
if "rxs" in ifstat and "txs" in ifstat:
|
||||
rxstat += " "+RNS.prettyspeed(ifstat["rxs"])
|
||||
txstat += " "+RNS.prettyspeed(ifstat["txs"])
|
||||
|
||||
print(f" Traffic : {txstat}\n {rxstat}")
|
||||
|
||||
lstr = ""
|
||||
if link_count != None and lstats:
|
||||
ms = "y" if link_count == 1 else "ies"
|
||||
if "transport_id" in stats and stats["transport_id"] != None:
|
||||
lstr = f", {link_count} entr{ms} in link table"
|
||||
else:
|
||||
lstr = f" {link_count} entr{ms} in link table"
|
||||
|
||||
if traffic_totals:
|
||||
rxb_str = "↓"+RNS.prettysize(stats["rxb"])
|
||||
txb_str = "↑"+RNS.prettysize(stats["txb"])
|
||||
strdiff = len(rxb_str)-len(txb_str)
|
||||
if strdiff > 0:
|
||||
txb_str += " "*strdiff
|
||||
elif strdiff < 0:
|
||||
rxb_str += " "*-strdiff
|
||||
|
||||
rxstat = rxb_str+" "+RNS.prettyspeed(stats["rxs"])
|
||||
txstat = txb_str+" "+RNS.prettyspeed(stats["txs"])
|
||||
print(f"\n Totals : {txstat}\n {rxstat}")
|
||||
|
||||
if "transport_id" in stats and stats["transport_id"] != None:
|
||||
print("\n Reticulum Transport Instance "+RNS.prettyhexrep(stats["transport_id"])+" is running")
|
||||
print("\n Transport Instance "+RNS.prettyhexrep(stats["transport_id"])+" running")
|
||||
if "probe_responder" in stats and stats["probe_responder"] != None:
|
||||
print(" Probe responder at "+RNS.prettyhexrep(stats["probe_responder"])+ " active")
|
||||
if "transport_uptime" in stats and stats["transport_uptime"] != None:
|
||||
print(" Uptime is "+RNS.prettytime(stats["transport_uptime"])+lstr)
|
||||
else:
|
||||
if lstr != "":
|
||||
print(f"\n{lstr}")
|
||||
|
||||
print("")
|
||||
|
||||
else:
|
||||
print("Could not get RNS status")
|
||||
if not remote:
|
||||
print("Could not get RNS status")
|
||||
else:
|
||||
print("Could not get RNS status from remote transport instance "+RNS.prettyhexrep(identity_hash))
|
||||
if must_exit:
|
||||
exit(2)
|
||||
else:
|
||||
return
|
||||
|
||||
def main():
|
||||
def main(must_exit=True, rns_instance=None):
|
||||
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)
|
||||
@@ -191,6 +445,47 @@ def main():
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-A",
|
||||
"--announce-stats",
|
||||
action="store_true",
|
||||
help="show announce stats",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--link-stats",
|
||||
action="store_true",
|
||||
help="show link stats",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--totals",
|
||||
action="store_true",
|
||||
help="display traffic totals",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--sort",
|
||||
action="store",
|
||||
help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, announces, arx, atx, held]",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--reverse",
|
||||
action="store_true",
|
||||
help="reverse sorting",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--json",
|
||||
@@ -199,6 +494,33 @@ def main():
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-R",
|
||||
action="store",
|
||||
metavar="hash",
|
||||
help="transport identity hash of remote instance to get status from",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
action="store",
|
||||
metavar="path",
|
||||
help="path to identity used for remote management",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
action="store",
|
||||
metavar="seconds",
|
||||
type=float,
|
||||
help="timeout before giving up on remote queries",
|
||||
default=RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0)
|
||||
|
||||
parser.add_argument("filter", nargs="?", default=None, help="only display interfaces with names including filter", type=str)
|
||||
@@ -210,11 +532,30 @@ def main():
|
||||
else:
|
||||
configarg = None
|
||||
|
||||
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json)
|
||||
program_setup(
|
||||
configdir = configarg,
|
||||
dispall = args.all,
|
||||
verbosity=args.verbose,
|
||||
name_filter=args.filter,
|
||||
json=args.json,
|
||||
astats=args.announce_stats,
|
||||
lstats=args.link_stats,
|
||||
sorting=args.sort,
|
||||
sort_reverse=args.reverse,
|
||||
remote=args.R,
|
||||
management_identity=args.i,
|
||||
remote_timeout=args.w,
|
||||
must_exit=must_exit,
|
||||
rns_instance=rns_instance,
|
||||
traffic_totals=args.totals,
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
if must_exit:
|
||||
exit()
|
||||
else:
|
||||
return
|
||||
|
||||
def speed_str(num, suffix='bps'):
|
||||
units = ['','k','M','G','T','P','E','Z']
|
||||
|
||||
@@ -28,8 +28,8 @@ import argparse
|
||||
import shlex
|
||||
import time
|
||||
import sys
|
||||
import tty
|
||||
import os
|
||||
#import tty
|
||||
|
||||
from RNS._version import __version__
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -24,6 +24,7 @@ import os
|
||||
import sys
|
||||
import glob
|
||||
import time
|
||||
import datetime
|
||||
import random
|
||||
import threading
|
||||
|
||||
@@ -38,13 +39,21 @@ from .Transport import Transport
|
||||
from .Destination import Destination
|
||||
from .Packet import Packet
|
||||
from .Packet import PacketReceipt
|
||||
from .Resolver import Resolver
|
||||
from .Resource import Resource, ResourceAdvertisement
|
||||
from .Cryptography import HKDF
|
||||
from .Cryptography import Hashes
|
||||
|
||||
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
|
||||
py_modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
|
||||
modules = py_modules+pyc_modules
|
||||
__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))]))
|
||||
|
||||
import importlib
|
||||
if importlib.util.find_spec("cython"): import cython; compiled = cython.compiled
|
||||
else: compiled = False
|
||||
|
||||
LOG_NONE = -1
|
||||
LOG_CRITICAL = 0
|
||||
LOG_ERROR = 1
|
||||
LOG_WARNING = 2
|
||||
@@ -56,13 +65,16 @@ LOG_EXTREME = 7
|
||||
|
||||
LOG_STDOUT = 0x91
|
||||
LOG_FILE = 0x92
|
||||
LOG_CALLBACK = 0x93
|
||||
|
||||
LOG_MAXSIZE = 5*1024*1024
|
||||
|
||||
loglevel = LOG_NOTICE
|
||||
logfile = None
|
||||
logdest = LOG_STDOUT
|
||||
logcall = None
|
||||
logtimefmt = "%Y-%m-%d %H:%M:%S"
|
||||
logtimefmt_p = "%H:%M:%S.%f"
|
||||
compact_log_fmt = False
|
||||
|
||||
instance_random = random.Random()
|
||||
@@ -74,21 +86,21 @@ logging_lock = threading.Lock()
|
||||
|
||||
def loglevelname(level):
|
||||
if (level == LOG_CRITICAL):
|
||||
return "Critical"
|
||||
return "[Critical]"
|
||||
if (level == LOG_ERROR):
|
||||
return "Error"
|
||||
return "[Error] "
|
||||
if (level == LOG_WARNING):
|
||||
return "Warning"
|
||||
return "[Warning] "
|
||||
if (level == LOG_NOTICE):
|
||||
return "Notice"
|
||||
return "[Notice] "
|
||||
if (level == LOG_INFO):
|
||||
return "Info"
|
||||
return "[Info] "
|
||||
if (level == LOG_VERBOSE):
|
||||
return "Verbose"
|
||||
return "[Verbose] "
|
||||
if (level == LOG_DEBUG):
|
||||
return "Debug"
|
||||
return "[Debug] "
|
||||
if (level == LOG_EXTREME):
|
||||
return "Extra"
|
||||
return "[Extra] "
|
||||
|
||||
return "Unknown"
|
||||
|
||||
@@ -103,46 +115,65 @@ def timestamp_str(time_s):
|
||||
timestamp = time.localtime(time_s)
|
||||
return time.strftime(logtimefmt, timestamp)
|
||||
|
||||
def log(msg, level=3, _override_destination = False):
|
||||
def precise_timestamp_str(time_s):
|
||||
return datetime.datetime.now().strftime(logtimefmt_p)[:-3]
|
||||
|
||||
def log(msg, level=3, _override_destination = False, pt=False):
|
||||
if loglevel == LOG_NONE: return
|
||||
global _always_override_destination, compact_log_fmt
|
||||
|
||||
msg = str(msg)
|
||||
if loglevel >= level:
|
||||
if not compact_log_fmt:
|
||||
logstring = "["+timestamp_str(time.time())+"] ["+loglevelname(level)+"] "+msg
|
||||
if pt:
|
||||
logstring = "["+precise_timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
|
||||
else:
|
||||
logstring = "["+timestamp_str(time.time())+"] "+msg
|
||||
if not compact_log_fmt:
|
||||
logstring = "["+timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
|
||||
else:
|
||||
logstring = "["+timestamp_str(time.time())+" "+msg
|
||||
|
||||
logging_lock.acquire()
|
||||
with logging_lock:
|
||||
if (logdest == LOG_STDOUT or _always_override_destination or _override_destination):
|
||||
if not threading.main_thread().is_alive(): return
|
||||
else: print(logstring)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
except Exception as e:
|
||||
_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)
|
||||
|
||||
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)
|
||||
elif logdest == LOG_CALLBACK:
|
||||
try:
|
||||
logcall(logstring)
|
||||
except Exception as e:
|
||||
_always_override_destination = True
|
||||
log("Exception occurred while calling external log handler: "+str(e), LOG_CRITICAL)
|
||||
log("Dumping future log events to console!", LOG_CRITICAL)
|
||||
log(msg, level)
|
||||
|
||||
|
||||
def rand():
|
||||
result = instance_random.random()
|
||||
return result
|
||||
|
||||
def trace_exception(e):
|
||||
import traceback
|
||||
exception_info = "".join(traceback.TracebackException.from_exception(e).format())
|
||||
log(f"An unhandled {str(type(e))} exception occurred: {str(e)}", LOG_ERROR)
|
||||
log(exception_info, LOG_ERROR)
|
||||
|
||||
def hexrep(data, delimit=True):
|
||||
try:
|
||||
iter(data)
|
||||
@@ -160,6 +191,9 @@ def prettyhexrep(data):
|
||||
hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">"
|
||||
return hexrep
|
||||
|
||||
def prettyspeed(num, suffix="b"):
|
||||
return prettysize(num/8, suffix=suffix)+"ps"
|
||||
|
||||
def prettysize(num, suffix='B'):
|
||||
units = ['','K','M','G','T','P','E','Z']
|
||||
last_unit = 'Y'
|
||||
@@ -179,32 +213,73 @@ def prettysize(num, suffix='B'):
|
||||
|
||||
return "%.2f%s%s" % (num, last_unit, suffix)
|
||||
|
||||
def prettytime(time, verbose=False):
|
||||
def prettyfrequency(hz, suffix="Hz"):
|
||||
num = hz*1e6
|
||||
units = ["µ", "m", "", "K","M","G","T","P","E","Z"]
|
||||
last_unit = "Y"
|
||||
|
||||
for unit in units:
|
||||
if abs(num) < 1000.0:
|
||||
return "%.2f %s%s" % (num, unit, suffix)
|
||||
num /= 1000.0
|
||||
|
||||
return "%.2f%s%s" % (num, last_unit, suffix)
|
||||
|
||||
def prettydistance(m, suffix="m"):
|
||||
num = m*1e6
|
||||
units = ["µ", "m", "c", ""]
|
||||
last_unit = "K"
|
||||
|
||||
for unit in units:
|
||||
divisor = 1000.0
|
||||
if unit == "m": divisor = 10
|
||||
if unit == "c": divisor = 100
|
||||
|
||||
if abs(num) < divisor:
|
||||
return "%.2f %s%s" % (num, unit, suffix)
|
||||
num /= divisor
|
||||
|
||||
return "%.2f %s%s" % (num, last_unit, suffix)
|
||||
|
||||
def prettytime(time, verbose=False, compact=False):
|
||||
neg = False
|
||||
if time < 0:
|
||||
time = abs(time)
|
||||
neg = True
|
||||
|
||||
days = int(time // (24 * 3600))
|
||||
time = time % (24 * 3600)
|
||||
hours = int(time // 3600)
|
||||
time %= 3600
|
||||
minutes = int(time // 60)
|
||||
time %= 60
|
||||
seconds = round(time, 2)
|
||||
if compact:
|
||||
seconds = int(time)
|
||||
else:
|
||||
seconds = round(time, 2)
|
||||
|
||||
ss = "" if seconds == 1 else "s"
|
||||
sm = "" if minutes == 1 else "s"
|
||||
sh = "" if hours == 1 else "s"
|
||||
sd = "" if days == 1 else "s"
|
||||
|
||||
displayed = 0
|
||||
components = []
|
||||
if days > 0:
|
||||
if days > 0 and ((not compact) or displayed < 2):
|
||||
components.append(str(days)+" day"+sd if verbose else str(days)+"d")
|
||||
displayed += 1
|
||||
|
||||
if hours > 0:
|
||||
if hours > 0 and ((not compact) or displayed < 2):
|
||||
components.append(str(hours)+" hour"+sh if verbose else str(hours)+"h")
|
||||
displayed += 1
|
||||
|
||||
if minutes > 0:
|
||||
if minutes > 0 and ((not compact) or displayed < 2):
|
||||
components.append(str(minutes)+" minute"+sm if verbose else str(minutes)+"m")
|
||||
displayed += 1
|
||||
|
||||
if seconds > 0:
|
||||
if seconds > 0 and ((not compact) or displayed < 2):
|
||||
components.append(str(seconds)+" second"+ss if verbose else str(seconds)+"s")
|
||||
displayed += 1
|
||||
|
||||
i = 0
|
||||
tstr = ""
|
||||
@@ -222,7 +297,64 @@ def prettytime(time, verbose=False):
|
||||
if tstr == "":
|
||||
return "0s"
|
||||
else:
|
||||
return tstr
|
||||
if not neg:
|
||||
return tstr
|
||||
else:
|
||||
return f"-{tstr}"
|
||||
|
||||
def prettyshorttime(time, verbose=False, compact=False):
|
||||
neg = False
|
||||
time = time*1e6
|
||||
if time < 0:
|
||||
time = abs(time)
|
||||
neg = True
|
||||
|
||||
seconds = int(time // 1e6); time %= 1e6
|
||||
milliseconds = int(time // 1e3); time %= 1e3
|
||||
|
||||
if compact:
|
||||
microseconds = int(time)
|
||||
else:
|
||||
microseconds = round(time, 2)
|
||||
|
||||
ss = "" if seconds == 1 else "s"
|
||||
sms = "" if milliseconds == 1 else "s"
|
||||
sus = "" if microseconds == 1 else "s"
|
||||
|
||||
displayed = 0
|
||||
components = []
|
||||
if seconds > 0 and ((not compact) or displayed < 2):
|
||||
components.append(str(seconds)+" second"+ss if verbose else str(seconds)+"s")
|
||||
displayed += 1
|
||||
|
||||
if milliseconds > 0 and ((not compact) or displayed < 2):
|
||||
components.append(str(milliseconds)+" millisecond"+sms if verbose else str(milliseconds)+"ms")
|
||||
displayed += 1
|
||||
|
||||
if microseconds > 0 and ((not compact) or displayed < 2):
|
||||
components.append(str(microseconds)+" microsecond"+sus if verbose else str(microseconds)+"µs")
|
||||
displayed += 1
|
||||
|
||||
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
|
||||
|
||||
if tstr == "":
|
||||
return "0us"
|
||||
else:
|
||||
if not neg:
|
||||
return tstr
|
||||
else:
|
||||
return f"-{tstr}"
|
||||
|
||||
def phyparams():
|
||||
print("Required Physical Layer MTU : "+str(Reticulum.MTU)+" bytes")
|
||||
@@ -236,6 +368,170 @@ def phyparams():
|
||||
def panic():
|
||||
os._exit(255)
|
||||
|
||||
def exit():
|
||||
print("")
|
||||
sys.exit(0)
|
||||
exit_called = False
|
||||
def exit(code=0):
|
||||
global exit_called
|
||||
if not exit_called:
|
||||
exit_called = True
|
||||
print("")
|
||||
Reticulum.exit_handler()
|
||||
os._exit(code)
|
||||
|
||||
class Profiler:
|
||||
_ran = False
|
||||
profilers = {}
|
||||
tags = {}
|
||||
|
||||
@staticmethod
|
||||
def get_profiler(tag=None, super_tag=None):
|
||||
if tag in Profiler.profilers:
|
||||
return Profiler.profilers[tag]
|
||||
else:
|
||||
profiler = Profiler(tag, super_tag)
|
||||
Profiler.profilers[tag] = profiler
|
||||
return profiler
|
||||
|
||||
def __init__(self, tag=None, super_tag=None):
|
||||
self.paused = False
|
||||
self.pause_time = 0
|
||||
self.pause_started = None
|
||||
self.tag = tag
|
||||
self.super_tag = super_tag
|
||||
if self.super_tag in Profiler.profilers:
|
||||
self.super_profiler = Profiler.profilers[self.super_tag]
|
||||
self.pause_super = self.super_profiler.pause
|
||||
self.resume_super = self.super_profiler.resume
|
||||
else:
|
||||
def noop(self=None):
|
||||
pass
|
||||
self.super_profiler = None
|
||||
self.pause_super = noop
|
||||
self.resume_super = noop
|
||||
|
||||
def __enter__(self):
|
||||
self.pause_super()
|
||||
tag = self.tag
|
||||
super_tag = self.super_tag
|
||||
thread_ident = threading.get_ident()
|
||||
if not tag in Profiler.tags:
|
||||
Profiler.tags[tag] = {"threads": {}, "super": super_tag}
|
||||
if not thread_ident in Profiler.tags[tag]["threads"]:
|
||||
Profiler.tags[tag]["threads"][thread_ident] = {"current_start": None, "captures": []}
|
||||
|
||||
Profiler.tags[tag]["threads"][thread_ident]["current_start"] = time.perf_counter()
|
||||
self.resume_super()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.pause_super()
|
||||
tag = self.tag
|
||||
super_tag = self.super_tag
|
||||
end = time.perf_counter() - self.pause_time
|
||||
self.pause_time = 0
|
||||
thread_ident = threading.get_ident()
|
||||
if tag in Profiler.tags and thread_ident in Profiler.tags[tag]["threads"]:
|
||||
if Profiler.tags[tag]["threads"][thread_ident]["current_start"] != None:
|
||||
begin = Profiler.tags[tag]["threads"][thread_ident]["current_start"]
|
||||
Profiler.tags[tag]["threads"][thread_ident]["current_start"] = None
|
||||
Profiler.tags[tag]["threads"][thread_ident]["captures"].append(end-begin)
|
||||
if not Profiler._ran:
|
||||
Profiler._ran = True
|
||||
self.resume_super()
|
||||
|
||||
def pause(self, pause_started=None):
|
||||
if not self.paused:
|
||||
self.paused = True
|
||||
self.pause_started = pause_started or time.perf_counter()
|
||||
self.pause_super(self.pause_started)
|
||||
|
||||
def resume(self):
|
||||
if self.paused:
|
||||
self.pause_time += time.perf_counter() - self.pause_started
|
||||
self.paused = False
|
||||
self.resume_super()
|
||||
|
||||
@staticmethod
|
||||
def ran():
|
||||
return Profiler._ran
|
||||
|
||||
@staticmethod
|
||||
def results():
|
||||
from statistics import mean, median, stdev
|
||||
results = {}
|
||||
|
||||
for tag in Profiler.tags:
|
||||
tag_captures = []
|
||||
tag_entry = Profiler.tags[tag]
|
||||
|
||||
for thread_ident in tag_entry["threads"]:
|
||||
thread_entry = tag_entry["threads"][thread_ident]
|
||||
thread_captures = thread_entry["captures"]
|
||||
sample_count = len(thread_captures)
|
||||
|
||||
if sample_count > 1:
|
||||
thread_results = {
|
||||
"count": sample_count,
|
||||
"mean": mean(thread_captures),
|
||||
"median": median(thread_captures),
|
||||
"stdev": stdev(thread_captures)
|
||||
}
|
||||
elif sample_count == 1:
|
||||
thread_results = {
|
||||
"count": sample_count,
|
||||
"mean": mean(thread_captures),
|
||||
"median": median(thread_captures),
|
||||
"stdev": None
|
||||
}
|
||||
|
||||
tag_captures.extend(thread_captures)
|
||||
|
||||
sample_count = len(tag_captures)
|
||||
if sample_count > 1:
|
||||
tag_results = {
|
||||
"name": tag,
|
||||
"super": tag_entry["super"],
|
||||
"count": len(tag_captures),
|
||||
"mean": mean(tag_captures),
|
||||
"median": median(tag_captures),
|
||||
"stdev": stdev(tag_captures)
|
||||
}
|
||||
elif sample_count == 1:
|
||||
tag_results = {
|
||||
"name": tag,
|
||||
"super": tag_entry["super"],
|
||||
"count": len(tag_captures),
|
||||
"mean": mean(tag_captures),
|
||||
"median": median(tag_captures),
|
||||
"stdev": None
|
||||
}
|
||||
|
||||
results[tag] = tag_results
|
||||
|
||||
def print_results_recursive(tag, results, level=0):
|
||||
print_tag_results(tag, level+1)
|
||||
|
||||
for tag_name in results:
|
||||
sub_tag = results[tag_name]
|
||||
if sub_tag["super"] == tag["name"]:
|
||||
print_results_recursive(sub_tag, results, level=level+1)
|
||||
|
||||
|
||||
def print_tag_results(tag, level):
|
||||
ind = " "*level
|
||||
name = tag["name"]; count = tag["count"]
|
||||
mean = tag["mean"]; median = tag["median"]; stdev = tag["stdev"]
|
||||
print( f"{ind}{name}")
|
||||
print( f"{ind} Samples : {count}")
|
||||
if stdev != None:
|
||||
print(f"{ind} Mean : {prettyshorttime(mean)}")
|
||||
print(f"{ind} Median : {prettyshorttime(median)}")
|
||||
print(f"{ind} St.dev. : {prettyshorttime(stdev)}")
|
||||
print( f"{ind} Total : {prettyshorttime(mean*count)}")
|
||||
print("")
|
||||
|
||||
print("\nProfiler results:\n")
|
||||
for tag_name in results:
|
||||
tag = results[tag_name]
|
||||
if tag["super"] == None:
|
||||
print_results_recursive(tag, results)
|
||||
|
||||
profile = Profiler.get_profiler
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.5.2"
|
||||
__version__ = "0.9.3"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import os
|
||||
import glob
|
||||
|
||||
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
|
||||
py_modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
|
||||
modules = py_modules+pyc_modules
|
||||
__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))]))
|
||||
@@ -11,6 +11,25 @@ def interfaces() -> List[str]:
|
||||
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
|
||||
return [a.name for a in adapters]
|
||||
|
||||
def interface_names_to_indexes() -> dict:
|
||||
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
|
||||
results = {}
|
||||
for adapter in adapters:
|
||||
results[adapter.name] = adapter.index
|
||||
return results
|
||||
|
||||
def interface_name_to_nice_name(ifname) -> str:
|
||||
try:
|
||||
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
|
||||
for adapter in adapters:
|
||||
if adapter.name == ifname:
|
||||
if hasattr(adapter, "nice_name"):
|
||||
return adapter.nice_name
|
||||
except:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def ifaddresses(ifname) -> dict:
|
||||
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
|
||||
ifa = {}
|
||||
|
||||
@@ -14,12 +14,17 @@ This document outlines the currently established development roadmap for Reticul
|
||||
## Currently Active Work Areas
|
||||
For each release cycle of Reticulum, improvements and additions from the five [Primary Efforts](#primary-efforts) are selected as active work areas, and can be expected to be included in the upcoming releases within that cycle. While not entirely set in stone for each release cycle, they serve as a pointer of what to expect in the near future.
|
||||
|
||||
- The current `0.5.x` release cycle aims at completing
|
||||
- The current `0.8.x` release cycle aims at completing
|
||||
- [ ] Hot-pluggable interface system
|
||||
- [ ] External interface plugins
|
||||
- [ ] Network-wide path balancing and multi-pathing
|
||||
- [ ] Expanded hardware support
|
||||
- [ ] Overhauling and updating the documentation
|
||||
- [ ] Distributed Destination Naming System
|
||||
- [ ] A standalone RNS Daemon app for Android
|
||||
- [ ] Addding automatic retries to all use cases of the `Request` API
|
||||
- [ ] Performance and memory optimisations of the Python reference implementation
|
||||
- [ ] Fixing potential bugs
|
||||
- [ ] Add automatic retries to all use cases of the `Request` API
|
||||
- [ ] Improve performance and efficiency of the `Buffer` and `Channel` API
|
||||
- [ ] Fixing bugs discovered while operating Reticulum systems and applications
|
||||
|
||||
## Primary Efforts
|
||||
The development path for Reticulum is currently laid out in five distinct areas: *Comprehensibility*, *Universality*, *Functionality*, *Usability & Utility* and *Interfaceability*. Conceptualising the development of Reticulum into these areas serves to advance the implementation and work towards the Foundational Goals & Values of Reticulum.
|
||||
@@ -33,17 +38,9 @@ These efforts are aimed at improving the ease of which Reticulum is understood,
|
||||
- Update announce description
|
||||
- Add in-depth explanation of the IFAC system
|
||||
- Software
|
||||
- Update Sideband screenshots
|
||||
- Update Sideband description
|
||||
- Update NomadNet screenshots
|
||||
- Update Sideband screenshots
|
||||
- Installation
|
||||
- Add a *Reticulum On Raspberry Pi* section
|
||||
- Update *Reticulum On Android* section if necessary
|
||||
- Update Android install documentation.
|
||||
- Update software descriptions and screenshots
|
||||
- Communications hardware section
|
||||
- Add information about RNode external displays.
|
||||
- Packet radio modems.
|
||||
- Possibly add other relevant types here as well.
|
||||
- Setup *Best Practices For...* / *Installation Examples* section.
|
||||
- Home or office (example)
|
||||
@@ -63,6 +60,8 @@ These efforts seek to broaden the universality of the Reticulum software and har
|
||||
### Functionality
|
||||
These efforts aim to expand and improve the core functionality and reliability of Reticulum.
|
||||
|
||||
- Add support for user-supplied external interface drivers
|
||||
- Add interface hot-plug and live up/down control to running instances
|
||||
- Add automatic retries to all use cases of the `Request` API
|
||||
- Network-wide path balancing
|
||||
- Distributed Destination Naming System
|
||||
@@ -73,7 +72,6 @@ These efforts aim to expand and improve the core functionality and reliability o
|
||||
### Usability & Utility
|
||||
These effors seek to make Reticulum easier to use and operate, and to expand the utility of the stack on deployed systems.
|
||||
|
||||
- Add bluetooth pairing code output to rnodeconf
|
||||
- Easy way to share interface configurations, see [#19](https://github.com/markqvist/Reticulum/discussions/19)
|
||||
- Transit traffic display in rnstatus
|
||||
- rnsconfig utility
|
||||
@@ -81,10 +79,10 @@ These effors seek to make Reticulum easier to use and operate, and to expand the
|
||||
### Interfaceability
|
||||
These efforts aim to expand the types of physical and virtual interfaces that Reticulum can natively use to transport data.
|
||||
|
||||
- Filesystem interface
|
||||
- Plain ESP32 devices (ESP-Now, WiFi, Bluetooth, etc.)
|
||||
- More LoRa transceivers
|
||||
- AT-compatible modems
|
||||
- Filesystem interface
|
||||
- Direct SDR Support
|
||||
- Optical mediums
|
||||
- IR Transceivers
|
||||
@@ -104,7 +102,7 @@ The Reticulum ecosystem is enriched by several other software and hardware proje
|
||||
This section lists, in no particular order, various important efforts that would be beneficial to the goals of Reticulum.
|
||||
|
||||
- The [RNode](https://unsigned.io/rnode/) project
|
||||
- [ ] Create a WebUSB-based bootstrapping utility, and integrate this directly into the [RNode Bootstrap Console](#), both on-device, and on an Internet-reachable copy. This will make it much easier to create new RNodes for average users.
|
||||
- [x] Create a WebUSB-based bootstrapping utility, and integrate this directly into the [RNode Bootstrap Console](#), both on-device, and on an Internet-reachable copy. This will make it much easier to create new RNodes for average users.
|
||||
|
||||
## Release History
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Sphinx build info version 1
|
||||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: c5670e7931d15fbef274e2eee17c3561
|
||||
config: 857be082cc412e043588c25a931b65f2
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 562 KiB After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 75 KiB |
@@ -125,4 +125,18 @@ interface to efficiently pass files of any size over a Reticulum :ref:`Link<api-
|
||||
|
||||
.. literalinclude:: ../../Examples/Filetransfer.py
|
||||
|
||||
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Filetransfer.py>`_.
|
||||
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/Filetransfer.py>`_.
|
||||
|
||||
.. _example-custominterface:
|
||||
|
||||
Custom Interfaces
|
||||
=================
|
||||
|
||||
The *ExampleInterface* demonstrates creating custom interfaces for Reticulum.
|
||||
Any number of custom interfaces can be loaded and utilised by Reticulum, and
|
||||
will be fully on-par with natively included interfaces, including all supported
|
||||
:ref:`interface modes<interfaces-modes>` and :ref:`common configuration options<interfaces-options>`.
|
||||
|
||||
.. literalinclude:: ../../Examples/ExampleInterface.py
|
||||
|
||||
This example can also be found at `<https://github.com/markqvist/Reticulum/blob/master/Examples/ExampleInterface.py>`_.
|
||||
@@ -27,9 +27,14 @@ and install them offline using ``pip``:
|
||||
|
||||
pip install ./rns-0.5.1-py3-none-any.whl
|
||||
|
||||
For more detailed installation instructions, please see the
|
||||
:ref:`Platform-Specific Install Notes<install-guides>` section.
|
||||
|
||||
After installation is complete, it might be helpful to refer to the
|
||||
:ref:`Using Reticulum on Your System<using-main>` chapter.
|
||||
|
||||
Resolving Dependency & Installation Issues
|
||||
=============================================
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On some platforms, there may not be binary packages available for all dependencies, and
|
||||
``pip`` installation may fail with an error message. In these cases, the issue can usually
|
||||
be resolved by installing the development essentials packages for your platform:
|
||||
@@ -70,7 +75,9 @@ Remote Shell
|
||||
|
||||
The `rnsh <https://github.com/acehoss/rnsh>`_ program lets you establish fully interactive
|
||||
remote shell sessions over Reticulum. It also allows you to pipe any program to or from a
|
||||
remote system, and is similar to how ``ssh`` works.
|
||||
remote system, and is similar to how ``ssh`` works. The ``rnsh`` is very efficient, and
|
||||
can facilitate fully interactive shell sessions, even over extremely low-bandwidth links,
|
||||
such as LoRa or packet radio.
|
||||
|
||||
Nomad Network
|
||||
^^^^^^^^^^^^^
|
||||
@@ -98,17 +105,19 @@ You can install Nomad Network via pip:
|
||||
# ... 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.
|
||||
.. note::
|
||||
If this is the very first time you use ``pip`` to install a program
|
||||
on your system, you might need to reboot your system for your program to become
|
||||
available. If you get a "command not found" error or similar when running the
|
||||
program, reboot your system and try again. In some cases, you may even need to
|
||||
manually add the ``pip`` install path to your ``PATH`` environment variable.
|
||||
|
||||
Sideband
|
||||
^^^^^^^^
|
||||
|
||||
If you would rather use a program with a graphical user interface, you can take
|
||||
a look at `Sideband <https://unsigned.io/sideband>`_, which is available for Android,
|
||||
Linux and macOS.
|
||||
Linux, macOS and Windows.
|
||||
|
||||
.. only:: html
|
||||
|
||||
@@ -127,6 +136,28 @@ systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, Encrypted Q
|
||||
Paper Messages, or anything else Reticulum supports. It also interoperates with
|
||||
the Nomad Network program.
|
||||
|
||||
MeshChat
|
||||
^^^^^^^^
|
||||
|
||||
The `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ application
|
||||
is a user-friendly LXMF client for macOS and Windows, that also includes voice call
|
||||
functionality, and a range of other interesting functions.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/meshchat_1.webp
|
||||
:align: center
|
||||
:target: _images/meshchat_1.webp
|
||||
|
||||
.. only:: latexpdf
|
||||
|
||||
.. image:: screenshots/meshchat_1.png
|
||||
:align: center
|
||||
:target: _images/meshchat_1.png
|
||||
|
||||
Reticulum MeshChat is of course also compatible with Sideband and Nomad Network, or
|
||||
any other LXMF client.
|
||||
|
||||
Using the Included Utilities
|
||||
=============================================
|
||||
Reticulum comes with a range of included utilities that make it easier to
|
||||
@@ -179,29 +210,29 @@ and :ref:`Interfaces<interfaces-main>` chapters of this manual.
|
||||
Connecting Reticulum Instances Over the Internet
|
||||
================================================
|
||||
Reticulum currently offers two interfaces suitable for connecting instances over the Internet: :ref:`TCP<interfaces-tcps>`
|
||||
and :ref:`I2P<interfaces-i2p>`. Each interface offers a different set of features, and Reticulum
|
||||
users should carefully choose the interface which best suites their needs.
|
||||
and :ref:`I2P<interfaces-i2p>`. Each interface offers a different set of features, and Reticulum
|
||||
users should carefully choose the interface which best suites their needs.
|
||||
|
||||
The ``TCPServerInterface`` allows users to host an instance accessible over TCP/IP. This
|
||||
method is generally faster, lower latency, and more energy efficient than using ``I2PInterface``,
|
||||
however it also leaks more data about the server host.
|
||||
|
||||
TCP connections reveal the IP address of both your instance and the server to anyone who can
|
||||
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
|
||||
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
|
||||
inspecting your packets may be able to record packet metadata like time of transmission and packet size.
|
||||
Even though Reticulum encrypts traffic, TCP does not, so an adversary may be able to use
|
||||
packet inspection to learn that a system is running Reticulum, and what other IP addresses connect to it.
|
||||
Hosting a publicly reachable instance over TCP also requires a publicly reachable IP address,
|
||||
which most Internet connections don't offer anymore.
|
||||
|
||||
The ``I2PInterface`` routes messages through the `Invisible Internet Protocol
|
||||
The ``I2PInterface`` routes messages through the `Invisible Internet Protocol
|
||||
(I2P) <https://geti2p.net/en/>`_. To use this interface, users must also run an I2P daemon in
|
||||
parallel to ``rnsd``. For always-on I2P nodes it is recommended to use `i2pd <https://i2pd.website/>`_.
|
||||
parallel to ``rnsd``. For always-on I2P nodes it is recommended to use `i2pd <https://i2pd.website/>`_.
|
||||
|
||||
By default, I2P will encrypt and mix all traffic sent over the Internet, and
|
||||
hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node
|
||||
By default, I2P will encrypt and mix all traffic sent over the Internet, and
|
||||
hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node
|
||||
will also relay other I2P user's encrypted packets, which will use extra
|
||||
bandwidth and compute power, but also makes timing attacks and other forms of
|
||||
bandwidth and compute power, but also makes timing attacks and other forms of
|
||||
deep-packet-inspection much more difficult.
|
||||
|
||||
I2P also allows users to host globally available Reticulum instances from non-public IP's and behind firewalls and NAT.
|
||||
@@ -219,30 +250,37 @@ by adding one of the following interfaces to your ``.reticulum/config`` file:
|
||||
|
||||
.. code::
|
||||
|
||||
# TCP/IP interface to the Dublin hub
|
||||
[[RNS Testnet Dublin]]
|
||||
# TCP/IP interface to the RNS Amsterdam Hub
|
||||
[[RNS Testnet Amsterdam]]
|
||||
type = TCPClientInterface
|
||||
enabled = yes
|
||||
target_host = dublin.connect.reticulum.network
|
||||
target_host = amsterdam.connect.reticulum.network
|
||||
target_port = 4965
|
||||
|
||||
# TCP/IP interface to the BetweenTheBorders Hub (community-provided)
|
||||
[[RNS Testnet BetweenTheBorders]]
|
||||
type = TCPClientInterface
|
||||
enabled = yes
|
||||
target_host = betweentheborders.com
|
||||
target_host = reticulum.betweentheborders.com
|
||||
target_port = 4242
|
||||
|
||||
# Interface to I2P hub A
|
||||
[[RNS Testnet I2P Hub A]]
|
||||
# Interface to Testnet I2P Hub
|
||||
[[RNS Testnet I2P Hub]]
|
||||
type = I2PInterface
|
||||
enabled = yes
|
||||
peers = uxg5kubabakh3jtnvsipingbr5574dle7bubvip7llfvwx2tgrua.b32.i2p
|
||||
peers = g3br23bvx3lq5uddcsjii74xgmn6y5q325ovrkq2zw2wbzbqgbuq.b32.i2p
|
||||
|
||||
Many other Reticulum instances are connecting to this testnet, and you can also join it
|
||||
via other entry points if you know them. There is absolutely no control over the network
|
||||
topography, usage or what types of instances connect. It will also occasionally be used
|
||||
to test various failure scenarios, and there are no availability or service guarantees.
|
||||
Expect weird things to happen on this network, as people experiment and try out things.
|
||||
|
||||
It probably goes without saying, but *don't use the testnet entry-points as
|
||||
hardcoded or default interfaces in any applications you ship to users*. When
|
||||
shipping applications, the best practice is to provide your own default
|
||||
connectivity solutions, if needed and applicable, or in most cases, simply
|
||||
leave it up to the user which networks to connect to, and how.
|
||||
|
||||
|
||||
Adding Radio Interfaces
|
||||
@@ -274,6 +312,20 @@ you are welcome to head over to the `GitHub discussion pages <https://github.com
|
||||
and propose adding an interface for the hardware.
|
||||
|
||||
|
||||
Creating and Using Custom Interfaces
|
||||
===========================================
|
||||
|
||||
While Reticulum includes a flexible and broad range of built-in interfaces, these
|
||||
will not cover every conceivable type of communications hardware that Reticulum
|
||||
can potentially use to communicate.
|
||||
|
||||
It is therefore possible to easily write your own interface modules, that can be
|
||||
loaded at run-time and used on-par with any of the built-in interface types.
|
||||
|
||||
For more information on this subject, and code examples to build on, please see
|
||||
the :ref:`Configuring Interfaces<interfaces-main>` chapter.
|
||||
|
||||
|
||||
Develop a Program with Reticulum
|
||||
===========================================
|
||||
If you want to develop programs that use Reticulum, the easiest way to get
|
||||
@@ -281,20 +333,14 @@ started is to install the latest release of Reticulum via pip:
|
||||
|
||||
.. code::
|
||||
|
||||
pip3 install rns
|
||||
pip install rns
|
||||
|
||||
The above command will install Reticulum and dependencies, and you will be
|
||||
ready to import and use RNS in your own programs. The next step will most
|
||||
likely be to look at some :ref:`Example Programs<examples-main>`.
|
||||
|
||||
For extended functionality, you can install optional dependencies:
|
||||
|
||||
.. code::
|
||||
|
||||
pip3 install pyserial
|
||||
|
||||
|
||||
Further information can be found in the :ref:`API Reference<api-main>`.
|
||||
The entire Reticulum API is documented in the :ref:`API Reference<api-main>`
|
||||
chapter of this manual.
|
||||
|
||||
|
||||
Participate in Reticulum Development
|
||||
@@ -306,7 +352,7 @@ don't use pip, but try this recipe:
|
||||
.. code::
|
||||
|
||||
# Install dependencies
|
||||
pip3 install cryptography pyserial
|
||||
pip install cryptography pyserial
|
||||
|
||||
# Clone repository
|
||||
git clone https://github.com/markqvist/Reticulum.git
|
||||
@@ -316,25 +362,25 @@ don't use pip, but try this recipe:
|
||||
ln -s ../RNS ./Examples/
|
||||
|
||||
# Run an example
|
||||
python3 Examples/Echo.py -s
|
||||
python Examples/Echo.py -s
|
||||
|
||||
# Unless you've manually created a config file, Reticulum will do so now,
|
||||
# and immediately exit. Make any necessary changes to the file:
|
||||
nano ~/.reticulum/config
|
||||
|
||||
# ... and launch the example again.
|
||||
python3 Examples/Echo.py -s
|
||||
python Examples/Echo.py -s
|
||||
|
||||
# You can now repeat the process on another computer,
|
||||
# and run the same example with -h to get command line options.
|
||||
python3 Examples/Echo.py -h
|
||||
python Examples/Echo.py -h
|
||||
|
||||
# Run the example in client mode to "ping" the server.
|
||||
# Replace the hash below with the actual destination hash of your server.
|
||||
python3 Examples/Echo.py 174a64852a75682259ad8b921b8bf416
|
||||
python Examples/Echo.py 174a64852a75682259ad8b921b8bf416
|
||||
|
||||
# Have a look at another example
|
||||
python3 Examples/Filetransfer.py -h
|
||||
python Examples/Filetransfer.py -h
|
||||
|
||||
When you have experimented with the basic examples, it's time to go read the
|
||||
:ref:`Understanding Reticulum<understanding-main>` chapter. Before submitting
|
||||
@@ -342,6 +388,7 @@ your first pull request, it is probably a good idea to introduce yourself on
|
||||
the `disucssion forum on GitHub <https://github.com/markqvist/Reticulum/discussions>`_,
|
||||
or ask one of the developers or maintainers for a good place to start.
|
||||
|
||||
.. _install-guides:
|
||||
|
||||
Platform-Specific Install Notes
|
||||
==============================================
|
||||
@@ -414,13 +461,13 @@ locally on your device using the following command:
|
||||
|
||||
It is also possible to include Reticulum in apps compiled and distributed as
|
||||
Android APKs. A detailed tutorial and example source code will be included
|
||||
here at a later point. Until then you can use the `Sideband source code <https://github.com/markqvist/sideband>`_ as an example and startig point.
|
||||
here at a later point. Until then you can use the `Sideband source code <https://github.com/markqvist/sideband>`_ as an example and starting point.
|
||||
|
||||
|
||||
ARM64
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On some architectures, including ARM64, not all dependencies have precompiled
|
||||
binaries. On such systems, you may need to install ``python3-dev`` before
|
||||
binaries. On such systems, you may need to install ``python3-dev`` (or similar) before
|
||||
installing Reticulum or programs that depend on Reticulum.
|
||||
|
||||
.. code::
|
||||
@@ -432,49 +479,197 @@ installing Reticulum or programs that depend on Reticulum.
|
||||
# Install Reticulum
|
||||
python3 -m pip install rns
|
||||
|
||||
With these packages installed, ``pip`` will be able to build any missing dependencies
|
||||
on your system locally.
|
||||
|
||||
|
||||
Debian Bookworm
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On versions of Debian released after April 2023, it is no longer possible by default
|
||||
to use ``pip`` to install packages onto your system. Unfortunately, you will need to
|
||||
use the replacement ``pipx`` command instead, which places installed packages in an
|
||||
isolated environment. This should not negatively affect Reticulum, but will not work
|
||||
for including and using Reticulum in your own scripts and programs.
|
||||
|
||||
.. code::
|
||||
|
||||
# Install pipx
|
||||
sudo apt install pipx
|
||||
|
||||
# Make installed programs available on the command line
|
||||
pipx ensurepath
|
||||
|
||||
# Install Reticulum
|
||||
pipx install rns
|
||||
|
||||
Alternatively, you can restore normal behaviour to ``pip`` by creating or editing
|
||||
the configuration file located at ``~/.config/pip/pip.conf``, and adding the
|
||||
following section:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[global]
|
||||
break-system-packages = true
|
||||
|
||||
For a one-shot installation of Reticulum, without globally enabling the ``break-system-packages``
|
||||
option, you can use the following command:
|
||||
|
||||
.. code:: text
|
||||
|
||||
pip install rns --break-system-packages
|
||||
|
||||
.. note::
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing ``pip`` packages user- and system-wide. While this *could* in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.
|
||||
|
||||
|
||||
MacOS
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
To install Reticulum on macOS, you will need to have Python and the ``pip`` package
|
||||
manager installed.
|
||||
|
||||
Systems running macOS can vary quite widely in whether or not Python is pre-installed,
|
||||
and if it is, which version is installed, and whether the ``pip`` package manager is
|
||||
also installed and set up. If in doubt, you can `download and install <https://www.python.org/downloads/>`_
|
||||
Python manually.
|
||||
|
||||
When Python and ``pip`` is available on your system, simply open a terminal window
|
||||
and use one of the following commands:
|
||||
|
||||
.. code::
|
||||
|
||||
# Install Reticulum and utilities with pip:
|
||||
pip3 install rns
|
||||
|
||||
# On some versions, you may need to use the
|
||||
# flag --break-system-packages to install:
|
||||
pip3 install rns --break-system-packages
|
||||
|
||||
.. note::
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing ``pip`` packages user- and system-wide. While this *could* in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.
|
||||
|
||||
Additionally, some version combinations of macOS and Python require you to
|
||||
manually add your installed ``pip`` packages directory to your `PATH` environment
|
||||
variable, before you can use installed commands in your terminal. Usually, adding
|
||||
the following line to your shell init script (for example ``~/.zshrc``) will be enough:
|
||||
|
||||
.. code::
|
||||
|
||||
export PATH=$PATH:~/Library/Python/3.9/bin
|
||||
|
||||
Adjust Python version and shell init script location according to your system.
|
||||
|
||||
|
||||
OpenWRT
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On OpenWRT systems with sufficient storage and memory, you can install
|
||||
Reticulum and related utilities using the `opkg` package manager and `pip`.
|
||||
|
||||
.. note::
|
||||
|
||||
At the time of releasing this manual, work is underway to create pre-built
|
||||
Reticulum packages for OpenWRT, with full configuration, service
|
||||
and ``uci`` integration. Please see the `feed-reticulum <https://github.com/gretel/feed-reticulum>`_
|
||||
and `reticulum-openwrt <https://github.com/gretel/reticulum-openwrt>`_
|
||||
repositories for more information.
|
||||
|
||||
To install Reticulum on OpenWRT, first log into a command line session, and
|
||||
then use the following instructions:
|
||||
|
||||
.. code::
|
||||
|
||||
# Install dependencies
|
||||
opkg install python3 python3-pip python3-cryptography python3-pyserial
|
||||
|
||||
# Install Reticulum
|
||||
pip install rns
|
||||
|
||||
# Start rnsd with debug logging enabled
|
||||
rnsd -vvv
|
||||
|
||||
.. note::
|
||||
|
||||
The above instructions have been verified and tested on OpenWRT 21.02 only.
|
||||
It is likely that other versions may require slightly altered installation
|
||||
commands or package names. You will also need enough free space in your
|
||||
overlay FS, and enough free RAM to actually run Reticulum and any related
|
||||
programs and utilities.
|
||||
|
||||
Depending on your device configuration, you may need to adjust firewall rules
|
||||
for Reticulum connectivity to and from your device to work. Until proper
|
||||
packaging is ready, you will also need to manually create a service or startup
|
||||
script to automatically laucnh Reticulum at boot time.
|
||||
|
||||
Please also note that the `AutoInterface` requires link-local IPv6 addresses
|
||||
to be enabled for any Ethernet and WiFi devices you intend to use. If ``ip a``
|
||||
shows an address starting with ``fe80::`` for the device in question,
|
||||
``AutoInterface`` should work for that device.
|
||||
|
||||
Raspberry Pi
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
It is currently recommended to use a 64-bit version of the Raspberry Pi OS
|
||||
if you want to run Reticulum on Raspberry Pi computers, since 32-bit versions
|
||||
don't always have packages available for some dependencies.
|
||||
|
||||
While it is possible to install and run Reticulum on 32-bit Rasperry Pi OSes,
|
||||
it will require manually configuring and installing some packages, and is not
|
||||
detailed in this manual.
|
||||
|
||||
|
||||
Debian Bookworm
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On versions of Debian released after April 2023, it is no longer possible
|
||||
to use ``pip`` to install packages onto your system. Unfortunately, you will need to
|
||||
use the replacement ``pipx`` command instead, which places installed packages in an
|
||||
isolated environment. This should not negatively affect Reticulum, but installation
|
||||
via this method is not fully tested yet.
|
||||
don't always have packages available for some dependencies. If Python and the
|
||||
`pip` package manager is not already installed, do that first, and then
|
||||
install Reticulum using `pip`.
|
||||
|
||||
.. code::
|
||||
|
||||
# Install pipx
|
||||
sudo apt install pipx
|
||||
# Install dependencies
|
||||
sudo apt install python3 python3-pip python3-cryptography python3-pyserial
|
||||
|
||||
# Install Reticulum
|
||||
pip install rns --break-system-packages
|
||||
|
||||
# Make installed programs available on the command line
|
||||
pipx ensurepath
|
||||
.. note::
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing ``pip`` packages user- and system-wide. While this *could* in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.
|
||||
|
||||
# Install Reticulum
|
||||
pipx install rns
|
||||
While it is possible to install and run Reticulum on 32-bit Rasperry Pi OSes,
|
||||
it will require manually configuring and installing required build dependencies,
|
||||
and is not detailed in this manual.
|
||||
|
||||
|
||||
RISC-V
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On some architectures, including RISC-V, not all dependencies have precompiled
|
||||
binaries. On such systems, you may need to install ``python3-dev`` (or similar) before
|
||||
installing Reticulum or programs that depend on Reticulum.
|
||||
|
||||
.. code::
|
||||
|
||||
# Install Python and development packages
|
||||
sudo apt update
|
||||
sudo apt install python3 python3-pip python3-dev
|
||||
|
||||
# Install Reticulum
|
||||
python3 -m pip install rns
|
||||
|
||||
With these packages installed, ``pip`` will be able to build any missing dependencies
|
||||
on your system locally.
|
||||
|
||||
|
||||
Ubuntu Lunar
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On versions of Ubuntu released after April 2023, it is no longer possible
|
||||
On versions of Ubuntu released after April 2023, it is no longer possible by default
|
||||
to use ``pip`` to install packages onto your system. Unfortunately, you will need to
|
||||
use the replacement ``pipx`` command instead, which places installed packages in an
|
||||
isolated environment. This should not negatively affect Reticulum, but installation
|
||||
via this method is not fully tested yet.
|
||||
isolated environment. This should not negatively affect Reticulum, but will not work
|
||||
for including and using Reticulum in your own scripts and programs.
|
||||
|
||||
.. code::
|
||||
|
||||
# Install pipx
|
||||
# Install pipx
|
||||
sudo apt install pipx
|
||||
|
||||
# Make installed programs available on the command line
|
||||
@@ -483,14 +678,63 @@ via this method is not fully tested yet.
|
||||
# Install Reticulum
|
||||
pipx install rns
|
||||
|
||||
Alternatively, you can restore normal behaviour to ``pip`` by creating or editing
|
||||
the configuration file located at ``~/.config/pip/pip.conf``, and adding the
|
||||
following section:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[global]
|
||||
break-system-packages = true
|
||||
|
||||
For a one-shot installation of Reticulum, without globally enabling the ``break-system-packages``
|
||||
option, you can use the following command:
|
||||
|
||||
.. code:: text
|
||||
|
||||
pip install rns --break-system-packages
|
||||
|
||||
.. note::
|
||||
The ``--break-system-packages`` directive is a somewhat misleading choice
|
||||
of words. Setting it will of course not break any system packages, but will simply
|
||||
allow installing ``pip`` packages user- and system-wide. While this *could* in rare
|
||||
cases lead to version conflicts, it does not generally pose any problems, especially
|
||||
not in the case of installing Reticulum.
|
||||
|
||||
Windows
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
On Windows operating systems, the easiest way to install Reticulum is by using the
|
||||
``pip`` package manager from the command line (either the command prompt or Windows
|
||||
Powershell).
|
||||
|
||||
If you don't already have Python installed, `download and install Python <https://www.python.org/downloads/>`_.
|
||||
At the time of publication of this manual, the recommended version is `Python 3.12.7 <https://www.python.org/downloads/release/python-3127>`_.
|
||||
|
||||
**Important!** When asked by the installer, make sure to add the Python program to
|
||||
your PATH environment variables. If you don't do this, you will not be able to
|
||||
use the ``pip`` installer, or run the included Reticulum utility programs (such as
|
||||
``rnsd`` and ``rnstatus``) from the command line.
|
||||
|
||||
After installing Python, open the command prompt or Windows Powershell, and type:
|
||||
|
||||
.. code::
|
||||
|
||||
pip install rns
|
||||
|
||||
You can now use Reticulum and all included utility programs directly from your
|
||||
preferred command line interface.
|
||||
|
||||
Pure-Python Reticulum
|
||||
==============================================
|
||||
In some rare cases, and on more obscure system types, it is not possible to
|
||||
install one or more dependencies
|
||||
|
||||
On more unusual systems, and in some rare cases, it might not be possible to
|
||||
install or even compile one or more of the above modules. In such situations,
|
||||
.. warning::
|
||||
If you use the ``rnspure`` package to run Reticulum on systems that
|
||||
do not support `PyCA/cryptography <https://github.com/pyca/cryptography>`_, it is
|
||||
important that you read and understand the :ref:`Cryptographic Primitives <understanding-primitives>`
|
||||
section of this manual.
|
||||
|
||||
In some rare cases, and on more obscure system types, it is not possible to
|
||||
install one or more dependencies. In such situations,
|
||||
you can use the ``rnspure`` package instead of the ``rns`` package, or use ``pip``
|
||||
with the ``--no-dependencies`` command-line option. The ``rnspure``
|
||||
package requires no external dependencies for installation. Please note that the
|
||||
@@ -503,8 +747,3 @@ only if they are *needed* and *available*. If for example you want to use Reticu
|
||||
on a system that cannot support ``pyserial``, it is perfectly possible to do so using
|
||||
the `rnspure` package, but Reticulum will not be able to use serial-based interfaces.
|
||||
All other available modules will still be loaded when needed.
|
||||
|
||||
**Please Note!** If you use the `rnspure` package to run Reticulum on systems that
|
||||
do not support `PyCA/cryptography <https://github.com/pyca/cryptography>`_, it is
|
||||
important that you read and understand the :ref:`Cryptographic Primitives <understanding-primitives>`
|
||||
section of this manual.
|
||||
|
||||
@@ -24,11 +24,20 @@ starting from scratch.
|
||||
This chapter will outline a few different sensible starting paths to get
|
||||
real-world functional wireless communications up and running with minimal cost
|
||||
and effort. Two fundamental devices categories will be covered, *RNodes* and
|
||||
*WiFi-based radios*.
|
||||
*WiFi-based radios*. Additionally, other common options will be briefly described.
|
||||
|
||||
Knowing how to employ just a few different types of hardware will make it possible
|
||||
to build a wide range of useful networks with little effort.
|
||||
|
||||
Combining Hardware Types
|
||||
========================
|
||||
|
||||
It is useful to combine different link and hardware types when designing and
|
||||
building a network. One useful design pattern is to employ high-capacity point-to-point
|
||||
links based on WiFi or millimeter-wave radios (with high-gain directional antennas)
|
||||
for the network backbone, and using LoRa-based RNodes for covering large areas with
|
||||
connectivity for client devices.
|
||||
|
||||
While there are many other device categories that are useful in building Reticulum
|
||||
networks, knowing how to employ just these two will make it possible to build
|
||||
a wide range of useful networks with little effort.
|
||||
|
||||
.. _rnode-main:
|
||||
|
||||
@@ -66,8 +75,8 @@ completely from scratch, to your exact desired specifications, this chapter
|
||||
will explain the easiest possible approach to creating RNodes: Using common
|
||||
LoRa development boards. This approach can be boiled down to two simple steps:
|
||||
|
||||
1. Obtain one or more supported development boards
|
||||
2. Install the RNode firmware with the automated installer
|
||||
1. Obtain one or more :ref:`supported development boards<rnode-supported>`
|
||||
2. Install the RNode firmware with the :ref:`automated installer<rnode-installation>`
|
||||
|
||||
Once the firmware has been installed and provisioned by the install script, it
|
||||
is ready to use with any software that supports RNodes, including Reticulum.
|
||||
@@ -76,82 +85,156 @@ to the configuration.
|
||||
|
||||
.. _rnode-supported:
|
||||
|
||||
Supported Boards
|
||||
^^^^^^^^^^^^^^^^
|
||||
Supported Boards and Devices
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
To create one or more RNodes, you will need to obtain supported development
|
||||
boards. The following boards are supported by the auto-installer.
|
||||
|
||||
LilyGO LoRa32 v2.1
|
||||
""""""""""""""""""
|
||||
.. image:: graphics/board_t3v21.png
|
||||
:width: 46%
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_tbeam_supreme.png
|
||||
:width: 75%
|
||||
:align: center
|
||||
|
||||
- **Supported Firmware Lines** v1.x & v2.x
|
||||
- **Transceiver IC** Semtech SX1276
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
|
||||
LilyGO LoRa32 v2.0
|
||||
""""""""""""""""""
|
||||
.. image:: graphics/board_t3v20.png
|
||||
:width: 46%
|
||||
:align: center
|
||||
|
||||
- **Supported Firmware Lines** v1.x & v2.x
|
||||
- **Transceiver IC** Semtech SX1276
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
|
||||
LilyGO T-Beam
|
||||
LilyGO T-Beam Supreme
|
||||
"""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262, SX1268
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_tbeam.png
|
||||
:width: 75%
|
||||
:align: center
|
||||
|
||||
- **Supported Firmware Lines** v1.x & v2.x
|
||||
- **Transceiver IC** Semtech SX1276
|
||||
LilyGO T-Beam
|
||||
"""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262, SX1268, SX1276 and SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
------------
|
||||
|
||||
Heltec LoRa32 v2.0
|
||||
""""""""""""""""""
|
||||
.. image:: graphics/board_heltec32.png
|
||||
:width: 58%
|
||||
.. image:: graphics/board_t3s3.png
|
||||
:width: 50%
|
||||
:align: center
|
||||
|
||||
- **Supported Firmware Lines** v1.x & v2.x
|
||||
- **Transceiver IC** Semtech SX1276
|
||||
LilyGO T3S3
|
||||
"""""""""""
|
||||
- **Transceiver IC** Semtech SX1262, SX1268, SX1276 and SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_rak4631.png
|
||||
:width: 45%
|
||||
:align: center
|
||||
|
||||
RAK4631-based Boards
|
||||
""""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262, SX1268
|
||||
- **Device Platform** nRF52
|
||||
- **Manufacturer** `RAK Wireless <https://www.rakwireless.com>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_rnodev2.png
|
||||
:width: 68%
|
||||
:align: center
|
||||
|
||||
Unsigned RNode v2.x
|
||||
"""""""""""""""""""
|
||||
.. image:: graphics/board_rnodev2.png
|
||||
:width: 58%
|
||||
:align: center
|
||||
|
||||
- **Supported Firmware Lines** v1.x & v2.x
|
||||
- **Transceiver IC** Semtech SX1276
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `unsigned.io <https://unsigned.io>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_t3v21.png
|
||||
:width: 46%
|
||||
:align: center
|
||||
|
||||
LilyGO LoRa32 v2.1
|
||||
""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_t3v20.png
|
||||
:width: 46%
|
||||
:align: center
|
||||
|
||||
LilyGO LoRa32 v2.0
|
||||
""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_t3v10.png
|
||||
:width: 46%
|
||||
:align: center
|
||||
|
||||
LilyGO LoRa32 v1.0
|
||||
""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_tdeck.png
|
||||
:width: 45%
|
||||
:align: center
|
||||
|
||||
LilyGO T-Deck
|
||||
"""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262, SX1268
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_heltec32v30.png
|
||||
:width: 58%
|
||||
:align: center
|
||||
|
||||
Heltec LoRa32 v3.0
|
||||
""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262 and SX1268
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_heltec32v20.png
|
||||
:width: 58%
|
||||
:align: center
|
||||
|
||||
Heltec LoRa32 v2.0
|
||||
""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
|
||||
|
||||
------------
|
||||
|
||||
Unsigned RNode v1.x
|
||||
"""""""""""""""""""
|
||||
.. image:: graphics/board_rnode.png
|
||||
:width: 50%
|
||||
:align: center
|
||||
|
||||
- **Supported Firmware Lines** v1.x
|
||||
- **Transceiver IC** Semtech SX1276
|
||||
Unsigned RNode v1.x
|
||||
"""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Device Platform** AVR ATmega1284p
|
||||
- **Manufacturer** `unsigned.io <https://unsigned.io>`_
|
||||
|
||||
------------
|
||||
|
||||
.. _rnode-installation:
|
||||
|
||||
@@ -185,17 +268,8 @@ Usage with Reticulum
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
When the devices have been installed and provisioned, you can use them with Reticulum
|
||||
by adding the :ref:`relevant interface section<interfaces-rnode>` to the configuration
|
||||
file of Reticulum. For v1.x firmwares, you will have to specify all interface parameters,
|
||||
such as serial port and on-air parameters. For v2.x firmwares, you just need to specify
|
||||
the Connection ID of the RNode, and Reticulum will automatically locate and connect to the
|
||||
RNode, using the parameters stored in the RNode itself.
|
||||
|
||||
.. _rnode-suppliers:
|
||||
|
||||
Suppliers
|
||||
^^^^^^^^^
|
||||
Get in touch if you want to have your RNode supplier listed here, or if you want help to
|
||||
get started with producing RNodes.
|
||||
file of Reticulum. In the configuraion you can specify all interface parameters,
|
||||
such as serial port and on-air parameters.
|
||||
|
||||
|
||||
WiFi-based Hardware
|
||||
@@ -231,11 +305,31 @@ that is relatively cheap while providing long range and high capacity for Reticu
|
||||
networks. As in all other cases, it is also possible for Reticulum to co-exist with IP
|
||||
networks running concurrently on such devices.
|
||||
|
||||
Combining Hardware Types
|
||||
========================
|
||||
Ethernet-based Hardware
|
||||
=======================
|
||||
|
||||
It is useful to combine different link and hardware types when designing and
|
||||
building a network. One useful design pattern is to employ high-capacity point-to-point
|
||||
links based on WiFi or millimeter-wave radios (with high-gain directional antennas)
|
||||
for the network backbone, and using LoRa-based RNodes for covering large areas with
|
||||
connectivity for client devices.
|
||||
Reticulum can run over any kind of hardware that can provide a switched Ethernet-based
|
||||
medium. This means that anything from a plain Ethernet switch, to fiber-optic systems,
|
||||
to data radios with Ethernet interfaces can be used by Reticulum.
|
||||
|
||||
The Ethernet medium does not need to have any IP infrastructure such as DHCP servers
|
||||
or routing set up, but in case such infrastructure does exist, Reticulum will simply
|
||||
co-exist with.
|
||||
|
||||
To use Reticulum over Ethernet-based mediums, it is generally enough to use the included
|
||||
:ref:`AutoInterface<interfaces-auto>`. This interface also works over any kind of
|
||||
virtual networking adapter, such as ``tun`` and ``tap`` devices in Linux.
|
||||
|
||||
Serial Lines & Devices
|
||||
======================
|
||||
|
||||
Using Reticulum over any kind of raw serial line is also possible with the
|
||||
:ref:`SerialInterface<interfaces-serial>`. This interface type is also useful for
|
||||
using Reticulum over communications hardware that provides a serial port interface.
|
||||
|
||||
Packet Radio Modems
|
||||
===================
|
||||
|
||||
Any packet radio modem that provides a standard KISS interface over USB, serial or TCP
|
||||
can be used with Reticulum. This includes virtual software modems such as
|
||||
`FreeDV TNC <https://github.com/xssfox/freedv-tnc>`_ and `Dire Wolf <https://github.com/wb2osz/direwolf>`_.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
.. _interfaces-main:
|
||||
|
||||
********************
|
||||
Supported Interfaces
|
||||
********************
|
||||
**********************
|
||||
Configuring Interfaces
|
||||
**********************
|
||||
|
||||
Reticulum supports using many kinds of devices as networking interfaces, and
|
||||
allows you to mix and match them in any way you choose. The number of distinct
|
||||
@@ -19,6 +19,16 @@ types, have a look at the :ref:`Building Networks<networks-main>` chapter of thi
|
||||
manual.
|
||||
|
||||
|
||||
.. _interfaces-custom:
|
||||
|
||||
Custom Interfaces
|
||||
=================
|
||||
|
||||
In addition to the built-in interface types, Reticulum is **fully extensible** with
|
||||
custom, user- or community-supplied interfaces, and creating custom interface
|
||||
modules is straightforward. Please see the :ref:`custom interface<example-custominterface>`
|
||||
example for basic interface code to build upon.
|
||||
|
||||
.. _interfaces-auto:
|
||||
|
||||
Auto Interface
|
||||
@@ -33,9 +43,20 @@ system, which should be enabled by default in almost all OSes.
|
||||
|
||||
.. code::
|
||||
|
||||
# This example demonstrates a TCP server interface.
|
||||
# It will listen for incoming connections on the
|
||||
# specified IP address and port number.
|
||||
# This example demonstrates a bare-minimum setup
|
||||
# of an Auto Interface. It will allow communica-
|
||||
# tion with all other reachable devices on all
|
||||
# usable physical ethernet-based devices that
|
||||
# are available on the system.
|
||||
|
||||
[[Default Interface]]
|
||||
type = AutoInterface
|
||||
interface_enabled = True
|
||||
|
||||
# This example demonstrates an more specifically
|
||||
# configured Auto Interface, that only uses spe-
|
||||
# cific physical interfaces, and has a number of
|
||||
# other configuration options set.
|
||||
|
||||
[[Default Interface]]
|
||||
type = AutoInterface
|
||||
@@ -47,6 +68,12 @@ system, which should be enabled by default in almost all OSes.
|
||||
|
||||
group_id = reticulum
|
||||
|
||||
# You can also choose the multicast address type:
|
||||
# temporary (default, Temporary Multicast Address)
|
||||
# or permanent (Permanent Multicast Address)
|
||||
|
||||
multicast_address_type = permanent
|
||||
|
||||
# You can also select specifically which
|
||||
# kernel networking devices to use.
|
||||
|
||||
@@ -135,11 +162,12 @@ 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.
|
||||
.. 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
|
||||
@@ -154,7 +182,7 @@ 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
|
||||
the Internet or private IPv4 and IPv6 networks. When a TCP server interface has been
|
||||
configured, other Reticulum peers can connect to it with a TCP Client interface.
|
||||
|
||||
.. code::
|
||||
@@ -183,8 +211,37 @@ configured, other Reticulum peers can connect to it with a TCP Client interface.
|
||||
# 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:
|
||||
If you are using the interface on a device which has both IPv4 and IPv6 addresses available,
|
||||
you can use the ``prefer_ipv6`` option to bind to the IPv6 address:
|
||||
|
||||
.. 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
|
||||
|
||||
device = eth0
|
||||
port = 4242
|
||||
prefer_ipv6 = True
|
||||
|
||||
To use the TCP Server Interface over `Yggdrasil <https://yggdrasil-network.github.io/>`_, you
|
||||
can simply specify the Yggdrasil ``tun`` device and a listening port, like so:
|
||||
|
||||
.. code::
|
||||
|
||||
[[Yggdrasil TCP Server Interface]]
|
||||
type = TCPServerInterface
|
||||
interface_enabled = yes
|
||||
device = tun0
|
||||
listen_port = 4343
|
||||
|
||||
.. note::
|
||||
The TCP interfaces support tunneling over I2P, but to do so reliably,
|
||||
you must use the i2p_tunneled option:
|
||||
|
||||
.. code::
|
||||
|
||||
@@ -214,7 +271,7 @@ and restore connectivity after a failure, once the other end of a TCP interface
|
||||
.. code::
|
||||
|
||||
# Here's an example of a TCP Client interface. The
|
||||
# target_host can either be an IP address or a hostname.
|
||||
# target_host can be a hostname or an IPv4 or IPv6 address.
|
||||
|
||||
[[TCP Client Interface]]
|
||||
type = TCPClientInterface
|
||||
@@ -222,6 +279,17 @@ and restore connectivity after a failure, once the other end of a TCP interface
|
||||
target_host = 127.0.0.1
|
||||
target_port = 4242
|
||||
|
||||
To use the TCP Client Interface over `Yggdrasil <https://yggdrasil-network.github.io/>`_, simply
|
||||
specify the target Yggdrasil IPv6 address and port, like so:
|
||||
|
||||
.. code::
|
||||
|
||||
[[Yggdrasil TCP Client Interface]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = yes
|
||||
target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5
|
||||
target_port = 4343
|
||||
|
||||
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:
|
||||
@@ -245,8 +313,9 @@ 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:
|
||||
.. note::
|
||||
The TCP interfaces support tunneling over I2P, but to do so reliably,
|
||||
you must use the i2p_tunneled option:
|
||||
|
||||
.. code::
|
||||
|
||||
@@ -268,11 +337,12 @@ 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.
|
||||
.. warning::
|
||||
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.
|
||||
|
||||
.. code::
|
||||
|
||||
@@ -327,6 +397,11 @@ 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.
|
||||
|
||||
.. warning::
|
||||
Radio frequency spectrum is a legally controlled resource, and legislation
|
||||
varies widely around the world. It is your responsibility to be aware of any
|
||||
relevant regulation for your location, and to make decisions accordingly.
|
||||
|
||||
.. code::
|
||||
|
||||
# Here's an example of how to add a LoRa interface
|
||||
@@ -341,6 +416,23 @@ can be used, and offers full control over LoRa parameters.
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB0
|
||||
|
||||
# It is also possible to use BLE devices
|
||||
# instead of wired serial ports. The
|
||||
# target RNode must be paired with the
|
||||
# host device before connecting. BLE
|
||||
# devices can be connected by name,
|
||||
# BLE MAC address or by any available.
|
||||
|
||||
# Connect to specific device by name
|
||||
# port = ble://RNode 3B87
|
||||
|
||||
# Or by BLE MAC address
|
||||
# port = ble://F4:12:73:29:4E:89
|
||||
|
||||
# Or connect to the first available,
|
||||
# paired device
|
||||
# port = ble://
|
||||
|
||||
# Set frequency to 867.2 MHz
|
||||
frequency = 867200000
|
||||
|
||||
@@ -365,6 +457,7 @@ can be used, and offers full control over LoRa parameters.
|
||||
# out identification on the channel with
|
||||
# a set interval by configuring the
|
||||
# following two parameters.
|
||||
|
||||
# id_callsign = MYCALL-0
|
||||
# id_interval = 600
|
||||
|
||||
@@ -372,7 +465,141 @@ can be used, and offers full control over LoRa parameters.
|
||||
# with low amounts of RAM, using packet
|
||||
# flow control can be useful. By default
|
||||
# it is disabled.
|
||||
flow_control = False
|
||||
|
||||
# flow_control = False
|
||||
|
||||
# It is possible to limit the airtime
|
||||
# utilisation of an RNode by using the
|
||||
# following two configuration options.
|
||||
# The short-term limit is applied in a
|
||||
# window of approximately 15 seconds,
|
||||
# and the long-term limit is enforced
|
||||
# over a rolling 60 minute window. Both
|
||||
# options are specified in percent.
|
||||
|
||||
# airtime_limit_long = 1.5
|
||||
# airtime_limit_short = 33
|
||||
|
||||
|
||||
.. _interfaces-rnode-multi:
|
||||
|
||||
RNode Multi Interface
|
||||
=====================
|
||||
|
||||
For RNodes that support multiple LoRa transceivers, the RNode
|
||||
Multi interface can be used to configure sub-interfaces individually.
|
||||
|
||||
.. warning::
|
||||
Radio frequency spectrum is a legally controlled resource, and legislation
|
||||
varies widely around the world. It is your responsibility to be aware of any
|
||||
relevant regulation for your location, and to make decisions accordingly.
|
||||
|
||||
.. code::
|
||||
|
||||
# Here's an example of how to add an RNode Multi interface
|
||||
# using the RNode LoRa transceiver.
|
||||
|
||||
[[RNode Multi Interface]]
|
||||
type = RNodeMultiInterface
|
||||
|
||||
# Enable interface if you want to use it!
|
||||
interface_enabled = True
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyACM0
|
||||
|
||||
# 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
|
||||
|
||||
# A subinterface
|
||||
[[[High Datarate]]]
|
||||
# Subinterfaces can be enabled and disabled in of themselves
|
||||
interface_enabled = True
|
||||
|
||||
# Set frequency to 2.4GHz
|
||||
frequency = 2400000000
|
||||
|
||||
# Set LoRa bandwidth to 1625 KHz
|
||||
bandwidth = 1625000
|
||||
|
||||
# Set TX power to 0 dBm (0.12 mW)
|
||||
txpower = 0
|
||||
|
||||
# The virtual port, only the manufacturer
|
||||
# or the person who wrote the board config
|
||||
# can tell you what it will be for which
|
||||
# physical hardware interface
|
||||
vport = 1
|
||||
|
||||
# Select spreading factor 5. Valid
|
||||
# range is 5 through 12, with 5
|
||||
# being the fastest and 12 having
|
||||
# the longest range.
|
||||
spreadingfactor = 5
|
||||
|
||||
# Select coding rate 5. Valid range
|
||||
# is 5 throough 8, with 5 being the
|
||||
# fastest, and 8 the longest range.
|
||||
codingrate = 5
|
||||
|
||||
# It is possible to limit the airtime
|
||||
# utilisation of an RNode by using the
|
||||
# following two configuration options.
|
||||
# The short-term limit is applied in a
|
||||
# window of approximately 15 seconds,
|
||||
# and the long-term limit is enforced
|
||||
# over a rolling 60 minute window. Both
|
||||
# options are specified in percent.
|
||||
|
||||
# airtime_limit_long = 100
|
||||
# airtime_limit_short = 100
|
||||
|
||||
[[[Low Datarate]]]
|
||||
# Subinterfaces can be enabled and disabled in of themselves
|
||||
interface_enabled = True
|
||||
|
||||
# Set frequency to 865.6 MHz
|
||||
frequency = 865600000
|
||||
|
||||
# The virtual port, only the manufacturer
|
||||
# or the person who wrote the board config
|
||||
# can tell you what it will be for which
|
||||
# physical hardware interface
|
||||
vport = 0
|
||||
|
||||
# Set LoRa bandwidth to 125 KHz
|
||||
bandwidth = 125000
|
||||
|
||||
# Set TX power to 0 dBm (0.12 mW)
|
||||
txpower = 0
|
||||
|
||||
# Select spreading factor 7. Valid
|
||||
# range is 5 through 12, with 5
|
||||
# being the fastest and 12 having
|
||||
# the longest range.
|
||||
spreadingfactor = 7
|
||||
|
||||
# Select coding rate 5. Valid range
|
||||
# is 5 throough 8, with 5 being the
|
||||
# fastest, and 8 the longest range.
|
||||
codingrate = 5
|
||||
|
||||
# It is possible to limit the airtime
|
||||
# utilisation of an RNode by using the
|
||||
# following two configuration options.
|
||||
# The short-term limit is applied in a
|
||||
# window of approximately 15 seconds,
|
||||
# and the long-term limit is enforced
|
||||
# over a rolling 60 minute window. Both
|
||||
# options are specified in percent.
|
||||
|
||||
# airtime_limit_long = 100
|
||||
# airtime_limit_short = 100
|
||||
|
||||
.. _interfaces-serial:
|
||||
|
||||
@@ -434,6 +661,11 @@ 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.
|
||||
|
||||
.. warning::
|
||||
Radio frequency spectrum is a legally controlled resource, and legislation
|
||||
varies widely around the world. It is your responsibility to be aware of any
|
||||
relevant regulation for your location, and to make decisions accordingly.
|
||||
|
||||
.. code::
|
||||
|
||||
[[Packet Radio KISS Interface]]
|
||||
@@ -497,6 +729,11 @@ encapsulate in AX.25.
|
||||
A more efficient way is to use the plain KISS interface with the
|
||||
beaconing functionality described above.
|
||||
|
||||
.. warning::
|
||||
Radio frequency spectrum is a legally controlled resource, and legislation
|
||||
varies widely around the world. It is your responsibility to be aware of any
|
||||
relevant regulation for your location, and to make decisions accordingly.
|
||||
|
||||
.. code::
|
||||
|
||||
[[Packet Radio AX.25 KISS Interface]]
|
||||
@@ -746,3 +983,74 @@ conserve bandwidth, while very fast networks can support applications that
|
||||
need very frequent announces. Reticulum implements these mechanisms to ensure
|
||||
that a large span of network types can seamlessly *co-exist* and interconnect.
|
||||
|
||||
.. _interfaces-ingress-control:
|
||||
|
||||
New Destination Rate Limiting
|
||||
=============================
|
||||
|
||||
On public interfaces, where anyone may connect and announce new destinations,
|
||||
it can be useful to control the rate at which announces for *new* destinations are
|
||||
processed.
|
||||
|
||||
If a large influx of announces for newly created or previously unknown destinations
|
||||
occur within a short amount of time, Reticulum will place these announces on hold,
|
||||
so that announce traffic for known and previously established destinations can
|
||||
continue to be processed without interruptions.
|
||||
|
||||
After the burst subsides, and an additional waiting period has passed, the held
|
||||
announces will be released at a slow rate, until the hold queue is cleared. This
|
||||
also means, that should a node decide to connect to a public interface, announce
|
||||
a large amount of bogus destinations, and then disconnect, these destination will
|
||||
never make it into path tables and waste network bandwidth on retransmitted
|
||||
announces.
|
||||
|
||||
**It's important to note** that the ingress control works at the level of *individual
|
||||
sub-interfaces*. As an example, this means that one client on a :ref:`TCP Server Interface<interfaces-tcps>`
|
||||
cannot disrupt processing of incoming announces for other connected clients on the same
|
||||
:ref:`TCP Server Interface<interfaces-tcps>`. All other clients on the same interface will still have new announces
|
||||
processed without interruption.
|
||||
|
||||
By default, Reticulum will handle this automatically, and ingress announce
|
||||
control will be enabled on interface where it is sensible to do so. It should
|
||||
generally not be neccessary to modify the ingress control configuration,
|
||||
but all the parameters are exposed for configuration if needed.
|
||||
|
||||
* | The ``ingress_control`` option tells Reticulum whether or not
|
||||
to enable announce ingress control on the interface. Defaults to
|
||||
``True``.
|
||||
|
||||
* | The ``ic_new_time`` option configures how long (in seconds) an
|
||||
interface is considered newly spawned. Defaults to ``2*60*60`` seconds. This
|
||||
option is useful on publicly accessible interfaces that spawn new
|
||||
sub-interfaces when a new client connects.
|
||||
|
||||
* | The ``ic_burst_freq_new`` option sets the maximum announce ingress
|
||||
frequency for newly spawned interfaces. Defaults to ``3.5``
|
||||
announces per second.
|
||||
|
||||
* | The ``ic_burst_freq`` option sets the maximum announce ingress
|
||||
frequency for other interfaces. Defaults to ``12`` announces
|
||||
per second.
|
||||
|
||||
*If an interface exceeds its burst frequency, incoming announces
|
||||
for unknown destinations will be temporarily held in a queue, and
|
||||
not processed until later.*
|
||||
|
||||
* | The ``ic_max_held_announces`` option sets the maximum amount of
|
||||
unique announces that will be held in the queue. Any additional
|
||||
unique announces will be dropped. Defaults to ``256`` announces.
|
||||
|
||||
* | The ``ic_burst_hold`` option sets how much time (in seconds) must
|
||||
pass after the burst frequency drops below its threshold, for the
|
||||
announce burst to be considered cleared. Defaults to ``60``
|
||||
seconds.
|
||||
|
||||
* | The ``ic_burst_penalty`` option sets how much time (in seconds) must
|
||||
pass after the burst is considered cleared, before held announces can
|
||||
start being released from the queue. Defaults to ``5*60``
|
||||
seconds.
|
||||
|
||||
* | The ``ic_held_release_interval`` option sets how much time (in seconds)
|
||||
must pass between releasing each held announce from the queue. Defaults
|
||||
to ``30`` seconds.
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ with Reticulum:
|
||||
|
||||
* | 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
|
||||
join and leave in a free and unorganised manner. This property allows an
|
||||
entirely new, and so far, mostly unexplored class of networked applications,
|
||||
where networks, and the information flow within them can form and dissolve
|
||||
organically.
|
||||
|
||||
@@ -32,7 +32,9 @@ Provide Feedback
|
||||
================
|
||||
All feedback on the usage, functioning and potential dysfunctioning of any and
|
||||
all components of the system is very valuable to the continued development and
|
||||
improvement of Reticulum. Absolutely no automated analytics, telemetry, error
|
||||
improvement of Reticulum.
|
||||
|
||||
Absolutely no automated analytics, telemetry, error
|
||||
reporting or statistics is collected and reported by Reticulum under any
|
||||
circumstances, so we rely on old-fashioned human feedback.
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ guide the design of Reticulum:
|
||||
it can be easily modified and replicated by anyone interested in doing so.
|
||||
* **Very low bandwidth requirements**
|
||||
Reticulum should be able to function reliably over links with a transmission capacity as low
|
||||
as *500 bits per second*.
|
||||
as *5 bits per second*.
|
||||
* **Encryption by default**
|
||||
Reticulum must use strong encryption by default for all communication.
|
||||
* **Initiator Anonymity**
|
||||
@@ -134,10 +134,11 @@ be sufficient, even far into the future.
|
||||
By default Reticulum encrypts all data using elliptic curve cryptography and AES. Any packet sent to a
|
||||
destination is encrypted with a per-packet derived key. Reticulum can also set up an encrypted
|
||||
channel to a destination, called a *Link*. Both data sent over Links and single packets offer
|
||||
*Initiator Anonymity*, and links additionally offer *Forward Secrecy* by using an Elliptic Curve
|
||||
Diffie Hellman key exchange on Curve25519 to derive per-link ephemeral keys. The multi-hop transport,
|
||||
coordination, verification and reliability layers are fully autonomous and also based on elliptic
|
||||
curve cryptography.
|
||||
*Initiator Anonymity*. Links additionally offer *Forward Secrecy* by default, employing an Elliptic Curve
|
||||
Diffie Hellman key exchange on Curve25519 to derive per-link ephemeral keys. Asymmetric, link-less
|
||||
packet communication can also provide forward secrecy, with automatic key ratcheting, by enabling
|
||||
ratchets on a per-destination basis. The multi-hop transport, coordination, verification and reliability
|
||||
layers are fully autonomous and also based on elliptic curve cryptography.
|
||||
|
||||
Reticulum also offers symmetric key encryption for group-oriented communications, as well as
|
||||
unencrypted packets for local broadcast purposes.
|
||||
@@ -431,7 +432,7 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
|
||||
|
||||
* | A packet is always created with an associated destination and some payload data. When the packet is sent
|
||||
to a *single* destination type, Reticulum will automatically create an ephemeral encryption key, perform
|
||||
an ECDH key exchange with the destination's public key, and encrypt the information.
|
||||
an ECDH key exchange with the destination's public key (or ratchet key, if available), 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
|
||||
@@ -595,7 +596,7 @@ or less any medium that allows you to send and receive data, which satisfies som
|
||||
minimum requirements.
|
||||
|
||||
The communication channel must support at least half-duplex operation, and provide an average
|
||||
throughput of around 500 bits per second, and supports a physical layer MTU of 500 bytes. The
|
||||
throughput of 5 bits per second or greater, and supports a physical layer MTU of 500 bytes. The
|
||||
Reticulum stack should be able to run on more or less any hardware that can provide a Python 3.x
|
||||
runtime environment.
|
||||
|
||||
@@ -693,7 +694,8 @@ Wire Format
|
||||
[HEADER 2 bytes] [ADDRESSES 16/32 bytes] [CONTEXT 1 byte] [DATA 0-465 bytes]
|
||||
|
||||
* The HEADER field is 2 bytes long.
|
||||
* Byte 1: [IFAC Flag], [Header Type], [Propagation Type], [Destination Type] and [Packet Type]
|
||||
* Byte 1: [IFAC Flag], [Header Type], [Context Flag], [Propagation Type],
|
||||
[Destination Type] and [Packet Type]
|
||||
* Byte 2: Number of hops
|
||||
|
||||
* Interface Access Code field if the IFAC flag was set.
|
||||
@@ -725,12 +727,16 @@ Wire Format
|
||||
type 2 1 Two byte header, two 16 byte address fields
|
||||
|
||||
|
||||
Context Flag
|
||||
-----------------
|
||||
unset 0 The context flag is used for various types
|
||||
set 1 of signalling, depending on packet context
|
||||
|
||||
|
||||
Propagation Types
|
||||
-----------------
|
||||
broadcast 00
|
||||
transport 01
|
||||
reserved 10
|
||||
reserved 11
|
||||
broadcast 0
|
||||
transport 1
|
||||
|
||||
|
||||
Destination Types
|
||||
@@ -858,15 +864,21 @@ both on general-purpose CPUs and on microcontrollers. The necessary primitives a
|
||||
|
||||
* Ed25519 for signatures
|
||||
|
||||
* X22519 for ECDH key exchanges
|
||||
* X25519 for ECDH key exchanges
|
||||
|
||||
* HKDF for key derivation
|
||||
|
||||
* Fernet for encrypted tokens
|
||||
* Encrypted tokens are based on the Fernet spec
|
||||
|
||||
* AES-128 in CBC mode
|
||||
* Ephemeral keys derived from an ECDH key exchange on Curve25519
|
||||
|
||||
* HMAC for message authentication
|
||||
* AES-128 in CBC mode with PKCS7 padding
|
||||
|
||||
* HMAC using SHA256 for message authentication
|
||||
|
||||
* IVs are generated through os.urandom()
|
||||
|
||||
* No Fernet version and timestamp metadata fields
|
||||
|
||||
* SHA-256
|
||||
|
||||
@@ -876,12 +888,12 @@ In the default installation configuration, the ``X25519``, ``Ed25519`` and ``AES
|
||||
primitives are provided by `OpenSSL <https://www.openssl.org/>`_ (via the `PyCA/cryptography <https://github.com/pyca/cryptography>`_
|
||||
package). The hashing functions ``SHA-256`` and ``SHA-512`` are provided by the standard
|
||||
Python `hashlib <https://docs.python.org/3/library/hashlib.html>`_. The ``HKDF``, ``HMAC``,
|
||||
``Fernet`` primitives, and the ``PKCS7`` padding function are always provided by the
|
||||
``Token`` primitives, and the ``PKCS7`` padding function are always provided by the
|
||||
following internal implementations:
|
||||
|
||||
- ``RNS/Cryptography/HKDF.py``
|
||||
- ``RNS/Cryptography/HMAC.py``
|
||||
- ``RNS/Cryptography/Fernet.py``
|
||||
- ``RNS/Cryptography/Token.py``
|
||||
- ``RNS/Cryptography/PKCS7.py``
|
||||
|
||||
|
||||
@@ -892,6 +904,7 @@ with the OpenSSL backend being *much* faster. The most important consequence how
|
||||
potential loss of security by using primitives that has not seen the same amount of scrutiny,
|
||||
testing and review as those from OpenSSL.
|
||||
|
||||
If you want to use the internal pure-python primitives, it is **highly advisable** that you
|
||||
have a good understanding of the risks that this pose, and make an informed decision on whether
|
||||
those risks are acceptable to you.
|
||||
.. warning::
|
||||
If you want to use the internal pure-python primitives, it is **highly advisable** that you
|
||||
have a good understanding of the risks that this pose, and make an informed decision on whether
|
||||
those risks are acceptable to you.
|
||||
@@ -99,6 +99,17 @@ configuration file is created. The default configuration looks like this:
|
||||
instance_control_port = 37429
|
||||
|
||||
|
||||
# On systems where running instances may not have access
|
||||
# to the same shared Reticulum configuration directory,
|
||||
# it is still possible to allow full interactivity for
|
||||
# running instances, by manually specifying a shared RPC
|
||||
# key. In almost all cases, this option is not needed, but
|
||||
# it can be useful on operating systems such as Android.
|
||||
# The key must be specified as bytes in hexadecimal.
|
||||
|
||||
# rpc_key = e5c032d3ec4e64a6aca9927ba8ab73336780f6d71790
|
||||
|
||||
|
||||
# You can configure Reticulum to panic and forcibly close
|
||||
# if an unrecoverable interface error occurs, such as the
|
||||
# hardware device for an interface disappearing. This is
|
||||
@@ -108,6 +119,17 @@ configuration file is created. The default configuration looks like this:
|
||||
panic_on_interface_error = No
|
||||
|
||||
|
||||
# When Transport is enabled, it is possible to allow the
|
||||
# Transport Instance to respond to probe requests from
|
||||
# the rnprobe utility. This can be a useful tool to test
|
||||
# connectivity. When this option is enabled, the probe
|
||||
# destination will be generated from the Identity of the
|
||||
# Transport Instance, and printed to the log at startup.
|
||||
# Optional, and disabled by default.
|
||||
|
||||
respond_to_probes = No
|
||||
|
||||
|
||||
[logging]
|
||||
# Valid log levels are 0 through 7:
|
||||
# 0: Log only critical information
|
||||
@@ -145,10 +167,19 @@ configuration file is created. The default configuration looks like this:
|
||||
If Reticulum infrastructure already exists locally, you probably don't need to
|
||||
change anything, and you may already be connected to a wider network. If not,
|
||||
you will probably need to add relevant *interfaces* to the configuration, in
|
||||
order to communicate with other systems. It is a good idea to read the comments
|
||||
and explanations in the above default config. It will teach you the basic
|
||||
concepts you need to understand to configure your network. Once you have done that,
|
||||
take a look at the :ref:`Interfaces<interfaces-main>` chapter of this manual.
|
||||
order to communicate with other systems.
|
||||
|
||||
You can generate a much more verbose configuration example by running the command:
|
||||
|
||||
``rnsd --exampleconfig``
|
||||
|
||||
The output includes examples for most interface types supported
|
||||
by Reticulum, along with additional options and configuration parameters.
|
||||
|
||||
It is a good idea to read the comments and explanations in the above default config.
|
||||
It will teach you the basic concepts you need to understand to configure your network.
|
||||
Once you have done that, take a look at the :ref:`Interfaces<interfaces-main>` chapter
|
||||
of this manual.
|
||||
|
||||
Included Utility Programs
|
||||
-------------------------
|
||||
@@ -170,28 +201,47 @@ When ``rnsd`` is running, it will keep all configured interfaces open, handle tr
|
||||
it is enabled, and allow any other programs to immediately utilise the
|
||||
Reticulum network it is configured for.
|
||||
|
||||
You can even run multiple instances of rnsd with different configurations on
|
||||
You can even run multiple instances of ``rnsd`` with different configurations on
|
||||
the same system.
|
||||
|
||||
.. code:: text
|
||||
**Usage Examples**
|
||||
|
||||
# Install Reticulum
|
||||
pip3 install rns
|
||||
|
||||
# Run rnsd
|
||||
rnsd
|
||||
Run ``rnsd``:
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnsd [-h] [--config CONFIG] [-v] [-q] [--version]
|
||||
$ rnsd
|
||||
|
||||
[2023-08-18 17:59:56] [Notice] Started rnsd version 0.5.8
|
||||
|
||||
Run ``rnsd`` in service mode, ensuring all logging output is sent directly to file:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsd -s
|
||||
|
||||
Generate a verbose and detailed configuration example, with explanations of all the
|
||||
various configuration options, and interface configuration examples:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsd --exampleconfig
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnsd.py [-h] [--config CONFIG] [-v] [-q] [-s] [--exampleconfig] [--version]
|
||||
|
||||
Reticulum Network Stack Daemon
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
-s, --service rnsd is running as a service and should log to file
|
||||
--exampleconfig print verbose configuration example to stdout and exit
|
||||
--version show program's version number and exit
|
||||
|
||||
You can easily add ``rnsd`` as an always-on service by :ref:`configuring a service<using-systemd>`.
|
||||
@@ -202,12 +252,14 @@ The rnstatus Utility
|
||||
Using the ``rnstatus`` utility, you can view the status of configured Reticulum
|
||||
interfaces, similar to the ``ifconfig`` program.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
Run ``rnstatus``:
|
||||
|
||||
.. code:: text
|
||||
|
||||
# Run rnstatus
|
||||
rnstatus
|
||||
$ rnstatus
|
||||
|
||||
# Example output
|
||||
Shared Instance[37428]
|
||||
Status : Up
|
||||
Serving : 1 program
|
||||
@@ -240,52 +292,195 @@ interfaces, similar to the ``ifconfig`` program.
|
||||
|
||||
Reticulum Transport Instance <5245a8efe1788c6a1cd36144a270e13b> running
|
||||
|
||||
Filter output to only show some interfaces:
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-v]
|
||||
$ rnstatus rnode
|
||||
|
||||
RNodeInterface[RNode UHF]
|
||||
Status : Up
|
||||
Mode : Access Point
|
||||
Rate : 1.30 kbps
|
||||
Access : 64-bit IFAC by <…e702c42ba8>
|
||||
Traffic : 8.49 KB↑
|
||||
9.23 KB↓
|
||||
|
||||
Reticulum Transport Instance <5245a8efe1788c6a1cd36144a270e13b> running
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A]
|
||||
[-l] [-s SORT] [-r] [-j] [-R hash] [-i path]
|
||||
[-w seconds] [-v] [filter]
|
||||
|
||||
Reticulum Network Stack Status
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
--version show program's version number and exit
|
||||
-a, --all show all interfaces
|
||||
positional arguments:
|
||||
filter only display interfaces with names including filter
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
--version show program's version number and exit
|
||||
-a, --all show all interfaces
|
||||
-A, --announce-stats show announce stats
|
||||
-l, --link-stats show link stats
|
||||
-s SORT, --sort SORT sort interfaces by [rate, traffic, rx, tx, announces, arx, atx, held]
|
||||
-r, --reverse reverse sorting
|
||||
-j, --json output in JSON format
|
||||
-R hash transport identity hash of remote instance to get status from
|
||||
-i path path to identity used for remote management
|
||||
-w seconds timeout before giving up on remote queries
|
||||
-v, --verbose
|
||||
|
||||
|
||||
The rnid Utility
|
||||
====================
|
||||
|
||||
With the ``rnid`` utility, you can generate, manage and view Reticulum Identities.
|
||||
The program can also calculate Destination hashes, and perform encryption and
|
||||
decryption of files.
|
||||
|
||||
Using ``rnid``, it is possible to asymmetrically encrypt files and information for
|
||||
any Reticulum destination hash, and also to create and verify cryptographic signatures.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
Generate a new Identity:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnid -g ./new_identity
|
||||
|
||||
Display Identity key information:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnid -i ./new_identity -p
|
||||
|
||||
Loaded Identity <984b74a3f768bef236af4371e6f248cd> from new_id
|
||||
Public Key : 0f4259fef4521ab75a3409e353fe9073eb10783b4912a6a9937c57bf44a62c1e
|
||||
Private Key : Hidden
|
||||
|
||||
Encrypt a file for an LXMF user:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnid -i 8dd57a738226809646089335a6b03695 -e my_file.txt
|
||||
|
||||
Recalled Identity <bc7291552be7a58f361522990465165c> for destination <8dd57a738226809646089335a6b03695>
|
||||
Encrypting my_file.txt
|
||||
File my_file.txt encrypted for <bc7291552be7a58f361522990465165c> to my_file.txt.rfe
|
||||
|
||||
If the Identity for the destination is not already known, you can fetch it from the network by using the ``-R`` command-line option:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnid -R -i 30602def3b3506a28ed33db6f60cc6c9 -e my_file.txt
|
||||
|
||||
Requesting unknown Identity for <30602def3b3506a28ed33db6f60cc6c9>...
|
||||
Received Identity <2b489d06eaf7c543808c76a5332a447d> for destination <30602def3b3506a28ed33db6f60cc6c9> from the network
|
||||
Encrypting my_file.txt
|
||||
File my_file.txt encrypted for <2b489d06eaf7c543808c76a5332a447d> to my_file.txt.rfe
|
||||
|
||||
Decrypt a file using the Reticulum Identity it was encrypted for:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnid -i ./my_identity -d my_file.txt.rfe
|
||||
|
||||
Loaded Identity <2225fdeecaf6e2db4556c3c2d7637294> from ./my_identity
|
||||
Decrypting ./my_file.txt.rfe...
|
||||
File ./my_file.txt.rfe decrypted with <2225fdeecaf6e2db4556c3c2d7637294> to ./my_file.txt
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnid.py [-h] [--config path] [-i identity] [-g path] [-v] [-q] [-a aspects]
|
||||
[-H aspects] [-e path] [-d path] [-s path] [-V path] [-r path] [-w path]
|
||||
[-f] [-R] [-t seconds] [-p] [-P] [--version]
|
||||
|
||||
Reticulum Identity & Encryption Utility
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config path path to alternative Reticulum config directory
|
||||
-i identity, --identity identity
|
||||
hexadecimal Reticulum Destination hash or path to Identity file
|
||||
-g path, --generate path
|
||||
generate a new Identity
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-a aspects, --announce aspects
|
||||
announce a destination based on this Identity
|
||||
-H aspects, --hash aspects
|
||||
show destination hashes for other aspects for this Identity
|
||||
-e path, --encrypt path
|
||||
encrypt file
|
||||
-d path, --decrypt path
|
||||
decrypt file
|
||||
-s path, --sign path sign file
|
||||
-V path, --validate path
|
||||
validate signature
|
||||
-r path, --read path input file path
|
||||
-w path, --write path
|
||||
output file path
|
||||
-f, --force write output even if it overwrites existing files
|
||||
-R, --request request unknown Identities from the network
|
||||
-t seconds identity request timeout before giving up
|
||||
-p, --print-identity print identity info and exit
|
||||
-P, --print-private allow displaying private keys
|
||||
--version show program's version number and exit
|
||||
|
||||
|
||||
The rnpath Utility
|
||||
====================
|
||||
|
||||
With the ``rnpath`` utility, you can look up and view paths for
|
||||
destinations on the Reticulum network.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
Resolve path to a destination:
|
||||
|
||||
.. code:: text
|
||||
|
||||
# Run rnpath
|
||||
rnpath c89b4da064bf66d280f0e4d8abfd9806
|
||||
$ rnpath c89b4da064bf66d280f0e4d8abfd9806
|
||||
|
||||
# Example output
|
||||
Path found, destination <c89b4da064bf66d280f0e4d8abfd9806> is 4 hops away via <f53a1c4278e0726bb73fcc623d6ce763> on TCPInterface[Testnet/dublin.connect.reticulum.network:4965]
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnpath [-h] [--config CONFIG] [--version] [-t] [-r] [-d] [-D] [-w seconds] [-v] [destination]
|
||||
|
||||
usage: rnpath [-h] [--config CONFIG] [--version] [-t] [-m hops]
|
||||
[-r] [-d] [-D] [-x] [-w seconds] [-R hash] [-i path]
|
||||
[-W seconds] [-j] [-v] [destination]
|
||||
|
||||
Reticulum Path Discovery Utility
|
||||
|
||||
|
||||
positional arguments:
|
||||
destination hexadecimal hash of the destination
|
||||
|
||||
optional arguments:
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
--version show program's version number and exit
|
||||
-t, --table show all known paths
|
||||
-m hops, --max hops maximum hops to filter path table by
|
||||
-r, --rates show announce rate info
|
||||
-d, --drop remove the path to a destination
|
||||
-D, --drop-announces drop all queued announces
|
||||
-x, --drop-via drop all paths via specified transport instance
|
||||
-w seconds timeout before giving up
|
||||
-R hash transport identity hash of remote instance to manage
|
||||
-i path path to identity used for remote management
|
||||
-W seconds timeout before giving up on remote queries
|
||||
-j, --json output in JSON format
|
||||
-v, --verbose
|
||||
|
||||
|
||||
@@ -295,32 +490,72 @@ The rnprobe Utility
|
||||
The ``rnprobe`` utility lets you probe a destination for connectivity, similar
|
||||
to the ``ping`` program. Please note that probes will only be answered if the
|
||||
specified destination is configured to send proofs for received packets. Many
|
||||
destinations will not have this option enabled, and will not be probable.
|
||||
destinations will not have this option enabled, so most destinations will not
|
||||
be probable.
|
||||
|
||||
You can enable a probe-reply destination on Reticulum Transport Instances by
|
||||
setting the ``respond_to_probes`` configuration directive. Reticulum will then
|
||||
print the probe destination to the log on Transport Instance startup.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
Probe a destination:
|
||||
|
||||
.. code:: text
|
||||
|
||||
# Run rnprobe
|
||||
rnprobe example_utilities.echo.request 2d03725b327348980d570f739a3a5708
|
||||
$ rnprobe rnstransport.probe 2d03725b327348980d570f739a3a5708
|
||||
|
||||
# Example output
|
||||
Sent 16 byte probe to <2d03725b327348980d570f739a3a5708>
|
||||
Valid reply received from <2d03725b327348980d570f739a3a5708>
|
||||
Round-trip time is 38.469 milliseconds over 2 hops
|
||||
|
||||
Send a larger probe:
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnprobe [-h] [--config CONFIG] [--version] [-v] [full_name] [destination_hash]
|
||||
$ rnprobe rnstransport.probe 2d03725b327348980d570f739a3a5708 -s 256
|
||||
|
||||
Sent 16 byte probe to <2d03725b327348980d570f739a3a5708>
|
||||
Valid reply received from <2d03725b327348980d570f739a3a5708>
|
||||
Round-trip time is 38.781 milliseconds over 2 hops
|
||||
|
||||
If the interface that receives the probe replies supports reporting radio
|
||||
parameters such as **RSSI** and **SNR**, the ``rnprobe`` utility will print
|
||||
these as part of the result as well.
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnprobe rnstransport.probe e7536ee90bd4a440e130490b87a25124
|
||||
|
||||
Sent 16 byte probe to <e7536ee90bd4a440e130490b87a25124>
|
||||
Valid reply received from <e7536ee90bd4a440e130490b87a25124>
|
||||
Round-trip time is 1.809 seconds over 1 hop [RSSI -73 dBm] [SNR 12.0 dB]
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnprobe [-h] [--config CONFIG] [-s SIZE] [-n PROBES]
|
||||
[-t seconds] [-w seconds] [--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
|
||||
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
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
-s SIZE, --size SIZE size of probe packet payload in bytes
|
||||
-n PROBES, --probes PROBES
|
||||
number of probes to send
|
||||
-t seconds, --timeout seconds
|
||||
timeout before giving up
|
||||
-w seconds, --wait seconds
|
||||
time between each probe
|
||||
--version show program's version number and exit
|
||||
-v, --verbose
|
||||
|
||||
|
||||
@@ -330,20 +565,40 @@ The rncp Utility
|
||||
The ``rncp`` utility is a simple file transfer tool. Using it, you can transfer
|
||||
files through Reticulum.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
Run rncp on the receiving system, specifying which identities are allowed to send files:
|
||||
|
||||
.. code:: text
|
||||
|
||||
# Run rncp on the receiving system, specifying which identities
|
||||
# are allowed to send files
|
||||
rncp --receive -a 1726dbad538775b5bf9b0ea25a4079c8 -a c50cc4e4f7838b6c31f60ab9032cbc62
|
||||
$ rncp --listen -a 1726dbad538775b5bf9b0ea25a4079c8 -a c50cc4e4f7838b6c31f60ab9032cbc62
|
||||
|
||||
# From another system, copy a file to the receiving system
|
||||
rncp ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e
|
||||
|
||||
You can specify as many allowed senders as needed, or complete disable authentication.
|
||||
You can also specify allowed identity hashes (one per line) in the file ~/.rncp/allowed_identities
|
||||
and simply running the program in listener mode:
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rncp [-h] [--config path] [-v] [-q] [-p] [-r] [-b] [-a allowed_hash] [-n] [-w seconds] [--version] [file] [destination]
|
||||
$ rncp --listen
|
||||
|
||||
From another system, copy a file to the receiving system:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rncp ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e
|
||||
|
||||
Or fetch a file from the remote system:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rncp --fetch ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rncp [-h] [--config path] [-v] [-q] [-S] [-l] [-F] [-f]
|
||||
[-j path] [-b seconds] [-a allowed_hash] [-n] [-p]
|
||||
[-w seconds] [--version] [file] [destination]
|
||||
|
||||
Reticulum File Transfer Utility
|
||||
|
||||
@@ -351,19 +606,22 @@ You can specify as many allowed senders as needed, or complete disable authentic
|
||||
file file to be transferred
|
||||
destination hexadecimal hash of the receiver
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config path path to alternative Reticulum config directory
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-S, --silent disable transfer progress output
|
||||
-l, --listen listen for incoming transfer requests
|
||||
-F, --allow-fetch allow authenticated clients to fetch files
|
||||
-f, --fetch fetch file from remote listener instead of sending
|
||||
-j path, --jail path restrict fetch requests to specified path
|
||||
-b seconds announce interval, 0 to only announce at startup
|
||||
-a allowed_hash allow this identity
|
||||
-n, --no-auth accept requests from anyone
|
||||
-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
|
||||
@@ -371,32 +629,43 @@ The rnx Utility
|
||||
|
||||
The ``rnx`` utility is a basic remote command execution program. It allows you to
|
||||
execute commands on remote systems over Reticulum, and to view returned command
|
||||
output.
|
||||
output. For a fully interactive remote shell solution, be sure to also take a look
|
||||
at the `rnsh <https://github.com/acehoss/rnsh>`_ program.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
Run rnx on the listening system, specifying which identities are allowed to execute commands:
|
||||
|
||||
.. code:: text
|
||||
|
||||
# Run rnx on the listening system, specifying which identities
|
||||
# are allowed to execute commands
|
||||
rnx --listen -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
|
||||
$ rnx --listen -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
|
||||
|
||||
# From another system, run a command
|
||||
rnx 7a55144adf826958a9529a3bcf08b149 "cat /proc/cpuinfo"
|
||||
|
||||
# Or enter the interactive mode pseudo-shell
|
||||
rnx 7a55144adf826958a9529a3bcf08b149 -x
|
||||
|
||||
# The default identity file is stored in
|
||||
# ~/.reticulum/identities/rnx, but you can use
|
||||
# another one, which will be created if it does
|
||||
# not already exist
|
||||
rnx 7a55144adf826958a9529a3bcf08b149 -i /path/to/identity -x
|
||||
|
||||
You can specify as many allowed senders as needed, or completely disable authentication.
|
||||
From another system, run a command on the remote:
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnx [-h] [--config path] [-v] [-q] [-p] [-l] [-i identity] [-x] [-b] [-a allowed_hash] [-n] [-N] [-d] [-m] [-w seconds] [-W seconds] [--stdin STDIN] [--stdout STDOUT] [--stderr STDERR] [--version]
|
||||
[destination] [command]
|
||||
$ rnx 7a55144adf826958a9529a3bcf08b149 "cat /proc/cpuinfo"
|
||||
|
||||
Or enter the interactive mode pseudo-shell:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnx 7a55144adf826958a9529a3bcf08b149 -x
|
||||
|
||||
The default identity file is stored in ``~/.reticulum/identities/rnx``, but you can use
|
||||
another one, which will be created if it does not already exist
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnx 7a55144adf826958a9529a3bcf08b149 -i /path/to/identity -x
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnx [-h] [--config path] [-v] [-q] [-p] [-l] [-i identity] [-x] [-b] [-n] [-N]
|
||||
[-d] [-m] [-a allowed_hash] [-w seconds] [-W seconds] [--stdin STDIN]
|
||||
[--stdout STDOUT] [--stderr STDERR] [--version] [destination] [command]
|
||||
|
||||
Reticulum Remote Execution Utility
|
||||
|
||||
@@ -433,11 +702,23 @@ The rnodeconf Utility
|
||||
The ``rnodeconf`` utility allows you to inspect and configure existing :ref:`RNodes<rnode-main>`, and
|
||||
to create and provision new :ref:`RNodes<rnode-main>` from any supported hardware devices.
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnodeconf [-h] [-i] [-a] [-u] [-U] [--fw-version version] [--nocheck] [-C] [-N] [-T] [-b] [-B] [-p] [--freq Hz] [--bw Hz] [--txp dBm] [--sf factor] [--cr rate] [--eeprom-backup] [--eeprom-dump] [--eeprom-wipe] [--version] [port]
|
||||
usage: rnodeconf [-h] [-i] [-a] [-u] [-U] [--fw-version version]
|
||||
[--fw-url url] [--nocheck] [-e] [-E] [-C]
|
||||
[--baud-flash baud_flash] [-N] [-T] [-b] [-B] [-p] [-D i]
|
||||
[--display-addr byte] [--freq Hz] [--bw Hz] [--txp dBm]
|
||||
[--sf factor] [--cr rate] [--eeprom-backup] [--eeprom-dump]
|
||||
[--eeprom-wipe] [-P] [--trust-key hexbytes] [--version] [-f]
|
||||
[-r] [-k] [-S] [-H FIRMWARE_HASH] [--platform platform]
|
||||
[--product product] [--model model] [--hwrev revision]
|
||||
[port]
|
||||
|
||||
RNode Configuration and firmware utility. This program allows you to change various settings and startup modes of RNode. It can also install, flash and update the firmware on supported devices.
|
||||
RNode Configuration and firmware utility. This program allows you to change
|
||||
various settings and startup modes of RNode. It can also install, flash and
|
||||
update the firmware on supported devices.
|
||||
|
||||
positional arguments:
|
||||
port serial port where RNode is attached
|
||||
@@ -447,17 +728,26 @@ to create and provision new :ref:`RNodes<rnode-main>` from any supported hardwar
|
||||
-i, --info Show device info
|
||||
-a, --autoinstall Automatic installation on various supported devices
|
||||
-u, --update Update firmware to the latest version
|
||||
-U, --force-update Update to specified firmware even if version matches or is older than installed version
|
||||
--fw-version version Use a specific firmware version for update or autoinstall
|
||||
-U, --force-update Update to specified firmware even if version matches
|
||||
or is older than installed version
|
||||
--fw-version version Use a specific firmware version for update or
|
||||
autoinstall
|
||||
--fw-url url Use an alternate firmware download URL
|
||||
--nocheck Don't check for firmware updates online
|
||||
-e, --extract Extract firmware from connected RNode for later use
|
||||
-E, --use-extracted Use the extracted firmware for autoinstallation or update
|
||||
-E, --use-extracted Use the extracted firmware for autoinstallation or
|
||||
update
|
||||
-C, --clear-cache Clear locally cached firmware files
|
||||
--baud-flash baud_flash
|
||||
Set specific baud rate when flashing device. Default
|
||||
is 921600
|
||||
-N, --normal Switch device to normal mode
|
||||
-T, --tnc Switch device to TNC mode
|
||||
-b, --bluetooth-on Turn device bluetooth on
|
||||
-B, --bluetooth-off Turn device bluetooth off
|
||||
-p, --bluetooth-pair Put device into bluetooth pairing mode
|
||||
-D i, --display i Set display intensity (0-255)
|
||||
--display-addr byte Set display address as hex byte (00 - FF)
|
||||
--freq Hz Frequency in Hz for TNC mode
|
||||
--bw Hz Bandwidth in Hz for TNC mode
|
||||
--txp dBm TX power in dBm for TNC mode
|
||||
@@ -466,11 +756,49 @@ to create and provision new :ref:`RNodes<rnode-main>` from any supported hardwar
|
||||
--eeprom-backup Backup EEPROM to file
|
||||
--eeprom-dump Dump EEPROM to console
|
||||
--eeprom-wipe Unlock and wipe EEPROM
|
||||
-P, --public Display public part of signing key
|
||||
--trust-key hexbytes Public key to trust for device verification
|
||||
--version Print program version and exit
|
||||
-f, --flash Flash firmware and bootstrap EEPROM
|
||||
-r, --rom Bootstrap EEPROM without flashing firmware
|
||||
-k, --key Generate a new signing key and exit
|
||||
-S, --sign Display public part of signing key
|
||||
-H FIRMWARE_HASH, --firmware-hash FIRMWARE_HASH
|
||||
Display installed firmware hash
|
||||
--platform platform Platform specification for device bootstrap
|
||||
--product product Product specification for device bootstrap
|
||||
--model model Model code for device bootstrap
|
||||
--hwrev revision Hardware revision for device bootstrap
|
||||
|
||||
|
||||
For more information on how to create your own RNodes, please read the :ref:`Creating RNodes<rnode-creating>`
|
||||
section of this manual.
|
||||
|
||||
Remote Management
|
||||
-----------------
|
||||
|
||||
It is possible to allow remote management of Reticulum
|
||||
systems using the various built-in utilities, such as
|
||||
``rnstatus`` and ``rnpath``. To do so, you will need to set
|
||||
the ``enable_remote_management`` directive in the ``[reticulum]``
|
||||
section of the configuration file. You will also need to specify
|
||||
one or more Reticulum Identity hashes for authenticating the
|
||||
queries from client programs. For this purpose, you can use
|
||||
existing identity files, or generate new ones with the rnid utility.
|
||||
|
||||
The following is a truncated example of enabling remote management
|
||||
in the Reticulum configuration file:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
enable_remote_management = yes
|
||||
remote_management_allowed = 9fb6d773498fb3feda407ed8ef2c3229, 2d882c5586e548d79b5af27bca1776dc
|
||||
...
|
||||
|
||||
For a complete example configuration, you can run ``rnsd --exampleconfig``.
|
||||
|
||||
Improving System Configuration
|
||||
------------------------------
|
||||
|
||||
@@ -519,6 +847,9 @@ Reticulum as a System Service
|
||||
Instead of starting Reticulum manually, you can install ``rnsd`` as a system
|
||||
service and have it start automatically at boot.
|
||||
|
||||
Systemwide Service
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you installed Reticulum with ``pip``, the ``rnsd`` program will most likely
|
||||
be located in a user-local installation path only, which means ``systemd`` will not
|
||||
be able to execute it. In this case, you can simply symlink the ``rnsd`` program
|
||||
@@ -566,3 +897,48 @@ If you want to automatically start ``rnsd`` at boot, run:
|
||||
.. code:: text
|
||||
|
||||
sudo systemctl enable rnsd
|
||||
|
||||
Userspace Service
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Alternatively you can use a user systemd service instead of a system wide one. This way the whole setup can be done as a regular user.
|
||||
Create a user systemd service files ``~/.config/systemd/user/rnsd.service`` with the following content:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[Unit]
|
||||
Description=Reticulum Network Stack Daemon
|
||||
After=default.target
|
||||
|
||||
[Service]
|
||||
# If you run Reticulum on WiFi devices,
|
||||
# or other devices that need some extra
|
||||
# time to initialise, you might want to
|
||||
# add a short delay before Reticulum is
|
||||
# started by systemd:
|
||||
# ExecStartPre=/bin/sleep 10
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
ExecStart=RNS_BIN_DIR/rnsd --service
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
Replace ``RNS_BIN_DIR`` with the path to your Reticulum binary directory (eg. /home/USERNAMEHERE/rns/bin).
|
||||
|
||||
Start user service:
|
||||
|
||||
.. code:: text
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start rnsd.service
|
||||
|
||||
If you want to automatically start ``rnsd`` without having to log in as the USERNAMEHERE, do:
|
||||
|
||||
.. code:: text
|
||||
|
||||
sudo loginctl enable-linger USERNAMEHERE
|
||||
systemctl --user enable rnsd.service
|
||||
|
||||
|
||||
|
||||
@@ -17,19 +17,19 @@ Reticulum enables secure digital communication that cannot be subjected to
|
||||
outside control, manipulation or censorship.
|
||||
|
||||
Reticulum enables the construction of both small and potentially planetary-scale
|
||||
networks, without any need for hierarchical or beaureucratic structures to control
|
||||
networks, without any need for hierarchical or bureaucratic structures to control
|
||||
or manage them, while ensuring individuals and communities full sovereignty
|
||||
over their own network segments.
|
||||
|
||||
Reticulum is a complete networking stack, and does not need IP or higher
|
||||
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
|
||||
No kernel modules or drivers are required. Reticulum can run completely in
|
||||
userland, and will run on practically any system that runs Python 3. Reticulum
|
||||
runs well even on small single-board computers like the Pi Zero.
|
||||
|
||||
|
||||
@@ -38,24 +38,35 @@ Current Status
|
||||
**Please know!** Reticulum should currently be considered beta software. All core protocol
|
||||
features are implemented and functioning, but additions will probably occur as
|
||||
real-world use is explored. *There will be bugs*. The API and wire-format can be
|
||||
considered stable at the moment, but could change if absolutely warranted.
|
||||
considered complete and stable at the moment, but could change if absolutely warranted.
|
||||
|
||||
|
||||
What does Reticulum Offer?
|
||||
==========================
|
||||
|
||||
* Coordination-less globally unique addressing and identification
|
||||
|
||||
* Fully self-configuring multi-hop routing
|
||||
* Fully self-configuring multi-hop routing over heterogeneous carriers
|
||||
|
||||
* Complete initiator anonymity, communicate without revealing your identity
|
||||
* Flexible scalability over heterogeneous topologies
|
||||
|
||||
* Asymmetric encryption based on X25519, and Ed25519 signatures as a basis for all communication
|
||||
* Reticulum can carry data over any mixture of physical mediums and topologies
|
||||
|
||||
* Forward Secrecy by using ephemeral Elliptic Curve Diffie-Hellman keys on Curve25519
|
||||
* Low-bandwidth networks can co-exist and interoperate with large, high-bandwidth networks
|
||||
|
||||
* Reticulum uses the `Fernet <https://github.com/fernet/spec/blob/master/Spec.md>`_ specification for on-the-wire / over-the-air encryption
|
||||
* Initiator anonymity, communicate without revealing your identity
|
||||
|
||||
* All keys are ephemeral and derived from an ECDH key exchange on Curve25519
|
||||
* Reticulum does not include source addresses on any packets
|
||||
|
||||
* Asymmetric X25519 encryption and Ed25519 signatures as a basis for all communication
|
||||
|
||||
* The foundational Reticulum Identity Keys are 512-bit Elliptic Curve keysets
|
||||
|
||||
* Forward Secrecy is available for all communication types, both for single packets and over links
|
||||
|
||||
* Reticulum uses the following format for encrypted tokens:
|
||||
|
||||
* Ephemeral per-packet and link keys and derived from an ECDH key exchange on Curve25519
|
||||
|
||||
* AES-128 in CBC mode with PKCS7 padding
|
||||
|
||||
@@ -65,33 +76,45 @@ What does Reticulum Offer?
|
||||
|
||||
* Unforgeable packet delivery confirmations
|
||||
|
||||
* A variety of supported interface types
|
||||
* Flexible and extensible interface system
|
||||
|
||||
* An intuitive and developer-friendly API
|
||||
* Reticulum includes a large variety of built-in interface types
|
||||
|
||||
* Efficient link establishment
|
||||
* Ability to load and utilise custom user- or community-supplied interface types
|
||||
|
||||
* Total bandwidth cost of setting up a link is only 3 packets, totalling 297 bytes
|
||||
* Easily create your own custom interfaces for communicating over anything
|
||||
|
||||
* Low cost of keeping links open at only 0.44 bits per second
|
||||
* Authentication and virtual network segmentation on all supported interface types
|
||||
|
||||
* An intuitive and easy-to-use API
|
||||
|
||||
* Simpler and easier to use than sockets APIs and simpler, but more powerful
|
||||
|
||||
* Makes building distributed and decentralised applications much simpler
|
||||
|
||||
* Reliable and efficient transfer of arbitrary amounts of data
|
||||
|
||||
* Reticulum can handle a few bytes of data or files of many gigabytes
|
||||
|
||||
* Sequencing, transfer coordination and checksumming is automatic
|
||||
* Sequencing, compression, transfer coordination and checksumming are automatic
|
||||
|
||||
* The API is very easy to use, and provides transfer progress
|
||||
|
||||
* Authentication and virtual network segmentation on all supported interface types
|
||||
* Lightweight, flexible and expandable Request/Response mechanism
|
||||
|
||||
* Flexible scalability allowing extremely low-bandwidth networks to co-exist and interoperate with large, high-bandwidth networks
|
||||
* Efficient link establishment
|
||||
|
||||
* Total cost of setting up an encrypted and verified link is only 3 packets, totalling 297 bytes
|
||||
|
||||
* Low cost of keeping links open at only 0.44 bits per second
|
||||
|
||||
* Reliable sequential delivery with Channel and Buffer mechanisms
|
||||
|
||||
|
||||
Where can Reticulum be Used?
|
||||
============================
|
||||
Over practically any medium that can support at least a half-duplex channel
|
||||
with 500 bits per second throughput, and an MTU of 500 bytes. Data radios,
|
||||
with greater throughput than 5 bits per second, and an MTU of 500 bytes. Data radios,
|
||||
modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes,
|
||||
ad-hoc WiFi, free-space optical links and similar systems are all examples
|
||||
of the types of interfaces Reticulum was designed for.
|
||||
@@ -99,7 +122,8 @@ 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.
|
||||
development board into one, or it can be purchased as a complete transceiver
|
||||
from various vendors.
|
||||
|
||||
Reticulum can also be encapsulated over existing IP networks, so there's
|
||||
nothing stopping you from using it over wired Ethernet or your local WiFi
|
||||
@@ -115,7 +139,7 @@ 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:
|
||||
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 simple to :ref:`implement an interface class<example-custominterface>`. Currently, Reticulum can use the following devices and communication mediums:
|
||||
|
||||
* Any Ethernet device
|
||||
|
||||
@@ -165,4 +189,4 @@ such. While it has been built with cryptography best-practices very foremost in
|
||||
mind, it has not yet been externally security audited, and there could very well be
|
||||
privacy-breaking bugs. To be considered secure, Reticulum needs a thorough
|
||||
security review by independent cryptographers and security researchers. If you
|
||||
want to help out with this, or can help sponsor an audit, please do get in touch.
|
||||
want to help out with this, or can help sponsor an audit, please do get in touch.
|
||||
@@ -236,16 +236,6 @@ div.body p, div.body dd, div.body li, div.body blockquote {
|
||||
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,
|
||||
@@ -334,11 +324,15 @@ aside.sidebar {
|
||||
p.sidebar-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
nav.contents,
|
||||
aside.topic,
|
||||
div.admonition, div.topic, blockquote {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
/* -- topics ---------------------------------------------------------------- */
|
||||
nav.contents,
|
||||
aside.topic,
|
||||
div.topic {
|
||||
border: 1px solid #ccc;
|
||||
padding: 7px;
|
||||
@@ -377,6 +371,8 @@ div.body p.centered {
|
||||
|
||||
div.sidebar > :last-child,
|
||||
aside.sidebar > :last-child,
|
||||
nav.contents > :last-child,
|
||||
aside.topic > :last-child,
|
||||
div.topic > :last-child,
|
||||
div.admonition > :last-child {
|
||||
margin-bottom: 0;
|
||||
@@ -384,6 +380,8 @@ div.admonition > :last-child {
|
||||
|
||||
div.sidebar::after,
|
||||
aside.sidebar::after,
|
||||
nav.contents::after,
|
||||
aside.topic::after,
|
||||
div.topic::after,
|
||||
div.admonition::after,
|
||||
blockquote::after {
|
||||
@@ -608,19 +606,26 @@ ol.simple p,
|
||||
ul.simple p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
dl.footnote > dt,
|
||||
dl.citation > dt {
|
||||
aside.footnote > span,
|
||||
div.citation > span {
|
||||
float: left;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
dl.footnote > dd,
|
||||
dl.citation > dd {
|
||||
aside.footnote > span:last-of-type,
|
||||
div.citation > span:last-of-type {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
aside.footnote > p {
|
||||
margin-left: 2em;
|
||||
}
|
||||
div.citation > p {
|
||||
margin-left: 4em;
|
||||
}
|
||||
aside.footnote > p:last-of-type,
|
||||
div.citation > p:last-of-type {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
dl.footnote > dd:after,
|
||||
dl.citation > dd:after {
|
||||
aside.footnote > p:last-of-type:after,
|
||||
div.citation > p:last-of-type:after {
|
||||
content: "";
|
||||
clear: both;
|
||||
}
|
||||
@@ -636,10 +641,6 @@ dl.field-list > dt {
|
||||
padding-left: 0.5em;
|
||||
padding-right: 5px;
|
||||
}
|
||||
dl.field-list > dt:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
|
||||
dl.field-list > dd {
|
||||
padding-left: 0.5em;
|
||||
|
||||
@@ -35,7 +35,8 @@ div.highlight {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.highlight:hover button.copybtn {
|
||||
/* Show the copybutton */
|
||||
.highlight:hover button.copybtn, button.copybtn.success {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const messages = {
|
||||
},
|
||||
'fr' : {
|
||||
'copy': 'Copier',
|
||||
'copy_to_clipboard': 'Copié dans le presse-papier',
|
||||
'copy_to_clipboard': 'Copier dans le presse-papier',
|
||||
'copy_success': 'Copié !',
|
||||
'copy_failure': 'Échec de la copie',
|
||||
},
|
||||
@@ -102,18 +102,25 @@ const clearSelection = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Changes tooltip text for two seconds, then changes it back
|
||||
// Changes tooltip text for a moment, then changes it back
|
||||
// We want the timeout of our `success` class to be a bit shorter than the
|
||||
// tooltip and icon change, so that we can hide the icon before changing back.
|
||||
var timeoutIcon = 2000;
|
||||
var timeoutSuccessClass = 1500;
|
||||
|
||||
const temporarilyChangeTooltip = (el, oldText, newText) => {
|
||||
el.setAttribute('data-tooltip', newText)
|
||||
el.classList.add('success')
|
||||
setTimeout(() => el.setAttribute('data-tooltip', oldText), 2000)
|
||||
setTimeout(() => el.classList.remove('success'), 2000)
|
||||
// Remove success a little bit sooner than we change the tooltip
|
||||
// So that we can use CSS to hide the copybutton first
|
||||
setTimeout(() => el.classList.remove('success'), timeoutSuccessClass)
|
||||
setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon)
|
||||
}
|
||||
|
||||
// Changes the copy button icon for two seconds, then changes it back
|
||||
const temporarilyChangeIcon = (el) => {
|
||||
el.innerHTML = iconCheck;
|
||||
setTimeout(() => {el.innerHTML = iconCopy}, 2000)
|
||||
setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon)
|
||||
}
|
||||
|
||||
const addCopyButtonToCodeCells = () => {
|
||||
@@ -125,7 +132,8 @@ const addCopyButtonToCodeCells = () => {
|
||||
}
|
||||
|
||||
// Add copybuttons to all of our code cells
|
||||
const codeCells = document.querySelectorAll('div.highlight pre')
|
||||
const COPYBUTTON_SELECTOR = 'div.highlight pre';
|
||||
const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR)
|
||||
codeCells.forEach((codeCell, index) => {
|
||||
const id = codeCellId(index)
|
||||
codeCell.setAttribute('id', id)
|
||||
@@ -141,10 +149,25 @@ function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes excluded text from a Node.
|
||||
*
|
||||
* @param {Node} target Node to filter.
|
||||
* @param {string} exclude CSS selector of nodes to exclude.
|
||||
* @returns {DOMString} Text from `target` with text removed.
|
||||
*/
|
||||
function filterText(target, exclude) {
|
||||
const clone = target.cloneNode(true); // clone as to not modify the live DOM
|
||||
if (exclude) {
|
||||
// remove excluded nodes
|
||||
clone.querySelectorAll(exclude).forEach(node => node.remove());
|
||||
}
|
||||
return clone.innerText;
|
||||
}
|
||||
|
||||
// Callback when a copy button is clicked. Will be passed the node that was clicked
|
||||
// should then grab the text and replace pieces of text that shouldn't be used in output
|
||||
function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
|
||||
|
||||
var regexp;
|
||||
var match;
|
||||
|
||||
@@ -199,7 +222,12 @@ function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onl
|
||||
|
||||
var copyTargetText = (trigger) => {
|
||||
var target = document.querySelector(trigger.attributes['data-clipboard-target'].value);
|
||||
return formatCopyText(target.innerText, '', false, true, true, true, '', '')
|
||||
|
||||
// get filtered text
|
||||
let exclude = '.linenos';
|
||||
|
||||
let text = filterText(target, exclude);
|
||||
return formatCopyText(text, '', false, true, true, true, '', '')
|
||||
}
|
||||
|
||||
// Initialize with a callback so we can modify the text before copy
|
||||
|
||||
@@ -2,10 +2,25 @@ function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes excluded text from a Node.
|
||||
*
|
||||
* @param {Node} target Node to filter.
|
||||
* @param {string} exclude CSS selector of nodes to exclude.
|
||||
* @returns {DOMString} Text from `target` with text removed.
|
||||
*/
|
||||
export function filterText(target, exclude) {
|
||||
const clone = target.cloneNode(true); // clone as to not modify the live DOM
|
||||
if (exclude) {
|
||||
// remove excluded nodes
|
||||
clone.querySelectorAll(exclude).forEach(node => node.remove());
|
||||
}
|
||||
return clone.innerText;
|
||||
}
|
||||
|
||||
// Callback when a copy button is clicked. Will be passed the node that was clicked
|
||||
// should then grab the text and replace pieces of text that shouldn't be used in output
|
||||
export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
|
||||
|
||||
var regexp;
|
||||
var match;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var DOCUMENTATION_OPTIONS = {
|
||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||
VERSION: '0.5.2 beta',
|
||||
VERSION: '0.9.3 beta',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
@@ -5,82 +5,83 @@
|
||||
.highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.highlight .hll { background-color: #ffffcc }
|
||||
.highlight { background: #f8f8f8; }
|
||||
.highlight .c { color: #8f5902; font-style: italic } /* Comment */
|
||||
.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */
|
||||
.highlight .g { color: #000000 } /* Generic */
|
||||
.highlight .k { color: #204a87; font-weight: bold } /* Keyword */
|
||||
.highlight .l { color: #000000 } /* Literal */
|
||||
.highlight .n { color: #000000 } /* Name */
|
||||
.highlight .o { color: #ce5c00; font-weight: bold } /* Operator */
|
||||
.highlight .x { color: #000000 } /* Other */
|
||||
.highlight .p { color: #000000; font-weight: bold } /* Punctuation */
|
||||
.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */
|
||||
.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */
|
||||
.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */
|
||||
.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */
|
||||
.highlight .gd { color: #a40000 } /* Generic.Deleted */
|
||||
.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */
|
||||
.highlight .gr { color: #ef2929 } /* Generic.Error */
|
||||
.highlight .c { color: #8F5902; font-style: italic } /* Comment */
|
||||
.highlight .err { color: #A40000; border: 1px solid #EF2929 } /* Error */
|
||||
.highlight .g { color: #000 } /* Generic */
|
||||
.highlight .k { color: #204A87; font-weight: bold } /* Keyword */
|
||||
.highlight .l { color: #000 } /* Literal */
|
||||
.highlight .n { color: #000 } /* Name */
|
||||
.highlight .o { color: #CE5C00; font-weight: bold } /* Operator */
|
||||
.highlight .x { color: #000 } /* Other */
|
||||
.highlight .p { color: #000; font-weight: bold } /* Punctuation */
|
||||
.highlight .ch { color: #8F5902; font-style: italic } /* Comment.Hashbang */
|
||||
.highlight .cm { color: #8F5902; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #8F5902; font-style: italic } /* Comment.Preproc */
|
||||
.highlight .cpf { color: #8F5902; font-style: italic } /* Comment.PreprocFile */
|
||||
.highlight .c1 { color: #8F5902; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #8F5902; font-style: italic } /* Comment.Special */
|
||||
.highlight .gd { color: #A40000 } /* Generic.Deleted */
|
||||
.highlight .ge { color: #000; font-style: italic } /* Generic.Emph */
|
||||
.highlight .ges { color: #000; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
.highlight .gr { color: #EF2929 } /* Generic.Error */
|
||||
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.highlight .gi { color: #00A000 } /* Generic.Inserted */
|
||||
.highlight .go { color: #000000; font-style: italic } /* Generic.Output */
|
||||
.highlight .gp { color: #8f5902 } /* Generic.Prompt */
|
||||
.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */
|
||||
.highlight .go { color: #000; font-style: italic } /* Generic.Output */
|
||||
.highlight .gp { color: #8F5902 } /* Generic.Prompt */
|
||||
.highlight .gs { color: #000; font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */
|
||||
.highlight .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #204a87; font-weight: bold } /* Keyword.Type */
|
||||
.highlight .ld { color: #000000 } /* Literal.Date */
|
||||
.highlight .m { color: #0000cf; font-weight: bold } /* Literal.Number */
|
||||
.highlight .s { color: #4e9a06 } /* Literal.String */
|
||||
.highlight .na { color: #c4a000 } /* Name.Attribute */
|
||||
.highlight .nb { color: #204a87 } /* Name.Builtin */
|
||||
.highlight .nc { color: #000000 } /* Name.Class */
|
||||
.highlight .no { color: #000000 } /* Name.Constant */
|
||||
.highlight .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */
|
||||
.highlight .ni { color: #ce5c00 } /* Name.Entity */
|
||||
.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #000000 } /* Name.Function */
|
||||
.highlight .nl { color: #f57900 } /* Name.Label */
|
||||
.highlight .nn { color: #000000 } /* Name.Namespace */
|
||||
.highlight .nx { color: #000000 } /* Name.Other */
|
||||
.highlight .py { color: #000000 } /* Name.Property */
|
||||
.highlight .nt { color: #204a87; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #000000 } /* Name.Variable */
|
||||
.highlight .ow { color: #204a87; font-weight: bold } /* Operator.Word */
|
||||
.highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */
|
||||
.highlight .w { color: #f8f8f8 } /* Text.Whitespace */
|
||||
.highlight .mb { color: #0000cf; font-weight: bold } /* Literal.Number.Bin */
|
||||
.highlight .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */
|
||||
.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */
|
||||
.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #4e9a06 } /* Literal.String.Char */
|
||||
.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */
|
||||
.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */
|
||||
.highlight .se { color: #4e9a06 } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #4e9a06 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */
|
||||
.highlight .fm { color: #000000 } /* Name.Function.Magic */
|
||||
.highlight .vc { color: #000000 } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #000000 } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #000000 } /* Name.Variable.Instance */
|
||||
.highlight .vm { color: #000000 } /* Name.Variable.Magic */
|
||||
.highlight .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */
|
||||
.highlight .gt { color: #A40000; font-weight: bold } /* Generic.Traceback */
|
||||
.highlight .kc { color: #204A87; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #204A87; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #204A87; font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #204A87; font-weight: bold } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #204A87; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #204A87; font-weight: bold } /* Keyword.Type */
|
||||
.highlight .ld { color: #000 } /* Literal.Date */
|
||||
.highlight .m { color: #0000CF; font-weight: bold } /* Literal.Number */
|
||||
.highlight .s { color: #4E9A06 } /* Literal.String */
|
||||
.highlight .na { color: #C4A000 } /* Name.Attribute */
|
||||
.highlight .nb { color: #204A87 } /* Name.Builtin */
|
||||
.highlight .nc { color: #000 } /* Name.Class */
|
||||
.highlight .no { color: #000 } /* Name.Constant */
|
||||
.highlight .nd { color: #5C35CC; font-weight: bold } /* Name.Decorator */
|
||||
.highlight .ni { color: #CE5C00 } /* Name.Entity */
|
||||
.highlight .ne { color: #C00; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #000 } /* Name.Function */
|
||||
.highlight .nl { color: #F57900 } /* Name.Label */
|
||||
.highlight .nn { color: #000 } /* Name.Namespace */
|
||||
.highlight .nx { color: #000 } /* Name.Other */
|
||||
.highlight .py { color: #000 } /* Name.Property */
|
||||
.highlight .nt { color: #204A87; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #000 } /* Name.Variable */
|
||||
.highlight .ow { color: #204A87; font-weight: bold } /* Operator.Word */
|
||||
.highlight .pm { color: #000; font-weight: bold } /* Punctuation.Marker */
|
||||
.highlight .w { color: #F8F8F8 } /* Text.Whitespace */
|
||||
.highlight .mb { color: #0000CF; font-weight: bold } /* Literal.Number.Bin */
|
||||
.highlight .mf { color: #0000CF; font-weight: bold } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #0000CF; font-weight: bold } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #0000CF; font-weight: bold } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #0000CF; font-weight: bold } /* Literal.Number.Oct */
|
||||
.highlight .sa { color: #4E9A06 } /* Literal.String.Affix */
|
||||
.highlight .sb { color: #4E9A06 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #4E9A06 } /* Literal.String.Char */
|
||||
.highlight .dl { color: #4E9A06 } /* Literal.String.Delimiter */
|
||||
.highlight .sd { color: #8F5902; font-style: italic } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #4E9A06 } /* Literal.String.Double */
|
||||
.highlight .se { color: #4E9A06 } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #4E9A06 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #4E9A06 } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #4E9A06 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #4E9A06 } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #4E9A06 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #4E9A06 } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #3465A4 } /* Name.Builtin.Pseudo */
|
||||
.highlight .fm { color: #000 } /* Name.Function.Magic */
|
||||
.highlight .vc { color: #000 } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #000 } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #000 } /* Name.Variable.Instance */
|
||||
.highlight .vm { color: #000 } /* Name.Variable.Magic */
|
||||
.highlight .il { color: #0000CF; font-weight: bold } /* Literal.Number.Integer.Long */
|
||||
@media not print {
|
||||
body[data-theme="dark"] .highlight pre { line-height: 125%; }
|
||||
body[data-theme="dark"] .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
@@ -88,84 +89,85 @@ body[data-theme="dark"] .highlight span.linenos { color: #aaaaaa; background-col
|
||||
body[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
body[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
body[data-theme="dark"] .highlight .hll { background-color: #404040 }
|
||||
body[data-theme="dark"] .highlight { background: #202020; color: #d0d0d0 }
|
||||
body[data-theme="dark"] .highlight .c { color: #ababab; font-style: italic } /* Comment */
|
||||
body[data-theme="dark"] .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
||||
body[data-theme="dark"] .highlight .esc { color: #d0d0d0 } /* Escape */
|
||||
body[data-theme="dark"] .highlight .g { color: #d0d0d0 } /* Generic */
|
||||
body[data-theme="dark"] .highlight .k { color: #6ebf26; font-weight: bold } /* Keyword */
|
||||
body[data-theme="dark"] .highlight .l { color: #d0d0d0 } /* Literal */
|
||||
body[data-theme="dark"] .highlight .n { color: #d0d0d0 } /* Name */
|
||||
body[data-theme="dark"] .highlight .o { color: #d0d0d0 } /* Operator */
|
||||
body[data-theme="dark"] .highlight .x { color: #d0d0d0 } /* Other */
|
||||
body[data-theme="dark"] .highlight .p { color: #d0d0d0 } /* Punctuation */
|
||||
body[data-theme="dark"] .highlight .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */
|
||||
body[data-theme="dark"] .highlight .cm { color: #ababab; font-style: italic } /* Comment.Multiline */
|
||||
body[data-theme="dark"] .highlight .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
|
||||
body[data-theme="dark"] .highlight .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */
|
||||
body[data-theme="dark"] .highlight .c1 { color: #ababab; font-style: italic } /* Comment.Single */
|
||||
body[data-theme="dark"] .highlight .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
||||
body[data-theme="dark"] .highlight .gd { color: #d22323 } /* Generic.Deleted */
|
||||
body[data-theme="dark"] .highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
|
||||
body[data-theme="dark"] .highlight .gr { color: #d22323 } /* Generic.Error */
|
||||
body[data-theme="dark"] .highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
|
||||
body[data-theme="dark"] .highlight { background: #202020; color: #D0D0D0 }
|
||||
body[data-theme="dark"] .highlight .c { color: #ABABAB; font-style: italic } /* Comment */
|
||||
body[data-theme="dark"] .highlight .err { color: #A61717; background-color: #E3D2D2 } /* Error */
|
||||
body[data-theme="dark"] .highlight .esc { color: #D0D0D0 } /* Escape */
|
||||
body[data-theme="dark"] .highlight .g { color: #D0D0D0 } /* Generic */
|
||||
body[data-theme="dark"] .highlight .k { color: #6EBF26; font-weight: bold } /* Keyword */
|
||||
body[data-theme="dark"] .highlight .l { color: #D0D0D0 } /* Literal */
|
||||
body[data-theme="dark"] .highlight .n { color: #D0D0D0 } /* Name */
|
||||
body[data-theme="dark"] .highlight .o { color: #D0D0D0 } /* Operator */
|
||||
body[data-theme="dark"] .highlight .x { color: #D0D0D0 } /* Other */
|
||||
body[data-theme="dark"] .highlight .p { color: #D0D0D0 } /* Punctuation */
|
||||
body[data-theme="dark"] .highlight .ch { color: #ABABAB; font-style: italic } /* Comment.Hashbang */
|
||||
body[data-theme="dark"] .highlight .cm { color: #ABABAB; font-style: italic } /* Comment.Multiline */
|
||||
body[data-theme="dark"] .highlight .cp { color: #FF3A3A; font-weight: bold } /* Comment.Preproc */
|
||||
body[data-theme="dark"] .highlight .cpf { color: #ABABAB; font-style: italic } /* Comment.PreprocFile */
|
||||
body[data-theme="dark"] .highlight .c1 { color: #ABABAB; font-style: italic } /* Comment.Single */
|
||||
body[data-theme="dark"] .highlight .cs { color: #E50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
||||
body[data-theme="dark"] .highlight .gd { color: #FF3A3A } /* Generic.Deleted */
|
||||
body[data-theme="dark"] .highlight .ge { color: #D0D0D0; font-style: italic } /* Generic.Emph */
|
||||
body[data-theme="dark"] .highlight .ges { color: #D0D0D0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
body[data-theme="dark"] .highlight .gr { color: #FF3A3A } /* Generic.Error */
|
||||
body[data-theme="dark"] .highlight .gh { color: #FFF; font-weight: bold } /* Generic.Heading */
|
||||
body[data-theme="dark"] .highlight .gi { color: #589819 } /* Generic.Inserted */
|
||||
body[data-theme="dark"] .highlight .go { color: #cccccc } /* Generic.Output */
|
||||
body[data-theme="dark"] .highlight .gp { color: #aaaaaa } /* Generic.Prompt */
|
||||
body[data-theme="dark"] .highlight .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
|
||||
body[data-theme="dark"] .highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
|
||||
body[data-theme="dark"] .highlight .gt { color: #d22323 } /* Generic.Traceback */
|
||||
body[data-theme="dark"] .highlight .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */
|
||||
body[data-theme="dark"] .highlight .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */
|
||||
body[data-theme="dark"] .highlight .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */
|
||||
body[data-theme="dark"] .highlight .kp { color: #6ebf26 } /* Keyword.Pseudo */
|
||||
body[data-theme="dark"] .highlight .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */
|
||||
body[data-theme="dark"] .highlight .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */
|
||||
body[data-theme="dark"] .highlight .ld { color: #d0d0d0 } /* Literal.Date */
|
||||
body[data-theme="dark"] .highlight .m { color: #51b2fd } /* Literal.Number */
|
||||
body[data-theme="dark"] .highlight .s { color: #ed9d13 } /* Literal.String */
|
||||
body[data-theme="dark"] .highlight .na { color: #bbbbbb } /* Name.Attribute */
|
||||
body[data-theme="dark"] .highlight .nb { color: #2fbccd } /* Name.Builtin */
|
||||
body[data-theme="dark"] .highlight .nc { color: #71adff; text-decoration: underline } /* Name.Class */
|
||||
body[data-theme="dark"] .highlight .no { color: #40ffff } /* Name.Constant */
|
||||
body[data-theme="dark"] .highlight .nd { color: #ffa500 } /* Name.Decorator */
|
||||
body[data-theme="dark"] .highlight .ni { color: #d0d0d0 } /* Name.Entity */
|
||||
body[data-theme="dark"] .highlight .ne { color: #bbbbbb } /* Name.Exception */
|
||||
body[data-theme="dark"] .highlight .nf { color: #71adff } /* Name.Function */
|
||||
body[data-theme="dark"] .highlight .nl { color: #d0d0d0 } /* Name.Label */
|
||||
body[data-theme="dark"] .highlight .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */
|
||||
body[data-theme="dark"] .highlight .nx { color: #d0d0d0 } /* Name.Other */
|
||||
body[data-theme="dark"] .highlight .py { color: #d0d0d0 } /* Name.Property */
|
||||
body[data-theme="dark"] .highlight .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */
|
||||
body[data-theme="dark"] .highlight .nv { color: #40ffff } /* Name.Variable */
|
||||
body[data-theme="dark"] .highlight .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */
|
||||
body[data-theme="dark"] .highlight .pm { color: #d0d0d0 } /* Punctuation.Marker */
|
||||
body[data-theme="dark"] .highlight .w { color: #666666 } /* Text.Whitespace */
|
||||
body[data-theme="dark"] .highlight .mb { color: #51b2fd } /* Literal.Number.Bin */
|
||||
body[data-theme="dark"] .highlight .mf { color: #51b2fd } /* Literal.Number.Float */
|
||||
body[data-theme="dark"] .highlight .mh { color: #51b2fd } /* Literal.Number.Hex */
|
||||
body[data-theme="dark"] .highlight .mi { color: #51b2fd } /* Literal.Number.Integer */
|
||||
body[data-theme="dark"] .highlight .mo { color: #51b2fd } /* Literal.Number.Oct */
|
||||
body[data-theme="dark"] .highlight .sa { color: #ed9d13 } /* Literal.String.Affix */
|
||||
body[data-theme="dark"] .highlight .sb { color: #ed9d13 } /* Literal.String.Backtick */
|
||||
body[data-theme="dark"] .highlight .sc { color: #ed9d13 } /* Literal.String.Char */
|
||||
body[data-theme="dark"] .highlight .dl { color: #ed9d13 } /* Literal.String.Delimiter */
|
||||
body[data-theme="dark"] .highlight .sd { color: #ed9d13 } /* Literal.String.Doc */
|
||||
body[data-theme="dark"] .highlight .s2 { color: #ed9d13 } /* Literal.String.Double */
|
||||
body[data-theme="dark"] .highlight .se { color: #ed9d13 } /* Literal.String.Escape */
|
||||
body[data-theme="dark"] .highlight .sh { color: #ed9d13 } /* Literal.String.Heredoc */
|
||||
body[data-theme="dark"] .highlight .si { color: #ed9d13 } /* Literal.String.Interpol */
|
||||
body[data-theme="dark"] .highlight .sx { color: #ffa500 } /* Literal.String.Other */
|
||||
body[data-theme="dark"] .highlight .sr { color: #ed9d13 } /* Literal.String.Regex */
|
||||
body[data-theme="dark"] .highlight .s1 { color: #ed9d13 } /* Literal.String.Single */
|
||||
body[data-theme="dark"] .highlight .ss { color: #ed9d13 } /* Literal.String.Symbol */
|
||||
body[data-theme="dark"] .highlight .bp { color: #2fbccd } /* Name.Builtin.Pseudo */
|
||||
body[data-theme="dark"] .highlight .fm { color: #71adff } /* Name.Function.Magic */
|
||||
body[data-theme="dark"] .highlight .vc { color: #40ffff } /* Name.Variable.Class */
|
||||
body[data-theme="dark"] .highlight .vg { color: #40ffff } /* Name.Variable.Global */
|
||||
body[data-theme="dark"] .highlight .vi { color: #40ffff } /* Name.Variable.Instance */
|
||||
body[data-theme="dark"] .highlight .vm { color: #40ffff } /* Name.Variable.Magic */
|
||||
body[data-theme="dark"] .highlight .il { color: #51b2fd } /* Literal.Number.Integer.Long */
|
||||
body[data-theme="dark"] .highlight .go { color: #CCC } /* Generic.Output */
|
||||
body[data-theme="dark"] .highlight .gp { color: #AAA } /* Generic.Prompt */
|
||||
body[data-theme="dark"] .highlight .gs { color: #D0D0D0; font-weight: bold } /* Generic.Strong */
|
||||
body[data-theme="dark"] .highlight .gu { color: #FFF; text-decoration: underline } /* Generic.Subheading */
|
||||
body[data-theme="dark"] .highlight .gt { color: #FF3A3A } /* Generic.Traceback */
|
||||
body[data-theme="dark"] .highlight .kc { color: #6EBF26; font-weight: bold } /* Keyword.Constant */
|
||||
body[data-theme="dark"] .highlight .kd { color: #6EBF26; font-weight: bold } /* Keyword.Declaration */
|
||||
body[data-theme="dark"] .highlight .kn { color: #6EBF26; font-weight: bold } /* Keyword.Namespace */
|
||||
body[data-theme="dark"] .highlight .kp { color: #6EBF26 } /* Keyword.Pseudo */
|
||||
body[data-theme="dark"] .highlight .kr { color: #6EBF26; font-weight: bold } /* Keyword.Reserved */
|
||||
body[data-theme="dark"] .highlight .kt { color: #6EBF26; font-weight: bold } /* Keyword.Type */
|
||||
body[data-theme="dark"] .highlight .ld { color: #D0D0D0 } /* Literal.Date */
|
||||
body[data-theme="dark"] .highlight .m { color: #51B2FD } /* Literal.Number */
|
||||
body[data-theme="dark"] .highlight .s { color: #ED9D13 } /* Literal.String */
|
||||
body[data-theme="dark"] .highlight .na { color: #BBB } /* Name.Attribute */
|
||||
body[data-theme="dark"] .highlight .nb { color: #2FBCCD } /* Name.Builtin */
|
||||
body[data-theme="dark"] .highlight .nc { color: #71ADFF; text-decoration: underline } /* Name.Class */
|
||||
body[data-theme="dark"] .highlight .no { color: #40FFFF } /* Name.Constant */
|
||||
body[data-theme="dark"] .highlight .nd { color: #FFA500 } /* Name.Decorator */
|
||||
body[data-theme="dark"] .highlight .ni { color: #D0D0D0 } /* Name.Entity */
|
||||
body[data-theme="dark"] .highlight .ne { color: #BBB } /* Name.Exception */
|
||||
body[data-theme="dark"] .highlight .nf { color: #71ADFF } /* Name.Function */
|
||||
body[data-theme="dark"] .highlight .nl { color: #D0D0D0 } /* Name.Label */
|
||||
body[data-theme="dark"] .highlight .nn { color: #71ADFF; text-decoration: underline } /* Name.Namespace */
|
||||
body[data-theme="dark"] .highlight .nx { color: #D0D0D0 } /* Name.Other */
|
||||
body[data-theme="dark"] .highlight .py { color: #D0D0D0 } /* Name.Property */
|
||||
body[data-theme="dark"] .highlight .nt { color: #6EBF26; font-weight: bold } /* Name.Tag */
|
||||
body[data-theme="dark"] .highlight .nv { color: #40FFFF } /* Name.Variable */
|
||||
body[data-theme="dark"] .highlight .ow { color: #6EBF26; font-weight: bold } /* Operator.Word */
|
||||
body[data-theme="dark"] .highlight .pm { color: #D0D0D0 } /* Punctuation.Marker */
|
||||
body[data-theme="dark"] .highlight .w { color: #666 } /* Text.Whitespace */
|
||||
body[data-theme="dark"] .highlight .mb { color: #51B2FD } /* Literal.Number.Bin */
|
||||
body[data-theme="dark"] .highlight .mf { color: #51B2FD } /* Literal.Number.Float */
|
||||
body[data-theme="dark"] .highlight .mh { color: #51B2FD } /* Literal.Number.Hex */
|
||||
body[data-theme="dark"] .highlight .mi { color: #51B2FD } /* Literal.Number.Integer */
|
||||
body[data-theme="dark"] .highlight .mo { color: #51B2FD } /* Literal.Number.Oct */
|
||||
body[data-theme="dark"] .highlight .sa { color: #ED9D13 } /* Literal.String.Affix */
|
||||
body[data-theme="dark"] .highlight .sb { color: #ED9D13 } /* Literal.String.Backtick */
|
||||
body[data-theme="dark"] .highlight .sc { color: #ED9D13 } /* Literal.String.Char */
|
||||
body[data-theme="dark"] .highlight .dl { color: #ED9D13 } /* Literal.String.Delimiter */
|
||||
body[data-theme="dark"] .highlight .sd { color: #ED9D13 } /* Literal.String.Doc */
|
||||
body[data-theme="dark"] .highlight .s2 { color: #ED9D13 } /* Literal.String.Double */
|
||||
body[data-theme="dark"] .highlight .se { color: #ED9D13 } /* Literal.String.Escape */
|
||||
body[data-theme="dark"] .highlight .sh { color: #ED9D13 } /* Literal.String.Heredoc */
|
||||
body[data-theme="dark"] .highlight .si { color: #ED9D13 } /* Literal.String.Interpol */
|
||||
body[data-theme="dark"] .highlight .sx { color: #FFA500 } /* Literal.String.Other */
|
||||
body[data-theme="dark"] .highlight .sr { color: #ED9D13 } /* Literal.String.Regex */
|
||||
body[data-theme="dark"] .highlight .s1 { color: #ED9D13 } /* Literal.String.Single */
|
||||
body[data-theme="dark"] .highlight .ss { color: #ED9D13 } /* Literal.String.Symbol */
|
||||
body[data-theme="dark"] .highlight .bp { color: #2FBCCD } /* Name.Builtin.Pseudo */
|
||||
body[data-theme="dark"] .highlight .fm { color: #71ADFF } /* Name.Function.Magic */
|
||||
body[data-theme="dark"] .highlight .vc { color: #40FFFF } /* Name.Variable.Class */
|
||||
body[data-theme="dark"] .highlight .vg { color: #40FFFF } /* Name.Variable.Global */
|
||||
body[data-theme="dark"] .highlight .vi { color: #40FFFF } /* Name.Variable.Instance */
|
||||
body[data-theme="dark"] .highlight .vm { color: #40FFFF } /* Name.Variable.Magic */
|
||||
body[data-theme="dark"] .highlight .il { color: #51B2FD } /* Literal.Number.Integer.Long */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body:not([data-theme="light"]) .highlight pre { line-height: 125%; }
|
||||
body:not([data-theme="light"]) .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
@@ -173,83 +175,84 @@ body:not([data-theme="light"]) .highlight span.linenos { color: #aaaaaa; backgro
|
||||
body:not([data-theme="light"]) .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
body:not([data-theme="light"]) .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
body:not([data-theme="light"]) .highlight .hll { background-color: #404040 }
|
||||
body:not([data-theme="light"]) .highlight { background: #202020; color: #d0d0d0 }
|
||||
body:not([data-theme="light"]) .highlight .c { color: #ababab; font-style: italic } /* Comment */
|
||||
body:not([data-theme="light"]) .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
||||
body:not([data-theme="light"]) .highlight .esc { color: #d0d0d0 } /* Escape */
|
||||
body:not([data-theme="light"]) .highlight .g { color: #d0d0d0 } /* Generic */
|
||||
body:not([data-theme="light"]) .highlight .k { color: #6ebf26; font-weight: bold } /* Keyword */
|
||||
body:not([data-theme="light"]) .highlight .l { color: #d0d0d0 } /* Literal */
|
||||
body:not([data-theme="light"]) .highlight .n { color: #d0d0d0 } /* Name */
|
||||
body:not([data-theme="light"]) .highlight .o { color: #d0d0d0 } /* Operator */
|
||||
body:not([data-theme="light"]) .highlight .x { color: #d0d0d0 } /* Other */
|
||||
body:not([data-theme="light"]) .highlight .p { color: #d0d0d0 } /* Punctuation */
|
||||
body:not([data-theme="light"]) .highlight .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */
|
||||
body:not([data-theme="light"]) .highlight .cm { color: #ababab; font-style: italic } /* Comment.Multiline */
|
||||
body:not([data-theme="light"]) .highlight .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
|
||||
body:not([data-theme="light"]) .highlight .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */
|
||||
body:not([data-theme="light"]) .highlight .c1 { color: #ababab; font-style: italic } /* Comment.Single */
|
||||
body:not([data-theme="light"]) .highlight .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
||||
body:not([data-theme="light"]) .highlight .gd { color: #d22323 } /* Generic.Deleted */
|
||||
body:not([data-theme="light"]) .highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
|
||||
body:not([data-theme="light"]) .highlight .gr { color: #d22323 } /* Generic.Error */
|
||||
body:not([data-theme="light"]) .highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
|
||||
body:not([data-theme="light"]) .highlight { background: #202020; color: #D0D0D0 }
|
||||
body:not([data-theme="light"]) .highlight .c { color: #ABABAB; font-style: italic } /* Comment */
|
||||
body:not([data-theme="light"]) .highlight .err { color: #A61717; background-color: #E3D2D2 } /* Error */
|
||||
body:not([data-theme="light"]) .highlight .esc { color: #D0D0D0 } /* Escape */
|
||||
body:not([data-theme="light"]) .highlight .g { color: #D0D0D0 } /* Generic */
|
||||
body:not([data-theme="light"]) .highlight .k { color: #6EBF26; font-weight: bold } /* Keyword */
|
||||
body:not([data-theme="light"]) .highlight .l { color: #D0D0D0 } /* Literal */
|
||||
body:not([data-theme="light"]) .highlight .n { color: #D0D0D0 } /* Name */
|
||||
body:not([data-theme="light"]) .highlight .o { color: #D0D0D0 } /* Operator */
|
||||
body:not([data-theme="light"]) .highlight .x { color: #D0D0D0 } /* Other */
|
||||
body:not([data-theme="light"]) .highlight .p { color: #D0D0D0 } /* Punctuation */
|
||||
body:not([data-theme="light"]) .highlight .ch { color: #ABABAB; font-style: italic } /* Comment.Hashbang */
|
||||
body:not([data-theme="light"]) .highlight .cm { color: #ABABAB; font-style: italic } /* Comment.Multiline */
|
||||
body:not([data-theme="light"]) .highlight .cp { color: #FF3A3A; font-weight: bold } /* Comment.Preproc */
|
||||
body:not([data-theme="light"]) .highlight .cpf { color: #ABABAB; font-style: italic } /* Comment.PreprocFile */
|
||||
body:not([data-theme="light"]) .highlight .c1 { color: #ABABAB; font-style: italic } /* Comment.Single */
|
||||
body:not([data-theme="light"]) .highlight .cs { color: #E50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
||||
body:not([data-theme="light"]) .highlight .gd { color: #FF3A3A } /* Generic.Deleted */
|
||||
body:not([data-theme="light"]) .highlight .ge { color: #D0D0D0; font-style: italic } /* Generic.Emph */
|
||||
body:not([data-theme="light"]) .highlight .ges { color: #D0D0D0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
body:not([data-theme="light"]) .highlight .gr { color: #FF3A3A } /* Generic.Error */
|
||||
body:not([data-theme="light"]) .highlight .gh { color: #FFF; font-weight: bold } /* Generic.Heading */
|
||||
body:not([data-theme="light"]) .highlight .gi { color: #589819 } /* Generic.Inserted */
|
||||
body:not([data-theme="light"]) .highlight .go { color: #cccccc } /* Generic.Output */
|
||||
body:not([data-theme="light"]) .highlight .gp { color: #aaaaaa } /* Generic.Prompt */
|
||||
body:not([data-theme="light"]) .highlight .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
|
||||
body:not([data-theme="light"]) .highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
|
||||
body:not([data-theme="light"]) .highlight .gt { color: #d22323 } /* Generic.Traceback */
|
||||
body:not([data-theme="light"]) .highlight .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */
|
||||
body:not([data-theme="light"]) .highlight .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */
|
||||
body:not([data-theme="light"]) .highlight .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */
|
||||
body:not([data-theme="light"]) .highlight .kp { color: #6ebf26 } /* Keyword.Pseudo */
|
||||
body:not([data-theme="light"]) .highlight .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */
|
||||
body:not([data-theme="light"]) .highlight .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */
|
||||
body:not([data-theme="light"]) .highlight .ld { color: #d0d0d0 } /* Literal.Date */
|
||||
body:not([data-theme="light"]) .highlight .m { color: #51b2fd } /* Literal.Number */
|
||||
body:not([data-theme="light"]) .highlight .s { color: #ed9d13 } /* Literal.String */
|
||||
body:not([data-theme="light"]) .highlight .na { color: #bbbbbb } /* Name.Attribute */
|
||||
body:not([data-theme="light"]) .highlight .nb { color: #2fbccd } /* Name.Builtin */
|
||||
body:not([data-theme="light"]) .highlight .nc { color: #71adff; text-decoration: underline } /* Name.Class */
|
||||
body:not([data-theme="light"]) .highlight .no { color: #40ffff } /* Name.Constant */
|
||||
body:not([data-theme="light"]) .highlight .nd { color: #ffa500 } /* Name.Decorator */
|
||||
body:not([data-theme="light"]) .highlight .ni { color: #d0d0d0 } /* Name.Entity */
|
||||
body:not([data-theme="light"]) .highlight .ne { color: #bbbbbb } /* Name.Exception */
|
||||
body:not([data-theme="light"]) .highlight .nf { color: #71adff } /* Name.Function */
|
||||
body:not([data-theme="light"]) .highlight .nl { color: #d0d0d0 } /* Name.Label */
|
||||
body:not([data-theme="light"]) .highlight .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */
|
||||
body:not([data-theme="light"]) .highlight .nx { color: #d0d0d0 } /* Name.Other */
|
||||
body:not([data-theme="light"]) .highlight .py { color: #d0d0d0 } /* Name.Property */
|
||||
body:not([data-theme="light"]) .highlight .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */
|
||||
body:not([data-theme="light"]) .highlight .nv { color: #40ffff } /* Name.Variable */
|
||||
body:not([data-theme="light"]) .highlight .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */
|
||||
body:not([data-theme="light"]) .highlight .pm { color: #d0d0d0 } /* Punctuation.Marker */
|
||||
body:not([data-theme="light"]) .highlight .w { color: #666666 } /* Text.Whitespace */
|
||||
body:not([data-theme="light"]) .highlight .mb { color: #51b2fd } /* Literal.Number.Bin */
|
||||
body:not([data-theme="light"]) .highlight .mf { color: #51b2fd } /* Literal.Number.Float */
|
||||
body:not([data-theme="light"]) .highlight .mh { color: #51b2fd } /* Literal.Number.Hex */
|
||||
body:not([data-theme="light"]) .highlight .mi { color: #51b2fd } /* Literal.Number.Integer */
|
||||
body:not([data-theme="light"]) .highlight .mo { color: #51b2fd } /* Literal.Number.Oct */
|
||||
body:not([data-theme="light"]) .highlight .sa { color: #ed9d13 } /* Literal.String.Affix */
|
||||
body:not([data-theme="light"]) .highlight .sb { color: #ed9d13 } /* Literal.String.Backtick */
|
||||
body:not([data-theme="light"]) .highlight .sc { color: #ed9d13 } /* Literal.String.Char */
|
||||
body:not([data-theme="light"]) .highlight .dl { color: #ed9d13 } /* Literal.String.Delimiter */
|
||||
body:not([data-theme="light"]) .highlight .sd { color: #ed9d13 } /* Literal.String.Doc */
|
||||
body:not([data-theme="light"]) .highlight .s2 { color: #ed9d13 } /* Literal.String.Double */
|
||||
body:not([data-theme="light"]) .highlight .se { color: #ed9d13 } /* Literal.String.Escape */
|
||||
body:not([data-theme="light"]) .highlight .sh { color: #ed9d13 } /* Literal.String.Heredoc */
|
||||
body:not([data-theme="light"]) .highlight .si { color: #ed9d13 } /* Literal.String.Interpol */
|
||||
body:not([data-theme="light"]) .highlight .sx { color: #ffa500 } /* Literal.String.Other */
|
||||
body:not([data-theme="light"]) .highlight .sr { color: #ed9d13 } /* Literal.String.Regex */
|
||||
body:not([data-theme="light"]) .highlight .s1 { color: #ed9d13 } /* Literal.String.Single */
|
||||
body:not([data-theme="light"]) .highlight .ss { color: #ed9d13 } /* Literal.String.Symbol */
|
||||
body:not([data-theme="light"]) .highlight .bp { color: #2fbccd } /* Name.Builtin.Pseudo */
|
||||
body:not([data-theme="light"]) .highlight .fm { color: #71adff } /* Name.Function.Magic */
|
||||
body:not([data-theme="light"]) .highlight .vc { color: #40ffff } /* Name.Variable.Class */
|
||||
body:not([data-theme="light"]) .highlight .vg { color: #40ffff } /* Name.Variable.Global */
|
||||
body:not([data-theme="light"]) .highlight .vi { color: #40ffff } /* Name.Variable.Instance */
|
||||
body:not([data-theme="light"]) .highlight .vm { color: #40ffff } /* Name.Variable.Magic */
|
||||
body:not([data-theme="light"]) .highlight .il { color: #51b2fd } /* Literal.Number.Integer.Long */
|
||||
body:not([data-theme="light"]) .highlight .go { color: #CCC } /* Generic.Output */
|
||||
body:not([data-theme="light"]) .highlight .gp { color: #AAA } /* Generic.Prompt */
|
||||
body:not([data-theme="light"]) .highlight .gs { color: #D0D0D0; font-weight: bold } /* Generic.Strong */
|
||||
body:not([data-theme="light"]) .highlight .gu { color: #FFF; text-decoration: underline } /* Generic.Subheading */
|
||||
body:not([data-theme="light"]) .highlight .gt { color: #FF3A3A } /* Generic.Traceback */
|
||||
body:not([data-theme="light"]) .highlight .kc { color: #6EBF26; font-weight: bold } /* Keyword.Constant */
|
||||
body:not([data-theme="light"]) .highlight .kd { color: #6EBF26; font-weight: bold } /* Keyword.Declaration */
|
||||
body:not([data-theme="light"]) .highlight .kn { color: #6EBF26; font-weight: bold } /* Keyword.Namespace */
|
||||
body:not([data-theme="light"]) .highlight .kp { color: #6EBF26 } /* Keyword.Pseudo */
|
||||
body:not([data-theme="light"]) .highlight .kr { color: #6EBF26; font-weight: bold } /* Keyword.Reserved */
|
||||
body:not([data-theme="light"]) .highlight .kt { color: #6EBF26; font-weight: bold } /* Keyword.Type */
|
||||
body:not([data-theme="light"]) .highlight .ld { color: #D0D0D0 } /* Literal.Date */
|
||||
body:not([data-theme="light"]) .highlight .m { color: #51B2FD } /* Literal.Number */
|
||||
body:not([data-theme="light"]) .highlight .s { color: #ED9D13 } /* Literal.String */
|
||||
body:not([data-theme="light"]) .highlight .na { color: #BBB } /* Name.Attribute */
|
||||
body:not([data-theme="light"]) .highlight .nb { color: #2FBCCD } /* Name.Builtin */
|
||||
body:not([data-theme="light"]) .highlight .nc { color: #71ADFF; text-decoration: underline } /* Name.Class */
|
||||
body:not([data-theme="light"]) .highlight .no { color: #40FFFF } /* Name.Constant */
|
||||
body:not([data-theme="light"]) .highlight .nd { color: #FFA500 } /* Name.Decorator */
|
||||
body:not([data-theme="light"]) .highlight .ni { color: #D0D0D0 } /* Name.Entity */
|
||||
body:not([data-theme="light"]) .highlight .ne { color: #BBB } /* Name.Exception */
|
||||
body:not([data-theme="light"]) .highlight .nf { color: #71ADFF } /* Name.Function */
|
||||
body:not([data-theme="light"]) .highlight .nl { color: #D0D0D0 } /* Name.Label */
|
||||
body:not([data-theme="light"]) .highlight .nn { color: #71ADFF; text-decoration: underline } /* Name.Namespace */
|
||||
body:not([data-theme="light"]) .highlight .nx { color: #D0D0D0 } /* Name.Other */
|
||||
body:not([data-theme="light"]) .highlight .py { color: #D0D0D0 } /* Name.Property */
|
||||
body:not([data-theme="light"]) .highlight .nt { color: #6EBF26; font-weight: bold } /* Name.Tag */
|
||||
body:not([data-theme="light"]) .highlight .nv { color: #40FFFF } /* Name.Variable */
|
||||
body:not([data-theme="light"]) .highlight .ow { color: #6EBF26; font-weight: bold } /* Operator.Word */
|
||||
body:not([data-theme="light"]) .highlight .pm { color: #D0D0D0 } /* Punctuation.Marker */
|
||||
body:not([data-theme="light"]) .highlight .w { color: #666 } /* Text.Whitespace */
|
||||
body:not([data-theme="light"]) .highlight .mb { color: #51B2FD } /* Literal.Number.Bin */
|
||||
body:not([data-theme="light"]) .highlight .mf { color: #51B2FD } /* Literal.Number.Float */
|
||||
body:not([data-theme="light"]) .highlight .mh { color: #51B2FD } /* Literal.Number.Hex */
|
||||
body:not([data-theme="light"]) .highlight .mi { color: #51B2FD } /* Literal.Number.Integer */
|
||||
body:not([data-theme="light"]) .highlight .mo { color: #51B2FD } /* Literal.Number.Oct */
|
||||
body:not([data-theme="light"]) .highlight .sa { color: #ED9D13 } /* Literal.String.Affix */
|
||||
body:not([data-theme="light"]) .highlight .sb { color: #ED9D13 } /* Literal.String.Backtick */
|
||||
body:not([data-theme="light"]) .highlight .sc { color: #ED9D13 } /* Literal.String.Char */
|
||||
body:not([data-theme="light"]) .highlight .dl { color: #ED9D13 } /* Literal.String.Delimiter */
|
||||
body:not([data-theme="light"]) .highlight .sd { color: #ED9D13 } /* Literal.String.Doc */
|
||||
body:not([data-theme="light"]) .highlight .s2 { color: #ED9D13 } /* Literal.String.Double */
|
||||
body:not([data-theme="light"]) .highlight .se { color: #ED9D13 } /* Literal.String.Escape */
|
||||
body:not([data-theme="light"]) .highlight .sh { color: #ED9D13 } /* Literal.String.Heredoc */
|
||||
body:not([data-theme="light"]) .highlight .si { color: #ED9D13 } /* Literal.String.Interpol */
|
||||
body:not([data-theme="light"]) .highlight .sx { color: #FFA500 } /* Literal.String.Other */
|
||||
body:not([data-theme="light"]) .highlight .sr { color: #ED9D13 } /* Literal.String.Regex */
|
||||
body:not([data-theme="light"]) .highlight .s1 { color: #ED9D13 } /* Literal.String.Single */
|
||||
body:not([data-theme="light"]) .highlight .ss { color: #ED9D13 } /* Literal.String.Symbol */
|
||||
body:not([data-theme="light"]) .highlight .bp { color: #2FBCCD } /* Name.Builtin.Pseudo */
|
||||
body:not([data-theme="light"]) .highlight .fm { color: #71ADFF } /* Name.Function.Magic */
|
||||
body:not([data-theme="light"]) .highlight .vc { color: #40FFFF } /* Name.Variable.Class */
|
||||
body:not([data-theme="light"]) .highlight .vg { color: #40FFFF } /* Name.Variable.Global */
|
||||
body:not([data-theme="light"]) .highlight .vi { color: #40FFFF } /* Name.Variable.Instance */
|
||||
body:not([data-theme="light"]) .highlight .vm { color: #40FFFF } /* Name.Variable.Magic */
|
||||
body:not([data-theme="light"]) .highlight .il { color: #51B2FD } /* Literal.Number.Integer.Long */
|
||||
}
|
||||
}
|
||||