Compare commits
576 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 622fd6cf46 | |||
| b9d73518dd | |||
| 17bdf45ac1 | |||
| 36052e2c61 | |||
| 06d232f889 | |||
| f9b3c749e0 | |||
| 63a59753af | |||
| 20696e7827 | |||
| 127c9862da | |||
| fee9473cac | |||
| 5337b72853 | |||
| 9bc5d91106 | |||
| 45ae66e9bf | |||
| f03cf34370 | |||
| 47db2a3bd5 | |||
| 40cd961eab | |||
| 34cdd4bf0f | |||
| b0ef58e5ca | |||
| b6020b5ea8 | |||
| ee544fcf31 | |||
| 886b0ac0ca | |||
| ed4070a3d1 | |||
| 6d6568852a | |||
| b479e14ca5 | |||
| 8fec5cedbe | |||
| 9852a3534b | |||
| 81fc920bdf | |||
| 5b1b18e84a | |||
| 9c8c143c62 | |||
| db9858d75f | |||
| 874405cbdd | |||
| 2a3f2b8bdc | |||
| 9aae06c694 | |||
| 70ffc38c49 | |||
| 73071b0755 | |||
| ab697dc583 | |||
| ecc78fa45f | |||
| e5309caf48 | |||
| 094d2f2079 | |||
| 5a917c9dac | |||
| 1df0eea0b7 | |||
| 718c3577db | |||
| 5111c32854 | |||
| 63d4e9a399 | |||
| 60773ceb16 | |||
| 5d6c3dd891 | |||
| a564dd2b2d | |||
| 16cf1ab1ba | |||
| 47e326c8a9 | |||
| 9a7585cbef | |||
| 902f7af64d | |||
| 004bf27526 | |||
| 9cad90266e | |||
| e9de01e10e | |||
| 372bedcd85 | |||
| 1141a3034d | |||
| 3f3276ca45 | |||
| 6e742f7267 | |||
| d3525943c2 | |||
| cb55189e5c | |||
| 0b98a9bff4 | |||
| a8d6e1780a | |||
| cb9840250a | |||
| 16f8725906 | |||
| 2656157462 | |||
| c9c7469b32 | |||
| 0f429e2385 | |||
| 89d8342ce5 | |||
| c18997bf5b | |||
| 1e4dd9d6f0 | |||
| b296c10541 | |||
| 9065de5fb4 | |||
| 7997fd104e | |||
| 11667504b2 | |||
| 7744c4ffe6 | |||
| 8a61d2c8d5 | |||
| 1380016995 | |||
| f2aff3fbd5 | |||
| b859984ebe | |||
| 9593b1c295 | |||
| 3d6455fb37 | |||
| b085127d6e | |||
| 80ffa5ebc3 | |||
| 76fb73f46c | |||
| e51b0077c7 | |||
| c18806c912 | |||
| 683881d6cd | |||
| f62d9946ac | |||
| 893a463663 | |||
| 39b788867d | |||
| 2abd8a1aae | |||
| 7940ac0812 | |||
| 3f2075da6f | |||
| e90b2866b4 | |||
| 8886ed5794 | |||
| 32ee4216fd | |||
| 571ad2c8fb | |||
| 0c47ff1ccc | |||
| 18f450c58b | |||
| b3d85b583f | |||
| 03695565ba | |||
| 3e380a8fc7 | |||
| fd35451927 | |||
| 921987c999 | |||
| 81e0989070 | |||
| 3fa7698438 | |||
| 75e32af1c5 | |||
| 9775893840 | |||
| e5c0ee4153 | |||
| 4042dd6ef7 | |||
| af538e0489 | |||
| 8f4cf433ba | |||
| c55e1e9628 | |||
| be02586133 | |||
| 6db742ade7 | |||
| 6a53298aa2 | |||
| f00b6a6fdb | |||
| dc0a0735db | |||
| b230edd21d | |||
| 30e75b1bfb | |||
| 7f70ffdc21 | |||
| 6e6b49dcd2 | |||
| 383f96d82a | |||
| ebef2da7a8 | |||
| 4946d9f2eb | |||
| fcb61e3ebf | |||
| eae788957a | |||
| 045a9d8451 | |||
| da644d33ea | |||
| e03fc38920 | |||
| c36c0368ef | |||
| 3d979e2d65 | |||
| 5158613501 | |||
| b53185779a | |||
| 5b63f84491 | |||
| fd2cc1231f | |||
| 76950ee3de | |||
| 8565b2fdf5 | |||
| 2a915eab2d | |||
| 36654c1414 | |||
| fdf0456cf0 | |||
| 8cff18f8ce | |||
| 5e072affe4 | |||
| fc4c7638a6 | |||
| 532f9ee665 | |||
| 4a725de935 | |||
| 2335a71427 | |||
| 3e70dd6134 | |||
| 474521056b | |||
| d33154bfdb | |||
| 8f82a2b87f | |||
| 304610c682 | |||
| bc39a1acf1 | |||
| 20b7278f7b | |||
| 1f66a9b0c0 | |||
| f464ecfcb5 | |||
| 49fdeb9bc4 | |||
| 40560a31f2 | |||
| f7d8a4b3b3 | |||
| c498bf5668 | |||
| 2e19304ebf | |||
| 1cd7c85a52 | |||
| 171f43f4e3 | |||
| 09a1088437 | |||
| 6346bc54a8 | |||
| 40e25d8e40 | |||
| e19438fdcc | |||
| d85ea07b5e | |||
| 4dda0e8a5b | |||
| 5faf13d505 | |||
| 2be1c7633d | |||
| 6ac2f437b9 | |||
| 2fe9dec459 | |||
| 8f8da080f5 | |||
| 01a973db91 | |||
| 1c4528dca1 | |||
| a99031873d | |||
| ab1186eaf7 | |||
| 940c889440 | |||
| ac7c36029b | |||
| c79811e040 | |||
| 7545613c52 | |||
| 7bd6da034a | |||
| 34f10d1196 | |||
| be84e8a731 | |||
| 7331bd2c09 | |||
| 6bfd0bf4eb | |||
| 3013c10180 | |||
| 95a34dad4b | |||
| a3bc2ef38f | |||
| aa255d0713 | |||
| 5a8152c589 | |||
| 8a24dbae40 | |||
| 2f1329e581 | |||
| 2166294a7a | |||
| 8042f5eaa1 | |||
| 1b1ab42aaa | |||
| ae8fcb88d8 | |||
| 98b232bc4c | |||
| d7a444556a | |||
| 58eaceb48c | |||
| 3c81f93d4a | |||
| 2685e043ea | |||
| 214ee9d771 | |||
| d39c1893e7 | |||
| 548cbd50d8 | |||
| 6b06875c42 | |||
| d7262c7cbe | |||
| d9a021465e | |||
| 8451bbe7e6 | |||
| 1ac7238347 | |||
| ea7762cbc0 | |||
| c4a7d17b2f | |||
| c758c4d279 | |||
| d136eac32b | |||
| f74e6d12c9 | |||
| 6f68d6edc4 | |||
| 076d6b09c4 | |||
| 8c484c786f | |||
| 363d56d49d | |||
| 2a581a9a9b | |||
| 2779852417 | |||
| e0f69344c2 | |||
| 469c9919cb | |||
| 6518370d79 | |||
| ffe61e701a | |||
| 7f65c767f0 | |||
| 157a54d4a4 | |||
| c8c0f77c81 | |||
| 4c3a82cf20 | |||
| 1ec83b535f | |||
| 31914a10aa | |||
| 6e369bf82f | |||
| 39059a365d | |||
| 0b2dba7977 | |||
| c6e2ba2cf3 | |||
| c5918395de | |||
| 861ac92c4c | |||
| 715e35d626 | |||
| a8ea7bcca6 | |||
| 534a8825eb | |||
| 89f3c0f649 | |||
| e4a82d5358 | |||
| 68cd79768b | |||
| 701c624d0a | |||
| ec90af750d | |||
| 2c1b3a0e5b | |||
| 02968baa76 | |||
| 06fefebc08 | |||
| 513a82e363 | |||
| a4b80e7ddb | |||
| be6910e4e0 | |||
| 0a8b755230 | |||
| d334613888 | |||
| 14bdcaf770 | |||
| 592c405067 | |||
| bb8012ad50 | |||
| 648e9a68b8 | |||
| 8c167b8f3d | |||
| bd933dc1df | |||
| 76f12b4854 | |||
| 30af212217 | |||
| 6c22ccc6d4 | |||
| 26dae3830e | |||
| a776d59f03 | |||
| 5b20caf759 | |||
| a800ce43f3 | |||
| 7916b8e7f4 | |||
| 60e3c7348a | |||
| cc9970c83e | |||
| c46b98f163 | |||
| 86061f9f47 | |||
| e0b795b4d0 | |||
| 34efbc6100 | |||
| 94edc8eff3 | |||
| e2aeb56c12 | |||
| 9a4325ce8e | |||
| 06fffe5a94 | |||
| 7a596882a8 | |||
| 76f86f782a | |||
| 4bd5f05e0e | |||
| 5d3a0efc89 | |||
| d1a461a2b3 | |||
| 0b1e7df31a | |||
| 301661c29e | |||
| b2b6708e8f | |||
| 19a033db96 | |||
| 5bb510b589 | |||
| f1dcda82ac | |||
| d24f3a490a | |||
| 715a84c6f2 | |||
| 379e56b2ce | |||
| c6df6293b2 | |||
| d99d31097b | |||
| 54488cfeb5 | |||
| d7e38d646e | |||
| b9057bee5f | |||
| 9bd64834ec | |||
| 9e20ba2dac | |||
| 49ed335e19 | |||
| 85c71b0b7b | |||
| 33fac728f8 | |||
| 49616a36cf | |||
| 1e77f85cd4 | |||
| 9e316ab989 | |||
| 94749e0dde | |||
| a6dbc53209 | |||
| 3af5a8f3ed | |||
| fb5172ff10 | |||
| 24d6de8490 | |||
| d3ab0878e0 | |||
| 7848b7e396 | |||
| fc80dd2614 | |||
| e00a758b2a | |||
| d44ec745df | |||
| 7573ac1970 | |||
| 88390f0cbc | |||
| 3b8490ae9c | |||
| 417ac9f8da | |||
| fe5e74bc2b | |||
| 30f71857ae | |||
| c24233845e | |||
| c0fbde5ad1 | |||
| 5da66402dd | |||
| 3bf5694238 | |||
| 9e6a5d5d91 | |||
| cf3e47f469 | |||
| f8db5a545d | |||
| a79f6e7efa | |||
| ac4606bcf7 | |||
| d1cb07356c | |||
| e811d54d0f | |||
| 49c8ada478 | |||
| 6ea7d78b31 | |||
| 0ace84367b | |||
| e63e6821e0 | |||
| 109132e09d | |||
| efd24ec134 | |||
| eefa37f808 | |||
| e4871f7667 | |||
| 44ba5624bc | |||
| e9c5e3c189 | |||
| f3ff71d9b8 | |||
| 81b92ffdc1 | |||
| 02bb9068cc | |||
| ecc9e84bc2 | |||
| 2b43436f56 | |||
| b2d61843d0 | |||
| ff74b5a0af | |||
| d66c31b4e9 | |||
| e825b0b8ff | |||
| b35f86643a | |||
| 3871d8615e | |||
| f2c0dac217 | |||
| 8636259886 | |||
| 4b38a776a3 | |||
| 7a331a8b60 | |||
| af1a05ff6a | |||
| 1b50f5267a | |||
| e95e9e6a89 | |||
| e8024e560f | |||
| 8cbbcb0fe9 | |||
| 8e4bfbbd94 | |||
| 600bd0e64d | |||
| 123fd1de92 | |||
| 29df5950c8 | |||
| b8ca89c2b6 | |||
| 79725a1637 | |||
| 1a2da0d7c7 | |||
| fe065f8bdd | |||
| 5d90ea565a | |||
| b701cdd07f | |||
| 8e5b3b4e83 | |||
| 24b7cb777f | |||
| cf1ca01a3b | |||
| 7c70f9d865 | |||
| 6cf9288b11 | |||
| 00816b55bb | |||
| 3856747e31 | |||
| 50799bd2a6 | |||
| ecffa1a7eb | |||
| 9fef53d083 | |||
| 0db64610b1 | |||
| 4af14a712c | |||
| 402b5fc461 | |||
| 38aeb1ab3b | |||
| b0a21b3aa9 | |||
| 5e6a1add6b | |||
| 104b186047 | |||
| 6d23da360d | |||
| 1be00a5c41 | |||
| 71e5eef8c1 | |||
| b3a439993d | |||
| 5606b64317 | |||
| 3d38ef27d4 | |||
| 93fa8e7240 | |||
| d53e8cf037 | |||
| be820b1965 | |||
| 425cf66cf7 | |||
| 8d294df3bb | |||
| da297aeb64 | |||
| 282239fc57 | |||
| 222437d851 | |||
| c9de260e00 | |||
| 31104c6e9c | |||
| 64593e27be | |||
| bac33d4e8b | |||
| 124ec006b4 | |||
| dd55899775 | |||
| cc0c01661d | |||
| 5f36c8601f | |||
| 2f71296816 | |||
| 7923322d92 | |||
| fef5ed6bad | |||
| 059b0743ef | |||
| 4d4d39651f | |||
| 6a1e6417bb | |||
| ed20b27e9d | |||
| 39f1258d0e | |||
| 03d3478b5e | |||
| b35122a872 | |||
| ae240f4697 | |||
| 4e1cdc638f | |||
| fc83c5b082 | |||
| ee90605b30 | |||
| 3684fe502f | |||
| d4aeb85191 | |||
| 04540f6e48 | |||
| 0db7eb1408 | |||
| 5fe55243c6 | |||
| b56830b36e | |||
| e3ea61c944 | |||
| 02f9c32da7 | |||
| a4a9a1dd53 | |||
| d7f9b30638 | |||
| 02676d3b25 | |||
| 089612bfc1 | |||
| ca345b20ff | |||
| 3b5973085f | |||
| dc6877927e | |||
| f01d838e17 | |||
| 9da6d39f64 | |||
| d17fbf1f34 | |||
| 7398e312fc | |||
| 82fc8720ad | |||
| 4b9686c31a | |||
| 86a5b3302a | |||
| c990aae648 | |||
| 3051b6897d | |||
| 550dfd44cb | |||
| 95d3346da6 | |||
| d4aabc8b89 | |||
| d487609dcf | |||
| c96c82f1d1 | |||
| cb023cde40 | |||
| 17be289f37 | |||
| b8105e23ff | |||
| f378d09cbe | |||
| 4dfa62833c | |||
| 2ec6d3ba6c | |||
| 15d027e11e | |||
| 87a274d177 | |||
| f8272793b4 | |||
| 3a215be859 | |||
| 0e1279d012 | |||
| 8ec356a28e | |||
| 49d7808835 | |||
| 48184134e4 | |||
| 987ff0658b | |||
| 27dea7c524 | |||
| 9c6fd132d4 | |||
| 8d58bb62ab | |||
| c357f7a94e | |||
| 4b3ead3db2 | |||
| b62e9af5d4 | |||
| fa82989a2e | |||
| 07a65609b4 | |||
| 257bd95da8 | |||
| 1ccfa9079c | |||
| 57226201ff | |||
| d9419cd895 | |||
| aae10ede72 | |||
| 291b3056cd | |||
| 3f53c89d32 | |||
| 05288d7c97 | |||
| b403441074 | |||
| d3a23e3b00 | |||
| 329d83587e | |||
| 0a4dd64434 | |||
| b96cbf1014 | |||
| 485558cd6b | |||
| 8d93867a22 | |||
| 6b20a98adc | |||
| f3d04ba90f | |||
| 1d2564cedb | |||
| bec8473695 | |||
| 25620415a0 | |||
| b6df952995 | |||
| a72aaf12ca | |||
| b978a993b2 | |||
| 5ae00264e8 | |||
| 5396b80e80 | |||
| fdaa58a6fa | |||
| 4253175627 | |||
| 81158c27e4 | |||
| eeb424ecee | |||
| 0273328b23 | |||
| 20dfbcf0cc | |||
| c96e067839 | |||
| 9ff37543f3 | |||
| 974ca48cb4 | |||
| 167d48c8ce | |||
| f253b08774 | |||
| 1c768e9219 | |||
| df39cff520 | |||
| e1e31692d7 | |||
| 293a834c35 | |||
| 1bbdd9b3f5 | |||
| d4b6b6ee59 | |||
| fca03bbdce | |||
| 29aa4f9315 | |||
| d5cac30a85 | |||
| 6500bc7390 | |||
| 81fed10855 | |||
| a39876106b | |||
| 90b39774d1 | |||
| 006c70cd09 | |||
| 02945f960d | |||
| e401ec870d | |||
| 90174fcc28 | |||
| c18ebed419 | |||
| 1d180a96f6 | |||
| 4241990690 | |||
| 3d49076602 | |||
| 2e0dd278b6 | |||
| b432a7c7de | |||
| c0383fa2b0 | |||
| 98d66e2ba5 | |||
| 2e4fcc659c | |||
| 8fe7c19c59 | |||
| 27b46c9e89 | |||
| 70a3637a98 | |||
| 2e0476e6b9 | |||
| 39911190aa | |||
| 9e9606b8cf | |||
| 8be1acee0a | |||
| ba39a69175 | |||
| a692d29c90 | |||
| 7092589388 | |||
| 2d3969aa3d | |||
| 1443f4c104 | |||
| d2232f19ba | |||
| c44c6f9086 | |||
| 259c2aa397 | |||
| 10854bfdbc | |||
| f5236878b0 | |||
| daf72f4237 | |||
| 652b884d72 | |||
| ea3716f48e | |||
| 165e620043 | |||
| 58f43b163e | |||
| 448ea8ceb5 | |||
| f7e8fc4719 | |||
| 1d6c877b4c | |||
| c3dcd9366d | |||
| 8d01586a5a | |||
| 3e5f613f66 | |||
| 614a139cd4 | |||
| 1cf6570c2d | |||
| d207cbcd9c | |||
| 18b20f2d8d | |||
| c37533d2c7 | |||
| fd13e20165 | |||
| 66ce58f0f4 | |||
| e8ee26f78d | |||
| c0fb419fe1 |
@@ -3,7 +3,12 @@
|
||||
testutils
|
||||
TODO
|
||||
Examples/RNS
|
||||
RNS/Utilities/RNS
|
||||
build
|
||||
dist
|
||||
docs/build
|
||||
rns*.egg-info
|
||||
rns*.egg-info
|
||||
profile.data
|
||||
tests/rnsconfig/storage
|
||||
*.data
|
||||
*.result
|
||||
|
||||
@@ -15,7 +15,7 @@ import RNS
|
||||
APP_NAME = "example_utilities"
|
||||
|
||||
# We initialise two lists of strings to use as app_data
|
||||
fruits = ["Peach", "Quince", "Date palm", "Tangerine", "Pomelo", "Carambola", "Grape"]
|
||||
fruits = ["Peach", "Quince", "Date", "Tangerine", "Pomelo", "Carambola", "Grape"]
|
||||
noble_gases = ["Helium", "Neon", "Argon", "Krypton", "Xenon", "Radon", "Oganesson"]
|
||||
|
||||
# This initialisation is executed when the program is started
|
||||
@@ -130,10 +130,11 @@ class ExampleAnnounceHandler:
|
||||
RNS.prettyhexrep(destination_hash)
|
||||
)
|
||||
|
||||
RNS.log(
|
||||
"The announce contained the following app data: "+
|
||||
app_data.decode("utf-8")
|
||||
)
|
||||
if app_data:
|
||||
RNS.log(
|
||||
"The announce contained the following app data: "+
|
||||
app_data.decode("utf-8")
|
||||
)
|
||||
|
||||
##########################################################
|
||||
#### Program Startup #####################################
|
||||
|
||||
@@ -22,6 +22,8 @@ APP_NAME = "example_utilities"
|
||||
# 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)
|
||||
|
||||
@@ -78,11 +80,32 @@ def announceLoop(destination):
|
||||
|
||||
|
||||
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.
|
||||
RNS.log("Received packet from echo client, proof sent")
|
||||
|
||||
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)
|
||||
|
||||
|
||||
##########################################################
|
||||
@@ -92,17 +115,21 @@ def server_callback(message, packet):
|
||||
# This initialisation is executed when the users chooses
|
||||
# to run as a client
|
||||
def client(destination_hexhash, configpath, timeout=None):
|
||||
global reticulum
|
||||
|
||||
# We need a binary representation of the destination
|
||||
# hash that was entered on the command line
|
||||
try:
|
||||
if len(destination_hexhash) != 20:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError(
|
||||
"Destination length is invalid, must be 20 hexadecimal characters (10 bytes)"
|
||||
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
|
||||
)
|
||||
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
except Exception as e:
|
||||
RNS.log("Invalid destination entered. Check your input!")
|
||||
RNS.log(str(e)+"\n")
|
||||
exit()
|
||||
|
||||
# We must first initialise Reticulum
|
||||
@@ -188,6 +215,8 @@ def client(destination_hexhash, configpath, timeout=None):
|
||||
# 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):
|
||||
@@ -197,10 +226,30 @@ def packet_delivered(receipt):
|
||||
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
|
||||
", round-trip time is "+rttstring+
|
||||
reception_stats
|
||||
)
|
||||
|
||||
# This function is called if a packet times out.
|
||||
|
||||
@@ -215,8 +215,12 @@ def client(destination_hexhash, configpath):
|
||||
# We need a binary representation of the destination
|
||||
# hash that was entered on the command line
|
||||
try:
|
||||
if len(destination_hexhash) != 20:
|
||||
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError(
|
||||
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
|
||||
)
|
||||
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
|
||||
@@ -84,7 +84,7 @@ def client_connected(link):
|
||||
def client_disconnected(link):
|
||||
RNS.log("Client disconnected")
|
||||
|
||||
def remote_identified(identity):
|
||||
def remote_identified(link, identity):
|
||||
RNS.log("Remote identified as: "+str(identity))
|
||||
|
||||
def server_packet_received(message, packet):
|
||||
@@ -124,8 +124,12 @@ def client(destination_hexhash, configpath):
|
||||
# We need a binary representation of the destination
|
||||
# hash that was entered on the command line
|
||||
try:
|
||||
if len(destination_hexhash) != 20:
|
||||
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError(
|
||||
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
|
||||
)
|
||||
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
|
||||
@@ -110,8 +110,12 @@ def client(destination_hexhash, configpath):
|
||||
# We need a binary representation of the destination
|
||||
# hash that was entered on the command line
|
||||
try:
|
||||
if len(destination_hexhash) != 20:
|
||||
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError(
|
||||
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
|
||||
)
|
||||
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
|
||||
@@ -110,8 +110,12 @@ def client(destination_hexhash, configpath):
|
||||
# We need a binary representation of the destination
|
||||
# hash that was entered on the command line
|
||||
try:
|
||||
if len(destination_hexhash) != 20:
|
||||
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError(
|
||||
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
|
||||
)
|
||||
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
##########################################################
|
||||
# This RNS example demonstrates a simple speedtest #
|
||||
# program to measure link throughput. #
|
||||
# #
|
||||
# The current configuration is suited for testing fast #
|
||||
# links. If you want to measure slow links like LoRa or #
|
||||
# packet radio, you must significantly lower the #
|
||||
# data_cap variable, which defines how much data is sent #
|
||||
# for each test. #
|
||||
##########################################################
|
||||
|
||||
import os
|
||||
@@ -160,8 +166,12 @@ def client(destination_hexhash, configpath):
|
||||
# We need a binary representation of the destination
|
||||
# hash that was entered on the command line
|
||||
try:
|
||||
if len(destination_hexhash) != 20:
|
||||
raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)")
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError(
|
||||
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
|
||||
)
|
||||
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License, unless otherwise noted
|
||||
|
||||
Copyright (c) 2018 Mark Qvist / unsigned.io
|
||||
Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
all: release
|
||||
|
||||
test:
|
||||
@echo Running tests...
|
||||
python -m tests.all
|
||||
|
||||
clean:
|
||||
@echo Cleaning...
|
||||
@-rm -rf ./build
|
||||
@-rm -rf ./dist
|
||||
@-rm -rf ./*.data
|
||||
@-rm -rf ./__pycache__
|
||||
@-rm -rf ./RNS/__pycache__
|
||||
@-rm -rf ./RNS/Cryptography/__pycache__
|
||||
@-rm -rf ./RNS/Cryptography/aes/__pycache__
|
||||
@-rm -rf ./RNS/Cryptography/pure25519/__pycache__
|
||||
@-rm -rf ./RNS/Interfaces/__pycache__
|
||||
@-rm -rf ./RNS/Utilities/__pycache__
|
||||
@-rm -rf ./RNS/vendor/__pycache__
|
||||
@-rm -rf ./RNS/vendor/i2plib/__pycache__
|
||||
@-rm -rf ./tests/__pycache__
|
||||
@-rm -rf ./tests/rnsconfig/storage
|
||||
@-rm -rf ./*.egg-info
|
||||
@make -C docs clean
|
||||
@echo Done
|
||||
|
||||
remove_symlinks:
|
||||
@echo Removing symlinks for build...
|
||||
-rm Examples/RNS
|
||||
-rm RNS/Utilities/RNS
|
||||
|
||||
create_symlinks:
|
||||
@echo Creating symlinks...
|
||||
-ln -s ../RNS ./Examples/
|
||||
-ln -s ../../RNS ./RNS/Utilities/
|
||||
|
||||
build_sdist_only:
|
||||
python3 setup.py sdist
|
||||
|
||||
build_wheel:
|
||||
python3 setup.py sdist bdist_wheel
|
||||
|
||||
build_pure_wheel:
|
||||
python3 setup.py sdist bdist_wheel --pure
|
||||
|
||||
documentation:
|
||||
make -C docs html
|
||||
|
||||
manual:
|
||||
make -C docs latexpdf
|
||||
|
||||
release: test remove_symlinks build_wheel build_pure_wheel documentation manual create_symlinks
|
||||
|
||||
debug: remove_symlinks build_wheel build_pure_wheel create_symlinks
|
||||
|
||||
upload:
|
||||
@echo Ready to publish release, hit enter to continue
|
||||
@read VOID
|
||||
@echo Uploading to PyPi...
|
||||
twine upload dist/*
|
||||
@echo Release published
|
||||
@@ -1,14 +1,38 @@
|
||||
Reticulum Network Stack β
|
||||
Reticulum Network Stack β <img align="right" src="https://static.pepy.tech/personalized-badge/rns?period=total&units=international_system&left_color=grey&right_color=blue&left_text=Installs"/>
|
||||
==========
|
||||
|
||||
Reticulum is a cryptography-based networking stack for wide-area networks built on readily available hardware, and can operate even with very high latency and extremely low bandwidth. Reticulum allows you to build very wide-area networks with off-the-shelf tools, and offers end-to-end encryption, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, unforgeable packet acknowledgements and more.
|
||||
<p align="center"><img width="200" src="https://raw.githubusercontent.com/markqvist/Reticulum/master/docs/source/graphics/rns_logo_512.png"></p>
|
||||
|
||||
Reticulum is a complete networking stack, and does not need IP or higher layers, although it is easy to use 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 the cryptography-based networking stack for building local and wide-area
|
||||
networks with readily available hardware. It can operate even with very high latency
|
||||
and extremely low bandwidth. Reticulum allows you to build wide-area networks
|
||||
with off-the-shelf tools, and offers end-to-end encryption and connectivity,
|
||||
initiator anonymity, autoconfiguring cryptographically backed multi-hop
|
||||
transport, efficient addressing, unforgeable delivery acknowledgements and
|
||||
more.
|
||||
|
||||
Having no dependencies on traditional networking stacks free up overhead that has been utilised to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality in open and trustless networks.
|
||||
The vision of Reticulum is to allow anyone to be their own network operator,
|
||||
and to make it cheap and easy to cover vast areas with a myriad of independent,
|
||||
inter-connectable and autonomous networks. Reticulum **is not** *one* network.
|
||||
It is **a tool** for building *thousands of networks*. Networks without
|
||||
kill-switches, surveillance, censorship and control. Networks that can freely
|
||||
interoperate, associate and disassociate with each other, and require no
|
||||
central oversight. Networks for human beings. *Networks for the people*.
|
||||
|
||||
No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python 3.
|
||||
Reticulum is a complete networking stack, and does not rely on IP or higher
|
||||
layers, but it is possible to use IP as the underlying carrier for Reticulum.
|
||||
It is therefore trivial to tunnel Reticulum over the Internet or private IP
|
||||
networks.
|
||||
|
||||
Having no dependencies on traditional networking stacks frees up overhead that
|
||||
has been used to implement a networking stack built directly on cryptographic
|
||||
principles, allowing resilience and stable functionality, even in open and
|
||||
trustless networks.
|
||||
|
||||
No kernel modules or drivers are required. Reticulum runs completely in
|
||||
userland, and can run on practically any system that runs Python 3.
|
||||
|
||||
## Read The Manual
|
||||
The full documentation for Reticulum is available 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)
|
||||
@@ -16,89 +40,336 @@ You can also [download the Reticulum manual as a PDF](https://github.com/markqvi
|
||||
For more info, see [unsigned.io/projects/reticulum](https://unsigned.io/projects/reticulum/)
|
||||
|
||||
## Notable Features
|
||||
- Coordination-less globally unique adressing and identification
|
||||
- Fully self-configuring multi-hop routing
|
||||
- Complete initiator anonymity, communicate without revealing your identity
|
||||
- Asymmetric X25519 encryption and Ed25519 signatures as a basis for all communication
|
||||
- Forward Secrecy with ephemereal Elliptic Curve Diffie-Hellman keys on Curve25519
|
||||
- Reticulum uses the [Fernet](https://github.com/fernet/spec/blob/master/Spec.md) specification for on-the-wire / over-the-air encryption
|
||||
- Keys are ephemeral and derived from an ECDH key exchange on Curve25519
|
||||
- AES-128 in CBC mode with PKCS7 padding
|
||||
- HMAC using SHA256 for authentication
|
||||
- IVs are generated through os.urandom()
|
||||
- Unforgeable packet delivery confirmations
|
||||
- A variety of supported interface types
|
||||
- An intuitive and easy-to-use API
|
||||
- Reliable and efficient transfer of arbritrary amounts of data
|
||||
- Reticulum can handle a few bytes of data or files of many gigabytes
|
||||
- Sequencing, transfer coordination and checksumming is automatic
|
||||
- The API is very easy to use, and provides transfer progress
|
||||
- Lightweight, flexible and expandable Request/Response mechanism
|
||||
- Efficient link establishment
|
||||
- Total bandwidth cost of setting up a link is 3 packets totalling 237 bytes
|
||||
- Low cost of keeping links open at only 0.62 bits per second
|
||||
- Coordination-less globally unique addressing and identification
|
||||
- Fully self-configuring multi-hop routing
|
||||
- Initiator anonymity, communicate without revealing your identity
|
||||
- Asymmetric X25519 encryption and Ed25519 signatures as a basis for all communication
|
||||
- Forward Secrecy with ephemeral Elliptic Curve Diffie-Hellman keys on Curve25519
|
||||
- Reticulum uses the [Fernet](https://github.com/fernet/spec/blob/master/Spec.md) specification for on-the-wire / over-the-air encryption
|
||||
- Keys are ephemeral and derived from an ECDH key exchange on Curve25519
|
||||
- AES-128 in CBC mode with PKCS7 padding
|
||||
- HMAC using SHA256 for authentication
|
||||
- IVs are generated through os.urandom()
|
||||
- Unforgeable packet delivery confirmations
|
||||
- A variety of supported interface types
|
||||
- An intuitive and easy-to-use API
|
||||
- Reliable and efficient transfer of arbitrary amounts of data
|
||||
- Reticulum can handle a few bytes of data or files of many gigabytes
|
||||
- Sequencing, 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
|
||||
- Low cost of keeping links open at only 0.44 bits per second
|
||||
|
||||
## Examples of Reticulum Applications
|
||||
If you want to quickly get an idea of what Reticulum can do, take a look at the following resources.
|
||||
If you want to quickly get an idea of what Reticulum can do, take a look at the
|
||||
following resources.
|
||||
|
||||
- For an off-grid, encrypted and resilient mesh communications platform, see [Nomad Network](https://github.com/markqvist/NomadNet)
|
||||
- For a distributed, delay and disruption tolerant message transfer protocol built on Reticulum, see [LXMF](https://github.com/markqvist/lxmf)
|
||||
- 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
|
||||
|
||||
## Where can Reticulum be used?
|
||||
Over practically any medium that can support at least a half-duplex channel with 1.000 bits per second throughput, and an MTU of 500 bytes. Data radios, modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, ad-hoc WiFi, free-space optical links and similar systems are all examples of the types of interfaces Reticulum was designed for.
|
||||
Over practically any medium that can support at least a half-duplex channel
|
||||
with 500 bits per second throughput, and an MTU of 500 bytes. Data radios,
|
||||
modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes,
|
||||
WiFi and Ethernet devices, free-space optical links, and similar systems are
|
||||
all examples of the types of physical devices Reticulum can use.
|
||||
|
||||
An open-source LoRa-based interface called [RNode](https://unsigned.io/projects/rnode/) has been designed specifically for use with Reticulum. It is possible to build yourself, or it can be purchased as a complete transceiver that just needs a USB connection to the host.
|
||||
An open-source LoRa-based interface called
|
||||
[RNode](https://markqvist.github.io/Reticulum/manual/hardware.html#rnode) has
|
||||
been designed specifically for use with Reticulum. It is possible to build
|
||||
yourself, or it can be purchased as a complete transceiver that just needs a
|
||||
USB connection to the host.
|
||||
|
||||
Reticulum can also be encapsulated over existing IP networks, so there's nothing stopping you from using it over wired ethernet or your local WiFi network, where it'll work just as well. In fact, one of the strengths of Reticulum is how easily it allows you to connect different mediums into a self-configuring, resilient and encrypted mesh.
|
||||
Reticulum can also be encapsulated over existing IP networks, so there's
|
||||
nothing stopping you from using it over wired Ethernet, your local WiFi network
|
||||
or the Internet, where it'll work just as well. In fact, one of the strengths
|
||||
of Reticulum is how easily it allows you to connect different mediums into a
|
||||
self-configuring, resilient and encrypted mesh, using any available mixture of
|
||||
available infrastructure.
|
||||
|
||||
As an example, it's possible to set up a Raspberry Pi connected to both a LoRa radio, a packet radio TNC and a WiFi network. Once the interfaces are configured, Reticulum will take care of the rest, and any device on the WiFi network can communicate with nodes on the LoRa and packet radio sides of the network, and vice versa.
|
||||
|
||||
## Current Status
|
||||
Reticulum should currently be considered beta software. All core protocol features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. The API and wire-format can be considered relatively stable at the moment, but could change if warranted.
|
||||
|
||||
## Supported interface types and devices
|
||||
|
||||
Reticulum implements a range of generalised interface types that covers most of the communications hardware that Reticulum can run over. If your hardware is not supported, it's relatively simple to implement an interface class. Currently, the following interfaces are supported:
|
||||
|
||||
- Any ethernet device
|
||||
- LoRa using [RNode](https://unsigned.io/projects/rnode/)
|
||||
- Packet Radio TNCs (with or without AX.25)
|
||||
- Any device with a serial port
|
||||
- TCP over IP networks
|
||||
- UDP over IP networks
|
||||
|
||||
|
||||
|
||||
## Feature Roadmap
|
||||
- Stream mode for links
|
||||
- More interface types for even broader compatibility
|
||||
- ESP32 devices (ESP-Now, Bluetooth, etc.)
|
||||
- More LoRa transceivers
|
||||
- AT-compatible modems
|
||||
- CAN-bus
|
||||
- ZeroMQ
|
||||
- MQTT
|
||||
- SPI
|
||||
- i²c
|
||||
|
||||
## Dependencies:
|
||||
- Python 3
|
||||
- cryptography.io
|
||||
- pyserial
|
||||
As an example, it's possible to set up a Raspberry Pi connected to both a LoRa
|
||||
radio, a packet radio TNC and a WiFi network. Once the interfaces are
|
||||
configured, Reticulum will take care of the rest, and any device on the WiFi
|
||||
network can communicate with nodes on the LoRa and packet radio sides of the
|
||||
network, and vice versa.
|
||||
|
||||
## How do I get started?
|
||||
The best way to get started with the Reticulum Network Stack depends on what
|
||||
you want to do. For full details and examples, have a look at the [Getting Started Fast](https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
|
||||
you want to do. For full details and examples, have a look at the
|
||||
[Getting Started Fast](https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html)
|
||||
section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
|
||||
|
||||
If you just need Reticulum as a dependency for another application, the easiest way is via pip:
|
||||
To simply install Reticulum and related utilities on your system, the easiest way is via pip:
|
||||
|
||||
```bash
|
||||
pip3 install rns
|
||||
pip install rns
|
||||
```
|
||||
|
||||
The default config file contains examples for using Reticulum with LoRa transceivers (specifically [RNode](https://unsigned.io/projects/rnode/)), packet radio TNCs/modems and UDP. By default a UDP interface is already enabled in the default config, which will enable Reticulum communication in your local ethernet broadcast domain.
|
||||
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).
|
||||
|
||||
You can use the examples in the config file to expand communication over other mediums such as packet radio or LoRa, or over fast IP links using the UDP interface. I'll add in-depth tutorials and explanations on these topics later. For now, the included examples will hopefully be enough to get started.
|
||||
When first started, Reticulum will create a default configuration file,
|
||||
providing basic connectivity to other Reticulum peers that might be locally
|
||||
reachable. The default config file contains a few examples, and references for
|
||||
creating a more complex configuration.
|
||||
|
||||
## Caveat Emptor
|
||||
Reticulum is experimental software, and should be considered as such. While it has been built with cryptography best-practices very foremost in mind, it _has not_ been externally security audited, and there could very well be privacy-breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch.
|
||||
If you have an old version of `pip` on your system, you may need to upgrade it first with `pip install pip --upgrade`. If you no not already have `pip` installed, you can install it using the package manager of your system with `sudo apt install python3-pip` or similar.
|
||||
|
||||
For more detailed examples on how to expand communication over many mediums such
|
||||
as packet radio or LoRa, serial ports, or over fast IP links and the Internet using
|
||||
the UDP and TCP interfaces, take a look at the [Supported Interfaces](https://markqvist.github.io/Reticulum/manual/interfaces.html)
|
||||
section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
|
||||
|
||||
## Included Utilities
|
||||
Reticulum includes a range of useful utilities for managing your networks,
|
||||
viewing status and information, and other tasks. You can read more about these
|
||||
programs in the [Included Utility Programs](https://markqvist.github.io/Reticulum/manual/using.html#included-utility-programs)
|
||||
section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
|
||||
|
||||
- The system daemon `rnsd` for running Reticulum as an always-available service
|
||||
- An interface status utility called `rnstatus`, that displays information about interfaces
|
||||
- The path lookup and management tool `rnpath` letting you view and modify path tables
|
||||
- A diagnostics tool called `rnprobe` for checking connectivity to destinations
|
||||
- A simple file transfer program called `rncp` making it easy to copy files to remote systems
|
||||
- The remote command execution program `rnx` let's you run commands and
|
||||
programs and retrieve output from remote systems
|
||||
|
||||
All tools, including `rnx` and `rncp`, work reliably and well even over very
|
||||
low-bandwidth links like LoRa or Packet Radio.
|
||||
|
||||
## 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.
|
||||
|
||||
Currently, the following interfaces are supported:
|
||||
|
||||
- Any Ethernet device
|
||||
- LoRa using [RNode](https://unsigned.io/projects/rnode/)
|
||||
- Packet Radio TNCs (with or without AX.25)
|
||||
- KISS-compatible hardware and software modems
|
||||
- Any device with a serial port
|
||||
- TCP over IP networks
|
||||
- UDP over IP networks
|
||||
- External programs via stdio or pipes
|
||||
- Custom hardware via stdio or pipes
|
||||
|
||||
## Performance
|
||||
Reticulum targets a *very* wide usable performance envelope, but prioritises
|
||||
functionality and performance on low-bandwidth mediums. The goal is to
|
||||
provide a dynamic performance envelope from 250 bits per second, to 1 gigabit
|
||||
per second on normal hardware.
|
||||
|
||||
Currently, the usable performance envelope is approximately 500 bits per second
|
||||
to 20 megabits per second, with physical mediums faster than that not being
|
||||
saturated. Performance beyond the current level is intended for future
|
||||
upgrades, but not highly prioritised at this point in time.
|
||||
|
||||
## Current Status
|
||||
Reticulum should currently be considered beta software. All core protocol
|
||||
features are implemented and functioning, but additions will probably occur as
|
||||
real-world use is explored. There will be bugs. The API and wire-format can be
|
||||
considered relatively stable at the moment, but could change if warranted.
|
||||
|
||||
## Development Roadmap
|
||||
- Version 0.4.0
|
||||
- Improving [the manual](https://markqvist.github.io/Reticulum/manual/) with sections specifically for beginners
|
||||
- Performance and memory optimisations
|
||||
- Utilities for managing identities, signing and encryption
|
||||
- User friendly interface configuration tool
|
||||
- More interface types for even broader compatibility
|
||||
- Plain ESP32 devices (ESP-Now, WiFi, Bluetooth, etc.)
|
||||
- More LoRa transceivers
|
||||
- IR Transceivers
|
||||
- Planned, but not yet scheduled
|
||||
- OpenWRT support
|
||||
- Metric-based path selection
|
||||
- Distributed Destination Naming System
|
||||
- Network-wide path balancing
|
||||
- Globally routable multicast
|
||||
- Bindings for other programming languages
|
||||
- Multiple paths in path table for quick recovery on link failures
|
||||
- A portable Reticulum implementation in C, see [#21](https://github.com/markqvist/Reticulum/discussions/21)
|
||||
- Easy way to share interface configurations, see [#19](https://github.com/markqvist/Reticulum/discussions/19)
|
||||
- More interface types
|
||||
- AT-compatible modems
|
||||
- Optical mediums
|
||||
- AWDL / OWL
|
||||
- HF Modems
|
||||
- CAN-bus
|
||||
- ZeroMQ
|
||||
- MQTT
|
||||
- XBee
|
||||
- SPI
|
||||
- i²c
|
||||
- Tor
|
||||
|
||||
## Dependencies
|
||||
The installation of the default `rns` package requires the dependencies listed
|
||||
below. Almost all systems and distributions have readily available packages for
|
||||
these dependencies, and when the `rns` package is installed with `pip`, they
|
||||
will be downloaded and installed as well.
|
||||
|
||||
- [PyCA/cryptography](https://github.com/pyca/cryptography)
|
||||
- [netifaces](https://github.com/al45tair/netifaces)
|
||||
- [pyserial](https://github.com/pyserial/pyserial)
|
||||
|
||||
On more unusual systems, and in some rare cases, it might not be possible to
|
||||
install or even compile one or more of the above modules. In such situations,
|
||||
you can use the `rnspure` package instead, which require no external
|
||||
dependencies for installation. Please note that the contents of the `rns` and
|
||||
`rnspure` packages are *identical*. The only difference is that the `rnspure`
|
||||
package lists no dependencies required for installation.
|
||||
|
||||
No matter how Reticulum is installed and started, it will load external
|
||||
dependencies only if they are *needed* and *available*. If for example you want
|
||||
to use Reticulum on a system that cannot support
|
||||
[pyserial](https://github.com/pyserial/pyserial), it is perfectly possible to
|
||||
do so using the `rnspure` package, but Reticulum will not be able to use
|
||||
serial-based interfaces. All other available modules will still be loaded when
|
||||
needed.
|
||||
|
||||
**Please Note!** If you use the `rnspure` package to run Reticulum on systems
|
||||
that do not support [PyCA/cryptography](https://github.com/pyca/cryptography),
|
||||
it is important that you read and understand the [Cryptographic
|
||||
Primitives](#cryptographic-primitives) section of this document.
|
||||
|
||||
## Public Testnet
|
||||
If you just want to get started experimenting without building any physical
|
||||
networks, you are welcome to join the Unsigned.io RNS Testnet. The testnet is
|
||||
just that, an informal network for testing and experimenting. It will be up
|
||||
most of the time, and anyone can join, but it also means that there's no
|
||||
guarantees for service availability.
|
||||
|
||||
The testnet runs the very latest version of Reticulum (often even a short while
|
||||
before it is publicly released). Sometimes experimental versions of Reticulum
|
||||
might be deployed to nodes on the testnet, which means strange behaviour might
|
||||
occur. If none of that scares you, you can join the testnet via either TCP or
|
||||
I2P. Just add one of the following interfaces to your Reticulum configuration
|
||||
file:
|
||||
|
||||
```
|
||||
# TCP/IP interface to the Dublin Hub
|
||||
[[RNS Testnet Dublin]]
|
||||
type = TCPClientInterface
|
||||
enabled = yes
|
||||
target_host = dublin.connect.reticulum.network
|
||||
target_port = 4965
|
||||
|
||||
# TCP/IP interface to the Frankfurt Hub
|
||||
[[RNS Testnet Frankfurt]]
|
||||
type = TCPClientInterface
|
||||
enabled = yes
|
||||
target_host = frankfurt.connect.reticulum.network
|
||||
target_port = 5377
|
||||
|
||||
# Interface to I2P Hub A
|
||||
[[RNS Testnet I2P Hub A]]
|
||||
type = I2PInterface
|
||||
enabled = yes
|
||||
peers = mrwqlsioq4hoo2lmeeud7dkfscnm7yxak7dmiyvsrnpfag3z5tsq.b32.i2p
|
||||
|
||||
# Interface to I2P Hub B
|
||||
[[RNS Testnet I2P Hub B]]
|
||||
type = I2PInterface
|
||||
enabled = yes
|
||||
peers = iwoqtz22dsr73aemwpw7guocplsjjoamyl7sogj33qtcd6ds4mza.b32.i2p
|
||||
```
|
||||
|
||||
The testnet also contains a number of [Nomad Network](https://github.com/markqvist/nomadnet) nodes, and LXMF propagation nodes.
|
||||
|
||||
## Support Reticulum
|
||||
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
|
||||
```
|
||||
- Bitcoin
|
||||
```
|
||||
3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq
|
||||
```
|
||||
- Ko-Fi: https://ko-fi.com/markqvist
|
||||
|
||||
Are certain features in the development roadmap are important to you or your
|
||||
organisation? Make them a reality quickly by sponsoring their implementation.
|
||||
|
||||
## Cryptographic Primitives
|
||||
Reticulum uses a simple suite of efficient, strong and modern cryptographic
|
||||
primitives, with widely available implementations that can be used both on
|
||||
general-purpose CPUs and on microcontrollers. The necessary primitives are:
|
||||
|
||||
- Ed25519 for signatures
|
||||
- X22519 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
|
||||
- SHA-256
|
||||
- SHA-512
|
||||
|
||||
In the default installation configuration, the `X25519`, `Ed25519` and
|
||||
`AES-128-CBC` primitives are provided by [OpenSSL](https://www.openssl.org/)
|
||||
(via the [PyCA/cryptography](https://github.com/pyca/cryptography) package).
|
||||
The hashing functions `SHA-256` and `SHA-512` are provided by the standard
|
||||
Python [hashlib](https://docs.python.org/3/library/hashlib.html). The `HKDF`,
|
||||
`HMAC`, `Fernet` primitives, and the `PKCS7` padding function are always
|
||||
provided by the following internal implementations:
|
||||
|
||||
- [HKDF.py](RNS/Cryptography/HKDF.py)
|
||||
- [HMAC.py](RNS/Cryptography/HMAC.py)
|
||||
- [Fernet.py](RNS/Cryptography/Fernet.py)
|
||||
- [PKCS7.py](RNS/Cryptography/PKCS7.py)
|
||||
|
||||
|
||||
Reticulum also includes a complete implementation of all necessary primitives
|
||||
in pure Python. If OpenSSL & PyCA are not available on the system when
|
||||
Reticulum is started, Reticulum will instead use the internal pure-python
|
||||
primitives. A trivial consequence of this is performance, with the OpenSSL
|
||||
backend being *much* faster. The most important consequence however, is the
|
||||
potential loss of security by using primitives that has not seen the same
|
||||
amount of scrutiny, testing and review as those from OpenSSL.
|
||||
|
||||
If you want to use the internal pure-python primitives, it is **highly
|
||||
advisable** that you have a good understanding of the risks that this pose, and
|
||||
make an informed decision on whether those risks are acceptable to you.
|
||||
|
||||
Reticulum is relatively young software, and should be considered as such. While
|
||||
it has been built with cryptography best-practices very foremost in mind, it
|
||||
_has not_ been externally security audited, and there could very well be
|
||||
privacy or security breaking bugs. If you want to help out, or help sponsor an
|
||||
audit, please do get in touch.
|
||||
|
||||
## Acknowledgements & Credits
|
||||
Reticulum can only exist because of the mountain of Open Source work it was
|
||||
built on top of, the contributions of everyone involved, and everyone that has
|
||||
supported the project through the years. To everyone who has helped, thank you
|
||||
so much.
|
||||
|
||||
A number of other modules and projects are either part of, or used by
|
||||
Reticulum. Sincere thanks to the authors and contributors of the following
|
||||
projects:
|
||||
|
||||
- [PyCA/cryptography](https://github.com/pyca/cryptography), *BSD License*
|
||||
- [Pure-25519](https://github.com/warner/python-pure25519) by [Brian Warner](https://github.com/warner), *MIT License*
|
||||
- [Pysha2](https://github.com/thomdixon/pysha2) by [Thom Dixon](https://github.com/thomdixon), *MIT License*
|
||||
- [Python-AES](https://github.com/orgurar/python-aes) by [Or Gur Arie](https://github.com/orgurar), *MIT License*
|
||||
- [Curve25519.py](https://gist.github.com/nickovs/cc3c22d15f239a2640c185035c06f8a3#file-curve25519-py) by [Nicko van Someren](https://gist.github.com/nickovs), *Public Domain*
|
||||
- [I2Plib](https://github.com/l-n-s/i2plib) by [Viktor Villainov](https://github.com/l-n-s)
|
||||
- [PySerial](https://github.com/pyserial/pyserial) by Chris Liechti, *BSD License*
|
||||
- [Netifaces](https://github.com/al45tair/netifaces) by [Alastair Houghton](https://github.com/al45tair), *MIT License*
|
||||
- [Configobj](https://github.com/DiffSK/configobj) by Michael Foord, Nicola Larosa, Rob Dennis & Eli Courtwright, *BSD License*
|
||||
- [Six](https://github.com/benjaminp/six) by [Benjamin Peterson](https://github.com/benjaminp), *MIT License*
|
||||
- [Umsgpack.py](https://github.com/vsergeev/u-msgpack-python) by [Ivan A. Sergeev](https://github.com/vsergeev)
|
||||
- [Python](https://www.python.org)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS.Cryptography.Provider as cp
|
||||
import RNS.vendor.platformutils as pu
|
||||
|
||||
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
|
||||
from .aes import AES
|
||||
|
||||
elif cp.PROVIDER == cp.PROVIDER_PYCA:
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
if pu.cryptography_old_api():
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
|
||||
class AES_128_CBC:
|
||||
|
||||
@staticmethod
|
||||
def encrypt(plaintext, key, iv):
|
||||
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
|
||||
cipher = AES(key)
|
||||
return cipher.encrypt(plaintext, iv)
|
||||
|
||||
elif cp.PROVIDER == cp.PROVIDER_PYCA:
|
||||
if not pu.cryptography_old_api():
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
||||
else:
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||
|
||||
encryptor = cipher.encryptor()
|
||||
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
||||
return ciphertext
|
||||
|
||||
@staticmethod
|
||||
def decrypt(ciphertext, key, iv):
|
||||
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
|
||||
cipher = AES(key)
|
||||
return cipher.decrypt(ciphertext, iv)
|
||||
|
||||
elif cp.PROVIDER == cp.PROVIDER_PYCA:
|
||||
if not pu.cryptography_old_api():
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
||||
else:
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||
|
||||
decryptor = cipher.decryptor()
|
||||
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
||||
return plaintext
|
||||
@@ -0,0 +1,41 @@
|
||||
import os
|
||||
from .pure25519 import ed25519_oop as ed25519
|
||||
|
||||
class Ed25519PrivateKey:
|
||||
def __init__(self, seed):
|
||||
self.seed = seed
|
||||
self.sk = ed25519.SigningKey(self.seed)
|
||||
#self.vk = self.sk.get_verifying_key()
|
||||
|
||||
@classmethod
|
||||
def generate(cls):
|
||||
return cls.from_private_bytes(os.urandom(32))
|
||||
|
||||
@classmethod
|
||||
def from_private_bytes(cls, data):
|
||||
return cls(seed=data)
|
||||
|
||||
def private_bytes(self):
|
||||
return self.seed
|
||||
|
||||
def public_key(self):
|
||||
return Ed25519PublicKey.from_public_bytes(self.sk.vk_s)
|
||||
|
||||
def sign(self, message):
|
||||
return self.sk.sign(message)
|
||||
|
||||
|
||||
class Ed25519PublicKey:
|
||||
def __init__(self, seed):
|
||||
self.seed = seed
|
||||
self.vk = ed25519.VerifyingKey(self.seed)
|
||||
|
||||
@classmethod
|
||||
def from_public_bytes(cls, data):
|
||||
return cls(data)
|
||||
|
||||
def public_bytes(self):
|
||||
return self.vk.to_bytes()
|
||||
|
||||
def verify(self, signature, message):
|
||||
self.vk.verify(signature, message)
|
||||
@@ -0,0 +1,110 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from RNS.Cryptography import HMAC
|
||||
from RNS.Cryptography import PKCS7
|
||||
from RNS.Cryptography.AES import AES_128_CBC
|
||||
|
||||
class Fernet():
|
||||
"""
|
||||
This class provides a slightly modified implementation of the Fernet spec
|
||||
found at: https://github.com/fernet/spec/blob/master/Spec.md
|
||||
|
||||
According to the spec, a Fernet token includes a one byte VERSION and
|
||||
eight byte TIMESTAMP field at the start of each token. These fields are
|
||||
not relevant to Reticulum. They are therefore stripped from this
|
||||
implementation, since they incur overhead and leak initiator metadata.
|
||||
"""
|
||||
FERNET_OVERHEAD = 48 # Bytes
|
||||
|
||||
@staticmethod
|
||||
def generate_key():
|
||||
return os.urandom(32)
|
||||
|
||||
def __init__(self, key = None):
|
||||
if key == None:
|
||||
raise ValueError("Fernet key cannot be None")
|
||||
|
||||
if len(key) != 32:
|
||||
raise ValueError("Fernet key must be 32 bytes, not "+str(len(key)))
|
||||
|
||||
self._signing_key = key[:16]
|
||||
self._encryption_key = key[16:]
|
||||
|
||||
|
||||
def verify_hmac(self, token):
|
||||
if len(token) <= 32:
|
||||
raise ValueError("Cannot verify HMAC on token of only "+str(len(token))+" bytes")
|
||||
else:
|
||||
received_hmac = token[-32:]
|
||||
expected_hmac = HMAC.new(self._signing_key, token[:-32]).digest()
|
||||
|
||||
if received_hmac == expected_hmac:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def encrypt(self, data = None):
|
||||
iv = os.urandom(16)
|
||||
current_time = int(time.time())
|
||||
|
||||
if not isinstance(data, bytes):
|
||||
raise TypeError("Fernet token plaintext input must be bytes")
|
||||
|
||||
ciphertext = AES_128_CBC.encrypt(
|
||||
plaintext = PKCS7.pad(data),
|
||||
key = self._encryption_key,
|
||||
iv = iv,
|
||||
)
|
||||
|
||||
signed_parts = iv+ciphertext
|
||||
|
||||
return signed_parts + HMAC.new(self._signing_key, signed_parts).digest()
|
||||
|
||||
|
||||
def decrypt(self, token = None):
|
||||
if not isinstance(token, bytes):
|
||||
raise TypeError("Fernet token must be bytes")
|
||||
|
||||
if not self.verify_hmac(token):
|
||||
raise ValueError("Fernet token HMAC was invalid")
|
||||
|
||||
iv = token[:16]
|
||||
ciphertext = token[16:-32]
|
||||
|
||||
try:
|
||||
plaintext = PKCS7.unpad(
|
||||
AES_128_CBC.decrypt(
|
||||
ciphertext,
|
||||
self._encryption_key,
|
||||
iv,
|
||||
)
|
||||
)
|
||||
|
||||
return plaintext
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError("Could not decrypt Fernet token")
|
||||
@@ -0,0 +1,57 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import hashlib
|
||||
from math import ceil
|
||||
from RNS.Cryptography import HMAC
|
||||
|
||||
def hkdf(length=None, derive_from=None, salt=None, context=None):
|
||||
hash_len = 32
|
||||
|
||||
def hmac_sha256(key, data):
|
||||
return HMAC.new(key, data).digest()
|
||||
|
||||
if length == None or length < 1:
|
||||
raise ValueError("Invalid output key length")
|
||||
|
||||
if derive_from == "None" or derive_from == "":
|
||||
raise ValueError("Cannot derive key from empty input material")
|
||||
|
||||
if salt == None or len(salt) == 0:
|
||||
salt = bytes([0] * hash_len)
|
||||
|
||||
if salt == None:
|
||||
salt = b""
|
||||
|
||||
if context == None:
|
||||
context = b""
|
||||
|
||||
pseudorandom_key = hmac_sha256(salt, derive_from)
|
||||
|
||||
block = b""
|
||||
derived = b""
|
||||
|
||||
for i in range(ceil(length / hash_len)):
|
||||
block = hmac_sha256(pseudorandom_key, block + context + bytes([i + 1]))
|
||||
derived += block
|
||||
|
||||
return derived[:length]
|
||||
@@ -0,0 +1,183 @@
|
||||
# This HMAC implementation comes directly from the HMAC implementation
|
||||
# included in Python 3.10.4, and is almost completely identical. It has
|
||||
# been modified to be a pure Python implementation, that is not dependent
|
||||
# on the system having OpenSSL binaries installed.
|
||||
|
||||
import warnings as _warnings
|
||||
import hashlib as _hashlib
|
||||
|
||||
trans_5C = bytes((x ^ 0x5C) for x in range(256))
|
||||
trans_36 = bytes((x ^ 0x36) for x in range(256))
|
||||
|
||||
# The size of the digests returned by HMAC depends on the underlying
|
||||
# hashing module used. Use digest_size from the instance of HMAC instead.
|
||||
digest_size = None
|
||||
|
||||
|
||||
class HMAC:
|
||||
"""RFC 2104 HMAC class. Also complies with RFC 4231.
|
||||
This supports the API for Cryptographic Hash Functions (PEP 247).
|
||||
"""
|
||||
blocksize = 64 # 512-bit HMAC; can be changed in subclasses.
|
||||
|
||||
__slots__ = (
|
||||
"_hmac", "_inner", "_outer", "block_size", "digest_size"
|
||||
)
|
||||
|
||||
def __init__(self, key, msg=None, digestmod=_hashlib.sha256):
|
||||
"""Create a new HMAC object.
|
||||
key: bytes or buffer, key for the keyed hash object.
|
||||
msg: bytes or buffer, Initial input for the hash or None.
|
||||
digestmod: A hash name suitable for hashlib.new(). *OR*
|
||||
A hashlib constructor returning a new hash object. *OR*
|
||||
A module supporting PEP 247.
|
||||
Required as of 3.8, despite its position after the optional
|
||||
msg argument. Passing it as a keyword argument is
|
||||
recommended, though not required for legacy API reasons.
|
||||
"""
|
||||
|
||||
if not isinstance(key, (bytes, bytearray)):
|
||||
raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__)
|
||||
|
||||
if not digestmod:
|
||||
raise TypeError("Missing required parameter 'digestmod'.")
|
||||
|
||||
self._hmac_init(key, msg, digestmod)
|
||||
|
||||
def _hmac_init(self, key, msg, digestmod):
|
||||
if callable(digestmod):
|
||||
digest_cons = digestmod
|
||||
elif isinstance(digestmod, str):
|
||||
digest_cons = lambda d=b'': _hashlib.new(digestmod, d)
|
||||
else:
|
||||
digest_cons = lambda d=b'': digestmod.new(d)
|
||||
|
||||
self._hmac = None
|
||||
self._outer = digest_cons()
|
||||
self._inner = digest_cons()
|
||||
self.digest_size = self._inner.digest_size
|
||||
|
||||
if hasattr(self._inner, 'block_size'):
|
||||
blocksize = self._inner.block_size
|
||||
if blocksize < 16:
|
||||
_warnings.warn('block_size of %d seems too small; using our '
|
||||
'default of %d.' % (blocksize, self.blocksize),
|
||||
RuntimeWarning, 2)
|
||||
blocksize = self.blocksize
|
||||
else:
|
||||
_warnings.warn('No block_size attribute on given digest object; '
|
||||
'Assuming %d.' % (self.blocksize),
|
||||
RuntimeWarning, 2)
|
||||
blocksize = self.blocksize
|
||||
|
||||
if len(key) > blocksize:
|
||||
key = digest_cons(key).digest()
|
||||
|
||||
# self.blocksize is the default blocksize. self.block_size is
|
||||
# effective block size as well as the public API attribute.
|
||||
self.block_size = blocksize
|
||||
|
||||
key = key.ljust(blocksize, b'\0')
|
||||
self._outer.update(key.translate(trans_5C))
|
||||
self._inner.update(key.translate(trans_36))
|
||||
if msg is not None:
|
||||
self.update(msg)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self._hmac:
|
||||
return self._hmac.name
|
||||
else:
|
||||
return f"hmac-{self._inner.name}"
|
||||
|
||||
def update(self, msg):
|
||||
"""Feed data from msg into this hashing object."""
|
||||
inst = self._hmac or self._inner
|
||||
inst.update(msg)
|
||||
|
||||
def copy(self):
|
||||
"""Return a separate copy of this hashing object.
|
||||
An update to this copy won't affect the original object.
|
||||
"""
|
||||
# Call __new__ directly to avoid the expensive __init__.
|
||||
other = self.__class__.__new__(self.__class__)
|
||||
other.digest_size = self.digest_size
|
||||
if self._hmac:
|
||||
other._hmac = self._hmac.copy()
|
||||
other._inner = other._outer = None
|
||||
else:
|
||||
other._hmac = None
|
||||
other._inner = self._inner.copy()
|
||||
other._outer = self._outer.copy()
|
||||
return other
|
||||
|
||||
def _current(self):
|
||||
"""Return a hash object for the current state.
|
||||
To be used only internally with digest() and hexdigest().
|
||||
"""
|
||||
if self._hmac:
|
||||
return self._hmac
|
||||
else:
|
||||
h = self._outer.copy()
|
||||
h.update(self._inner.digest())
|
||||
return h
|
||||
|
||||
def digest(self):
|
||||
"""Return the hash value of this hashing object.
|
||||
This returns the hmac value as bytes. The object is
|
||||
not altered in any way by this function; you can continue
|
||||
updating the object after calling this function.
|
||||
"""
|
||||
h = self._current()
|
||||
return h.digest()
|
||||
|
||||
def hexdigest(self):
|
||||
"""Like digest(), but returns a string of hexadecimal digits instead.
|
||||
"""
|
||||
h = self._current()
|
||||
return h.hexdigest()
|
||||
|
||||
def new(key, msg=None, digestmod=_hashlib.sha256):
|
||||
"""Create a new hashing object and return it.
|
||||
key: bytes or buffer, The starting key for the hash.
|
||||
msg: bytes or buffer, Initial input for the hash, or None.
|
||||
digestmod: A hash name suitable for hashlib.new(). *OR*
|
||||
A hashlib constructor returning a new hash object. *OR*
|
||||
A module supporting PEP 247.
|
||||
Required as of 3.8, despite its position after the optional
|
||||
msg argument. Passing it as a keyword argument is
|
||||
recommended, though not required for legacy API reasons.
|
||||
You can now feed arbitrary bytes into the object using its update()
|
||||
method, and can ask for the hash value at any time by calling its digest()
|
||||
or hexdigest() methods.
|
||||
"""
|
||||
return HMAC(key, msg, digestmod)
|
||||
|
||||
|
||||
def digest(key, msg, digest):
|
||||
"""Fast inline implementation of HMAC.
|
||||
key: bytes or buffer, The key for the keyed hash object.
|
||||
msg: bytes or buffer, Input message.
|
||||
digest: A hash name suitable for hashlib.new() for best performance. *OR*
|
||||
A hashlib constructor returning a new hash object. *OR*
|
||||
A module supporting PEP 247.
|
||||
"""
|
||||
if callable(digest):
|
||||
digest_cons = digest
|
||||
elif isinstance(digest, str):
|
||||
digest_cons = lambda d=b'': _hashlib.new(digest, d)
|
||||
else:
|
||||
digest_cons = lambda d=b'': digest.new(d)
|
||||
|
||||
inner = digest_cons()
|
||||
outer = digest_cons()
|
||||
blocksize = getattr(inner, 'block_size', 64)
|
||||
if len(key) > blocksize:
|
||||
key = digest_cons(key).digest()
|
||||
|
||||
key = key + b'\x00' * (blocksize - len(key))
|
||||
inner.update(key.translate(trans_36))
|
||||
outer.update(key.translate(trans_5C))
|
||||
inner.update(msg)
|
||||
outer.update(inner.digest())
|
||||
return outer.digest()
|
||||
@@ -0,0 +1,34 @@
|
||||
import importlib
|
||||
if importlib.util.find_spec('hashlib') != None:
|
||||
import hashlib
|
||||
else:
|
||||
hashlib = None
|
||||
|
||||
if hasattr(hashlib, "sha512"):
|
||||
from hashlib import sha512 as ext_sha512
|
||||
else:
|
||||
from .SHA512 import sha512 as ext_sha512
|
||||
|
||||
if hasattr(hashlib, "sha256"):
|
||||
from hashlib import sha256 as ext_sha256
|
||||
else:
|
||||
from .SHA256 import sha256 as ext_sha256
|
||||
|
||||
"""
|
||||
The SHA primitives are abstracted here to allow platform-
|
||||
aware hardware acceleration in the future. Currently only
|
||||
uses Python's internal SHA-256 implementation. All SHA-256
|
||||
calls in RNS end up here.
|
||||
"""
|
||||
|
||||
def sha256(data):
|
||||
digest = ext_sha256()
|
||||
digest.update(data)
|
||||
|
||||
return digest.digest()
|
||||
|
||||
def sha512(data):
|
||||
digest = ext_sha512()
|
||||
digest.update(data)
|
||||
|
||||
return digest.digest()
|
||||
@@ -0,0 +1,40 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
class PKCS7:
|
||||
BLOCKSIZE = 16
|
||||
|
||||
@staticmethod
|
||||
def pad(data, bs=BLOCKSIZE):
|
||||
l = len(data)
|
||||
n = bs-l%bs
|
||||
v = bytes([n])
|
||||
return data+v*n
|
||||
|
||||
@staticmethod
|
||||
def unpad(data, bs=BLOCKSIZE):
|
||||
l = len(data)
|
||||
n = data[-1]
|
||||
if n > bs:
|
||||
raise ValueError("Cannot unpad, invalid padding length of "+str(n)+" bytes")
|
||||
else:
|
||||
return data[:l-n]
|
||||
@@ -0,0 +1,38 @@
|
||||
import importlib
|
||||
|
||||
PROVIDER_NONE = 0x00
|
||||
PROVIDER_INTERNAL = 0x01
|
||||
PROVIDER_PYCA = 0x02
|
||||
|
||||
PROVIDER = PROVIDER_NONE
|
||||
|
||||
pyca_v = None
|
||||
use_pyca = False
|
||||
|
||||
try:
|
||||
if importlib.util.find_spec('cryptography') != None:
|
||||
import cryptography
|
||||
pyca_v = cryptography.__version__
|
||||
v = pyca_v.split(".")
|
||||
|
||||
if int(v[0]) == 2:
|
||||
if int(v[1]) >= 8:
|
||||
use_pyca = True
|
||||
elif int(v[0]) >= 3:
|
||||
use_pyca = True
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if use_pyca:
|
||||
PROVIDER = PROVIDER_PYCA
|
||||
else:
|
||||
PROVIDER = PROVIDER_INTERNAL
|
||||
|
||||
def backend():
|
||||
if PROVIDER == PROVIDER_NONE:
|
||||
return "none"
|
||||
elif PROVIDER == PROVIDER_INTERNAL:
|
||||
return "internal"
|
||||
elif PROVIDER == PROVIDER_PYCA:
|
||||
return "openssl, PyCA "+str(pyca_v)
|
||||
@@ -0,0 +1,90 @@
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
||||
|
||||
# These proxy classes exist to create a uniform API accross
|
||||
# cryptography primitive providers.
|
||||
|
||||
class X25519PrivateKeyProxy:
|
||||
def __init__(self, real):
|
||||
self.real = real
|
||||
|
||||
@classmethod
|
||||
def generate(cls):
|
||||
return cls(X25519PrivateKey.generate())
|
||||
|
||||
@classmethod
|
||||
def from_private_bytes(cls, data):
|
||||
return cls(X25519PrivateKey.from_private_bytes(data))
|
||||
|
||||
def private_bytes(self):
|
||||
return self.real.private_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PrivateFormat.Raw,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
|
||||
def public_key(self):
|
||||
return X25519PublicKeyProxy(self.real.public_key())
|
||||
|
||||
def exchange(self, peer_public_key):
|
||||
return self.real.exchange(peer_public_key.real)
|
||||
|
||||
|
||||
class X25519PublicKeyProxy:
|
||||
def __init__(self, real):
|
||||
self.real = real
|
||||
|
||||
@classmethod
|
||||
def from_public_bytes(cls, data):
|
||||
return cls(X25519PublicKey.from_public_bytes(data))
|
||||
|
||||
def public_bytes(self):
|
||||
return self.real.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
|
||||
|
||||
class Ed25519PrivateKeyProxy:
|
||||
def __init__(self, real):
|
||||
self.real = real
|
||||
|
||||
@classmethod
|
||||
def generate(cls):
|
||||
return cls(Ed25519PrivateKey.generate())
|
||||
|
||||
@classmethod
|
||||
def from_private_bytes(cls, data):
|
||||
return cls(Ed25519PrivateKey.from_private_bytes(data))
|
||||
|
||||
def private_bytes(self):
|
||||
return self.real.private_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PrivateFormat.Raw,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
|
||||
def public_key(self):
|
||||
return Ed25519PublicKeyProxy(self.real.public_key())
|
||||
|
||||
def sign(self, message):
|
||||
return self.real.sign(message)
|
||||
|
||||
|
||||
class Ed25519PublicKeyProxy:
|
||||
def __init__(self, real):
|
||||
self.real = real
|
||||
|
||||
@classmethod
|
||||
def from_public_bytes(cls, data):
|
||||
return cls(Ed25519PublicKey.from_public_bytes(data))
|
||||
|
||||
def public_bytes(self):
|
||||
return self.real.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
|
||||
def verify(self, signature, message):
|
||||
self.real.verify(signature, message)
|
||||
@@ -0,0 +1,129 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2017 Thomas Dixon
|
||||
#
|
||||
# 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 copy
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def new(m=None):
|
||||
return sha256(m)
|
||||
|
||||
class sha256(object):
|
||||
_k = (0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
||||
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
||||
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
||||
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
||||
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
||||
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
||||
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
||||
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
||||
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2)
|
||||
_h = (0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
||||
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19)
|
||||
_output_size = 8
|
||||
|
||||
blocksize = 1
|
||||
block_size = 64
|
||||
digest_size = 32
|
||||
|
||||
def __init__(self, m=None):
|
||||
self._buffer = b""
|
||||
self._counter = 0
|
||||
|
||||
if m is not None:
|
||||
if type(m) is not bytes:
|
||||
raise TypeError('%s() argument 1 must be bytes, not %s' % (self.__class__.__name__, type(m).__name__))
|
||||
self.update(m)
|
||||
|
||||
def _rotr(self, x, y):
|
||||
return ((x >> y) | (x << (32-y))) & 0xFFFFFFFF
|
||||
|
||||
def _sha256_process(self, c):
|
||||
w = [0]*64
|
||||
w[0:16] = struct.unpack('!16L', c)
|
||||
|
||||
for i in range(16, 64):
|
||||
s0 = self._rotr(w[i-15], 7) ^ self._rotr(w[i-15], 18) ^ (w[i-15] >> 3)
|
||||
s1 = self._rotr(w[i-2], 17) ^ self._rotr(w[i-2], 19) ^ (w[i-2] >> 10)
|
||||
w[i] = (w[i-16] + s0 + w[i-7] + s1) & 0xFFFFFFFF
|
||||
|
||||
a,b,c,d,e,f,g,h = self._h
|
||||
|
||||
for i in range(64):
|
||||
s0 = self._rotr(a, 2) ^ self._rotr(a, 13) ^ self._rotr(a, 22)
|
||||
maj = (a & b) ^ (a & c) ^ (b & c)
|
||||
t2 = s0 + maj
|
||||
s1 = self._rotr(e, 6) ^ self._rotr(e, 11) ^ self._rotr(e, 25)
|
||||
ch = (e & f) ^ ((~e) & g)
|
||||
t1 = h + s1 + ch + self._k[i] + w[i]
|
||||
|
||||
h = g
|
||||
g = f
|
||||
f = e
|
||||
e = (d + t1) & 0xFFFFFFFF
|
||||
d = c
|
||||
c = b
|
||||
b = a
|
||||
a = (t1 + t2) & 0xFFFFFFFF
|
||||
|
||||
self._h = [(x+y) & 0xFFFFFFFF for x,y in zip(self._h, [a,b,c,d,e,f,g,h])]
|
||||
|
||||
def update(self, m):
|
||||
if not m:
|
||||
return
|
||||
|
||||
if type(m) is not bytes:
|
||||
raise TypeError('%s() argument 1 must be bytes, not %s' % (sys._getframe().f_code.co_name, type(m).__name__))
|
||||
|
||||
self._buffer += m
|
||||
self._counter += len(m)
|
||||
|
||||
while len(self._buffer) >= 64:
|
||||
self._sha256_process(self._buffer[:64])
|
||||
self._buffer = self._buffer[64:]
|
||||
|
||||
def digest(self):
|
||||
mdi = self._counter & 0x3F
|
||||
length = struct.pack('!Q', self._counter<<3)
|
||||
|
||||
if mdi < 56:
|
||||
padlen = 55-mdi
|
||||
else:
|
||||
padlen = 119-mdi
|
||||
|
||||
r = self.copy()
|
||||
r.update(b'\x80'+(b'\x00'*padlen)+length)
|
||||
return b''.join([struct.pack('!L', i) for i in r._h[:self._output_size]])
|
||||
|
||||
def hexdigest(self):
|
||||
return self.digest().encode('hex')
|
||||
|
||||
def copy(self):
|
||||
return copy.deepcopy(self)
|
||||
@@ -0,0 +1,129 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2017 Thomas Dixon
|
||||
#
|
||||
# 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 copy, struct, sys
|
||||
|
||||
def new(m=None):
|
||||
return sha512(m)
|
||||
|
||||
class sha512(object):
|
||||
_k = (0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc,
|
||||
0x3956c25bf348b538, 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118,
|
||||
0xd807aa98a3030242, 0x12835b0145706fbe, 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2,
|
||||
0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, 0xc19bf174cf692694,
|
||||
0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65,
|
||||
0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5,
|
||||
0x983e5152ee66dfab, 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4,
|
||||
0xc6e00bf33da88fc2, 0xd5a79147930aa725, 0x06ca6351e003826f, 0x142929670a0e6e70,
|
||||
0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, 0x53380d139d95b3df,
|
||||
0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b,
|
||||
0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30,
|
||||
0xd192e819d6ef5218, 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8,
|
||||
0x19a4c116b8d2d0c8, 0x1e376c085141ab53, 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8,
|
||||
0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, 0x682e6ff3d6b2b8a3,
|
||||
0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec,
|
||||
0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b,
|
||||
0xca273eceea26619c, 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178,
|
||||
0x06f067aa72176fba, 0x0a637dc5a2c898a6, 0x113f9804bef90dae, 0x1b710b35131c471b,
|
||||
0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, 0x431d67c49c100d4c,
|
||||
0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817)
|
||||
_h = (0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1,
|
||||
0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179)
|
||||
_output_size = 8
|
||||
|
||||
blocksize = 1
|
||||
block_size = 128
|
||||
digest_size = 64
|
||||
|
||||
def __init__(self, m=None):
|
||||
self._buffer = b''
|
||||
self._counter = 0
|
||||
|
||||
if m is not None:
|
||||
if type(m) is not bytes:
|
||||
raise TypeError('%s() argument 1 must be bytes, not %s' % (self.__class__.__name__, type(m).__name__))
|
||||
self.update(m)
|
||||
|
||||
def _rotr(self, x, y):
|
||||
return ((x >> y) | (x << (64-y))) & 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
def _sha512_process(self, chunk):
|
||||
w = [0]*80
|
||||
w[0:16] = struct.unpack('!16Q', chunk)
|
||||
|
||||
for i in range(16, 80):
|
||||
s0 = self._rotr(w[i-15], 1) ^ self._rotr(w[i-15], 8) ^ (w[i-15] >> 7)
|
||||
s1 = self._rotr(w[i-2], 19) ^ self._rotr(w[i-2], 61) ^ (w[i-2] >> 6)
|
||||
w[i] = (w[i-16] + s0 + w[i-7] + s1) & 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
a,b,c,d,e,f,g,h = self._h
|
||||
|
||||
for i in range(80):
|
||||
s0 = self._rotr(a, 28) ^ self._rotr(a, 34) ^ self._rotr(a, 39)
|
||||
maj = (a & b) ^ (a & c) ^ (b & c)
|
||||
t2 = s0 + maj
|
||||
s1 = self._rotr(e, 14) ^ self._rotr(e, 18) ^ self._rotr(e, 41)
|
||||
ch = (e & f) ^ ((~e) & g)
|
||||
t1 = h + s1 + ch + self._k[i] + w[i]
|
||||
|
||||
h = g
|
||||
g = f
|
||||
f = e
|
||||
e = (d + t1) & 0xFFFFFFFFFFFFFFFF
|
||||
d = c
|
||||
c = b
|
||||
b = a
|
||||
a = (t1 + t2) & 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
self._h = [(x+y) & 0xFFFFFFFFFFFFFFFF for x,y in zip(self._h, [a,b,c,d,e,f,g,h])]
|
||||
|
||||
def update(self, m):
|
||||
if not m:
|
||||
return
|
||||
if type(m) is not bytes:
|
||||
raise TypeError('%s() argument 1 must be bytes, not %s' % (sys._getframe().f_code.co_name, type(m).__name__))
|
||||
|
||||
self._buffer += m
|
||||
self._counter += len(m)
|
||||
|
||||
while len(self._buffer) >= 128:
|
||||
self._sha512_process(self._buffer[:128])
|
||||
self._buffer = self._buffer[128:]
|
||||
|
||||
def digest(self):
|
||||
mdi = self._counter & 0x7F
|
||||
length = struct.pack('!Q', self._counter<<3)
|
||||
|
||||
if mdi < 112:
|
||||
padlen = 111-mdi
|
||||
else:
|
||||
padlen = 239-mdi
|
||||
|
||||
r = self.copy()
|
||||
r.update(b'\x80'+(b'\x00'*(padlen+8))+length)
|
||||
return b''.join([struct.pack('!Q', i) for i in r._h[:self._output_size]])
|
||||
|
||||
def hexdigest(self):
|
||||
return self.digest().encode('hex')
|
||||
|
||||
def copy(self):
|
||||
return copy.deepcopy(self)
|
||||
@@ -0,0 +1,171 @@
|
||||
# By Nicko van Someren, 2021. This code is released into the public domain.
|
||||
# Small modifications for use in Reticulum, and constant time key exchange
|
||||
# added by Mark Qvist in 2022.
|
||||
|
||||
# WARNING! Only the X25519PrivateKey.exchange() method attempts to hide execution time.
|
||||
# In the context of Reticulum, this is sufficient, but it may not be in other systems. If
|
||||
# this code is to be used to provide cryptographic security in an environment where the
|
||||
# start and end times of the execution can be guessed, inferred or measured then it is
|
||||
# critical that steps are taken to hide the execution time, for instance by adding a
|
||||
# delay so that encrypted packets are not sent until a fixed time after the _start_ of
|
||||
# execution.
|
||||
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
P = 2 ** 255 - 19
|
||||
_A = 486662
|
||||
|
||||
|
||||
def _point_add(point_n, point_m, point_diff):
|
||||
"""Given the projection of two points and their difference, return their sum"""
|
||||
(xn, zn) = point_n
|
||||
(xm, zm) = point_m
|
||||
(x_diff, z_diff) = point_diff
|
||||
x = (z_diff << 2) * (xm * xn - zm * zn) ** 2
|
||||
z = (x_diff << 2) * (xm * zn - zm * xn) ** 2
|
||||
return x % P, z % P
|
||||
|
||||
|
||||
def _point_double(point_n):
|
||||
"""Double a point provided in projective coordinates"""
|
||||
(xn, zn) = point_n
|
||||
xn2 = xn ** 2
|
||||
zn2 = zn ** 2
|
||||
x = (xn2 - zn2) ** 2
|
||||
xzn = xn * zn
|
||||
z = 4 * xzn * (xn2 + _A * xzn + zn2)
|
||||
return x % P, z % P
|
||||
|
||||
|
||||
def _const_time_swap(a, b, swap):
|
||||
"""Swap two values in constant time"""
|
||||
index = int(swap) * 2
|
||||
temp = (a, b, b, a)
|
||||
return temp[index:index+2]
|
||||
|
||||
|
||||
def _raw_curve25519(base, n):
|
||||
"""Raise the point base to the power n"""
|
||||
zero = (1, 0)
|
||||
one = (base, 1)
|
||||
mP, m1P = zero, one
|
||||
|
||||
for i in reversed(range(256)):
|
||||
bit = bool(n & (1 << i))
|
||||
mP, m1P = _const_time_swap(mP, m1P, bit)
|
||||
mP, m1P = _point_double(mP), _point_add(mP, m1P, one)
|
||||
mP, m1P = _const_time_swap(mP, m1P, bit)
|
||||
|
||||
x, z = mP
|
||||
inv_z = pow(z, P - 2, P)
|
||||
return (x * inv_z) % P
|
||||
|
||||
|
||||
def _unpack_number(s):
|
||||
"""Unpack 32 bytes to a 256 bit value"""
|
||||
if len(s) != 32:
|
||||
raise ValueError('Curve25519 values must be 32 bytes')
|
||||
return int.from_bytes(s, "little")
|
||||
|
||||
|
||||
def _pack_number(n):
|
||||
"""Pack a value into 32 bytes"""
|
||||
return n.to_bytes(32, "little")
|
||||
|
||||
|
||||
def _fix_secret(n):
|
||||
"""Mask a value to be an acceptable exponent"""
|
||||
n &= ~7
|
||||
n &= ~(128 << 8 * 31)
|
||||
n |= 64 << 8 * 31
|
||||
return n
|
||||
|
||||
|
||||
def curve25519(base_point_raw, secret_raw):
|
||||
"""Raise the base point to a given power"""
|
||||
base_point = _unpack_number(base_point_raw)
|
||||
secret = _fix_secret(_unpack_number(secret_raw))
|
||||
return _pack_number(_raw_curve25519(base_point, secret))
|
||||
|
||||
|
||||
def curve25519_base(secret_raw):
|
||||
"""Raise the generator point to a given power"""
|
||||
secret = _fix_secret(_unpack_number(secret_raw))
|
||||
return _pack_number(_raw_curve25519(9, secret))
|
||||
|
||||
|
||||
class X25519PublicKey:
|
||||
def __init__(self, x):
|
||||
self.x = x
|
||||
|
||||
@classmethod
|
||||
def from_public_bytes(cls, data):
|
||||
return cls(_unpack_number(data))
|
||||
|
||||
def public_bytes(self):
|
||||
return _pack_number(self.x)
|
||||
|
||||
|
||||
class X25519PrivateKey:
|
||||
MIN_EXEC_TIME = 0.002
|
||||
MAX_EXEC_TIME = 0.5
|
||||
DELAY_WINDOW = 10
|
||||
|
||||
T_CLEAR = None
|
||||
T_MAX = 0
|
||||
|
||||
def __init__(self, a):
|
||||
self.a = a
|
||||
|
||||
@classmethod
|
||||
def generate(cls):
|
||||
return cls.from_private_bytes(os.urandom(32))
|
||||
|
||||
@classmethod
|
||||
def from_private_bytes(cls, data):
|
||||
return cls(_fix_secret(_unpack_number(data)))
|
||||
|
||||
def private_bytes(self):
|
||||
return _pack_number(self.a)
|
||||
|
||||
def public_key(self):
|
||||
return X25519PublicKey.from_public_bytes(_pack_number(_raw_curve25519(9, self.a)))
|
||||
|
||||
def exchange(self, peer_public_key):
|
||||
if isinstance(peer_public_key, bytes):
|
||||
peer_public_key = X25519PublicKey.from_public_bytes(peer_public_key)
|
||||
|
||||
start = time.time()
|
||||
|
||||
shared = _pack_number(_raw_curve25519(peer_public_key.x, self.a))
|
||||
|
||||
end = time.time()
|
||||
duration = end-start
|
||||
|
||||
if X25519PrivateKey.T_CLEAR == None:
|
||||
X25519PrivateKey.T_CLEAR = end + X25519PrivateKey.DELAY_WINDOW
|
||||
|
||||
if end > X25519PrivateKey.T_CLEAR:
|
||||
X25519PrivateKey.T_CLEAR = end + X25519PrivateKey.DELAY_WINDOW
|
||||
X25519PrivateKey.T_MAX = 0
|
||||
|
||||
if duration < X25519PrivateKey.T_MAX or duration < X25519PrivateKey.MIN_EXEC_TIME:
|
||||
target = start+X25519PrivateKey.T_MAX
|
||||
|
||||
if target > start+X25519PrivateKey.MAX_EXEC_TIME:
|
||||
target = start+X25519PrivateKey.MAX_EXEC_TIME
|
||||
|
||||
if target < start+X25519PrivateKey.MIN_EXEC_TIME:
|
||||
target = start+X25519PrivateKey.MIN_EXEC_TIME
|
||||
|
||||
try:
|
||||
time.sleep(target-time.time())
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
elif duration > X25519PrivateKey.T_MAX:
|
||||
X25519PrivateKey.T_MAX = duration
|
||||
|
||||
return shared
|
||||
@@ -0,0 +1,24 @@
|
||||
import os
|
||||
import glob
|
||||
|
||||
from .Hashes import sha256
|
||||
from .Hashes import sha512
|
||||
from .HKDF import hkdf
|
||||
from .PKCS7 import PKCS7
|
||||
from .Fernet import Fernet
|
||||
from .Provider import backend
|
||||
|
||||
import RNS.Cryptography.Provider as cp
|
||||
|
||||
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
|
||||
from RNS.Cryptography.X25519 import X25519PrivateKey, X25519PublicKey
|
||||
from RNS.Cryptography.Ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
|
||||
elif cp.PROVIDER == cp.PROVIDER_PYCA:
|
||||
from RNS.Cryptography.Proxies import X25519PrivateKeyProxy as X25519PrivateKey
|
||||
from RNS.Cryptography.Proxies import X25519PublicKeyProxy as X25519PublicKey
|
||||
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')]
|
||||
@@ -0,0 +1 @@
|
||||
from .aes import AES
|
||||
@@ -0,0 +1,271 @@
|
||||
# MIT License
|
||||
|
||||
# Copyright (c) 2021 Or Gur Arie
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .utils import *
|
||||
|
||||
|
||||
class AES:
|
||||
# AES-128 block size
|
||||
block_size = 16
|
||||
# AES-128 encrypts messages with 10 rounds
|
||||
_rounds = 10
|
||||
|
||||
|
||||
# initiate the AES objecy
|
||||
def __init__(self, key):
|
||||
"""
|
||||
Initializes the object with a given key.
|
||||
"""
|
||||
# make sure key length is right
|
||||
assert len(key) == AES.block_size
|
||||
|
||||
# ExpandKey
|
||||
self._round_keys = self._expand_key(key)
|
||||
|
||||
|
||||
# will perform the AES ExpandKey phase
|
||||
def _expand_key(self, master_key):
|
||||
"""
|
||||
Expands and returns a list of key matrices for the given master_key.
|
||||
"""
|
||||
|
||||
# Initialize round keys with raw key material.
|
||||
key_columns = bytes2matrix(master_key)
|
||||
iteration_size = len(master_key) // 4
|
||||
|
||||
# Each iteration has exactly as many columns as the key material.
|
||||
i = 1
|
||||
while len(key_columns) < (self._rounds + 1) * 4:
|
||||
# Copy previous word.
|
||||
word = list(key_columns[-1])
|
||||
|
||||
# Perform schedule_core once every "row".
|
||||
if len(key_columns) % iteration_size == 0:
|
||||
# Circular shift.
|
||||
word.append(word.pop(0))
|
||||
# Map to S-BOX.
|
||||
word = [s_box[b] for b in word]
|
||||
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
|
||||
word[0] ^= r_con[i]
|
||||
i += 1
|
||||
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
|
||||
# Run word through S-box in the fourth iteration when using a
|
||||
# 256-bit key.
|
||||
word = [s_box[b] for b in word]
|
||||
|
||||
# XOR with equivalent word from previous iteration.
|
||||
word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size]))
|
||||
key_columns.append(word)
|
||||
|
||||
# Group key words in 4x4 byte matrices.
|
||||
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
|
||||
|
||||
|
||||
# encrypt a single block of data with AES
|
||||
def _encrypt_block(self, plaintext):
|
||||
"""
|
||||
Encrypts a single block of 16 byte long plaintext.
|
||||
"""
|
||||
# length of a single block
|
||||
assert len(plaintext) == AES.block_size
|
||||
|
||||
# perform on a matrix
|
||||
state = bytes2matrix(plaintext)
|
||||
|
||||
# AddRoundKey
|
||||
add_round_key(state, self._round_keys[0])
|
||||
|
||||
# 9 main rounds
|
||||
for i in range(1, self._rounds):
|
||||
# SubBytes
|
||||
sub_bytes(state)
|
||||
# ShiftRows
|
||||
shift_rows(state)
|
||||
# MixCols
|
||||
mix_columns(state)
|
||||
# AddRoundKey
|
||||
add_round_key(state, self._round_keys[i])
|
||||
|
||||
# last round, w/t AddRoundKey step
|
||||
sub_bytes(state)
|
||||
shift_rows(state)
|
||||
add_round_key(state, self._round_keys[-1])
|
||||
|
||||
# return the encrypted matrix as bytes
|
||||
return matrix2bytes(state)
|
||||
|
||||
|
||||
# decrypt a single block of data with AES
|
||||
def _decrypt_block(self, ciphertext):
|
||||
"""
|
||||
Decrypts a single block of 16 byte long ciphertext.
|
||||
"""
|
||||
# length of a single block
|
||||
assert len(ciphertext) == AES.block_size
|
||||
|
||||
# perform on a matrix
|
||||
state = bytes2matrix(ciphertext)
|
||||
|
||||
# in reverse order, last round is first
|
||||
add_round_key(state, self._round_keys[-1])
|
||||
inv_shift_rows(state)
|
||||
inv_sub_bytes(state)
|
||||
|
||||
for i in range(self._rounds - 1, 0, -1):
|
||||
# nain rounds
|
||||
add_round_key(state, self._round_keys[i])
|
||||
inv_mix_columns(state)
|
||||
inv_shift_rows(state)
|
||||
inv_sub_bytes(state)
|
||||
|
||||
# initial AddRoundKey phase
|
||||
add_round_key(state, self._round_keys[0])
|
||||
|
||||
# return bytes
|
||||
return matrix2bytes(state)
|
||||
|
||||
|
||||
# will encrypt the entire data
|
||||
def encrypt(self, plaintext, iv):
|
||||
"""
|
||||
Encrypts `plaintext` using CBC mode and PKCS#7 padding, with the given
|
||||
initialization vector (iv).
|
||||
"""
|
||||
# iv length must be same as block size
|
||||
assert len(iv) == AES.block_size
|
||||
|
||||
assert len(plaintext) % AES.block_size == 0
|
||||
|
||||
ciphertext_blocks = []
|
||||
|
||||
previous = iv
|
||||
for plaintext_block in split_blocks(plaintext):
|
||||
# in CBC mode every block is XOR'd with the previous block
|
||||
xorred = xor_bytes(plaintext_block, previous)
|
||||
|
||||
# encrypt current block
|
||||
block = self._encrypt_block(xorred)
|
||||
previous = block
|
||||
|
||||
# append to ciphertext
|
||||
ciphertext_blocks.append(block)
|
||||
|
||||
# return as bytes
|
||||
return b''.join(ciphertext_blocks)
|
||||
|
||||
|
||||
# will decrypt the entire data
|
||||
def decrypt(self, ciphertext, iv):
|
||||
"""
|
||||
Decrypts `ciphertext` using CBC mode and PKCS#7 padding, with the given
|
||||
initialization vector (iv).
|
||||
"""
|
||||
# iv length must be same as block size
|
||||
assert len(iv) == AES.block_size
|
||||
|
||||
plaintext_blocks = []
|
||||
|
||||
previous = iv
|
||||
for ciphertext_block in split_blocks(ciphertext):
|
||||
# in CBC mode every block is XOR'd with the previous block
|
||||
xorred = xor_bytes(previous, self._decrypt_block(ciphertext_block))
|
||||
|
||||
# append plaintext
|
||||
plaintext_blocks.append(xorred)
|
||||
previous = ciphertext_block
|
||||
|
||||
return b''.join(plaintext_blocks)
|
||||
|
||||
|
||||
def test():
|
||||
# modules and classes requiered for test only
|
||||
import os
|
||||
class bcolors:
|
||||
OK = '\033[92m' #GREEN
|
||||
WARNING = '\033[93m' #YELLOW
|
||||
FAIL = '\033[91m' #RED
|
||||
RESET = '\033[0m' #RESET COLOR
|
||||
|
||||
# will test AES class by performing an encryption / decryption
|
||||
print("AES Tests")
|
||||
print("=========")
|
||||
|
||||
# generate a secret key and print details
|
||||
key = os.urandom(AES.block_size)
|
||||
_aes = AES(key)
|
||||
print(f"Algorithm: AES-CBC-{AES.block_size*8}")
|
||||
print(f"Secret Key: {key.hex()}")
|
||||
print()
|
||||
|
||||
# test single block encryption / decryption
|
||||
iv = os.urandom(AES.block_size)
|
||||
|
||||
single_block_text = b"SingleBlock Text"
|
||||
print("Single Block Tests")
|
||||
print("------------------")
|
||||
print(f"iv: {iv.hex()}")
|
||||
|
||||
print(f"plain text: '{single_block_text.decode()}'")
|
||||
ciphertext_block = _aes._encrypt_block(single_block_text)
|
||||
plaintext_block = _aes._decrypt_block(ciphertext_block)
|
||||
print(f"Ciphertext Hex: {ciphertext_block.hex()}")
|
||||
print(f"Plaintext: {plaintext_block.decode()}")
|
||||
assert plaintext_block == single_block_text
|
||||
print(bcolors.OK + "Single Block Test Passed Successfully" + bcolors.RESET)
|
||||
print()
|
||||
|
||||
# test a less than a block length phrase
|
||||
iv = os.urandom(AES.block_size)
|
||||
|
||||
short_text = b"Just Text"
|
||||
print("Short Text Tests")
|
||||
print("----------------")
|
||||
print(f"iv: {iv.hex()}")
|
||||
print(f"plain text: '{short_text.decode()}'")
|
||||
ciphertext_short = _aes.encrypt(short_text, iv)
|
||||
plaintext_short = _aes.decrypt(ciphertext_short, iv)
|
||||
print(f"Ciphertext Hex: {ciphertext_short.hex()}")
|
||||
print(f"Plaintext: {plaintext_short.decode()}")
|
||||
assert short_text == plaintext_short
|
||||
print(bcolors.OK + "Short Text Test Passed Successfully" + bcolors.RESET)
|
||||
print()
|
||||
|
||||
# test an arbitrary length phrase
|
||||
iv = os.urandom(AES.block_size)
|
||||
|
||||
text = b"This Text is longer than one block"
|
||||
print("Arbitrary Length Tests")
|
||||
print("----------------------")
|
||||
print(f"iv: {iv.hex()}")
|
||||
print(f"plain text: '{text.decode()}'")
|
||||
ciphertext = _aes.encrypt(text, iv)
|
||||
plaintext = _aes.decrypt(ciphertext, iv)
|
||||
print(f"Ciphertext Hex: {ciphertext.hex()}")
|
||||
print(f"Plaintext: {plaintext.decode()}")
|
||||
assert text == plaintext
|
||||
print(bcolors.OK + "Arbitrary Length Text Test Passed Successfully" + bcolors.RESET)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# test AES class
|
||||
test()
|
||||
@@ -0,0 +1,159 @@
|
||||
# MIT License
|
||||
|
||||
# Copyright (c) 2021 Or Gur Arie
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
'''
|
||||
Utils class for AES encryption / decryption
|
||||
'''
|
||||
|
||||
## AES lookup tables
|
||||
# resource: https://en.wikipedia.org/wiki/Rijndael_S-box
|
||||
s_box = (
|
||||
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
|
||||
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
|
||||
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
|
||||
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
|
||||
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
|
||||
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
|
||||
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
|
||||
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
|
||||
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
|
||||
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
|
||||
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
|
||||
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
|
||||
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
|
||||
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
|
||||
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
|
||||
)
|
||||
|
||||
inv_s_box = (
|
||||
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
|
||||
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
|
||||
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
|
||||
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
|
||||
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
|
||||
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
|
||||
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
|
||||
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
|
||||
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
|
||||
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
|
||||
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
|
||||
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
|
||||
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
|
||||
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
|
||||
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
|
||||
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
|
||||
)
|
||||
|
||||
|
||||
## AES AddRoundKey
|
||||
# Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants
|
||||
r_con = (
|
||||
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
|
||||
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
|
||||
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
|
||||
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
|
||||
)
|
||||
|
||||
def add_round_key(s, k):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
s[i][j] ^= k[i][j]
|
||||
|
||||
|
||||
## AES SubBytes
|
||||
def sub_bytes(s):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
s[i][j] = s_box[s[i][j]]
|
||||
|
||||
|
||||
def inv_sub_bytes(s):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
s[i][j] = inv_s_box[s[i][j]]
|
||||
|
||||
|
||||
## AES ShiftRows
|
||||
def shift_rows(s):
|
||||
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
|
||||
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
|
||||
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
|
||||
|
||||
|
||||
def inv_shift_rows(s):
|
||||
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
|
||||
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
|
||||
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
|
||||
|
||||
|
||||
## AES MixColumns
|
||||
# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
|
||||
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
|
||||
|
||||
|
||||
def mix_single_column(a):
|
||||
# see Sec 4.1.2 in The Design of Rijndael
|
||||
t = a[0] ^ a[1] ^ a[2] ^ a[3]
|
||||
u = a[0]
|
||||
a[0] ^= t ^ xtime(a[0] ^ a[1])
|
||||
a[1] ^= t ^ xtime(a[1] ^ a[2])
|
||||
a[2] ^= t ^ xtime(a[2] ^ a[3])
|
||||
a[3] ^= t ^ xtime(a[3] ^ u)
|
||||
|
||||
|
||||
def mix_columns(s):
|
||||
for i in range(4):
|
||||
mix_single_column(s[i])
|
||||
|
||||
|
||||
def inv_mix_columns(s):
|
||||
# see Sec 4.1.3 in The Design of Rijndael
|
||||
for i in range(4):
|
||||
u = xtime(xtime(s[i][0] ^ s[i][2]))
|
||||
v = xtime(xtime(s[i][1] ^ s[i][3]))
|
||||
s[i][0] ^= u
|
||||
s[i][1] ^= v
|
||||
s[i][2] ^= u
|
||||
s[i][3] ^= v
|
||||
|
||||
mix_columns(s)
|
||||
|
||||
|
||||
## AES Bytes
|
||||
def bytes2matrix(text):
|
||||
""" Converts a 16-byte array into a 4x4 matrix. """
|
||||
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
|
||||
|
||||
def matrix2bytes(matrix):
|
||||
""" Converts a 4x4 matrix into a 16-byte array. """
|
||||
return bytes(sum(matrix, []))
|
||||
|
||||
|
||||
def xor_bytes(a, b):
|
||||
""" Returns a new byte array with the elements xor'ed. """
|
||||
return bytes(i^j for i, j in zip(a, b))
|
||||
|
||||
|
||||
def split_blocks(message, block_size=16, require_padding=True):
|
||||
assert len(message) % block_size == 0 or not require_padding
|
||||
return [message[i:i+16] for i in range(0, len(message), block_size)]
|
||||
@@ -0,0 +1,58 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2015 Brian Warner and other 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 . import eddsa
|
||||
|
||||
class BadSignatureError(Exception):
|
||||
pass
|
||||
|
||||
SECRETKEYBYTES = 64
|
||||
PUBLICKEYBYTES = 32
|
||||
SIGNATUREKEYBYTES = 64
|
||||
|
||||
def publickey(seed32):
|
||||
assert len(seed32) == 32
|
||||
vk32 = eddsa.publickey(seed32)
|
||||
return vk32, seed32+vk32
|
||||
|
||||
def sign(msg, skvk):
|
||||
assert len(skvk) == 64
|
||||
sk = skvk[:32]
|
||||
vk = skvk[32:]
|
||||
sig = eddsa.signature(msg, sk, vk)
|
||||
return sig+msg
|
||||
|
||||
def open(sigmsg, vk):
|
||||
assert len(vk) == 32
|
||||
sig = sigmsg[:64]
|
||||
msg = sigmsg[64:]
|
||||
try:
|
||||
valid = eddsa.checkvalid(sig, msg, vk)
|
||||
except ValueError as e:
|
||||
raise BadSignatureError(e)
|
||||
except Exception as e:
|
||||
if str(e) == "decoding point that is not on curve":
|
||||
raise BadSignatureError(e)
|
||||
raise
|
||||
if not valid:
|
||||
raise BadSignatureError()
|
||||
return msg
|
||||
@@ -0,0 +1,368 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2015 Brian Warner and other 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.
|
||||
|
||||
import binascii, hashlib, itertools
|
||||
|
||||
Q = 2**255 - 19
|
||||
L = 2**252 + 27742317777372353535851937790883648493
|
||||
|
||||
def inv(x):
|
||||
return pow(x, Q-2, Q)
|
||||
|
||||
d = -121665 * inv(121666)
|
||||
I = pow(2,(Q-1)//4,Q)
|
||||
|
||||
def xrecover(y):
|
||||
xx = (y*y-1) * inv(d*y*y+1)
|
||||
x = pow(xx,(Q+3)//8,Q)
|
||||
if (x*x - xx) % Q != 0: x = (x*I) % Q
|
||||
if x % 2 != 0: x = Q-x
|
||||
return x
|
||||
|
||||
By = 4 * inv(5)
|
||||
Bx = xrecover(By)
|
||||
B = [Bx % Q,By % Q]
|
||||
|
||||
# Extended Coordinates: x=X/Z, y=Y/Z, x*y=T/Z
|
||||
# http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html
|
||||
|
||||
def xform_affine_to_extended(pt):
|
||||
(x, y) = pt
|
||||
return (x%Q, y%Q, 1, (x*y)%Q) # (X,Y,Z,T)
|
||||
|
||||
def xform_extended_to_affine(pt):
|
||||
(x, y, z, _) = pt
|
||||
return ((x*inv(z))%Q, (y*inv(z))%Q)
|
||||
|
||||
def double_element(pt): # extended->extended
|
||||
# dbl-2008-hwcd
|
||||
(X1, Y1, Z1, _) = pt
|
||||
A = (X1*X1)
|
||||
B = (Y1*Y1)
|
||||
C = (2*Z1*Z1)
|
||||
D = (-A) % Q
|
||||
J = (X1+Y1) % Q
|
||||
E = (J*J-A-B) % Q
|
||||
G = (D+B) % Q
|
||||
F = (G-C) % Q
|
||||
H = (D-B) % Q
|
||||
X3 = (E*F) % Q
|
||||
Y3 = (G*H) % Q
|
||||
Z3 = (F*G) % Q
|
||||
T3 = (E*H) % Q
|
||||
return (X3, Y3, Z3, T3)
|
||||
|
||||
def add_elements(pt1, pt2): # extended->extended
|
||||
# add-2008-hwcd-3 . Slightly slower than add-2008-hwcd-4, but -3 is
|
||||
# unified, so it's safe for general-purpose addition
|
||||
(X1, Y1, Z1, T1) = pt1
|
||||
(X2, Y2, Z2, T2) = pt2
|
||||
A = ((Y1-X1)*(Y2-X2)) % Q
|
||||
B = ((Y1+X1)*(Y2+X2)) % Q
|
||||
C = T1*(2*d)*T2 % Q
|
||||
D = Z1*2*Z2 % Q
|
||||
E = (B-A) % Q
|
||||
F = (D-C) % Q
|
||||
G = (D+C) % Q
|
||||
H = (B+A) % Q
|
||||
X3 = (E*F) % Q
|
||||
Y3 = (G*H) % Q
|
||||
T3 = (E*H) % Q
|
||||
Z3 = (F*G) % Q
|
||||
return (X3, Y3, Z3, T3)
|
||||
|
||||
def scalarmult_element_safe_slow(pt, n):
|
||||
# this form is slightly slower, but tolerates arbitrary points, including
|
||||
# those which are not in the main 1*L subgroup. This includes points of
|
||||
# order 1 (the neutral element Zero), 2, 4, and 8.
|
||||
assert n >= 0
|
||||
if n==0:
|
||||
return xform_affine_to_extended((0,1))
|
||||
_ = double_element(scalarmult_element_safe_slow(pt, n>>1))
|
||||
return add_elements(_, pt) if n&1 else _
|
||||
|
||||
def _add_elements_nonunfied(pt1, pt2): # extended->extended
|
||||
# add-2008-hwcd-4 : NOT unified, only for pt1!=pt2. About 10% faster than
|
||||
# the (unified) add-2008-hwcd-3, and safe to use inside scalarmult if you
|
||||
# aren't using points of order 1/2/4/8
|
||||
(X1, Y1, Z1, T1) = pt1
|
||||
(X2, Y2, Z2, T2) = pt2
|
||||
A = ((Y1-X1)*(Y2+X2)) % Q
|
||||
B = ((Y1+X1)*(Y2-X2)) % Q
|
||||
C = (Z1*2*T2) % Q
|
||||
D = (T1*2*Z2) % Q
|
||||
E = (D+C) % Q
|
||||
F = (B-A) % Q
|
||||
G = (B+A) % Q
|
||||
H = (D-C) % Q
|
||||
X3 = (E*F) % Q
|
||||
Y3 = (G*H) % Q
|
||||
Z3 = (F*G) % Q
|
||||
T3 = (E*H) % Q
|
||||
return (X3, Y3, Z3, T3)
|
||||
|
||||
def scalarmult_element(pt, n): # extended->extended
|
||||
# This form only works properly when given points that are a member of
|
||||
# the main 1*L subgroup. It will give incorrect answers when called with
|
||||
# the points of order 1/2/4/8, including point Zero. (it will also work
|
||||
# properly when given points of order 2*L/4*L/8*L)
|
||||
assert n >= 0
|
||||
if n==0:
|
||||
return xform_affine_to_extended((0,1))
|
||||
_ = double_element(scalarmult_element(pt, n>>1))
|
||||
return _add_elements_nonunfied(_, pt) if n&1 else _
|
||||
|
||||
# points are encoded as 32-bytes little-endian, b255 is sign, b2b1b0 are 0
|
||||
|
||||
def encodepoint(P):
|
||||
x = P[0]
|
||||
y = P[1]
|
||||
# MSB of output equals x.b0 (=x&1)
|
||||
# rest of output is little-endian y
|
||||
assert 0 <= y < (1<<255) # always < 0x7fff..ff
|
||||
if x & 1:
|
||||
y += 1<<255
|
||||
return binascii.unhexlify("%064x" % y)[::-1]
|
||||
|
||||
def isoncurve(P):
|
||||
x = P[0]
|
||||
y = P[1]
|
||||
return (-x*x + y*y - 1 - d*x*x*y*y) % Q == 0
|
||||
|
||||
class NotOnCurve(Exception):
|
||||
pass
|
||||
|
||||
def decodepoint(s):
|
||||
unclamped = int(binascii.hexlify(s[:32][::-1]), 16)
|
||||
clamp = (1 << 255) - 1
|
||||
y = unclamped & clamp # clear MSB
|
||||
x = xrecover(y)
|
||||
if bool(x & 1) != bool(unclamped & (1<<255)): x = Q-x
|
||||
P = [x,y]
|
||||
if not isoncurve(P): raise NotOnCurve("decoding point that is not on curve")
|
||||
return P
|
||||
|
||||
# scalars are encoded as 32-bytes little-endian
|
||||
|
||||
def bytes_to_scalar(s):
|
||||
assert len(s) == 32, len(s)
|
||||
return int(binascii.hexlify(s[::-1]), 16)
|
||||
|
||||
def bytes_to_clamped_scalar(s):
|
||||
# Ed25519 private keys clamp the scalar to ensure two things:
|
||||
# 1: integer value is in L/2 .. L, to avoid small-logarithm
|
||||
# non-wraparaound
|
||||
# 2: low-order 3 bits are zero, so a small-subgroup attack won't learn
|
||||
# any information
|
||||
# set the top two bits to 01, and the bottom three to 000
|
||||
a_unclamped = bytes_to_scalar(s)
|
||||
AND_CLAMP = (1<<254) - 1 - 7
|
||||
OR_CLAMP = (1<<254)
|
||||
a_clamped = (a_unclamped & AND_CLAMP) | OR_CLAMP
|
||||
return a_clamped
|
||||
|
||||
def random_scalar(entropy_f): # 0..L-1 inclusive
|
||||
# reduce the bias to a safe level by generating 256 extra bits
|
||||
oversized = int(binascii.hexlify(entropy_f(32+32)), 16)
|
||||
return oversized % L
|
||||
|
||||
def password_to_scalar(pw):
|
||||
oversized = hashlib.sha512(pw).digest()
|
||||
return int(binascii.hexlify(oversized), 16) % L
|
||||
|
||||
def scalar_to_bytes(y):
|
||||
y = y % L
|
||||
assert 0 <= y < 2**256
|
||||
return binascii.unhexlify("%064x" % y)[::-1]
|
||||
|
||||
# Elements, of various orders
|
||||
|
||||
def is_extended_zero(XYTZ):
|
||||
# catch Zero
|
||||
(X, Y, Z, T) = XYTZ
|
||||
Y = Y % Q
|
||||
Z = Z % Q
|
||||
if X==0 and Y==Z and Y!=0:
|
||||
return True
|
||||
return False
|
||||
|
||||
class ElementOfUnknownGroup:
|
||||
# This is used for points of order 2,4,8,2*L,4*L,8*L
|
||||
def __init__(self, XYTZ):
|
||||
assert isinstance(XYTZ, tuple)
|
||||
assert len(XYTZ) == 4
|
||||
self.XYTZ = XYTZ
|
||||
|
||||
def add(self, other):
|
||||
if not isinstance(other, ElementOfUnknownGroup):
|
||||
raise TypeError("elements can only be added to other elements")
|
||||
sum_XYTZ = add_elements(self.XYTZ, other.XYTZ)
|
||||
if is_extended_zero(sum_XYTZ):
|
||||
return Zero
|
||||
return ElementOfUnknownGroup(sum_XYTZ)
|
||||
|
||||
def scalarmult(self, s):
|
||||
if isinstance(s, ElementOfUnknownGroup):
|
||||
raise TypeError("elements cannot be multiplied together")
|
||||
assert s >= 0
|
||||
product = scalarmult_element_safe_slow(self.XYTZ, s)
|
||||
return ElementOfUnknownGroup(product)
|
||||
|
||||
def to_bytes(self):
|
||||
return encodepoint(xform_extended_to_affine(self.XYTZ))
|
||||
def __eq__(self, other):
|
||||
return self.to_bytes() == other.to_bytes()
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
class Element(ElementOfUnknownGroup):
|
||||
# this only holds elements in the main 1*L subgroup. It never holds Zero,
|
||||
# or elements of order 1/2/4/8, or 2*L/4*L/8*L.
|
||||
|
||||
def add(self, other):
|
||||
if not isinstance(other, ElementOfUnknownGroup):
|
||||
raise TypeError("elements can only be added to other elements")
|
||||
sum_element = ElementOfUnknownGroup.add(self, other)
|
||||
if sum_element is Zero:
|
||||
return sum_element
|
||||
if isinstance(other, Element):
|
||||
# adding two subgroup elements results in another subgroup
|
||||
# element, or Zero, and we've already excluded Zero
|
||||
return Element(sum_element.XYTZ)
|
||||
# not necessarily a subgroup member, so assume not
|
||||
return sum_element
|
||||
|
||||
def scalarmult(self, s):
|
||||
if isinstance(s, ElementOfUnknownGroup):
|
||||
raise TypeError("elements cannot be multiplied together")
|
||||
# scalarmult of subgroup members can be done modulo the subgroup
|
||||
# order, and using the faster non-unified function.
|
||||
s = s % L
|
||||
# scalarmult(s=0) gets you Zero
|
||||
if s == 0:
|
||||
return Zero
|
||||
# scalarmult(s=1) gets you self, which is a subgroup member
|
||||
# scalarmult(s<grouporder) gets you a different subgroup member
|
||||
return Element(scalarmult_element(self.XYTZ, s))
|
||||
|
||||
# negation and subtraction only make sense for the main subgroup
|
||||
def negate(self):
|
||||
# slow. Prefer e.scalarmult(-pw) to e.scalarmult(pw).negate()
|
||||
return Element(scalarmult_element(self.XYTZ, L-2))
|
||||
def subtract(self, other):
|
||||
return self.add(other.negate())
|
||||
|
||||
class _ZeroElement(ElementOfUnknownGroup):
|
||||
def add(self, other):
|
||||
return other # zero+anything = anything
|
||||
def scalarmult(self, s):
|
||||
return self # zero*anything = zero
|
||||
def negate(self):
|
||||
return self # -zero = zero
|
||||
def subtract(self, other):
|
||||
return self.add(other.negate())
|
||||
|
||||
|
||||
Base = Element(xform_affine_to_extended(B))
|
||||
Zero = _ZeroElement(xform_affine_to_extended((0,1))) # the neutral (identity) element
|
||||
|
||||
_zero_bytes = Zero.to_bytes()
|
||||
|
||||
|
||||
def arbitrary_element(seed): # unknown DL
|
||||
# TODO: if we don't need uniformity, maybe use just sha256 here?
|
||||
hseed = hashlib.sha512(seed).digest()
|
||||
y = int(binascii.hexlify(hseed), 16) % Q
|
||||
|
||||
# we try successive Y values until we find a valid point
|
||||
for plus in itertools.count(0):
|
||||
y_plus = (y + plus) % Q
|
||||
x = xrecover(y_plus)
|
||||
Pa = [x,y_plus] # no attempt to use both "positive" and "negative" X
|
||||
|
||||
# only about 50% of Y coordinates map to valid curve points (I think
|
||||
# the other half give you points on the "twist").
|
||||
if not isoncurve(Pa):
|
||||
continue
|
||||
|
||||
P = ElementOfUnknownGroup(xform_affine_to_extended(Pa))
|
||||
# even if the point is on our curve, it may not be in our particular
|
||||
# (order=L) subgroup. The curve has order 8*L, so an arbitrary point
|
||||
# could have order 1,2,4,8,1*L,2*L,4*L,8*L (everything which divides
|
||||
# the group order).
|
||||
|
||||
# [I MAY BE COMPLETELY WRONG ABOUT THIS, but my brief statistical
|
||||
# tests suggest it's not too far off] There are phi(x) points with
|
||||
# order x, so:
|
||||
# 1 element of order 1: [(x=0,y=1)=Zero]
|
||||
# 1 element of order 2 [(x=0,y=-1)]
|
||||
# 2 elements of order 4
|
||||
# 4 elements of order 8
|
||||
# L-1 elements of order L (including Base)
|
||||
# L-1 elements of order 2*L
|
||||
# 2*(L-1) elements of order 4*L
|
||||
# 4*(L-1) elements of order 8*L
|
||||
|
||||
# So 50% of random points will have order 8*L, 25% will have order
|
||||
# 4*L, 13% order 2*L, and 13% will have our desired order 1*L (and a
|
||||
# vanishingly small fraction will have 1/2/4/8). If we multiply any
|
||||
# of the 8*L points by 2, we're sure to get an 4*L point (and
|
||||
# multiplying a 4*L point by 2 gives us a 2*L point, and so on).
|
||||
# Multiplying a 1*L point by 2 gives us a different 1*L point. So
|
||||
# multiplying by 8 gets us from almost any point into a uniform point
|
||||
# on the correct 1*L subgroup.
|
||||
|
||||
P8 = P.scalarmult(8)
|
||||
|
||||
# if we got really unlucky and picked one of the 8 low-order points,
|
||||
# multiplying by 8 will get us to the identity (Zero), which we check
|
||||
# for explicitly.
|
||||
if is_extended_zero(P8.XYTZ):
|
||||
continue
|
||||
|
||||
# Test that we're finally in the right group. We want to scalarmult
|
||||
# by L, and we want to *not* use the trick in Group.scalarmult()
|
||||
# which does x%L, because that would bypass the check we care about.
|
||||
# P is still an _ElementOfUnknownGroup, which doesn't use x%L because
|
||||
# that's not correct for points outside the main group.
|
||||
assert is_extended_zero(P8.scalarmult(L).XYTZ)
|
||||
|
||||
return Element(P8.XYTZ)
|
||||
# never reached
|
||||
|
||||
def bytes_to_unknown_group_element(bytes):
|
||||
# this accepts all elements, including Zero and wrong-subgroup ones
|
||||
if bytes == _zero_bytes:
|
||||
return Zero
|
||||
XYTZ = xform_affine_to_extended(decodepoint(bytes))
|
||||
return ElementOfUnknownGroup(XYTZ)
|
||||
|
||||
def bytes_to_element(bytes):
|
||||
# this strictly only accepts elements in the right subgroup
|
||||
P = bytes_to_unknown_group_element(bytes)
|
||||
if P is Zero:
|
||||
raise ValueError("element was Zero")
|
||||
if not is_extended_zero(P.scalarmult(L).XYTZ):
|
||||
raise ValueError("element is not in the right group")
|
||||
# the point is in the expected 1*L subgroup, not in the 2/4/8 groups,
|
||||
# or in the 2*L/4*L/8*L groups. Promote it to a correct-group Element.
|
||||
return Element(P.XYTZ)
|
||||
@@ -0,0 +1,213 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2015 Brian Warner and other 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.
|
||||
|
||||
import os
|
||||
import base64
|
||||
from . import _ed25519
|
||||
BadSignatureError = _ed25519.BadSignatureError
|
||||
|
||||
def create_keypair(entropy=os.urandom):
|
||||
SEEDLEN = int(_ed25519.SECRETKEYBYTES/2)
|
||||
assert SEEDLEN == 32
|
||||
seed = entropy(SEEDLEN)
|
||||
sk = SigningKey(seed)
|
||||
vk = sk.get_verifying_key()
|
||||
return sk, vk
|
||||
|
||||
class BadPrefixError(Exception):
|
||||
pass
|
||||
|
||||
def remove_prefix(s_bytes, prefix):
|
||||
assert(type(s_bytes) == type(prefix))
|
||||
if s_bytes[:len(prefix)] != prefix:
|
||||
raise BadPrefixError("did not see expected '%s' prefix" % (prefix,))
|
||||
return s_bytes[len(prefix):]
|
||||
|
||||
def to_ascii(s_bytes, prefix="", encoding="base64"):
|
||||
"""Return a version-prefixed ASCII representation of the given binary
|
||||
string. 'encoding' indicates how to do the encoding, and can be one of:
|
||||
* base64
|
||||
* base32
|
||||
* base16 (or hex)
|
||||
|
||||
This function handles bytes, not bits, so it does not append any trailing
|
||||
'=' (unlike standard base64.b64encode). It also lowercases the base32
|
||||
output.
|
||||
|
||||
'prefix' will be prepended to the encoded form, and is useful for
|
||||
distinguishing the purpose and version of the binary string. E.g. you
|
||||
could prepend 'pub0-' to a VerifyingKey string to allow the receiving
|
||||
code to raise a useful error if someone pasted in a signature string by
|
||||
mistake.
|
||||
"""
|
||||
assert isinstance(s_bytes, bytes)
|
||||
if not isinstance(prefix, bytes):
|
||||
prefix = prefix.encode('ascii')
|
||||
if encoding == "base64":
|
||||
s_ascii = base64.b64encode(s_bytes).decode('ascii').rstrip("=")
|
||||
elif encoding == "base32":
|
||||
s_ascii = base64.b32encode(s_bytes).decode('ascii').rstrip("=").lower()
|
||||
elif encoding in ("base16", "hex"):
|
||||
s_ascii = base64.b16encode(s_bytes).decode('ascii').lower()
|
||||
else:
|
||||
raise NotImplementedError
|
||||
return prefix+s_ascii.encode('ascii')
|
||||
|
||||
def from_ascii(s_ascii, prefix="", encoding="base64"):
|
||||
"""This is the opposite of to_ascii. It will throw BadPrefixError if
|
||||
the prefix is not found.
|
||||
"""
|
||||
if isinstance(s_ascii, bytes):
|
||||
s_ascii = s_ascii.decode('ascii')
|
||||
if isinstance(prefix, bytes):
|
||||
prefix = prefix.decode('ascii')
|
||||
s_ascii = remove_prefix(s_ascii.strip(), prefix)
|
||||
if encoding == "base64":
|
||||
s_ascii += "="*((4 - len(s_ascii)%4)%4)
|
||||
s_bytes = base64.b64decode(s_ascii)
|
||||
elif encoding == "base32":
|
||||
s_ascii += "="*((8 - len(s_ascii)%8)%8)
|
||||
s_bytes = base64.b32decode(s_ascii.upper())
|
||||
elif encoding in ("base16", "hex"):
|
||||
s_bytes = base64.b16decode(s_ascii.upper())
|
||||
else:
|
||||
raise NotImplementedError
|
||||
return s_bytes
|
||||
|
||||
class SigningKey(object):
|
||||
# this can only be used to reconstruct a key created by create_keypair().
|
||||
def __init__(self, sk_s, prefix="", encoding=None):
|
||||
assert isinstance(sk_s, bytes)
|
||||
if not isinstance(prefix, bytes):
|
||||
prefix = prefix.encode('ascii')
|
||||
sk_s = remove_prefix(sk_s, prefix)
|
||||
if encoding is not None:
|
||||
sk_s = from_ascii(sk_s, encoding=encoding)
|
||||
if len(sk_s) == 32:
|
||||
# create from seed
|
||||
vk_s, sk_s = _ed25519.publickey(sk_s)
|
||||
else:
|
||||
if len(sk_s) != 32+32:
|
||||
raise ValueError("SigningKey takes 32-byte seed or 64-byte string")
|
||||
self.sk_s = sk_s # seed+pubkey
|
||||
self.vk_s = sk_s[32:] # just pubkey
|
||||
|
||||
def to_bytes(self, prefix=""):
|
||||
if not isinstance(prefix, bytes):
|
||||
prefix = prefix.encode('ascii')
|
||||
return prefix+self.sk_s
|
||||
|
||||
def to_ascii(self, prefix="", encoding=None):
|
||||
assert encoding
|
||||
if not isinstance(prefix, bytes):
|
||||
prefix = prefix.encode('ascii')
|
||||
return to_ascii(self.to_seed(), prefix, encoding)
|
||||
|
||||
def to_seed(self, prefix=""):
|
||||
if not isinstance(prefix, bytes):
|
||||
prefix = prefix.encode('ascii')
|
||||
return prefix+self.sk_s[:32]
|
||||
|
||||
def __eq__(self, them):
|
||||
if not isinstance(them, object): return False
|
||||
return (them.__class__ == self.__class__
|
||||
and them.sk_s == self.sk_s)
|
||||
|
||||
def get_verifying_key(self):
|
||||
return VerifyingKey(self.vk_s)
|
||||
|
||||
def sign(self, msg, prefix="", encoding=None):
|
||||
assert isinstance(msg, bytes)
|
||||
if not isinstance(prefix, bytes):
|
||||
prefix = prefix.encode('ascii')
|
||||
sig_and_msg = _ed25519.sign(msg, self.sk_s)
|
||||
# the response is R+S+msg
|
||||
sig_R = sig_and_msg[0:32]
|
||||
sig_S = sig_and_msg[32:64]
|
||||
msg_out = sig_and_msg[64:]
|
||||
sig_out = sig_R + sig_S
|
||||
assert msg_out == msg
|
||||
if encoding:
|
||||
return to_ascii(sig_out, prefix, encoding)
|
||||
return prefix+sig_out
|
||||
|
||||
class VerifyingKey(object):
|
||||
def __init__(self, vk_s, prefix="", encoding=None):
|
||||
if not isinstance(prefix, bytes):
|
||||
prefix = prefix.encode('ascii')
|
||||
if not isinstance(vk_s, bytes):
|
||||
vk_s = vk_s.encode('ascii')
|
||||
assert isinstance(vk_s, bytes)
|
||||
vk_s = remove_prefix(vk_s, prefix)
|
||||
if encoding is not None:
|
||||
vk_s = from_ascii(vk_s, encoding=encoding)
|
||||
|
||||
assert len(vk_s) == 32
|
||||
self.vk_s = vk_s
|
||||
|
||||
def to_bytes(self, prefix=""):
|
||||
if not isinstance(prefix, bytes):
|
||||
prefix = prefix.encode('ascii')
|
||||
return prefix+self.vk_s
|
||||
|
||||
def to_ascii(self, prefix="", encoding=None):
|
||||
assert encoding
|
||||
if not isinstance(prefix, bytes):
|
||||
prefix = prefix.encode('ascii')
|
||||
return to_ascii(self.vk_s, prefix, encoding)
|
||||
|
||||
def __eq__(self, them):
|
||||
if not isinstance(them, object): return False
|
||||
return (them.__class__ == self.__class__
|
||||
and them.vk_s == self.vk_s)
|
||||
|
||||
def verify(self, sig, msg, prefix="", encoding=None):
|
||||
if not isinstance(sig, bytes):
|
||||
sig = sig.encode('ascii')
|
||||
if not isinstance(prefix, bytes):
|
||||
prefix = prefix.encode('ascii')
|
||||
assert isinstance(sig, bytes)
|
||||
assert isinstance(msg, bytes)
|
||||
if encoding:
|
||||
sig = from_ascii(sig, prefix, encoding)
|
||||
else:
|
||||
sig = remove_prefix(sig, prefix)
|
||||
assert len(sig) == 64
|
||||
sig_R = sig[:32]
|
||||
sig_S = sig[32:]
|
||||
sig_and_msg = sig_R + sig_S + msg
|
||||
# this might raise BadSignatureError
|
||||
msg2 = _ed25519.open(sig_and_msg, self.vk_s)
|
||||
assert msg2 == msg
|
||||
|
||||
def selftest():
|
||||
message = b"crypto libraries should always test themselves at powerup"
|
||||
sk = SigningKey(b"priv0-VIsfn5OFGa09Un2MR6Hm7BQ5++xhcQskU2OGXG8jSJl4cWLZrRrVcSN2gVYMGtZT+3354J5jfmqAcuRSD9KIyg",
|
||||
prefix="priv0-", encoding="base64")
|
||||
vk = VerifyingKey(b"pub0-eHFi2a0a1XEjdoFWDBrWU/t9+eCeY35qgHLkUg/SiMo",
|
||||
prefix="pub0-", encoding="base64")
|
||||
assert sk.get_verifying_key() == vk
|
||||
sig = sk.sign(message, prefix="sig0-", encoding="base64")
|
||||
assert sig == b"sig0-E/QrwtSF52x8+q0l4ahA7eJbRKc777ClKNg217Q0z4fiYMCdmAOI+rTLVkiFhX6k3D+wQQfKdJYMxaTUFfv1DQ", sig
|
||||
vk.verify(sig, message, prefix="sig0-", encoding="base64")
|
||||
|
||||
selftest()
|
||||
@@ -0,0 +1,94 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2015 Brian Warner and other 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 RNS.Cryptography.Hashes import sha512
|
||||
from .basic import (bytes_to_clamped_scalar,
|
||||
bytes_to_scalar, scalar_to_bytes,
|
||||
bytes_to_element, Base)
|
||||
import hashlib, binascii
|
||||
|
||||
def H(m):
|
||||
return sha512(m)
|
||||
|
||||
def publickey(seed):
|
||||
# turn first half of SHA512(seed) into scalar, then into point
|
||||
assert len(seed) == 32
|
||||
a = bytes_to_clamped_scalar(H(seed)[:32])
|
||||
A = Base.scalarmult(a)
|
||||
return A.to_bytes()
|
||||
|
||||
def Hint(m):
|
||||
h = H(m)
|
||||
return int(binascii.hexlify(h[::-1]), 16)
|
||||
|
||||
def signature(m,sk,pk):
|
||||
assert len(sk) == 32 # seed
|
||||
assert len(pk) == 32
|
||||
h = H(sk[:32])
|
||||
a_bytes, inter = h[:32], h[32:]
|
||||
a = bytes_to_clamped_scalar(a_bytes)
|
||||
r = Hint(inter + m)
|
||||
R = Base.scalarmult(r)
|
||||
R_bytes = R.to_bytes()
|
||||
S = r + Hint(R_bytes + pk + m) * a
|
||||
return R_bytes + scalar_to_bytes(S)
|
||||
|
||||
def checkvalid(s, m, pk):
|
||||
if len(s) != 64: raise Exception("signature length is wrong")
|
||||
if len(pk) != 32: raise Exception("public-key length is wrong")
|
||||
R = bytes_to_element(s[:32])
|
||||
A = bytes_to_element(pk)
|
||||
S = bytes_to_scalar(s[32:])
|
||||
h = Hint(s[:32] + pk + m)
|
||||
v1 = Base.scalarmult(S)
|
||||
v2 = R.add(A.scalarmult(h))
|
||||
return v1==v2
|
||||
|
||||
# wrappers
|
||||
|
||||
import os
|
||||
|
||||
def create_signing_key():
|
||||
seed = os.urandom(32)
|
||||
return seed
|
||||
|
||||
def create_verifying_key(signing_key):
|
||||
return publickey(signing_key)
|
||||
|
||||
def sign(skbytes, msg):
|
||||
"""Return just the signature, given the message and just the secret
|
||||
key."""
|
||||
if len(skbytes) != 32:
|
||||
raise ValueError("Bad signing key length %d" % len(skbytes))
|
||||
vkbytes = create_verifying_key(skbytes)
|
||||
sig = signature(msg, skbytes, vkbytes)
|
||||
return sig
|
||||
|
||||
def verify(vkbytes, sig, msg):
|
||||
if len(vkbytes) != 32:
|
||||
raise ValueError("Bad verifying key length %d" % len(vkbytes))
|
||||
if len(sig) != 64:
|
||||
raise ValueError("Bad signature length %d" % len(sig))
|
||||
rc = checkvalid(sig, msg, vkbytes)
|
||||
if not rc:
|
||||
raise ValueError("rc != 0", rc)
|
||||
return True
|
||||
@@ -1,10 +1,30 @@
|
||||
import base64
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import math
|
||||
import time
|
||||
import RNS
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from RNS.Cryptography import Fernet
|
||||
|
||||
class Callbacks:
|
||||
def __init__(self):
|
||||
@@ -50,7 +70,7 @@ class Destination:
|
||||
directions = [IN, OUT]
|
||||
|
||||
@staticmethod
|
||||
def full_name(app_name, *aspects):
|
||||
def expand_name(identity, app_name, *aspects):
|
||||
"""
|
||||
:returns: A string containing the full human-readable name of the destination, for an app_name and a number of aspects.
|
||||
"""
|
||||
@@ -61,23 +81,25 @@ class Destination:
|
||||
name = app_name
|
||||
for aspect in aspects:
|
||||
if "." in aspect: raise ValueError("Dots can't be used in aspects")
|
||||
name = name + "." + aspect
|
||||
name += "." + aspect
|
||||
|
||||
if identity != None:
|
||||
name += "." + identity.hexhash
|
||||
|
||||
return name
|
||||
|
||||
|
||||
@staticmethod
|
||||
def hash(app_name, *aspects):
|
||||
def hash(identity, app_name, *aspects):
|
||||
"""
|
||||
:returns: A destination name in adressable hash form, for an app_name and a number of aspects.
|
||||
"""
|
||||
name = Destination.full_name(app_name, *aspects)
|
||||
name_hash = RNS.Identity.full_hash(Destination.expand_name(None, app_name, *aspects).encode("utf-8"))[:(RNS.Identity.NAME_HASH_LENGTH//8)]
|
||||
addr_hash_material = name_hash
|
||||
if identity != None:
|
||||
addr_hash_material += identity.hash
|
||||
|
||||
# Create a digest for the destination
|
||||
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
|
||||
digest.update(name.encode("UTF-8"))
|
||||
|
||||
return digest.finalize()[:10]
|
||||
return RNS.Identity.full_hash(addr_hash_material)[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8]
|
||||
|
||||
@staticmethod
|
||||
def app_and_aspects_from_name(full_name):
|
||||
@@ -93,14 +115,16 @@ class Destination:
|
||||
:returns: A destination name in adressable hash form, for a full name string and Identity instance.
|
||||
"""
|
||||
app_name, aspects = Destination.app_and_aspects_from_name(full_name)
|
||||
aspects.append(identity.hexhash)
|
||||
return Destination.hash(app_name, *aspects)
|
||||
|
||||
return Destination.hash(identity, app_name, *aspects)
|
||||
|
||||
def __init__(self, identity, direction, type, app_name, *aspects):
|
||||
# Check input values and build name string
|
||||
if "." in app_name: raise ValueError("Dots can't be used in app names")
|
||||
if not type in Destination.types: raise ValueError("Unknown destination type")
|
||||
if not direction in Destination.directions: raise ValueError("Unknown destination direction")
|
||||
|
||||
self.accept_link_requests = True
|
||||
self.callbacks = Callbacks()
|
||||
self.request_handlers = {}
|
||||
self.type = type
|
||||
@@ -110,20 +134,22 @@ class Destination:
|
||||
|
||||
self.links = []
|
||||
|
||||
if identity != None and type == Destination.SINGLE:
|
||||
aspects = aspects+(identity.hexhash,)
|
||||
|
||||
if identity == None and direction == Destination.IN and self.type != Destination.PLAIN:
|
||||
identity = RNS.Identity()
|
||||
aspects = aspects+(identity.hexhash,)
|
||||
|
||||
if identity != None and self.type == Destination.PLAIN:
|
||||
raise TypeError("Selected destination type PLAIN cannot hold an identity")
|
||||
|
||||
self.identity = identity
|
||||
self.name = Destination.expand_name(identity, app_name, *aspects)
|
||||
|
||||
self.name = Destination.full_name(app_name, *aspects)
|
||||
self.hash = Destination.hash(app_name, *aspects)
|
||||
# Generate the destination address hash
|
||||
self.hash = Destination.hash(self.identity, app_name, *aspects)
|
||||
self.name_hash = RNS.Identity.full_hash(self.expand_name(None, app_name, *aspects).encode("utf-8"))[:(RNS.Identity.NAME_HASH_LENGTH//8)]
|
||||
self.hexhash = self.hash.hex()
|
||||
self.default_app_data = None
|
||||
|
||||
self.default_app_data = None
|
||||
self.callback = None
|
||||
self.proofcallback = None
|
||||
|
||||
@@ -137,7 +163,7 @@ class Destination:
|
||||
return "<"+self.name+"/"+self.hexhash+">"
|
||||
|
||||
|
||||
def announce(self, app_data=None, path_response=False):
|
||||
def announce(self, app_data=None, path_response=False, send=True):
|
||||
"""
|
||||
Creates an announce packet for this destination and broadcasts it on all
|
||||
relevant interfaces. Application specific data can be added to the announce.
|
||||
@@ -145,8 +171,11 @@ class Destination:
|
||||
:param app_data: *bytes* containing the app_data.
|
||||
:param path_response: Internal flag used by :ref:`RNS.Transport<api-transport>`. Ignore.
|
||||
"""
|
||||
if self.type != Destination.SINGLE:
|
||||
raise TypeError("Only SINGLE destination types can be announced")
|
||||
|
||||
destination_hash = self.hash
|
||||
random_hash = RNS.Identity.get_random_hash()
|
||||
random_hash = RNS.Identity.get_random_hash()[0:5]+int(time.time()).to_bytes(5, "big")
|
||||
|
||||
if app_data == None and self.default_app_data != None:
|
||||
if isinstance(self.default_app_data, bytes):
|
||||
@@ -156,13 +185,13 @@ class Destination:
|
||||
if isinstance(returned_app_data, bytes):
|
||||
app_data = returned_app_data
|
||||
|
||||
signed_data = self.hash+self.identity.get_public_key()+random_hash
|
||||
signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash
|
||||
if app_data != None:
|
||||
signed_data += app_data
|
||||
|
||||
signature = self.identity.sign(signed_data)
|
||||
|
||||
announce_data = self.identity.get_public_key()+random_hash+signature
|
||||
announce_data = self.identity.get_public_key()+self.name_hash+random_hash+signature
|
||||
|
||||
if app_data != None:
|
||||
announce_data += app_data
|
||||
@@ -172,15 +201,34 @@ class Destination:
|
||||
else:
|
||||
announce_context = RNS.Packet.NONE
|
||||
|
||||
RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context).send()
|
||||
announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context)
|
||||
|
||||
if send:
|
||||
announce_packet.send()
|
||||
else:
|
||||
return announce_packet
|
||||
|
||||
def accepts_links(self, accepts = None):
|
||||
"""
|
||||
Set or query whether the destination accepts incoming link requests.
|
||||
|
||||
:param accepts: If ``True`` or ``False``, this method sets whether the destination accepts incoming link requests. If not provided or ``None``, the method returns whether the destination currently accepts link requests.
|
||||
:returns: ``True`` or ``False`` depending on whether the destination accepts incoming link requests, if the *accepts* parameter is not provided or ``None``.
|
||||
"""
|
||||
if accepts == None:
|
||||
return self.accept_link_requests
|
||||
|
||||
if accepts:
|
||||
self.accept_link_requests = True
|
||||
else:
|
||||
self.accept_link_requests = False
|
||||
|
||||
def set_link_established_callback(self, callback):
|
||||
"""
|
||||
Registers a function to be called when a link has been established to
|
||||
this destination.
|
||||
|
||||
:param callback: A function or method to be called.
|
||||
:param callback: A function or method with the signature *callback(link)* to be called when a new link is established with this destination.
|
||||
"""
|
||||
self.callbacks.link_established = callback
|
||||
|
||||
@@ -189,7 +237,7 @@ class Destination:
|
||||
Registers a function to be called when a packet has been received by
|
||||
this destination.
|
||||
|
||||
:param callback: A function or method to be called.
|
||||
:param callback: A function or method with the signature *callback(data, packet)* to be called when this destination receives a packet.
|
||||
"""
|
||||
self.callbacks.packet = callback
|
||||
|
||||
@@ -199,7 +247,7 @@ class Destination:
|
||||
a packet sent to this destination. Allows control over when and if
|
||||
proofs should be returned for received packets.
|
||||
|
||||
:param callback: A function or method to be called. The callback must return one of True or False. If the callback returns True, a proof will be sent. If it returns False, a proof will not be sent.
|
||||
:param callback: A function or method to with the signature *callback(packet)* be called when a packet that requests a proof is received. The callback must return one of True or False. If the callback returns True, a proof will be sent. If it returns False, a proof will not be sent.
|
||||
"""
|
||||
self.callbacks.proof_requested = callback
|
||||
|
||||
@@ -262,12 +310,17 @@ class Destination:
|
||||
if plaintext != None:
|
||||
if packet.packet_type == RNS.Packet.DATA:
|
||||
if self.callbacks.packet != None:
|
||||
self.callbacks.packet(plaintext, packet)
|
||||
try:
|
||||
self.callbacks.packet(plaintext, packet)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing receive callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
def incoming_link_request(self, data, packet):
|
||||
link = RNS.Link.validate_request(self, data, packet)
|
||||
if link != None:
|
||||
self.links.append(link)
|
||||
if self.accept_link_requests:
|
||||
link = RNS.Link.validate_request(self, data, packet)
|
||||
if link != None:
|
||||
self.links.append(link)
|
||||
|
||||
def create_keys(self):
|
||||
"""
|
||||
@@ -282,8 +335,8 @@ class Destination:
|
||||
raise TypeError("A single destination holds keys through an Identity instance")
|
||||
|
||||
if self.type == Destination.GROUP:
|
||||
self.prv_bytes = base64.urlsafe_b64decode(Fernet.generate_key())
|
||||
self.prv = Fernet(base64.urlsafe_b64encode(self.prv_bytes))
|
||||
self.prv_bytes = Fernet.generate_key()
|
||||
self.prv = Fernet(self.prv_bytes)
|
||||
|
||||
|
||||
def get_private_key(self):
|
||||
@@ -315,7 +368,7 @@ class Destination:
|
||||
|
||||
if self.type == Destination.GROUP:
|
||||
self.prv_bytes = key
|
||||
self.prv = Fernet(base64.urlsafe_b64encode(self.prv_bytes))
|
||||
self.prv = Fernet(self.prv_bytes)
|
||||
|
||||
def load_public_key(self, key):
|
||||
if self.type != Destination.SINGLE:
|
||||
@@ -340,7 +393,7 @@ class Destination:
|
||||
if self.type == Destination.GROUP:
|
||||
if hasattr(self, "prv") and self.prv != None:
|
||||
try:
|
||||
return base64.urlsafe_b64decode(self.prv.encrypt(plaintext))
|
||||
return self.prv.encrypt(plaintext)
|
||||
except Exception as e:
|
||||
RNS.log("The GROUP destination could not encrypt data", RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -365,7 +418,7 @@ class Destination:
|
||||
if self.type == Destination.GROUP:
|
||||
if hasattr(self, "prv") and self.prv != None:
|
||||
try:
|
||||
return self.prv.decrypt(base64.urlsafe_b64encode(ciphertext))
|
||||
return self.prv.decrypt(ciphertext)
|
||||
except Exception as e:
|
||||
RNS.log("The GROUP destination could not decrypt data", RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
import base64
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import math
|
||||
import os
|
||||
import RNS
|
||||
import time
|
||||
import atexit
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
from .vendor import umsgpack as umsgpack
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
from RNS.Cryptography import X25519PrivateKey, X25519PublicKey, Ed25519PrivateKey, Ed25519PublicKey
|
||||
from RNS.Cryptography import Fernet
|
||||
|
||||
|
||||
class Identity:
|
||||
"""
|
||||
@@ -34,12 +53,12 @@ class Identity:
|
||||
"""
|
||||
|
||||
# Non-configurable constants
|
||||
FERNET_VERSION = 0x80
|
||||
FERNET_OVERHEAD = 54 # In bytes
|
||||
AES128_BLOCKSIZE = 16 # In bytes
|
||||
HASHLENGTH = 256 # In bits
|
||||
SIGLENGTH = KEYSIZE # In bits
|
||||
FERNET_OVERHEAD = RNS.Cryptography.Fernet.FERNET_OVERHEAD
|
||||
AES128_BLOCKSIZE = 16 # In bytes
|
||||
HASHLENGTH = 256 # In bits
|
||||
SIGLENGTH = KEYSIZE # In bits
|
||||
|
||||
NAME_HASH_LENGTH = 80
|
||||
TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH
|
||||
"""
|
||||
Constant specifying the truncated hash length (in bits) used by Reticulum
|
||||
@@ -72,6 +91,13 @@ class Identity:
|
||||
identity.app_data = identity_data[3]
|
||||
return identity
|
||||
else:
|
||||
for registered_destination in RNS.Transport.destinations:
|
||||
if destination_hash == registered_destination.hash:
|
||||
identity = Identity(create_keys=False)
|
||||
identity.load_public_key(registered_destination.identity.get_public_key())
|
||||
identity.app_data = None
|
||||
return identity
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@@ -90,7 +116,26 @@ class Identity:
|
||||
|
||||
@staticmethod
|
||||
def save_known_destinations():
|
||||
# TODO: Improve the storage method so we don't have to
|
||||
# deserialize and serialize the entire table on every
|
||||
# save, but the only changes. It might be possible to
|
||||
# simply overwrite on exit now that every local client
|
||||
# disconnect triggers a data persist.
|
||||
|
||||
try:
|
||||
if hasattr(Identity, "saving_known_destinations"):
|
||||
wait_interval = 0.2
|
||||
wait_timeout = 5
|
||||
wait_start = time.time()
|
||||
while Identity.saving_known_destinations:
|
||||
time.sleep(wait_interval)
|
||||
if time.time() > wait_start+wait_timeout:
|
||||
RNS.log("Could not save known destinations to storage, waiting for previous save operation timed out.", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
Identity.saving_known_destinations = True
|
||||
save_start = time.time()
|
||||
|
||||
storage_known_destinations = {}
|
||||
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
||||
try:
|
||||
@@ -104,21 +149,37 @@ class Identity:
|
||||
if not destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
|
||||
|
||||
RNS.log("Saving known destinations to storage...", RNS.LOG_VERBOSE)
|
||||
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_DEBUG)
|
||||
file = open(RNS.Reticulum.storagepath+"/known_destinations","wb")
|
||||
umsgpack.dump(Identity.known_destinations, file)
|
||||
file.close()
|
||||
RNS.log("Done saving known destinations to storage", RNS.LOG_VERBOSE)
|
||||
|
||||
save_time = time.time() - save_start
|
||||
if save_time < 1:
|
||||
time_str = str(round(save_time*1000,2))+"ms"
|
||||
else:
|
||||
time_str = str(round(save_time,2))+"s"
|
||||
|
||||
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_DEBUG)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while saving known destinations to disk, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
Identity.saving_known_destinations = False
|
||||
|
||||
@staticmethod
|
||||
def load_known_destinations():
|
||||
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
||||
try:
|
||||
file = open(RNS.Reticulum.storagepath+"/known_destinations","rb")
|
||||
Identity.known_destinations = umsgpack.load(file)
|
||||
loaded_known_destinations = umsgpack.load(file)
|
||||
file.close()
|
||||
|
||||
Identity.known_destinations = {}
|
||||
for known_destination in loaded_known_destinations:
|
||||
if len(known_destination) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
|
||||
Identity.known_destinations[known_destination] = loaded_known_destinations[known_destination]
|
||||
|
||||
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE)
|
||||
except:
|
||||
RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR)
|
||||
@@ -133,10 +194,7 @@ class Identity:
|
||||
:param data: Data to be hashed as *bytes*.
|
||||
:returns: SHA-256 hash as *bytes*
|
||||
"""
|
||||
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
|
||||
digest.update(data)
|
||||
|
||||
return digest.finalize()
|
||||
return RNS.Cryptography.sha256(data)
|
||||
|
||||
@staticmethod
|
||||
def truncated_hash(data):
|
||||
@@ -156,41 +214,75 @@ class Identity:
|
||||
:param data: Data to be hashed as *bytes*.
|
||||
:returns: Truncated SHA-256 hash of random data as *bytes*
|
||||
"""
|
||||
return Identity.truncated_hash(os.urandom(10))
|
||||
return Identity.truncated_hash(os.urandom(Identity.TRUNCATED_HASHLENGTH//8))
|
||||
|
||||
@staticmethod
|
||||
def validate_announce(packet):
|
||||
if packet.packet_type == RNS.Packet.ANNOUNCE:
|
||||
RNS.log("Validating announce from "+RNS.prettyhexrep(packet.destination_hash), RNS.LOG_DEBUG)
|
||||
destination_hash = packet.destination_hash
|
||||
public_key = packet.data[:Identity.KEYSIZE//8]
|
||||
random_hash = packet.data[Identity.KEYSIZE//8:Identity.KEYSIZE//8+10]
|
||||
signature = packet.data[Identity.KEYSIZE//8+10:Identity.KEYSIZE//8+10+Identity.KEYSIZE//8]
|
||||
app_data = b""
|
||||
if len(packet.data) > Identity.KEYSIZE//8+10+Identity.KEYSIZE//8:
|
||||
app_data = packet.data[Identity.KEYSIZE//8+10+Identity.KEYSIZE//8:]
|
||||
try:
|
||||
if packet.packet_type == RNS.Packet.ANNOUNCE:
|
||||
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+random_hash+app_data
|
||||
signed_data = destination_hash+public_key+name_hash+random_hash+app_data
|
||||
|
||||
if not len(packet.data) > Identity.KEYSIZE//8+10+Identity.KEYSIZE//8:
|
||||
app_data = None
|
||||
if not len(packet.data) > Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:
|
||||
app_data = None
|
||||
|
||||
announced_identity = Identity(create_keys=False)
|
||||
announced_identity.load_public_key(public_key)
|
||||
announced_identity = Identity(create_keys=False)
|
||||
announced_identity.load_public_key(public_key)
|
||||
|
||||
if announced_identity.pub != None and announced_identity.validate(signature, signed_data):
|
||||
RNS.Identity.remember(packet.get_hash(), destination_hash, public_key, app_data)
|
||||
RNS.log("Stored valid announce from "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG)
|
||||
del announced_identity
|
||||
return True
|
||||
else:
|
||||
RNS.log("Received invalid announce", RNS.LOG_DEBUG)
|
||||
del announced_identity
|
||||
return False
|
||||
if announced_identity.pub != None and announced_identity.validate(signature, signed_data):
|
||||
hash_material = name_hash+announced_identity.hash
|
||||
expected_hash = RNS.Identity.full_hash(hash_material)[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8]
|
||||
|
||||
if destination_hash == expected_hash:
|
||||
# Check if we already have a public key for this destination
|
||||
# and make sure the public key is not different.
|
||||
if destination_hash in Identity.known_destinations:
|
||||
if public_key != Identity.known_destinations[destination_hash][2]:
|
||||
# In reality, this should never occur, but in the odd case
|
||||
# that someone manages a hash collision, we reject the announce.
|
||||
RNS.log("Received announce with valid signature and destination hash, but announced public key does not match already known public key.", RNS.LOG_CRITICAL)
|
||||
RNS.log("This may indicate an attempt to modify network paths, or a random hash collision. The announce was rejected.", RNS.LOG_CRITICAL)
|
||||
return False
|
||||
|
||||
RNS.Identity.remember(packet.get_hash(), destination_hash, public_key, app_data)
|
||||
del announced_identity
|
||||
|
||||
if hasattr(packet, "transport_id") and packet.transport_id != None:
|
||||
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received via "+RNS.prettyhexrep(packet.transport_id)+" on "+str(packet.receiving_interface), RNS.LOG_EXTREME)
|
||||
else:
|
||||
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received on "+str(packet.receiving_interface), RNS.LOG_EXTREME)
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
RNS.log("Received invalid announce for "+RNS.prettyhexrep(destination_hash)+": Destination mismatch.", RNS.LOG_DEBUG)
|
||||
return False
|
||||
|
||||
else:
|
||||
RNS.log("Received invalid announce for "+RNS.prettyhexrep(destination_hash)+": Invalid signature.", RNS.LOG_DEBUG)
|
||||
del announced_identity
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error occurred while validating announce. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def persist_data():
|
||||
if not RNS.Transport.owner.is_connected_to_shared_instance:
|
||||
Identity.save_known_destinations()
|
||||
|
||||
@staticmethod
|
||||
def exit_handler():
|
||||
Identity.save_known_destinations()
|
||||
Identity.persist_data()
|
||||
|
||||
|
||||
@staticmethod
|
||||
@@ -262,30 +354,16 @@ class Identity:
|
||||
|
||||
def create_keys(self):
|
||||
self.prv = X25519PrivateKey.generate()
|
||||
self.prv_bytes = self.prv.private_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PrivateFormat.Raw,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
self.prv_bytes = self.prv.private_bytes()
|
||||
|
||||
self.sig_prv = Ed25519PrivateKey.generate()
|
||||
self.sig_prv_bytes = self.sig_prv.private_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PrivateFormat.Raw,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
self.sig_prv_bytes = self.sig_prv.private_bytes()
|
||||
|
||||
self.pub = self.prv.public_key()
|
||||
self.pub_bytes = self.pub.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
self.pub_bytes = self.pub.public_bytes()
|
||||
|
||||
self.sig_pub = self.sig_prv.public_key()
|
||||
self.sig_pub_bytes = self.sig_pub.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
self.sig_pub_bytes = self.sig_pub.public_bytes()
|
||||
|
||||
self.update_hashes()
|
||||
|
||||
@@ -317,16 +395,10 @@ class Identity:
|
||||
self.sig_prv = Ed25519PrivateKey.from_private_bytes(self.sig_prv_bytes)
|
||||
|
||||
self.pub = self.prv.public_key()
|
||||
self.pub_bytes = self.pub.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
self.pub_bytes = self.pub.public_bytes()
|
||||
|
||||
self.sig_pub = self.sig_prv.public_key()
|
||||
self.sig_pub_bytes = self.sig_pub.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
self.sig_pub_bytes = self.sig_pub.public_bytes()
|
||||
|
||||
self.update_hashes()
|
||||
|
||||
@@ -386,21 +458,19 @@ class Identity:
|
||||
"""
|
||||
if self.pub != None:
|
||||
ephemeral_key = X25519PrivateKey.generate()
|
||||
ephemeral_pub_bytes = ephemeral_key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
ephemeral_pub_bytes = ephemeral_key.public_key().public_bytes()
|
||||
|
||||
shared_key = ephemeral_key.exchange(self.pub)
|
||||
derived_key = derived_key = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
info=self.get_context(),
|
||||
).derive(shared_key)
|
||||
context=self.get_context(),
|
||||
)
|
||||
|
||||
fernet = Fernet(base64.urlsafe_b64encode(derived_key))
|
||||
ciphertext = base64.urlsafe_b64decode(fernet.encrypt(plaintext))
|
||||
fernet = Fernet(derived_key)
|
||||
ciphertext = fernet.encrypt(plaintext)
|
||||
token = ephemeral_pub_bytes+ciphertext
|
||||
|
||||
return token
|
||||
@@ -424,16 +494,17 @@ class Identity:
|
||||
peer_pub = X25519PublicKey.from_public_bytes(peer_pub_bytes)
|
||||
|
||||
shared_key = self.prv.exchange(peer_pub)
|
||||
derived_key = derived_key = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=self.get_salt(),
|
||||
info=self.get_context(),
|
||||
).derive(shared_key)
|
||||
|
||||
fernet = Fernet(base64.urlsafe_b64encode(derived_key))
|
||||
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(base64.urlsafe_b64encode(ciphertext))
|
||||
plaintext = fernet.decrypt(ciphertext)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
@@ -459,7 +530,7 @@ class Identity:
|
||||
return self.sig_prv.sign(message)
|
||||
except Exception as e:
|
||||
RNS.log("The identity "+str(self)+" could not sign the requested message. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
raise e
|
||||
raise e
|
||||
else:
|
||||
raise KeyError("Signing failed because identity does not hold a private key")
|
||||
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import serial
|
||||
import threading
|
||||
import time
|
||||
import RNS
|
||||
@@ -38,6 +58,7 @@ class AX25():
|
||||
|
||||
class AX25KISSInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
BITRATE_GUESS = 1200
|
||||
|
||||
owner = None
|
||||
port = None
|
||||
@@ -48,9 +69,20 @@ class AX25KISSInterface(Interface):
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, name, callsign, ssid, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control):
|
||||
import importlib
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
else:
|
||||
RNS.log("Using the AX.25 KISS interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 564
|
||||
|
||||
self.pyserial = serial
|
||||
self.serial = None
|
||||
self.owner = owner
|
||||
self.name = name
|
||||
@@ -65,6 +97,7 @@ class AX25KISSInterface(Interface):
|
||||
self.stopbits = stopbits
|
||||
self.timeout = 100
|
||||
self.online = False
|
||||
self.bitrate = KISSInterface.BITRATE_GUESS
|
||||
|
||||
self.packet_queue = []
|
||||
self.flow_control = flow_control
|
||||
@@ -90,44 +123,48 @@ class AX25KISSInterface(Interface):
|
||||
self.parity = serial.PARITY_ODD
|
||||
|
||||
try:
|
||||
RNS.log("Opening serial port "+self.port+"...")
|
||||
self.serial = serial.Serial(
|
||||
port = self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = 0,
|
||||
inter_byte_timeout = None,
|
||||
write_timeout = None,
|
||||
dsrdtr = False,
|
||||
)
|
||||
self.open_port()
|
||||
except Exception as e:
|
||||
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
if self.serial.is_open:
|
||||
# Allow time for interface to initialise before config
|
||||
sleep(2.0)
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
thread.setDaemon(True)
|
||||
thread.start()
|
||||
self.online = True
|
||||
RNS.log("Serial port "+self.port+" is now open")
|
||||
RNS.log("Configuring AX.25 KISS interface parameters...")
|
||||
self.setPreamble(self.preamble)
|
||||
self.setTxTail(self.txtail)
|
||||
self.setPersistence(self.persistence)
|
||||
self.setSlotTime(self.slottime)
|
||||
self.setFlowControl(self.flow_control)
|
||||
self.interface_ready = True
|
||||
RNS.log("AX.25 KISS interface configured")
|
||||
sleep(2)
|
||||
self.configure_device()
|
||||
else:
|
||||
raise IOError("Could not open serial port")
|
||||
|
||||
def open_port(self):
|
||||
RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE)
|
||||
self.serial = self.pyserial.Serial(
|
||||
port = self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = 0,
|
||||
inter_byte_timeout = None,
|
||||
write_timeout = None,
|
||||
dsrdtr = False,
|
||||
)
|
||||
|
||||
def configure_device(self):
|
||||
# Allow time for interface to initialise before config
|
||||
sleep(2.0)
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
self.online = True
|
||||
RNS.log("Serial port "+self.port+" is now open")
|
||||
RNS.log("Configuring AX.25 KISS interface parameters...")
|
||||
self.setPreamble(self.preamble)
|
||||
self.setTxTail(self.txtail)
|
||||
self.setPersistence(self.persistence)
|
||||
self.setSlotTime(self.slottime)
|
||||
self.setFlowControl(self.flow_control)
|
||||
self.interface_ready = True
|
||||
RNS.log("AX.25 KISS interface configured")
|
||||
|
||||
def setPreamble(self, preamble):
|
||||
preamble_ms = preamble
|
||||
@@ -269,7 +306,7 @@ class AX25KISSInterface(Interface):
|
||||
in_frame = True
|
||||
command = KISS.CMD_UNKNOWN
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU+AX25.HEADER_SIZE):
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU+AX25.HEADER_SIZE):
|
||||
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
|
||||
# We only support one HDLC port for now, so
|
||||
# strip off the port nibble
|
||||
@@ -287,8 +324,6 @@ class AX25KISSInterface(Interface):
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
elif (command == KISS.CMD_READY):
|
||||
# TODO: add timeout and reset if ready
|
||||
# command never arrives
|
||||
self.process_queue()
|
||||
else:
|
||||
time_since_last = int(time.time()*1000) - last_read_ms
|
||||
@@ -308,10 +343,29 @@ class AX25KISSInterface(Interface):
|
||||
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 being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
|
||||
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
RNS.panic()
|
||||
|
||||
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
|
||||
|
||||
self.online = False
|
||||
self.serial.close()
|
||||
self.reconnect_port()
|
||||
|
||||
def reconnect_port(self):
|
||||
while not self.online:
|
||||
try:
|
||||
time.sleep(5)
|
||||
RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
self.open_port()
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
except Exception as e:
|
||||
RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
def __str__(self):
|
||||
return "AX25KISSInterface["+self.name+"]"
|
||||
@@ -0,0 +1,407 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import RNS
|
||||
|
||||
class KISS():
|
||||
FEND = 0xC0
|
||||
FESC = 0xDB
|
||||
TFEND = 0xDC
|
||||
TFESC = 0xDD
|
||||
CMD_UNKNOWN = 0xFE
|
||||
CMD_DATA = 0x00
|
||||
CMD_TXDELAY = 0x01
|
||||
CMD_P = 0x02
|
||||
CMD_SLOTTIME = 0x03
|
||||
CMD_TXTAIL = 0x04
|
||||
CMD_FULLDUPLEX = 0x05
|
||||
CMD_SETHARDWARE = 0x06
|
||||
CMD_READY = 0x0F
|
||||
CMD_RETURN = 0xFF
|
||||
|
||||
@staticmethod
|
||||
def escape(data):
|
||||
data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
|
||||
data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
|
||||
return data
|
||||
|
||||
class KISSInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
BITRATE_GUESS = 1200
|
||||
|
||||
owner = None
|
||||
port = None
|
||||
speed = None
|
||||
databits = None
|
||||
parity = None
|
||||
stopbits = None
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control, beacon_interval, beacon_data):
|
||||
import importlib
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
self.on_android = True
|
||||
if importlib.util.find_spec('usbserial4a') != None:
|
||||
if importlib.util.find_spec('jnius') == None:
|
||||
RNS.log("Could not load jnius API wrapper for Android, KISS interface cannot be created.", RNS.LOG_CRITICAL)
|
||||
RNS.log("This probably means you are trying to use an USB-based interface from within Termux or similar.", RNS.LOG_CRITICAL)
|
||||
RNS.log("This is currently not possible, due to this environment limiting access to the native Android APIs.", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
from usbserial4a import serial4a as serial
|
||||
self.parity = "N"
|
||||
|
||||
else:
|
||||
RNS.log("Could not load USB serial module for Android, KISS interface cannot be created.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install this module by issuing: pip install usbserial4a", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
else:
|
||||
raise SystemError("Android-specific interface was used on non-Android OS")
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 564
|
||||
|
||||
if beacon_data == None:
|
||||
beacon_data = ""
|
||||
|
||||
self.pyserial = serial
|
||||
self.serial = None
|
||||
self.owner = owner
|
||||
self.name = name
|
||||
self.port = port
|
||||
self.speed = speed
|
||||
self.databits = databits
|
||||
self.parity = "N"
|
||||
self.stopbits = stopbits
|
||||
self.timeout = 100
|
||||
self.online = False
|
||||
self.beacon_i = beacon_interval
|
||||
self.beacon_d = beacon_data.encode("utf-8")
|
||||
self.first_tx = None
|
||||
self.bitrate = KISSInterface.BITRATE_GUESS
|
||||
|
||||
self.packet_queue = []
|
||||
self.flow_control = flow_control
|
||||
self.interface_ready = False
|
||||
self.flow_control_timeout = 5
|
||||
self.flow_control_locked = time.time()
|
||||
|
||||
self.preamble = preamble if preamble != None else 350;
|
||||
self.txtail = txtail if txtail != None else 20;
|
||||
self.persistence = persistence if persistence != None else 64;
|
||||
self.slottime = slottime if slottime != None else 20;
|
||||
|
||||
if parity.lower() == "e" or parity.lower() == "even":
|
||||
self.parity = "E"
|
||||
|
||||
if parity.lower() == "o" or parity.lower() == "odd":
|
||||
self.parity = "O"
|
||||
|
||||
try:
|
||||
self.open_port()
|
||||
except Exception as e:
|
||||
RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
else:
|
||||
raise IOError("Could not open serial port")
|
||||
|
||||
|
||||
def open_port(self):
|
||||
RNS.log("Opening serial port "+self.port+"...")
|
||||
# Get device parameters
|
||||
from usb4a import usb
|
||||
device = usb.get_usb_device(self.port)
|
||||
if device:
|
||||
vid = device.getVendorId()
|
||||
pid = device.getProductId()
|
||||
|
||||
# Driver overrides for speficic chips
|
||||
proxy = self.pyserial.get_serial_port
|
||||
if vid == 0x1A86 and pid == 0x55D4:
|
||||
# Force CDC driver for Qinheng CH34x
|
||||
RNS.log(str(self)+" using CDC driver for "+RNS.hexrep(vid)+":"+RNS.hexrep(pid), RNS.LOG_DEBUG)
|
||||
from usbserial4a.cdcacmserial4a import CdcAcmSerial
|
||||
proxy = CdcAcmSerial
|
||||
|
||||
self.serial = proxy(
|
||||
self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = None,
|
||||
inter_byte_timeout = None,
|
||||
# write_timeout = wtimeout,
|
||||
dsrdtr = False,
|
||||
)
|
||||
|
||||
if vid == 0x0403:
|
||||
# Hardware parameters for FTDI devices @ 115200 baud
|
||||
self.serial.DEFAULT_READ_BUFFER_SIZE = 16 * 1024
|
||||
self.serial.USB_READ_TIMEOUT_MILLIS = 100
|
||||
self.serial.timeout = 0.1
|
||||
elif vid == 0x10C4:
|
||||
# Hardware parameters for SiLabs CP210x @ 115200 baud
|
||||
self.serial.DEFAULT_READ_BUFFER_SIZE = 64
|
||||
self.serial.USB_READ_TIMEOUT_MILLIS = 12
|
||||
self.serial.timeout = 0.012
|
||||
elif vid == 0x1A86 and pid == 0x55D4:
|
||||
# Hardware parameters for Qinheng CH34x @ 115200 baud
|
||||
self.serial.DEFAULT_READ_BUFFER_SIZE = 64
|
||||
self.serial.USB_READ_TIMEOUT_MILLIS = 12
|
||||
self.serial.timeout = 0.1
|
||||
else:
|
||||
# Default values
|
||||
self.serial.DEFAULT_READ_BUFFER_SIZE = 1 * 1024
|
||||
self.serial.USB_READ_TIMEOUT_MILLIS = 100
|
||||
self.serial.timeout = 0.1
|
||||
|
||||
RNS.log(str(self)+" USB read buffer size set to "+RNS.prettysize(self.serial.DEFAULT_READ_BUFFER_SIZE), RNS.LOG_DEBUG)
|
||||
RNS.log(str(self)+" USB read timeout set to "+str(self.serial.USB_READ_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
|
||||
RNS.log(str(self)+" USB write timeout set to "+str(self.serial.USB_WRITE_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
|
||||
|
||||
def configure_device(self):
|
||||
# Allow time for interface to initialise before config
|
||||
sleep(2.0)
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
self.online = True
|
||||
RNS.log("Serial port "+self.port+" is now open")
|
||||
RNS.log("Configuring KISS interface parameters...")
|
||||
self.setPreamble(self.preamble)
|
||||
self.setTxTail(self.txtail)
|
||||
self.setPersistence(self.persistence)
|
||||
self.setSlotTime(self.slottime)
|
||||
self.setFlowControl(self.flow_control)
|
||||
self.interface_ready = True
|
||||
RNS.log("KISS interface configured")
|
||||
|
||||
def setPreamble(self, preamble):
|
||||
preamble_ms = preamble
|
||||
preamble = int(preamble_ms / 10)
|
||||
if preamble < 0:
|
||||
preamble = 0
|
||||
if preamble > 255:
|
||||
preamble = 255
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("Could not configure KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")")
|
||||
|
||||
def setTxTail(self, txtail):
|
||||
txtail_ms = txtail
|
||||
txtail = int(txtail_ms / 10)
|
||||
if txtail < 0:
|
||||
txtail = 0
|
||||
if txtail > 255:
|
||||
txtail = 255
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("Could not configure KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")")
|
||||
|
||||
def setPersistence(self, persistence):
|
||||
if persistence < 0:
|
||||
persistence = 0
|
||||
if persistence > 255:
|
||||
persistence = 255
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("Could not configure KISS interface persistence to "+str(persistence))
|
||||
|
||||
def setSlotTime(self, slottime):
|
||||
slottime_ms = slottime
|
||||
slottime = int(slottime_ms / 10)
|
||||
if slottime < 0:
|
||||
slottime = 0
|
||||
if slottime > 255:
|
||||
slottime = 255
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("Could not configure KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")")
|
||||
|
||||
def setFlowControl(self, flow_control):
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
if (flow_control):
|
||||
raise IOError("Could not enable KISS interface flow control")
|
||||
else:
|
||||
raise IOError("Could not enable KISS interface flow control")
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
self.rxb += len(data)
|
||||
def af():
|
||||
self.owner.inbound(data, self)
|
||||
threading.Thread(target=af, daemon=True).start()
|
||||
|
||||
def processOutgoing(self,data):
|
||||
datalen = len(data)
|
||||
if self.online:
|
||||
if self.interface_ready:
|
||||
if self.flow_control:
|
||||
self.interface_ready = False
|
||||
self.flow_control_locked = time.time()
|
||||
|
||||
data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd]))
|
||||
data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc]))
|
||||
frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND])
|
||||
|
||||
written = self.serial.write(frame)
|
||||
self.txb += datalen
|
||||
|
||||
if data == self.beacon_d:
|
||||
self.first_tx = None
|
||||
else:
|
||||
if self.first_tx == None:
|
||||
self.first_tx = time.time()
|
||||
|
||||
if written != len(frame):
|
||||
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
|
||||
|
||||
else:
|
||||
self.queue(data)
|
||||
|
||||
def queue(self, data):
|
||||
self.packet_queue.append(data)
|
||||
|
||||
def process_queue(self):
|
||||
if len(self.packet_queue) > 0:
|
||||
data = self.packet_queue.pop(0)
|
||||
self.interface_ready = True
|
||||
self.processOutgoing(data)
|
||||
elif len(self.packet_queue) == 0:
|
||||
self.interface_ready = True
|
||||
|
||||
def readLoop(self):
|
||||
try:
|
||||
in_frame = False
|
||||
escape = False
|
||||
command = KISS.CMD_UNKNOWN
|
||||
data_buffer = b""
|
||||
last_read_ms = int(time.time()*1000)
|
||||
|
||||
while self.serial.is_open:
|
||||
serial_bytes = self.serial.read()
|
||||
got = len(serial_bytes)
|
||||
|
||||
for byte in serial_bytes:
|
||||
last_read_ms = int(time.time()*1000)
|
||||
|
||||
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
elif (byte == KISS.FEND):
|
||||
in_frame = True
|
||||
command = KISS.CMD_UNKNOWN
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
|
||||
# We only support one HDLC port for now, so
|
||||
# strip off the port nibble
|
||||
byte = byte & 0x0F
|
||||
command = byte
|
||||
elif (command == KISS.CMD_DATA):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
elif (command == KISS.CMD_READY):
|
||||
self.process_queue()
|
||||
|
||||
if got == 0:
|
||||
time_since_last = int(time.time()*1000) - last_read_ms
|
||||
if len(data_buffer) > 0 and time_since_last > self.timeout:
|
||||
data_buffer = b""
|
||||
in_frame = False
|
||||
command = KISS.CMD_UNKNOWN
|
||||
escape = False
|
||||
sleep(0.05)
|
||||
|
||||
if self.flow_control:
|
||||
if not self.interface_ready:
|
||||
if time.time() > self.flow_control_locked + self.flow_control_timeout:
|
||||
RNS.log("Interface "+str(self)+" is unlocking flow control due to time-out. This should not happen. Your hardware might have missed a flow-control READY command, or maybe it does not support flow-control.", RNS.LOG_WARNING)
|
||||
self.process_queue()
|
||||
|
||||
if self.beacon_i != None and self.beacon_d != None:
|
||||
if self.first_tx != None:
|
||||
if time.time() > self.first_tx + self.beacon_i:
|
||||
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.beacon_d.decode("utf-8")), RNS.LOG_DEBUG)
|
||||
self.first_tx = None
|
||||
self.processOutgoing(self.beacon_d)
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
|
||||
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
RNS.panic()
|
||||
|
||||
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
|
||||
|
||||
self.online = False
|
||||
self.serial.close()
|
||||
self.reconnect_port()
|
||||
|
||||
def reconnect_port(self):
|
||||
while not self.online:
|
||||
try:
|
||||
time.sleep(5)
|
||||
RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
self.open_port()
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
except Exception as e:
|
||||
RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
def __str__(self):
|
||||
return "KISSInterface["+self.name+"]"
|
||||
@@ -0,0 +1,258 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import RNS
|
||||
|
||||
class HDLC():
|
||||
# The Serial Interface packetizes data using
|
||||
# simplified HDLC framing, similar to PPP
|
||||
FLAG = 0x7E
|
||||
ESC = 0x7D
|
||||
ESC_MASK = 0x20
|
||||
|
||||
@staticmethod
|
||||
def escape(data):
|
||||
data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
|
||||
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
|
||||
return data
|
||||
|
||||
class SerialInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
|
||||
owner = None
|
||||
port = None
|
||||
speed = None
|
||||
databits = None
|
||||
parity = None
|
||||
stopbits = None
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, name, port, speed, databits, parity, stopbits):
|
||||
import importlib
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
self.on_android = True
|
||||
if importlib.util.find_spec('usbserial4a') != None:
|
||||
if importlib.util.find_spec('jnius') == None:
|
||||
RNS.log("Could not load jnius API wrapper for Android, Serial interface cannot be created.", RNS.LOG_CRITICAL)
|
||||
RNS.log("This probably means you are trying to use an USB-based interface from within Termux or similar.", RNS.LOG_CRITICAL)
|
||||
RNS.log("This is currently not possible, due to this environment limiting access to the native Android APIs.", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
from usbserial4a import serial4a as serial
|
||||
self.parity = "N"
|
||||
|
||||
else:
|
||||
RNS.log("Could not load USB serial module for Android, Serial interface cannot be created.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install this module by issuing: pip install usbserial4a", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
else:
|
||||
raise SystemError("Android-specific interface was used on non-Android OS")
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 564
|
||||
|
||||
self.pyserial = serial
|
||||
self.serial = None
|
||||
self.owner = owner
|
||||
self.name = name
|
||||
self.port = port
|
||||
self.speed = speed
|
||||
self.databits = databits
|
||||
self.parity = "N"
|
||||
self.stopbits = stopbits
|
||||
self.timeout = 100
|
||||
self.online = False
|
||||
self.bitrate = self.speed
|
||||
|
||||
if parity.lower() == "e" or parity.lower() == "even":
|
||||
self.parity = "E"
|
||||
|
||||
if parity.lower() == "o" or parity.lower() == "odd":
|
||||
self.parity = "O"
|
||||
|
||||
try:
|
||||
self.open_port()
|
||||
except Exception as e:
|
||||
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
else:
|
||||
raise IOError("Could not open serial port")
|
||||
|
||||
|
||||
def open_port(self):
|
||||
RNS.log("Opening serial port "+self.port+"...")
|
||||
# Get device parameters
|
||||
from usb4a import usb
|
||||
device = usb.get_usb_device(self.port)
|
||||
if device:
|
||||
vid = device.getVendorId()
|
||||
pid = device.getProductId()
|
||||
|
||||
# Driver overrides for speficic chips
|
||||
proxy = self.pyserial.get_serial_port
|
||||
if vid == 0x1A86 and pid == 0x55D4:
|
||||
# Force CDC driver for Qinheng CH34x
|
||||
RNS.log(str(self)+" using CDC driver for "+RNS.hexrep(vid)+":"+RNS.hexrep(pid), RNS.LOG_DEBUG)
|
||||
from usbserial4a.cdcacmserial4a import CdcAcmSerial
|
||||
proxy = CdcAcmSerial
|
||||
|
||||
self.serial = proxy(
|
||||
self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = None,
|
||||
inter_byte_timeout = None,
|
||||
# write_timeout = wtimeout,
|
||||
dsrdtr = False,
|
||||
)
|
||||
|
||||
if vid == 0x0403:
|
||||
# Hardware parameters for FTDI devices @ 115200 baud
|
||||
self.serial.DEFAULT_READ_BUFFER_SIZE = 16 * 1024
|
||||
self.serial.USB_READ_TIMEOUT_MILLIS = 100
|
||||
self.serial.timeout = 0.1
|
||||
elif vid == 0x10C4:
|
||||
# Hardware parameters for SiLabs CP210x @ 115200 baud
|
||||
self.serial.DEFAULT_READ_BUFFER_SIZE = 64
|
||||
self.serial.USB_READ_TIMEOUT_MILLIS = 12
|
||||
self.serial.timeout = 0.012
|
||||
elif vid == 0x1A86 and pid == 0x55D4:
|
||||
# Hardware parameters for Qinheng CH34x @ 115200 baud
|
||||
self.serial.DEFAULT_READ_BUFFER_SIZE = 64
|
||||
self.serial.USB_READ_TIMEOUT_MILLIS = 12
|
||||
self.serial.timeout = 0.1
|
||||
else:
|
||||
# Default values
|
||||
self.serial.DEFAULT_READ_BUFFER_SIZE = 1 * 1024
|
||||
self.serial.USB_READ_TIMEOUT_MILLIS = 100
|
||||
self.serial.timeout = 0.1
|
||||
|
||||
RNS.log(str(self)+" USB read buffer size set to "+RNS.prettysize(self.serial.DEFAULT_READ_BUFFER_SIZE), RNS.LOG_DEBUG)
|
||||
RNS.log(str(self)+" USB read timeout set to "+str(self.serial.USB_READ_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
|
||||
RNS.log(str(self)+" USB write timeout set to "+str(self.serial.USB_WRITE_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
|
||||
|
||||
def configure_device(self):
|
||||
sleep(0.5)
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
self.online = True
|
||||
RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
self.rxb += len(data)
|
||||
def af():
|
||||
self.owner.inbound(data, self)
|
||||
threading.Thread(target=af, daemon=True).start()
|
||||
|
||||
def processOutgoing(self,data):
|
||||
if self.online:
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
written = self.serial.write(data)
|
||||
self.txb += len(data)
|
||||
if written != len(data):
|
||||
raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
|
||||
|
||||
def readLoop(self):
|
||||
try:
|
||||
in_frame = False
|
||||
escape = False
|
||||
data_buffer = b""
|
||||
last_read_ms = int(time.time()*1000)
|
||||
|
||||
while self.serial.is_open:
|
||||
serial_bytes = self.serial.read()
|
||||
got = len(serial_bytes)
|
||||
|
||||
for byte in serial_bytes:
|
||||
last_read_ms = int(time.time()*1000)
|
||||
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (byte == HDLC.ESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.FLAG
|
||||
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.ESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
|
||||
if got == 0:
|
||||
time_since_last = int(time.time()*1000) - last_read_ms
|
||||
if len(data_buffer) > 0 and time_since_last > self.timeout:
|
||||
data_buffer = b""
|
||||
in_frame = False
|
||||
escape = False
|
||||
# sleep(0.08)
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
|
||||
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
RNS.panic()
|
||||
|
||||
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
|
||||
|
||||
self.online = False
|
||||
self.serial.close()
|
||||
self.reconnect_port()
|
||||
|
||||
def reconnect_port(self):
|
||||
while not self.online:
|
||||
try:
|
||||
time.sleep(5)
|
||||
RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
self.open_port()
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
except Exception as e:
|
||||
RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
def __str__(self):
|
||||
return "SerialInterface["+self.name+"]"
|
||||
@@ -0,0 +1,27 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
import glob
|
||||
|
||||
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
|
||||
@@ -0,0 +1,376 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
import socketserver
|
||||
import threading
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
import sys
|
||||
import RNS
|
||||
|
||||
|
||||
class AutoInterface(Interface):
|
||||
DEFAULT_DISCOVERY_PORT = 29716
|
||||
DEFAULT_DATA_PORT = 42671
|
||||
DEFAULT_GROUP_ID = "reticulum".encode("utf-8")
|
||||
|
||||
SCOPE_LINK = "2"
|
||||
SCOPE_ADMIN = "4"
|
||||
SCOPE_SITE = "5"
|
||||
SCOPE_ORGANISATION = "8"
|
||||
SCOPE_GLOBAL = "e"
|
||||
|
||||
PEERING_TIMEOUT = 7.5
|
||||
|
||||
DARWIN_IGNORE_IFS = ["awdl0", "llw0", "lo0", "en5"]
|
||||
ANDROID_IGNORE_IFS = ["dummy0", "lo", "tun0"]
|
||||
|
||||
BITRATE_GUESS = 10*1000*1000
|
||||
|
||||
def __init__(self, owner, name, group_id=None, discovery_scope=None, discovery_port=None, data_port=None, allowed_interfaces=None, ignored_interfaces=None, configured_bitrate=None):
|
||||
import importlib
|
||||
if importlib.util.find_spec('netifaces') != None:
|
||||
import netifaces
|
||||
else:
|
||||
RNS.log("Using AutoInterface requires the netifaces module.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install it with the command: python3 -m pip install netifaces", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.netifaces = netifaces
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.name = name
|
||||
self.online = False
|
||||
self.peers = {}
|
||||
self.link_local_addresses = []
|
||||
self.adopted_interfaces = {}
|
||||
self.multicast_echoes = {}
|
||||
self.timed_out_interfaces = {}
|
||||
|
||||
self.outbound_udp_socket = None
|
||||
|
||||
self.announce_rate_target = None
|
||||
self.announce_interval = AutoInterface.PEERING_TIMEOUT/6.0
|
||||
self.peer_job_interval = AutoInterface.PEERING_TIMEOUT*1.1
|
||||
self.peering_timeout = AutoInterface.PEERING_TIMEOUT
|
||||
self.multicast_echo_timeout = AutoInterface.PEERING_TIMEOUT/2
|
||||
|
||||
if allowed_interfaces == None:
|
||||
self.allowed_interfaces = []
|
||||
else:
|
||||
self.allowed_interfaces = allowed_interfaces
|
||||
|
||||
if ignored_interfaces == None:
|
||||
self.ignored_interfaces = []
|
||||
else:
|
||||
self.ignored_interfaces = ignored_interfaces
|
||||
|
||||
if group_id == None:
|
||||
self.group_id = AutoInterface.DEFAULT_GROUP_ID
|
||||
else:
|
||||
self.group_id = group_id.encode("utf-8")
|
||||
|
||||
if discovery_port == None:
|
||||
self.discovery_port = AutoInterface.DEFAULT_DISCOVERY_PORT
|
||||
else:
|
||||
self.discovery_port = discovery_port
|
||||
|
||||
if data_port == None:
|
||||
self.data_port = AutoInterface.DEFAULT_DATA_PORT
|
||||
else:
|
||||
self.data_port = data_port
|
||||
|
||||
if discovery_scope == None:
|
||||
self.discovery_scope = AutoInterface.SCOPE_LINK
|
||||
elif str(discovery_scope).lower() == "link":
|
||||
self.discovery_scope = AutoInterface.SCOPE_LINK
|
||||
elif str(discovery_scope).lower() == "admin":
|
||||
self.discovery_scope = AutoInterface.SCOPE_ADMIN
|
||||
elif str(discovery_scope).lower() == "site":
|
||||
self.discovery_scope = AutoInterface.SCOPE_SITE
|
||||
elif str(discovery_scope).lower() == "organisation":
|
||||
self.discovery_scope = AutoInterface.SCOPE_ORGANISATION
|
||||
elif str(discovery_scope).lower() == "global":
|
||||
self.discovery_scope = AutoInterface.SCOPE_GLOBAL
|
||||
|
||||
self.group_hash = RNS.Identity.full_hash(self.group_id)
|
||||
g = self.group_hash
|
||||
#gt = "{:02x}".format(g[1]+(g[0]<<8))
|
||||
gt = "0"
|
||||
gt += ":"+"{:02x}".format(g[3]+(g[2]<<8))
|
||||
gt += ":"+"{:02x}".format(g[5]+(g[4]<<8))
|
||||
gt += ":"+"{:02x}".format(g[7]+(g[6]<<8))
|
||||
gt += ":"+"{:02x}".format(g[9]+(g[8]<<8))
|
||||
gt += ":"+"{:02x}".format(g[11]+(g[10]<<8))
|
||||
gt += ":"+"{:02x}".format(g[13]+(g[12]<<8))
|
||||
self.mcast_discovery_address = "ff1"+self.discovery_scope+":"+gt
|
||||
|
||||
suitable_interfaces = 0
|
||||
for ifname in self.netifaces.interfaces():
|
||||
if RNS.vendor.platformutils.is_darwin() and ifname in AutoInterface.DARWIN_IGNORE_IFS and not ifname in self.allowed_interfaces:
|
||||
RNS.log(str(self)+" skipping Darwin AWDL or tethering interface "+str(ifname), RNS.LOG_EXTREME)
|
||||
elif RNS.vendor.platformutils.is_darwin() and ifname == "lo0":
|
||||
RNS.log(str(self)+" skipping Darwin loopback interface "+str(ifname), RNS.LOG_EXTREME)
|
||||
elif RNS.vendor.platformutils.is_android() and ifname in AutoInterface.ANDROID_IGNORE_IFS and not ifname in self.allowed_interfaces:
|
||||
RNS.log(str(self)+" skipping Android system interface "+str(ifname), RNS.LOG_EXTREME)
|
||||
elif ifname in self.ignored_interfaces:
|
||||
RNS.log(str(self)+" ignoring disallowed interface "+str(ifname), RNS.LOG_EXTREME)
|
||||
else:
|
||||
if len(self.allowed_interfaces) > 0 and not ifname in self.allowed_interfaces:
|
||||
RNS.log(str(self)+" ignoring interface "+str(ifname)+" since it was not allowed", RNS.LOG_EXTREME)
|
||||
else:
|
||||
addresses = self.netifaces.ifaddresses(ifname)
|
||||
if self.netifaces.AF_INET6 in addresses:
|
||||
link_local_addr = None
|
||||
for address in addresses[self.netifaces.AF_INET6]:
|
||||
if "addr" in address:
|
||||
if address["addr"].startswith("fe80:"):
|
||||
link_local_addr = address["addr"]
|
||||
self.link_local_addresses.append(link_local_addr.split("%")[0])
|
||||
self.adopted_interfaces[ifname] = link_local_addr.split("%")[0]
|
||||
self.multicast_echoes[ifname] = time.time()
|
||||
RNS.log(str(self)+" Selecting link-local address "+str(link_local_addr)+" for interface "+str(ifname), RNS.LOG_EXTREME)
|
||||
|
||||
if link_local_addr == None:
|
||||
RNS.log(str(self)+" No link-local IPv6 address configured for "+str(ifname)+", skipping interface", RNS.LOG_EXTREME)
|
||||
else:
|
||||
mcast_addr = self.mcast_discovery_address
|
||||
RNS.log(str(self)+" Creating multicast discovery listener on "+str(ifname)+" with address "+str(mcast_addr), RNS.LOG_EXTREME)
|
||||
|
||||
# Struct with interface index
|
||||
if_struct = struct.pack("I", socket.if_nametoindex(ifname))
|
||||
|
||||
# Set up multicast socket
|
||||
discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
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
|
||||
addr_info = socket.getaddrinfo(mcast_addr+"%"+ifname, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
discovery_socket.bind(addr_info[0][4])
|
||||
|
||||
# Set up thread for discovery packets
|
||||
def discovery_loop():
|
||||
self.discovery_handler(discovery_socket, ifname)
|
||||
|
||||
thread = threading.Thread(target=discovery_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
suitable_interfaces += 1
|
||||
|
||||
if suitable_interfaces == 0:
|
||||
RNS.log(str(self)+" could not autoconfigure. This interface currently provides no connectivity.", RNS.LOG_WARNING)
|
||||
else:
|
||||
self.receives = True
|
||||
|
||||
peering_wait = self.announce_interval*1.2
|
||||
RNS.log(str(self)+" discovering peers for "+str(round(peering_wait, 2))+" seconds...", RNS.LOG_VERBOSE)
|
||||
|
||||
def handlerFactory(callback):
|
||||
def createHandler(*args, **keys):
|
||||
return AutoInterfaceHandler(callback, *args, **keys)
|
||||
return createHandler
|
||||
|
||||
self.owner = owner
|
||||
socketserver.UDPServer.address_family = socket.AF_INET6
|
||||
|
||||
for ifname in self.adopted_interfaces:
|
||||
local_addr = self.adopted_interfaces[ifname]+"%"+ifname
|
||||
addr_info = socket.getaddrinfo(local_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
address = addr_info[0][4]
|
||||
|
||||
self.server = socketserver.UDPServer(address, handlerFactory(self.processIncoming))
|
||||
|
||||
thread = threading.Thread(target=self.server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
job_thread = threading.Thread(target=self.peer_jobs)
|
||||
job_thread.daemon = True
|
||||
job_thread.start()
|
||||
|
||||
time.sleep(peering_wait)
|
||||
|
||||
if configured_bitrate != None:
|
||||
self.bitrate = configured_bitrate
|
||||
else:
|
||||
self.bitrate = AutoInterface.BITRATE_GUESS
|
||||
|
||||
self.online = True
|
||||
|
||||
|
||||
def discovery_handler(self, socket, ifname):
|
||||
def announce_loop():
|
||||
self.announce_handler(ifname)
|
||||
|
||||
thread = threading.Thread(target=announce_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
while True:
|
||||
data, ipv6_src = socket.recvfrom(1024)
|
||||
expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
|
||||
if data == expected_hash:
|
||||
self.add_peer(ipv6_src[0], ifname)
|
||||
else:
|
||||
RNS.log(str(self)+" received peering packet on "+str(ifname)+" from "+str(ipv6_src[0])+", but authentication hash was incorrect.", RNS.LOG_DEBUG)
|
||||
|
||||
def peer_jobs(self):
|
||||
while True:
|
||||
time.sleep(self.peer_job_interval)
|
||||
now = time.time()
|
||||
timed_out_peers = []
|
||||
|
||||
# Check for timed out peers
|
||||
for peer_addr in self.peers:
|
||||
peer = self.peers[peer_addr]
|
||||
last_heard = peer[1]
|
||||
if now > last_heard+self.peering_timeout:
|
||||
timed_out_peers.append(peer_addr)
|
||||
|
||||
# Remove any timed out peers
|
||||
for peer_addr in timed_out_peers:
|
||||
removed_peer = self.peers.pop(peer_addr)
|
||||
RNS.log(str(self)+" removed peer "+str(peer_addr)+" on "+str(removed_peer[0]), RNS.LOG_DEBUG)
|
||||
|
||||
for ifname in self.adopted_interfaces:
|
||||
# Check that the link-local address has not changed
|
||||
try:
|
||||
addresses = self.netifaces.ifaddresses(ifname)
|
||||
if self.netifaces.AF_INET6 in addresses:
|
||||
link_local_addr = None
|
||||
for address in addresses[self.netifaces.AF_INET6]:
|
||||
if "addr" in address:
|
||||
if address["addr"].startswith("fe80:"):
|
||||
link_local_addr = address["addr"].split("%")[0]
|
||||
if link_local_addr != self.adopted_interfaces[ifname]:
|
||||
self.adopted_interfaces[ifname] = link_local_addr
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not get device information while updating link-local addresses for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
# Check multicast echo timeouts
|
||||
last_multicast_echo = 0
|
||||
if ifname in self.multicast_echoes:
|
||||
last_multicast_echo = self.multicast_echoes[ifname]
|
||||
|
||||
if now - last_multicast_echo > self.multicast_echo_timeout:
|
||||
if ifname in self.timed_out_interfaces and self.timed_out_interfaces[ifname] == False:
|
||||
RNS.log("Multicast echo timeout for "+str(ifname)+". Carrier lost.", RNS.LOG_WARNING)
|
||||
self.timed_out_interfaces[ifname] = True
|
||||
else:
|
||||
if ifname in self.timed_out_interfaces and self.timed_out_interfaces[ifname] == True:
|
||||
RNS.log(str(self)+" Carrier recovered on "+str(ifname), RNS.LOG_WARNING)
|
||||
self.timed_out_interfaces[ifname] = False
|
||||
|
||||
|
||||
def announce_handler(self, ifname):
|
||||
while True:
|
||||
self.peer_announce(ifname)
|
||||
time.sleep(self.announce_interval)
|
||||
|
||||
def peer_announce(self, ifname):
|
||||
try:
|
||||
link_local_address = self.adopted_interfaces[ifname]
|
||||
discovery_token = RNS.Identity.full_hash(self.group_id+link_local_address.encode("utf-8"))
|
||||
announce_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
addr_info = socket.getaddrinfo(self.mcast_discovery_address, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
|
||||
ifis = struct.pack("I", socket.if_nametoindex(ifname))
|
||||
announce_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifis)
|
||||
announce_socket.sendto(discovery_token, addr_info[0][4])
|
||||
announce_socket.close()
|
||||
|
||||
except Exception as e:
|
||||
if (ifname in self.timed_out_interfaces and self.timed_out_interfaces[ifname] == False) or not ifname in self.timed_out_interfaces:
|
||||
RNS.log(str(self)+" Detected possible carrier loss on "+str(ifname)+": "+str(e), RNS.LOG_WARNING)
|
||||
else:
|
||||
pass
|
||||
|
||||
def add_peer(self, addr, ifname):
|
||||
if addr in self.link_local_addresses:
|
||||
ifname = None
|
||||
for interface_name in self.adopted_interfaces:
|
||||
if self.adopted_interfaces[interface_name] == addr:
|
||||
ifname = interface_name
|
||||
|
||||
if ifname != None:
|
||||
self.multicast_echoes[ifname] = time.time()
|
||||
else:
|
||||
RNS.log(str(self)+" received multicast echo on unexpected interface "+str(ifname), RNS.LOG_WARNING)
|
||||
|
||||
else:
|
||||
if not addr in self.peers:
|
||||
self.peers[addr] = [ifname, time.time()]
|
||||
RNS.log(str(self)+" added peer "+str(addr)+" on "+str(ifname), RNS.LOG_DEBUG)
|
||||
else:
|
||||
self.refresh_peer(addr)
|
||||
|
||||
def refresh_peer(self, addr):
|
||||
self.peers[addr][1] = time.time()
|
||||
|
||||
def processIncoming(self, data):
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
def processOutgoing(self,data):
|
||||
for peer in self.peers:
|
||||
try:
|
||||
if self.outbound_udp_socket == None:
|
||||
self.outbound_udp_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
|
||||
peer_addr = str(peer)+"%"+str(self.peers[peer][0])
|
||||
addr_info = socket.getaddrinfo(peer_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
self.outbound_udp_socket.sendto(data, addr_info[0][4])
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not transmit on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
self.txb += len(data)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "AutoInterface["+self.name+"]"
|
||||
|
||||
class AutoInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
def __init__(self, callback, *args, **keys):
|
||||
self.callback = callback
|
||||
socketserver.BaseRequestHandler.__init__(self, *args, **keys)
|
||||
|
||||
def handle(self):
|
||||
data = self.request[0]
|
||||
self.callback(data)
|
||||
@@ -0,0 +1,882 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
import socketserver
|
||||
import threading
|
||||
import platform
|
||||
import socket
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import RNS
|
||||
import asyncio
|
||||
|
||||
class HDLC():
|
||||
FLAG = 0x7E
|
||||
ESC = 0x7D
|
||||
ESC_MASK = 0x20
|
||||
|
||||
@staticmethod
|
||||
def escape(data):
|
||||
data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
|
||||
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
|
||||
return data
|
||||
|
||||
class KISS():
|
||||
FEND = 0xC0
|
||||
FESC = 0xDB
|
||||
TFEND = 0xDC
|
||||
TFESC = 0xDD
|
||||
CMD_DATA = 0x00
|
||||
CMD_UNKNOWN = 0xFE
|
||||
|
||||
@staticmethod
|
||||
def escape(data):
|
||||
data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
|
||||
data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
|
||||
return data
|
||||
|
||||
# TODO: Neater shutdown of the event loop and
|
||||
# better error handling is needed. Sometimes
|
||||
# errors occur in I2P that leave tunnel setup
|
||||
# hanging indefinitely, and right now we have
|
||||
# no way of catching it. Sometimes the server
|
||||
# and client tasks are also not cancelled on
|
||||
# shutdown, which leads to errors dumped to
|
||||
# the console. This should also be remedied.
|
||||
|
||||
class I2PController:
|
||||
def __init__(self, rns_storagepath):
|
||||
import RNS.vendor.i2plib as i2plib
|
||||
import RNS.vendor.i2plib.utils
|
||||
|
||||
self.client_tunnels = {}
|
||||
self.server_tunnels = {}
|
||||
self.i2plib_tunnels = {}
|
||||
self.loop = None
|
||||
self.i2plib = i2plib
|
||||
self.utils = i2plib.utils
|
||||
self.sam_address = i2plib.get_sam_address()
|
||||
self.ready = False
|
||||
|
||||
self.storagepath = rns_storagepath+"/i2p"
|
||||
if not os.path.isdir(self.storagepath):
|
||||
os.makedirs(self.storagepath)
|
||||
|
||||
|
||||
def start(self):
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
self.loop = asyncio.get_event_loop()
|
||||
|
||||
time.sleep(0.10)
|
||||
if self.loop == None:
|
||||
RNS.log("Could not get event loop for "+str(self)+", waiting for event loop to appear", RNS.LOG_VERBOSE)
|
||||
|
||||
while self.loop == None:
|
||||
self.loop = asyncio.get_event_loop()
|
||||
sleep(0.25)
|
||||
|
||||
try:
|
||||
self.ready = True
|
||||
self.loop.run_forever()
|
||||
except Exception as e:
|
||||
self.ready = False
|
||||
RNS.log("Exception on event loop for "+str(self)+": "+str(e), RNS.LOG_ERROR)
|
||||
finally:
|
||||
self.loop.close()
|
||||
|
||||
|
||||
def stop(self):
|
||||
for i2ptunnel in self.i2plib_tunnels:
|
||||
if hasattr(i2ptunnel, "stop") and callable(i2ptunnel.stop):
|
||||
i2ptunnel.stop()
|
||||
|
||||
if hasattr(asyncio.Task, "all_tasks") and callable(asyncio.Task.all_tasks):
|
||||
for task in asyncio.Task.all_tasks(loop=self.loop):
|
||||
task.cancel()
|
||||
|
||||
time.sleep(0.2)
|
||||
|
||||
self.loop.stop()
|
||||
|
||||
|
||||
def get_free_port(self):
|
||||
return self.i2plib.utils.get_free_port()
|
||||
|
||||
|
||||
def stop_tunnel(self, i2ptunnel):
|
||||
if hasattr(i2ptunnel, "stop") and callable(i2ptunnel.stop):
|
||||
i2ptunnel.stop()
|
||||
|
||||
def client_tunnel(self, owner, i2p_destination):
|
||||
self.client_tunnels[i2p_destination] = False
|
||||
self.i2plib_tunnels[i2p_destination] = None
|
||||
|
||||
while True:
|
||||
if not self.client_tunnels[i2p_destination]:
|
||||
try:
|
||||
async def tunnel_up():
|
||||
RNS.log("Bringing up I2P tunnel to "+str(owner)+", this may take a while...", RNS.LOG_INFO)
|
||||
tunnel = self.i2plib.ClientTunnel(i2p_destination, owner.local_addr, sam_address=self.sam_address, loop=self.loop)
|
||||
self.i2plib_tunnels[i2p_destination] = tunnel
|
||||
await tunnel.run()
|
||||
|
||||
self.loop.ext_owner = self
|
||||
result = asyncio.run_coroutine_threadsafe(tunnel_up(), self.loop).result()
|
||||
|
||||
if not i2p_destination in self.i2plib_tunnels:
|
||||
raise IOError("No tunnel control instance was created")
|
||||
|
||||
else:
|
||||
tn = self.i2plib_tunnels[i2p_destination]
|
||||
if tn != None and hasattr(tn, "status"):
|
||||
|
||||
RNS.log("Waiting for status from I2P control process", RNS.LOG_EXTREME)
|
||||
while not tn.status["setup_ran"]:
|
||||
time.sleep(0.1)
|
||||
RNS.log("Got status from I2P control process", RNS.LOG_EXTREME)
|
||||
|
||||
if tn.status["setup_failed"]:
|
||||
self.stop_tunnel(tn)
|
||||
raise tn.status["exception"]
|
||||
|
||||
else:
|
||||
self.client_tunnels[i2p_destination] = True
|
||||
owner.awaiting_i2p_tunnel = False
|
||||
if owner.socket != None:
|
||||
if hasattr(owner.socket, "close"):
|
||||
if callable(owner.socket.close):
|
||||
try:
|
||||
owner.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
RNS.log("Error while shutting down socket for "+str(owner)+": "+str(e))
|
||||
|
||||
try:
|
||||
owner.socket.close()
|
||||
except Exception as e:
|
||||
RNS.log("Error while closing socket for "+str(owner)+": "+str(e))
|
||||
|
||||
RNS.log(str(owner)+" tunnel setup complete", RNS.LOG_VERBOSE)
|
||||
|
||||
else:
|
||||
raise IOError("Got no status response from SAM API")
|
||||
|
||||
except ConnectionRefusedError as e:
|
||||
raise e
|
||||
|
||||
except ConnectionAbortedError as e:
|
||||
raise e
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Unexpected error type from I2P SAM: "+str(e), RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
else:
|
||||
i2ptunnel = self.i2plib_tunnels[i2p_destination]
|
||||
if hasattr(i2ptunnel, "status"):
|
||||
i2p_exception = i2ptunnel.status["exception"]
|
||||
|
||||
if i2ptunnel.status["setup_ran"] == False:
|
||||
RNS.log(str(self)+" I2P tunnel setup did not complete", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
elif i2p_exception != None:
|
||||
RNS.log("An error ocurred while setting up I2P tunnel to "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
if isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.CantReachPeer):
|
||||
RNS.log("The I2P daemon can't reach peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedDest):
|
||||
RNS.log("The I2P daemon reported that the destination is already in use", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedId):
|
||||
RNS.log("The I2P daemon reported that the ID is arleady in use", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidId):
|
||||
RNS.log("The I2P daemon reported that the stream session ID doesn't exist", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidKey):
|
||||
RNS.log("The I2P daemon reported that the key for "+str(i2p_destination)+" is invalid", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.KeyNotFound):
|
||||
RNS.log("The I2P daemon could not find the key for "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.PeerNotFound):
|
||||
RNS.log("The I2P daemon mould not find the peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.I2PError):
|
||||
RNS.log("The I2P daemon experienced an unspecified error", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.Timeout):
|
||||
RNS.log("I2P daemon timed out while setting up client tunnel to "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Resetting I2P tunnel and retrying later", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
elif i2ptunnel.status["setup_failed"] == True:
|
||||
RNS.log(str(self)+" Unspecified I2P tunnel setup error, resetting I2P tunnel", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
else:
|
||||
RNS.log(str(self)+" Got no status from SAM API, resetting I2P tunnel", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
# Wait for status from I2P control process
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def server_tunnel(self, owner):
|
||||
while RNS.Transport.identity == None:
|
||||
time.sleep(1)
|
||||
|
||||
# Old format
|
||||
i2p_dest_hash_of = RNS.Identity.full_hash(RNS.Identity.full_hash(owner.name.encode("utf-8")))
|
||||
i2p_keyfile_of = self.storagepath+"/"+RNS.hexrep(i2p_dest_hash_of, delimit=False)+".i2p"
|
||||
|
||||
# New format
|
||||
i2p_dest_hash_nf = RNS.Identity.full_hash(RNS.Identity.full_hash(owner.name.encode("utf-8"))+RNS.Identity.full_hash(RNS.Transport.identity.hash))
|
||||
i2p_keyfile_nf = self.storagepath+"/"+RNS.hexrep(i2p_dest_hash_nf, delimit=False)+".i2p"
|
||||
|
||||
# Use old format if a key is already present
|
||||
if os.path.isfile(i2p_keyfile_of):
|
||||
i2p_keyfile = i2p_keyfile_of
|
||||
else:
|
||||
i2p_keyfile = i2p_keyfile_nf
|
||||
|
||||
i2p_dest = None
|
||||
if not os.path.isfile(i2p_keyfile):
|
||||
coro = self.i2plib.new_destination(sam_address=self.sam_address, loop=self.loop)
|
||||
i2p_dest = asyncio.run_coroutine_threadsafe(coro, self.loop).result()
|
||||
key_file = open(i2p_keyfile, "w")
|
||||
key_file.write(i2p_dest.private_key.base64)
|
||||
key_file.close()
|
||||
else:
|
||||
key_file = open(i2p_keyfile, "r")
|
||||
prvd = key_file.read()
|
||||
key_file.close()
|
||||
i2p_dest = self.i2plib.Destination(data=prvd, has_private_key=True)
|
||||
|
||||
i2p_b32 = i2p_dest.base32
|
||||
owner.b32 = i2p_b32
|
||||
|
||||
self.server_tunnels[i2p_b32] = False
|
||||
self.i2plib_tunnels[i2p_b32] = None
|
||||
|
||||
while True:
|
||||
if self.server_tunnels[i2p_b32] == False:
|
||||
try:
|
||||
async def tunnel_up():
|
||||
RNS.log(str(owner)+" Bringing up I2P endpoint, this may take a while...", RNS.LOG_INFO)
|
||||
tunnel = self.i2plib.ServerTunnel((owner.bind_ip, owner.bind_port), loop=self.loop, destination=i2p_dest, sam_address=self.sam_address)
|
||||
self.i2plib_tunnels[i2p_b32] = tunnel
|
||||
await tunnel.run()
|
||||
owner.online = True
|
||||
RNS.log(str(owner)+ " endpoint setup complete. Now reachable at: "+str(i2p_dest.base32)+".b32.i2p", RNS.LOG_VERBOSE)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(tunnel_up(), self.loop).result()
|
||||
self.server_tunnels[i2p_b32] = True
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
else:
|
||||
i2ptunnel = self.i2plib_tunnels[i2p_b32]
|
||||
if hasattr(i2ptunnel, "status"):
|
||||
i2p_exception = i2ptunnel.status["exception"]
|
||||
|
||||
if i2ptunnel.status["setup_ran"] == False:
|
||||
RNS.log(str(self)+" I2P tunnel setup did not complete", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
elif i2p_exception != None:
|
||||
RNS.log("An error ocurred while setting up I2P tunnel", RNS.LOG_ERROR)
|
||||
|
||||
if isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.CantReachPeer):
|
||||
RNS.log("The I2P daemon can't reach peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedDest):
|
||||
RNS.log("The I2P daemon reported that the destination is already in use", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedId):
|
||||
RNS.log("The I2P daemon reported that the ID is arleady in use", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidId):
|
||||
RNS.log("The I2P daemon reported that the stream session ID doesn't exist", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidKey):
|
||||
RNS.log("The I2P daemon reported that the key for "+str(i2p_destination)+" is invalid", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.KeyNotFound):
|
||||
RNS.log("The I2P daemon could not find the key for "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.PeerNotFound):
|
||||
RNS.log("The I2P daemon mould not find the peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.I2PError):
|
||||
RNS.log("The I2P daemon experienced an unspecified error", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.Timeout):
|
||||
RNS.log("I2P daemon timed out while setting up client tunnel to "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Resetting I2P tunnel and retrying later", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
elif i2ptunnel.status["setup_failed"] == True:
|
||||
RNS.log(str(self)+" Unspecified I2P tunnel setup error, resetting I2P tunnel", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
else:
|
||||
RNS.log(str(self)+" Got no status from SAM API, resetting I2P tunnel", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
def get_loop(self):
|
||||
return asyncio.get_event_loop()
|
||||
|
||||
|
||||
class ThreadingI2PServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
pass
|
||||
|
||||
class I2PInterfacePeer(Interface):
|
||||
RECONNECT_WAIT = 15
|
||||
RECONNECT_MAX_TRIES = None
|
||||
|
||||
# TCP socket options
|
||||
I2P_USER_TIMEOUT = 45
|
||||
I2P_PROBE_AFTER = 10
|
||||
I2P_PROBE_INTERVAL = 9
|
||||
I2P_PROBES = 5
|
||||
|
||||
def __init__(self, parent_interface, owner, name, target_i2p_dest=None, connected_socket=None, max_reconnect_tries=None):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.socket = None
|
||||
self.parent_interface = parent_interface
|
||||
self.parent_count = True
|
||||
self.name = name
|
||||
self.initiator = False
|
||||
self.reconnecting = False
|
||||
self.never_connected = True
|
||||
self.owner = owner
|
||||
self.writing = False
|
||||
self.online = False
|
||||
self.detached = False
|
||||
self.kiss_framing = False
|
||||
self.i2p_tunneled = True
|
||||
self.i2p_dest = None
|
||||
self.i2p_tunnel_ready = False
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
self.bitrate = I2PInterface.BITRATE_GUESS
|
||||
|
||||
self.announce_rate_target = None
|
||||
self.announce_rate_grace = None
|
||||
self.announce_rate_penalty = None
|
||||
|
||||
if max_reconnect_tries == None:
|
||||
self.max_reconnect_tries = I2PInterfacePeer.RECONNECT_MAX_TRIES
|
||||
else:
|
||||
self.max_reconnect_tries = max_reconnect_tries
|
||||
|
||||
if connected_socket != None:
|
||||
self.receives = True
|
||||
self.target_ip = None
|
||||
self.target_port = None
|
||||
self.socket = connected_socket
|
||||
|
||||
if platform.system() == "Linux":
|
||||
self.set_timeouts_linux()
|
||||
elif platform.system() == "Darwin":
|
||||
self.set_timeouts_osx()
|
||||
|
||||
elif target_i2p_dest != None:
|
||||
self.receives = True
|
||||
self.initiator = True
|
||||
|
||||
self.bind_ip = "127.0.0.1"
|
||||
|
||||
self.awaiting_i2p_tunnel = True
|
||||
|
||||
def tunnel_job():
|
||||
while self.awaiting_i2p_tunnel:
|
||||
try:
|
||||
self.bind_port = self.parent_interface.i2p.get_free_port()
|
||||
self.local_addr = (self.bind_ip, self.bind_port)
|
||||
self.target_ip = self.bind_ip
|
||||
self.target_port = self.bind_port
|
||||
|
||||
if not self.parent_interface.i2p.client_tunnel(self, target_i2p_dest):
|
||||
RNS.log(str(self)+" I2P control process experienced an error, requesting new tunnel...", RNS.LOG_ERROR)
|
||||
self.awaiting_i2p_tunnel = True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while while configuring "+str(self)+": "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("Check that I2P is installed and running, and that SAM is enabled. Retrying tunnel setup later.", RNS.LOG_ERROR)
|
||||
|
||||
time.sleep(15)
|
||||
|
||||
thread = threading.Thread(target=tunnel_job)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def wait_job():
|
||||
while self.awaiting_i2p_tunnel:
|
||||
time.sleep(0.25)
|
||||
|
||||
if not self.kiss_framing:
|
||||
self.wants_tunnel = True
|
||||
|
||||
if not self.connect(initial=True):
|
||||
thread = threading.Thread(target=self.reconnect)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
else:
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
thread = threading.Thread(target=wait_job)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
|
||||
def set_timeouts_linux(self):
|
||||
if not self.i2p_tunneled:
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(I2PInterfacePeer.TCP_USER_TIMEOUT * 1000))
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(I2PInterfacePeer.TCP_PROBE_AFTER))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(I2PInterfacePeer.TCP_PROBE_INTERVAL))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(I2PInterfacePeer.TCP_PROBES))
|
||||
else:
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(I2PInterfacePeer.I2P_USER_TIMEOUT * 1000))
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(I2PInterfacePeer.I2P_PROBE_AFTER))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(I2PInterfacePeer.I2P_PROBE_INTERVAL))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(I2PInterfacePeer.I2P_PROBES))
|
||||
|
||||
def set_timeouts_osx(self):
|
||||
if hasattr(socket, "TCP_KEEPALIVE"):
|
||||
TCP_KEEPIDLE = socket.TCP_KEEPALIVE
|
||||
else:
|
||||
TCP_KEEPIDLE = 0x10
|
||||
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
|
||||
if not self.i2p_tunneled:
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(I2PInterfacePeer.TCP_PROBE_AFTER))
|
||||
else:
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(I2PInterfacePeer.I2P_PROBE_AFTER))
|
||||
|
||||
def shutdown_socket(self, socket):
|
||||
if callable(socket.close):
|
||||
|
||||
try:
|
||||
socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
|
||||
|
||||
try:
|
||||
socket.close()
|
||||
except Exception as e:
|
||||
RNS.log("Error while closing socket for "+str(self)+": "+str(e))
|
||||
|
||||
def detach(self):
|
||||
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
|
||||
if self.socket != None:
|
||||
if hasattr(self.socket, "close"):
|
||||
if callable(self.socket.close):
|
||||
self.detached = True
|
||||
|
||||
try:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
|
||||
|
||||
try:
|
||||
self.socket.close()
|
||||
except Exception as e:
|
||||
RNS.log("Error while closing socket for "+str(self)+": "+str(e))
|
||||
|
||||
self.socket = None
|
||||
|
||||
def connect(self, initial=False):
|
||||
try:
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.connect((self.target_ip, self.target_port))
|
||||
self.online = True
|
||||
|
||||
except Exception as e:
|
||||
if initial:
|
||||
if not self.awaiting_i2p_tunnel:
|
||||
RNS.log("Initial connection for "+str(self)+" could not be established: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("Leaving unconnected and retrying connection in "+str(I2PInterfacePeer.RECONNECT_WAIT)+" seconds.", RNS.LOG_ERROR)
|
||||
|
||||
return False
|
||||
|
||||
else:
|
||||
raise e
|
||||
|
||||
if platform.system() == "Linux":
|
||||
self.set_timeouts_linux()
|
||||
elif platform.system() == "Darwin":
|
||||
self.set_timeouts_osx()
|
||||
|
||||
self.online = True
|
||||
self.writing = False
|
||||
self.never_connected = False
|
||||
|
||||
if not self.kiss_framing and self.wants_tunnel:
|
||||
RNS.Transport.synthesize_tunnel(self)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def reconnect(self):
|
||||
if self.initiator:
|
||||
if not self.reconnecting:
|
||||
self.reconnecting = True
|
||||
attempts = 0
|
||||
while not self.online:
|
||||
time.sleep(I2PInterfacePeer.RECONNECT_WAIT)
|
||||
attempts += 1
|
||||
|
||||
if self.max_reconnect_tries != None and attempts > self.max_reconnect_tries:
|
||||
RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_ERROR)
|
||||
self.teardown()
|
||||
break
|
||||
|
||||
try:
|
||||
self.connect()
|
||||
|
||||
except Exception as e:
|
||||
if not self.awaiting_i2p_tunnel:
|
||||
RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
else:
|
||||
RNS.log(str(self)+" still waiting for I2P tunnel to appear", RNS.LOG_VERBOSE)
|
||||
|
||||
if not self.never_connected:
|
||||
RNS.log(str(self)+" Re-established connection via I2P tunnel", RNS.LOG_INFO)
|
||||
|
||||
self.reconnecting = False
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
if not self.kiss_framing:
|
||||
RNS.Transport.synthesize_tunnel(self)
|
||||
|
||||
else:
|
||||
RNS.log("Attempt to reconnect on a non-initiator I2P interface. This should not happen.", RNS.LOG_ERROR)
|
||||
raise IOError("Attempt to reconnect on a non-initiator I2P interface")
|
||||
|
||||
def processIncoming(self, data):
|
||||
self.rxb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None and self.parent_count:
|
||||
self.parent_interface.rxb += len(data)
|
||||
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
def processOutgoing(self, data):
|
||||
if self.online:
|
||||
while self.writing:
|
||||
time.sleep(0.001)
|
||||
|
||||
try:
|
||||
self.writing = True
|
||||
|
||||
if self.kiss_framing:
|
||||
data = bytes([KISS.FEND])+bytes([KISS.CMD_DATA])+KISS.escape(data)+bytes([KISS.FEND])
|
||||
else:
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
|
||||
self.socket.sendall(data)
|
||||
self.writing = False
|
||||
self.txb += len(data)
|
||||
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None and self.parent_count:
|
||||
self.parent_interface.txb += len(data)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
self.teardown()
|
||||
|
||||
|
||||
def read_loop(self):
|
||||
try:
|
||||
in_frame = False
|
||||
escape = False
|
||||
data_buffer = b""
|
||||
command = KISS.CMD_UNKNOWN
|
||||
|
||||
while True:
|
||||
data_in = self.socket.recv(4096)
|
||||
if len(data_in) > 0:
|
||||
pointer = 0
|
||||
while pointer < len(data_in):
|
||||
byte = data_in[pointer]
|
||||
pointer += 1
|
||||
|
||||
if self.kiss_framing:
|
||||
# Read loop for KISS framing
|
||||
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
elif (byte == KISS.FEND):
|
||||
in_frame = True
|
||||
command = KISS.CMD_UNKNOWN
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
|
||||
# We only support one HDLC port for now, so
|
||||
# strip off the port nibble
|
||||
byte = byte & 0x0F
|
||||
command = byte
|
||||
elif (command == KISS.CMD_DATA):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
|
||||
else:
|
||||
# Read loop for HDLC framing
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (byte == HDLC.ESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.FLAG
|
||||
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.ESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
else:
|
||||
self.online = False
|
||||
if self.initiator and not self.detached:
|
||||
RNS.log("Socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
|
||||
self.reconnect()
|
||||
else:
|
||||
RNS.log("Socket for remote client "+str(self)+" was closed.", RNS.LOG_VERBOSE)
|
||||
self.teardown()
|
||||
|
||||
break
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
RNS.log("An interface error occurred for "+str(self)+", the contained exception was: "+str(e), RNS.LOG_WARNING)
|
||||
|
||||
if self.initiator:
|
||||
RNS.log("Attempting to reconnect...", RNS.LOG_WARNING)
|
||||
self.reconnect()
|
||||
else:
|
||||
self.teardown()
|
||||
|
||||
def teardown(self):
|
||||
if self.initiator and not self.detached:
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
RNS.panic()
|
||||
|
||||
else:
|
||||
RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_VERBOSE)
|
||||
|
||||
self.online = False
|
||||
self.OUT = False
|
||||
self.IN = False
|
||||
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
if self.parent_interface.clients > 0:
|
||||
self.parent_interface.clients -= 1
|
||||
|
||||
if self in RNS.Transport.interfaces:
|
||||
if not self.initiator:
|
||||
RNS.Transport.interfaces.remove(self)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "I2PInterfacePeer["+str(self.name)+"]"
|
||||
|
||||
|
||||
class I2PInterface(Interface):
|
||||
BITRATE_GUESS = 256*1000
|
||||
|
||||
def __init__(self, owner, name, rns_storagepath, peers, connectable = False):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
self.online = False
|
||||
self.clients = 0
|
||||
self.owner = owner
|
||||
self.connectable = connectable
|
||||
self.i2p_tunneled = True
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
|
||||
self.b32 = None
|
||||
self.i2p = I2PController(rns_storagepath)
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.name = name
|
||||
|
||||
|
||||
self.receives = True
|
||||
self.bind_ip = "127.0.0.1"
|
||||
self.bind_port = self.i2p.get_free_port()
|
||||
self.address = (self.bind_ip, self.bind_port)
|
||||
self.bitrate = I2PInterface.BITRATE_GUESS
|
||||
|
||||
self.online = False
|
||||
|
||||
i2p_thread = threading.Thread(target=self.i2p.start)
|
||||
i2p_thread.daemon = True
|
||||
i2p_thread.start()
|
||||
|
||||
i2p_notready_warning = False
|
||||
time.sleep(0.25)
|
||||
|
||||
if not self.i2p.ready:
|
||||
RNS.log("I2P controller did not become available in time, waiting for controller", RNS.LOG_VERBOSE)
|
||||
i2p_notready_warning = True
|
||||
|
||||
while not self.i2p.ready:
|
||||
time.sleep(0.25)
|
||||
|
||||
if i2p_notready_warning == True:
|
||||
RNS.log("I2P controller ready, continuing setup", RNS.LOG_VERBOSE)
|
||||
|
||||
def handlerFactory(callback):
|
||||
def createHandler(*args, **keys):
|
||||
return I2PInterfaceHandler(callback, *args, **keys)
|
||||
return createHandler
|
||||
|
||||
ThreadingI2PServer.allow_reuse_address = True
|
||||
self.server = ThreadingI2PServer(self.address, handlerFactory(self.incoming_connection))
|
||||
|
||||
thread = threading.Thread(target=self.server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
if self.connectable:
|
||||
def tunnel_job():
|
||||
while True:
|
||||
try:
|
||||
if not self.i2p.server_tunnel(self):
|
||||
RNS.log(str(self)+" I2P control process experienced an error, requesting new tunnel...", RNS.LOG_ERROR)
|
||||
self.online = False
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while while configuring "+str(self)+": "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("Check that I2P is installed and running, and that SAM is enabled. Retrying tunnel setup later.", RNS.LOG_ERROR)
|
||||
|
||||
time.sleep(15)
|
||||
|
||||
|
||||
thread = threading.Thread(target=tunnel_job)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
if peers != None:
|
||||
for peer_addr in peers:
|
||||
interface_name = self.name+" to "+peer_addr
|
||||
peer_interface = I2PInterfacePeer(self, self.owner, interface_name, peer_addr)
|
||||
peer_interface.OUT = True
|
||||
peer_interface.IN = True
|
||||
peer_interface.parent_interface = self
|
||||
peer_interface.parent_count = False
|
||||
RNS.Transport.interfaces.append(peer_interface)
|
||||
|
||||
def incoming_connection(self, handler):
|
||||
RNS.log("Accepting incoming I2P connection", RNS.LOG_VERBOSE)
|
||||
interface_name = "Connected peer on "+self.name
|
||||
spawned_interface = I2PInterfacePeer(self, self.owner, interface_name, connected_socket=handler.request)
|
||||
spawned_interface.OUT = True
|
||||
spawned_interface.IN = True
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.online = True
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
spawned_interface.ifac_size = self.ifac_size
|
||||
spawned_interface.ifac_netname = self.ifac_netname
|
||||
spawned_interface.ifac_netkey = self.ifac_netkey
|
||||
spawned_interface.announce_rate_target = self.announce_rate_target
|
||||
spawned_interface.announce_rate_grace = self.announce_rate_grace
|
||||
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
RNS.log("Spawned new I2PInterface Peer: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
self.clients += 1
|
||||
spawned_interface.read_loop()
|
||||
|
||||
def processOutgoing(self, data):
|
||||
pass
|
||||
|
||||
def detach(self):
|
||||
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
|
||||
self.i2p.stop()
|
||||
|
||||
def __str__(self):
|
||||
return "I2PInterface["+self.name+"]"
|
||||
|
||||
class I2PInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
def __init__(self, callback, *args, **keys):
|
||||
self.callback = callback
|
||||
socketserver.BaseRequestHandler.__init__(self, *args, **keys)
|
||||
|
||||
def handle(self):
|
||||
self.callback(handler=self)
|
||||
@@ -1,4 +1,28 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import time
|
||||
import threading
|
||||
|
||||
class Interface:
|
||||
IN = False
|
||||
@@ -7,6 +31,18 @@ class Interface:
|
||||
RPT = False
|
||||
name = None
|
||||
|
||||
# Interface mode definitions
|
||||
MODE_FULL = 0x01
|
||||
MODE_POINT_TO_POINT = 0x02
|
||||
MODE_ACCESS_POINT = 0x03
|
||||
MODE_ROAMING = 0x04
|
||||
MODE_BOUNDARY = 0x05
|
||||
MODE_GATEWAY = 0x06
|
||||
|
||||
# Which interface modes a Transport Node
|
||||
# should actively discover paths for.
|
||||
DISCOVER_PATHS_FOR = [MODE_ACCESS_POINT, MODE_GATEWAY]
|
||||
|
||||
def __init__(self):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
@@ -15,5 +51,46 @@ class Interface:
|
||||
def get_hash(self):
|
||||
return RNS.Identity.full_hash(str(self).encode("utf-8"))
|
||||
|
||||
def process_announce_queue(self):
|
||||
if not hasattr(self, "announce_cap"):
|
||||
self.announce_cap = RNS.Reticulum.ANNOUNCE_CAP
|
||||
|
||||
if hasattr(self, "announce_queue"):
|
||||
try:
|
||||
now = time.time()
|
||||
stale = []
|
||||
for a in self.announce_queue:
|
||||
if now > a["time"]+RNS.Reticulum.QUEUED_ANNOUNCE_LIFE:
|
||||
stale.append(a)
|
||||
|
||||
for s in stale:
|
||||
if s in self.announce_queue:
|
||||
self.announce_queue.remove(s)
|
||||
|
||||
if len(self.announce_queue) > 0:
|
||||
min_hops = min(entry["hops"] for entry in self.announce_queue)
|
||||
entries = list(filter(lambda e: e["hops"] == min_hops, self.announce_queue))
|
||||
entries.sort(key=lambda e: e["time"])
|
||||
selected = entries[0]
|
||||
|
||||
now = time.time()
|
||||
tx_time = (len(selected["raw"])*8) / self.bitrate
|
||||
wait_time = (tx_time / self.announce_cap)
|
||||
self.announce_allowed_at = now + wait_time
|
||||
|
||||
self.processOutgoing(selected["raw"])
|
||||
|
||||
if selected in self.announce_queue:
|
||||
self.announce_queue.remove(selected)
|
||||
|
||||
if len(self.announce_queue) > 0:
|
||||
timer = threading.Timer(wait_time, self.process_announce_queue)
|
||||
timer.start()
|
||||
|
||||
except Exception as e:
|
||||
self.announce_queue = []
|
||||
RNS.log("Error while processing announce queue on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("The announce queue for this interface has been cleared.", RNS.LOG_ERROR)
|
||||
|
||||
def detach(self):
|
||||
pass
|
||||
@@ -1,7 +1,28 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import serial
|
||||
import threading
|
||||
import time
|
||||
import RNS
|
||||
@@ -30,6 +51,7 @@ class KISS():
|
||||
|
||||
class KISSInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
BITRATE_GUESS = 1200
|
||||
|
||||
owner = None
|
||||
port = None
|
||||
@@ -40,12 +62,23 @@ class KISSInterface(Interface):
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, name, port, speed, databits, parity, stopbits, preamble, txtail, persistence, slottime, flow_control, beacon_interval, beacon_data):
|
||||
import importlib
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
else:
|
||||
RNS.log("Using the KISS interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 564
|
||||
|
||||
if beacon_data == None:
|
||||
beacon_data = ""
|
||||
|
||||
self.pyserial = serial
|
||||
self.serial = None
|
||||
self.owner = owner
|
||||
self.name = name
|
||||
@@ -59,6 +92,7 @@ class KISSInterface(Interface):
|
||||
self.beacon_i = beacon_interval
|
||||
self.beacon_d = beacon_data.encode("utf-8")
|
||||
self.first_tx = None
|
||||
self.bitrate = KISSInterface.BITRATE_GUESS
|
||||
|
||||
self.packet_queue = []
|
||||
self.flow_control = flow_control
|
||||
@@ -78,44 +112,52 @@ class KISSInterface(Interface):
|
||||
self.parity = serial.PARITY_ODD
|
||||
|
||||
try:
|
||||
RNS.log("Opening serial port "+self.port+"...")
|
||||
self.serial = serial.Serial(
|
||||
port = self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = 0,
|
||||
inter_byte_timeout = None,
|
||||
write_timeout = None,
|
||||
dsrdtr = False,
|
||||
)
|
||||
self.open_port()
|
||||
except Exception as e:
|
||||
RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
if self.serial.is_open:
|
||||
# Allow time for interface to initialise before config
|
||||
sleep(2.0)
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
thread.setDaemon(True)
|
||||
thread.start()
|
||||
self.online = True
|
||||
RNS.log("Serial port "+self.port+" is now open")
|
||||
RNS.log("Configuring KISS interface parameters...")
|
||||
self.setPreamble(self.preamble)
|
||||
self.setTxTail(self.txtail)
|
||||
self.setPersistence(self.persistence)
|
||||
self.setSlotTime(self.slottime)
|
||||
self.setFlowControl(self.flow_control)
|
||||
self.interface_ready = True
|
||||
RNS.log("KISS interface configured")
|
||||
self.configure_device()
|
||||
else:
|
||||
raise IOError("Could not open serial port")
|
||||
|
||||
|
||||
def open_port(self):
|
||||
RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE)
|
||||
self.serial = self.pyserial.Serial(
|
||||
port = self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = 0,
|
||||
inter_byte_timeout = None,
|
||||
write_timeout = None,
|
||||
dsrdtr = False,
|
||||
)
|
||||
|
||||
|
||||
def configure_device(self):
|
||||
# Allow time for interface to initialise before config
|
||||
sleep(2.0)
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
self.online = True
|
||||
RNS.log("Serial port "+self.port+" is now open")
|
||||
RNS.log("Configuring KISS interface parameters...")
|
||||
self.setPreamble(self.preamble)
|
||||
self.setTxTail(self.txtail)
|
||||
self.setPersistence(self.persistence)
|
||||
self.setSlotTime(self.slottime)
|
||||
self.setFlowControl(self.flow_control)
|
||||
self.interface_ready = True
|
||||
RNS.log("KISS interface configured")
|
||||
|
||||
|
||||
def setPreamble(self, preamble):
|
||||
preamble_ms = preamble
|
||||
preamble = int(preamble_ms / 10)
|
||||
@@ -239,7 +281,7 @@ class KISSInterface(Interface):
|
||||
in_frame = True
|
||||
command = KISS.CMD_UNKNOWN
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU):
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
|
||||
# We only support one HDLC port for now, so
|
||||
# strip off the port nibble
|
||||
@@ -283,10 +325,29 @@ class KISSInterface(Interface):
|
||||
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 being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
|
||||
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
RNS.panic()
|
||||
|
||||
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
|
||||
|
||||
self.online = False
|
||||
self.serial.close()
|
||||
self.reconnect_port()
|
||||
|
||||
def reconnect_port(self):
|
||||
while not self.online:
|
||||
try:
|
||||
time.sleep(5)
|
||||
RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
self.open_port()
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
except Exception as e:
|
||||
RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
def __str__(self):
|
||||
return "KISSInterface["+self.name+"]"
|
||||
@@ -1,3 +1,25 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
import socketserver
|
||||
import threading
|
||||
@@ -22,17 +44,28 @@ class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
pass
|
||||
|
||||
class LocalClientInterface(Interface):
|
||||
RECONNECT_WAIT = 3
|
||||
|
||||
def __init__(self, owner, name, target_port = None, connected_socket=None):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
# TODO: Remove at some point
|
||||
# self.rxptime = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
self.online = False
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.socket = None
|
||||
self.parent_interface = None
|
||||
self.reconnecting = False
|
||||
self.never_connected = True
|
||||
self.detached = False
|
||||
self.name = name
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
|
||||
if connected_socket != None:
|
||||
self.receives = True
|
||||
@@ -46,33 +79,79 @@ class LocalClientInterface(Interface):
|
||||
self.receives = True
|
||||
self.target_ip = "127.0.0.1"
|
||||
self.target_port = target_port
|
||||
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.connect((self.target_ip, self.target_port))
|
||||
|
||||
self.is_connected_to_shared_instance = True
|
||||
self.connect()
|
||||
|
||||
self.owner = owner
|
||||
self.bitrate = 1000*1000*1000
|
||||
self.online = True
|
||||
self.writing = False
|
||||
|
||||
self.announce_rate_target = None
|
||||
self.announce_rate_grace = None
|
||||
self.announce_rate_penalty = None
|
||||
|
||||
if connected_socket == None:
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def connect(self):
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.connect((self.target_ip, self.target_port))
|
||||
|
||||
self.online = True
|
||||
self.is_connected_to_shared_instance = True
|
||||
self.never_connected = False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def reconnect(self):
|
||||
if self.is_connected_to_shared_instance:
|
||||
if not self.reconnecting:
|
||||
self.reconnecting = True
|
||||
attempts = 0
|
||||
|
||||
while not self.online:
|
||||
time.sleep(LocalClientInterface.RECONNECT_WAIT)
|
||||
attempts += 1
|
||||
|
||||
try:
|
||||
self.connect()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
|
||||
if not self.never_connected:
|
||||
RNS.log("Reconnected socket for "+str(self)+".", RNS.LOG_INFO)
|
||||
|
||||
self.reconnecting = False
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
RNS.Transport.shared_connection_reappeared()
|
||||
|
||||
else:
|
||||
RNS.log("Attempt to reconnect on a non-initiator shared local interface. This should not happen.", RNS.LOG_ERROR)
|
||||
raise IOError("Attempt to reconnect on a non-initiator local interface")
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
self.rxb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.rxb += len(data)
|
||||
|
||||
|
||||
# TODO: Remove at some point
|
||||
# processing_start = time.time()
|
||||
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
# TODO: Remove at some point
|
||||
# duration = time.time() - processing_start
|
||||
# self.rxptime += duration
|
||||
|
||||
def processOutgoing(self, data):
|
||||
if self.online:
|
||||
while self.writing:
|
||||
time.sleep(0.01)
|
||||
|
||||
try:
|
||||
self.writing = True
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
@@ -107,7 +186,7 @@ class LocalClientInterface(Interface):
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU):
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (byte == HDLC.ESC):
|
||||
escape = True
|
||||
else:
|
||||
@@ -119,8 +198,14 @@ class LocalClientInterface(Interface):
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
else:
|
||||
RNS.log("Socket for "+str(self)+" was closed, tearing down interface", RNS.LOG_VERBOSE)
|
||||
self.teardown(nowarning=True)
|
||||
self.online = False
|
||||
if self.is_connected_to_shared_instance and not self.detached:
|
||||
RNS.log("Socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
|
||||
RNS.Transport.shared_connection_disappeared()
|
||||
self.reconnect()
|
||||
else:
|
||||
self.teardown(nowarning=True)
|
||||
|
||||
break
|
||||
|
||||
|
||||
@@ -161,6 +246,7 @@ 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 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)
|
||||
@@ -168,13 +254,10 @@ class LocalClientInterface(Interface):
|
||||
RNS.panic()
|
||||
|
||||
if self.is_connected_to_shared_instance:
|
||||
# TODO: Maybe add automatic recovery here.
|
||||
# Needs thinking through, since user needs
|
||||
# to now that all connectivity has been cut
|
||||
# while service is recovering. Better for
|
||||
# now to take down entire stack.
|
||||
RNS.log("Lost connection to local shared RNS instance. Exiting now.", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
if nowarning == False:
|
||||
RNS.log("Permanently lost connection to local shared RNS instance. Exiting now.", RNS.LOG_CRITICAL)
|
||||
|
||||
RNS.exit()
|
||||
|
||||
|
||||
def __str__(self):
|
||||
@@ -192,6 +275,7 @@ class LocalServerInterface(Interface):
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.name = "Reticulum"
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
|
||||
if (bindport != None):
|
||||
self.receives = True
|
||||
@@ -212,9 +296,14 @@ class LocalServerInterface(Interface):
|
||||
self.server = ThreadingTCPServer(address, handlerFactory(self.incoming_connection))
|
||||
|
||||
thread = threading.Thread(target=self.server.serve_forever)
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
self.announce_rate_target = None
|
||||
self.announce_rate_grace = None
|
||||
self.announce_rate_penalty = None
|
||||
|
||||
self.bitrate = 1000*1000*1000
|
||||
self.online = True
|
||||
|
||||
|
||||
@@ -227,7 +316,8 @@ class LocalServerInterface(Interface):
|
||||
spawned_interface.target_ip = handler.client_address[0]
|
||||
spawned_interface.target_port = str(handler.client_address[1])
|
||||
spawned_interface.parent_interface = self
|
||||
RNS.log("Accepting new connection to shared instance: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
RNS.log("Accepting new connection to shared instance: "+str(spawned_interface), RNS.LOG_EXTREME)
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.local_client_interfaces.append(spawned_interface)
|
||||
self.clients += 1
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import RNS
|
||||
|
||||
import subprocess
|
||||
import shlex
|
||||
|
||||
class HDLC():
|
||||
# The Pipe Interface packetizes data using
|
||||
# simplified HDLC framing, similar to PPP
|
||||
FLAG = 0x7E
|
||||
ESC = 0x7D
|
||||
ESC_MASK = 0x20
|
||||
|
||||
@staticmethod
|
||||
def escape(data):
|
||||
data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
|
||||
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
|
||||
return data
|
||||
|
||||
class PipeInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
BITRATE_GUESS = 1*1000*1000
|
||||
|
||||
owner = None
|
||||
command = None
|
||||
|
||||
def __init__(self, owner, name, command, respawn_delay):
|
||||
if respawn_delay == None:
|
||||
respawn_delay = 5
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
self.owner = owner
|
||||
self.name = name
|
||||
self.command = command
|
||||
self.process = None
|
||||
self.timeout = 100
|
||||
self.online = False
|
||||
self.pipe_is_open = False
|
||||
self.bitrate = PipeInterface.BITRATE_GUESS
|
||||
self.respawn_delay = respawn_delay
|
||||
|
||||
try:
|
||||
self.open_pipe()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could connect pipe for interface "+str(self), RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
if self.pipe_is_open:
|
||||
self.configure_pipe()
|
||||
else:
|
||||
raise IOError("Could not connect pipe")
|
||||
|
||||
|
||||
def open_pipe(self):
|
||||
RNS.log("Connecting subprocess pipe for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
|
||||
try:
|
||||
self.process = subprocess.Popen(shlex.split(self.command), stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
self.pipe_is_open = True
|
||||
except Exception as e:
|
||||
raise e
|
||||
self.pipe_is_open = False
|
||||
|
||||
|
||||
def configure_pipe(self):
|
||||
sleep(0.01)
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
self.online = True
|
||||
RNS.log("Subprocess pipe for "+str(self)+" is now connected", RNS.LOG_VERBOSE)
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
|
||||
def processOutgoing(self,data):
|
||||
if self.online:
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
written = self.process.stdin.write(data)
|
||||
self.process.stdin.flush()
|
||||
self.txb += len(data)
|
||||
if written != len(data):
|
||||
raise IOError("Pipe interface only wrote "+str(written)+" bytes of "+str(len(data)))
|
||||
|
||||
|
||||
def readLoop(self):
|
||||
try:
|
||||
in_frame = False
|
||||
escape = False
|
||||
data_buffer = b""
|
||||
last_read_ms = int(time.time()*1000)
|
||||
|
||||
while True:
|
||||
process_output = self.process.stdout.read(1)
|
||||
if len(process_output) == 0 and self.process.poll() is not None:
|
||||
break
|
||||
|
||||
else:
|
||||
byte = ord(process_output)
|
||||
last_read_ms = int(time.time()*1000)
|
||||
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (byte == HDLC.ESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.FLAG
|
||||
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.ESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
|
||||
RNS.log("Subprocess terminated on "+str(self))
|
||||
self.process.kill()
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
try:
|
||||
self.process.kill()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
RNS.log("A pipe error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
|
||||
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
RNS.panic()
|
||||
|
||||
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
|
||||
|
||||
self.online = False
|
||||
self.reconnect_pipe()
|
||||
|
||||
def reconnect_pipe(self):
|
||||
while not self.online:
|
||||
try:
|
||||
time.sleep(self.respawn_delay)
|
||||
RNS.log("Attempting to respawn subprocess for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
self.open_pipe()
|
||||
if self.pipe_is_open:
|
||||
self.configure_pipe()
|
||||
except Exception as e:
|
||||
RNS.log("Error while spawning subprocess, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Reconnected pipe for "+str(self))
|
||||
|
||||
def __str__(self):
|
||||
return "PipeInterface["+self.name+"]"
|
||||
@@ -1,8 +1,28 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import serial
|
||||
import threading
|
||||
import time
|
||||
import math
|
||||
@@ -24,6 +44,7 @@ class KISS():
|
||||
CMD_RADIO_STATE = 0x06
|
||||
CMD_RADIO_LOCK = 0x07
|
||||
CMD_DETECT = 0x08
|
||||
CMD_LEAVE = 0x0A
|
||||
CMD_READY = 0x0F
|
||||
CMD_STAT_RX = 0x21
|
||||
CMD_STAT_TX = 0x22
|
||||
@@ -31,8 +52,14 @@ class KISS():
|
||||
CMD_STAT_SNR = 0x24
|
||||
CMD_BLINK = 0x30
|
||||
CMD_RANDOM = 0x40
|
||||
CMD_FB_EXT = 0x41
|
||||
CMD_FB_READ = 0x42
|
||||
CMD_FB_WRITE = 0x43
|
||||
CMD_PLATFORM = 0x48
|
||||
CMD_MCU = 0x49
|
||||
CMD_FW_VERSION = 0x50
|
||||
CMD_ROM_READ = 0x51
|
||||
CMD_RESET = 0x55
|
||||
|
||||
DETECT_REQ = 0x73
|
||||
DETECT_RESP = 0x46
|
||||
@@ -46,6 +73,9 @@ class KISS():
|
||||
ERROR_TXFAILED = 0x02
|
||||
ERROR_EEPROM_LOCKED = 0x03
|
||||
|
||||
PLATFORM_AVR = 0x90
|
||||
PLATFORM_ESP32 = 0x80
|
||||
|
||||
@staticmethod
|
||||
def escape(data):
|
||||
data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
|
||||
@@ -56,14 +86,6 @@ class KISS():
|
||||
class RNodeInterface(Interface):
|
||||
MAX_CHUNK = 32768
|
||||
|
||||
owner = None
|
||||
port = None
|
||||
speed = None
|
||||
databits = None
|
||||
parity = None
|
||||
stopbits = None
|
||||
serial = None
|
||||
|
||||
FREQ_MIN = 137000000
|
||||
FREQ_MAX = 1020000000
|
||||
|
||||
@@ -71,17 +93,35 @@ class RNodeInterface(Interface):
|
||||
|
||||
CALLSIGN_MAX_LEN = 32
|
||||
|
||||
REQUIRED_FW_VER_MAJ = 1
|
||||
REQUIRED_FW_VER_MIN = 52
|
||||
|
||||
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):
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
raise SystemError("Invlaid interface type. The Android-specific RNode interface must be used on Android")
|
||||
|
||||
import importlib
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
else:
|
||||
RNS.log("Using the RNode interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 508
|
||||
|
||||
self.pyserial = serial
|
||||
self.serial = None
|
||||
self.owner = owner
|
||||
self.name = name
|
||||
self.port = port
|
||||
self.speed = 115200
|
||||
self.databits = 8
|
||||
self.parity = serial.PARITY_NONE
|
||||
self.stopbits = 1
|
||||
self.timeout = 100
|
||||
self.online = False
|
||||
@@ -93,9 +133,17 @@ class RNodeInterface(Interface):
|
||||
self.cr = cr
|
||||
self.state = KISS.RADIO_STATE_OFF
|
||||
self.bitrate = 0
|
||||
self.platform = None
|
||||
self.display = None
|
||||
self.mcu = None
|
||||
self.detected = False
|
||||
self.firmware_ok = False
|
||||
self.maj_version = 0
|
||||
self.min_version = 0
|
||||
|
||||
self.last_id = 0
|
||||
self.first_tx = None
|
||||
self.reconnect_w = RNodeInterface.RECONNECT_WAIT
|
||||
|
||||
self.r_frequency = None
|
||||
self.r_bandwidth = None
|
||||
@@ -112,6 +160,7 @@ class RNodeInterface(Interface):
|
||||
self.packet_queue = []
|
||||
self.flow_control = flow_control
|
||||
self.interface_ready = False
|
||||
self.announce_rate_target = None
|
||||
|
||||
self.validcfg = True
|
||||
if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
|
||||
@@ -150,47 +199,70 @@ class RNodeInterface(Interface):
|
||||
raise ValueError("The configuration for "+str(self)+" contains errors, interface is offline")
|
||||
|
||||
try:
|
||||
RNS.log("Opening serial port "+self.port+"...")
|
||||
self.serial = serial.Serial(
|
||||
port = self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = 0,
|
||||
inter_byte_timeout = None,
|
||||
write_timeout = None,
|
||||
dsrdtr = False,
|
||||
)
|
||||
self.open_port()
|
||||
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
else:
|
||||
raise IOError("Could not open serial port")
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
if self.serial.is_open:
|
||||
sleep(2.0)
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
thread.setDaemon(True)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR)
|
||||
thread = threading.Thread(target=self.reconnect_port)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
self.online = True
|
||||
RNS.log("Serial port "+self.port+" is now open")
|
||||
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
|
||||
self.initRadio()
|
||||
if (self.validateRadioState()):
|
||||
self.interface_ready = True
|
||||
RNS.log(str(self)+" is configured and powered up")
|
||||
sleep(1.0)
|
||||
else:
|
||||
RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
|
||||
RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
|
||||
RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
|
||||
self.serial.close()
|
||||
raise IOError("RNode interface did not pass validation")
|
||||
else:
|
||||
raise IOError("Could not open serial port")
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
def configure_device(self):
|
||||
sleep(2.0)
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
self.detect()
|
||||
sleep(0.2)
|
||||
|
||||
if not self.detected:
|
||||
raise IOError("Could not detect device")
|
||||
else:
|
||||
if self.platform == KISS.PLATFORM_ESP32:
|
||||
self.display = True
|
||||
|
||||
RNS.log("Serial port "+self.port+" is now open")
|
||||
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
|
||||
self.initRadio()
|
||||
if (self.validateRadioState()):
|
||||
self.interface_ready = True
|
||||
RNS.log(str(self)+" is configured and powered up")
|
||||
sleep(0.3)
|
||||
self.online = True
|
||||
else:
|
||||
RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
|
||||
RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
|
||||
RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
|
||||
self.serial.close()
|
||||
raise IOError("RNode interface did not pass configuration validation")
|
||||
|
||||
|
||||
def initRadio(self):
|
||||
self.setFrequency()
|
||||
self.setBandwidth()
|
||||
@@ -199,6 +271,63 @@ class RNodeInterface(Interface):
|
||||
self.setCodingRate()
|
||||
self.setRadioState(KISS.RADIO_STATE_ON)
|
||||
|
||||
def detect(self):
|
||||
kiss_command = bytes([KISS.FEND, KISS.CMD_DETECT, KISS.DETECT_REQ, KISS.FEND, KISS.CMD_FW_VERSION, 0x00, KISS.FEND, KISS.CMD_PLATFORM, 0x00, KISS.FEND, KISS.CMD_MCU, 0x00, KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while detecting hardware for "+str(self))
|
||||
|
||||
def leave(self):
|
||||
kiss_command = bytes([KISS.FEND, KISS.CMD_LEAVE, 0xFF, KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while sending host left command to device")
|
||||
|
||||
def enable_external_framebuffer(self):
|
||||
if self.display != None:
|
||||
kiss_command = bytes([KISS.FEND, KISS.CMD_FB_EXT, 0x01, KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while enabling external framebuffer on device")
|
||||
|
||||
def disable_external_framebuffer(self):
|
||||
if self.display != None:
|
||||
kiss_command = bytes([KISS.FEND, KISS.CMD_FB_EXT, 0x00, KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while disabling external framebuffer on device")
|
||||
|
||||
FB_PIXEL_WIDTH = 64
|
||||
FB_BITS_PER_PIXEL = 1
|
||||
FB_PIXELS_PER_BYTE = 8//FB_BITS_PER_PIXEL
|
||||
FB_BYTES_PER_LINE = FB_PIXEL_WIDTH//FB_PIXELS_PER_BYTE
|
||||
def display_image(self, imagedata):
|
||||
if self.display != None:
|
||||
lines = len(imagedata)//8
|
||||
for line in range(lines):
|
||||
line_start = line*RNodeInterface.FB_BYTES_PER_LINE
|
||||
line_end = line_start+RNodeInterface.FB_BYTES_PER_LINE
|
||||
line_data = bytes(imagedata[line_start:line_end])
|
||||
self.write_framebuffer(line, line_data)
|
||||
|
||||
def write_framebuffer(self, line, line_data):
|
||||
if self.display != None:
|
||||
line_byte = line.to_bytes(1, byteorder="big", signed=False)
|
||||
data = line_byte+line_data
|
||||
escaped_data = KISS.escape(data)
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FB_WRITE])+escaped_data+bytes([KISS.FEND])
|
||||
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while writing framebuffer data device")
|
||||
|
||||
def hard_reset(self):
|
||||
kiss_command = bytes([KISS.FEND, KISS.CMD_RESET, 0xf8, KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while restarting device")
|
||||
sleep(2.25);
|
||||
|
||||
def setFrequency(self):
|
||||
c1 = self.frequency >> 24
|
||||
c2 = self.frequency >> 16 & 0xFF
|
||||
@@ -209,7 +338,7 @@ class RNodeInterface(Interface):
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring frequency for "+self(str))
|
||||
raise IOError("An IO error occurred while configuring frequency for "+str(self))
|
||||
|
||||
def setBandwidth(self):
|
||||
c1 = self.bandwidth >> 24
|
||||
@@ -221,39 +350,56 @@ class RNodeInterface(Interface):
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring bandwidth for "+self(str))
|
||||
raise IOError("An IO error occurred while configuring bandwidth for "+str(self))
|
||||
|
||||
def setTXPower(self):
|
||||
txp = bytes([self.txpower])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring TX power for "+self(str))
|
||||
raise IOError("An IO error occurred while configuring TX power for "+str(self))
|
||||
|
||||
def setSpreadingFactor(self):
|
||||
sf = bytes([self.sf])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring spreading factor for "+self(str))
|
||||
raise IOError("An IO error occurred while configuring spreading factor for "+str(self))
|
||||
|
||||
def setCodingRate(self):
|
||||
cr = bytes([self.cr])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring coding rate for "+self(str))
|
||||
raise IOError("An IO error occurred while configuring coding rate for "+str(self))
|
||||
|
||||
def setRadioState(self, state):
|
||||
self.state = state
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring radio state for "+self(str))
|
||||
raise IOError("An IO error occurred while configuring radio state for "+str(self))
|
||||
|
||||
def validate_firmware(self):
|
||||
if (self.maj_version >= RNodeInterface.REQUIRED_FW_VER_MAJ):
|
||||
if (self.min_version >= RNodeInterface.REQUIRED_FW_VER_MIN):
|
||||
self.firmware_ok = True
|
||||
|
||||
if self.firmware_ok:
|
||||
return
|
||||
|
||||
RNS.log("The firmware version of the connected RNode is "+str(self.maj_version)+"."+str(self.min_version), RNS.LOG_ERROR)
|
||||
RNS.log("This version of Reticulum requires at least version "+str(RNodeInterface.REQUIRED_FW_VER_MAJ)+"."+str(RNodeInterface.REQUIRED_FW_VER_MIN), RNS.LOG_ERROR)
|
||||
RNS.log("Please update your RNode firmware with rnodeconf from https://github.com/markqvist/rnodeconfigutil/")
|
||||
RNS.panic()
|
||||
|
||||
|
||||
def validateRadioState(self):
|
||||
RNS.log("Validating radio configuration for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
RNS.log("Wating for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
sleep(0.25);
|
||||
if (self.frequency != self.r_frequency):
|
||||
|
||||
self.validcfg = True
|
||||
if (self.r_frequency != None and abs(self.frequency - int(self.r_frequency)) > 100):
|
||||
RNS.log("Frequency mismatch", RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
if (self.bandwidth != self.r_bandwidth):
|
||||
@@ -265,6 +411,9 @@ class RNodeInterface(Interface):
|
||||
if (self.sf != self.r_sf):
|
||||
RNS.log("Spreading factor mismatch", RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
if (self.state != self.r_state):
|
||||
RNS.log("Radio state mismatch", RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
if (self.validcfg):
|
||||
return True
|
||||
@@ -281,8 +430,10 @@ class RNodeInterface(Interface):
|
||||
self.bitrate = 0
|
||||
|
||||
def processIncoming(self, data):
|
||||
self.rxb += len(data)
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
self.r_stat_rssi = None
|
||||
self.r_stat_snr = None
|
||||
|
||||
|
||||
def processOutgoing(self,data):
|
||||
@@ -344,7 +495,7 @@ class RNodeInterface(Interface):
|
||||
command = KISS.CMD_UNKNOWN
|
||||
data_buffer = b""
|
||||
command_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU):
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
|
||||
command = byte
|
||||
elif (command == KISS.CMD_DATA):
|
||||
@@ -403,8 +554,30 @@ class RNodeInterface(Interface):
|
||||
self.updateBitrate()
|
||||
elif (command == KISS.CMD_RADIO_STATE):
|
||||
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)
|
||||
|
||||
elif (command == KISS.CMD_RADIO_LOCK):
|
||||
self.r_lock = byte
|
||||
elif (command == KISS.CMD_FW_VERSION):
|
||||
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):
|
||||
self.maj_version = int(command_buffer[0])
|
||||
self.min_version = int(command_buffer[1])
|
||||
self.validate_firmware()
|
||||
|
||||
elif (command == KISS.CMD_STAT_RX):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
@@ -439,15 +612,33 @@ class RNodeInterface(Interface):
|
||||
self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25
|
||||
elif (command == KISS.CMD_RANDOM):
|
||||
self.r_random = byte
|
||||
elif (command == KISS.CMD_PLATFORM):
|
||||
self.platform = byte
|
||||
elif (command == KISS.CMD_MCU):
|
||||
self.mcu = byte
|
||||
elif (command == KISS.CMD_ERROR):
|
||||
if (byte == KISS.ERROR_INITRADIO):
|
||||
RNS.log(str(self)+" hardware initialisation error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
|
||||
raise IOError("Radio initialisation failure")
|
||||
elif (byte == KISS.ERROR_INITRADIO):
|
||||
RNS.log(str(self)+" hardware TX error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
|
||||
raise IOError("Hardware transmit failure")
|
||||
else:
|
||||
RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
|
||||
raise IOError("Unknown hardware failure")
|
||||
elif (command == KISS.CMD_RESET):
|
||||
if (byte == 0xF8):
|
||||
if self.platform == KISS.PLATFORM_ESP32:
|
||||
if self.online:
|
||||
RNS.log("Detected reset while device was online, reinitialising device...", RNS.LOG_ERROR)
|
||||
raise IOError("ESP32 reset")
|
||||
elif (command == KISS.CMD_READY):
|
||||
self.process_queue()
|
||||
elif (command == KISS.CMD_DETECT):
|
||||
if byte == KISS.DETECT_RESP:
|
||||
self.detected = True
|
||||
else:
|
||||
self.detected = False
|
||||
|
||||
else:
|
||||
time_since_last = int(time.time()*1000) - last_read_ms
|
||||
@@ -469,11 +660,35 @@ class RNodeInterface(Interface):
|
||||
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 being torn down. Restart Reticulum to attempt to open this interface again.", 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()
|
||||
|
||||
def __str__(self):
|
||||
return "RNodeInterface["+self.name+"]"
|
||||
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
|
||||
|
||||
self.online = False
|
||||
self.serial.close()
|
||||
self.reconnect_port()
|
||||
|
||||
def reconnect_port(self):
|
||||
while not self.online:
|
||||
try:
|
||||
time.sleep(5)
|
||||
RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
self.open_port()
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
except Exception as e:
|
||||
RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
if self.online:
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
def detach(self):
|
||||
self.disable_external_framebuffer()
|
||||
self.setRadioState(KISS.RADIO_STATE_OFF)
|
||||
self.leave()
|
||||
|
||||
def __str__(self):
|
||||
return "RNodeInterface["+str(self.name)+"]"
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import serial
|
||||
import threading
|
||||
import time
|
||||
import RNS
|
||||
@@ -31,9 +52,20 @@ class SerialInterface(Interface):
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, name, port, speed, databits, parity, stopbits):
|
||||
import importlib
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
else:
|
||||
RNS.log("Using the Serial interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 564
|
||||
|
||||
self.pyserial = serial
|
||||
self.serial = None
|
||||
self.owner = owner
|
||||
self.name = name
|
||||
@@ -44,6 +76,7 @@ class SerialInterface(Interface):
|
||||
self.stopbits = stopbits
|
||||
self.timeout = 100
|
||||
self.online = False
|
||||
self.bitrate = self.speed
|
||||
|
||||
if parity.lower() == "e" or parity.lower() == "even":
|
||||
self.parity = serial.PARITY_EVEN
|
||||
@@ -52,35 +85,43 @@ class SerialInterface(Interface):
|
||||
self.parity = serial.PARITY_ODD
|
||||
|
||||
try:
|
||||
RNS.log("Opening serial port "+self.port+"...")
|
||||
self.serial = serial.Serial(
|
||||
port = self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = 0,
|
||||
inter_byte_timeout = None,
|
||||
write_timeout = None,
|
||||
dsrdtr = False,
|
||||
)
|
||||
self.open_port()
|
||||
except Exception as e:
|
||||
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
if self.serial.is_open:
|
||||
sleep(0.5)
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
thread.setDaemon(True)
|
||||
thread.start()
|
||||
self.online = True
|
||||
RNS.log("Serial port "+self.port+" is now open")
|
||||
self.configure_device()
|
||||
else:
|
||||
raise IOError("Could not open serial port")
|
||||
|
||||
|
||||
def open_port(self):
|
||||
RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE)
|
||||
self.serial = self.pyserial.Serial(
|
||||
port = self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = 0,
|
||||
inter_byte_timeout = None,
|
||||
write_timeout = None,
|
||||
dsrdtr = False,
|
||||
)
|
||||
|
||||
|
||||
def configure_device(self):
|
||||
sleep(0.5)
|
||||
thread = threading.Thread(target=self.readLoop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
self.online = True
|
||||
RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
|
||||
|
||||
|
||||
def processIncoming(self, data):
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
@@ -113,7 +154,7 @@ class SerialInterface(Interface):
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU):
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (byte == HDLC.ESC):
|
||||
escape = True
|
||||
else:
|
||||
@@ -132,13 +173,33 @@ class SerialInterface(Interface):
|
||||
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 being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
|
||||
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
RNS.panic()
|
||||
|
||||
RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
|
||||
|
||||
self.online = False
|
||||
self.serial.close()
|
||||
self.reconnect_port()
|
||||
|
||||
def reconnect_port(self):
|
||||
while not self.online:
|
||||
try:
|
||||
time.sleep(5)
|
||||
RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
self.open_port()
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
except Exception as e:
|
||||
RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
|
||||
def __str__(self):
|
||||
return "SerialInterface["+self.name+"]"
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
import socketserver
|
||||
import threading
|
||||
import netifaces
|
||||
import platform
|
||||
import socket
|
||||
import time
|
||||
@@ -20,23 +41,49 @@ class HDLC():
|
||||
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
|
||||
return data
|
||||
|
||||
class KISS():
|
||||
FEND = 0xC0
|
||||
FESC = 0xDB
|
||||
TFEND = 0xDC
|
||||
TFESC = 0xDD
|
||||
CMD_DATA = 0x00
|
||||
CMD_UNKNOWN = 0xFE
|
||||
|
||||
@staticmethod
|
||||
def escape(data):
|
||||
data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
|
||||
data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
|
||||
return data
|
||||
|
||||
class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
pass
|
||||
|
||||
class TCPClientInterface(Interface):
|
||||
BITRATE_GUESS = 10*1000*1000
|
||||
|
||||
RECONNECT_WAIT = 5
|
||||
RECONNECT_MAX_TRIES = None
|
||||
|
||||
# TCP socket options
|
||||
TCP_USER_TIMEOUT = 20
|
||||
TCP_USER_TIMEOUT = 24
|
||||
TCP_PROBE_AFTER = 5
|
||||
TCP_PROBE_INTERVAL = 3
|
||||
TCP_PROBES = 5
|
||||
TCP_PROBE_INTERVAL = 2
|
||||
TCP_PROBES = 12
|
||||
|
||||
def __init__(self, owner, name, target_ip=None, target_port=None, connected_socket=None, max_reconnect_tries=None):
|
||||
INITIAL_CONNECT_TIMEOUT = 5
|
||||
SYNCHRONOUS_START = True
|
||||
|
||||
I2P_USER_TIMEOUT = 45
|
||||
I2P_PROBE_AFTER = 10
|
||||
I2P_PROBE_INTERVAL = 9
|
||||
I2P_PROBES = 5
|
||||
|
||||
def __init__(self, owner, name, target_ip=None, target_port=None, connected_socket=None, max_reconnect_tries=None, kiss_framing=False, i2p_tunneled = False, connect_timeout = None):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.socket = None
|
||||
@@ -49,7 +96,11 @@ class TCPClientInterface(Interface):
|
||||
self.writing = False
|
||||
self.online = False
|
||||
self.detached = False
|
||||
|
||||
self.kiss_framing = kiss_framing
|
||||
self.i2p_tunneled = i2p_tunneled
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
self.bitrate = TCPClientInterface.BITRATE_GUESS
|
||||
|
||||
if max_reconnect_tries == None:
|
||||
self.max_reconnect_tries = TCPClientInterface.RECONNECT_MAX_TRIES
|
||||
else:
|
||||
@@ -71,25 +122,45 @@ class TCPClientInterface(Interface):
|
||||
self.target_ip = target_ip
|
||||
self.target_port = target_port
|
||||
self.initiator = True
|
||||
|
||||
if not self.connect(initial=True):
|
||||
thread = threading.Thread(target=self.reconnect)
|
||||
thread.setDaemon(True)
|
||||
thread.start()
|
||||
|
||||
if connect_timeout != None:
|
||||
self.connect_timeout = connect_timeout
|
||||
else:
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.setDaemon(True)
|
||||
self.connect_timeout = TCPClientInterface.INITIAL_CONNECT_TIMEOUT
|
||||
|
||||
if TCPClientInterface.SYNCHRONOUS_START:
|
||||
self.initial_connect()
|
||||
else:
|
||||
thread = threading.Thread(target=self.initial_connect)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def initial_connect(self):
|
||||
if not self.connect(initial=True):
|
||||
thread = threading.Thread(target=self.reconnect)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
else:
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
if not self.kiss_framing:
|
||||
self.wants_tunnel = True
|
||||
|
||||
|
||||
def set_timeouts_linux(self):
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(TCPClientInterface.TCP_USER_TIMEOUT * 1000))
|
||||
if not self.i2p_tunneled:
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(TCPClientInterface.TCP_USER_TIMEOUT * 1000))
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(TCPClientInterface.TCP_PROBE_AFTER))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(TCPClientInterface.TCP_PROBE_INTERVAL))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(TCPClientInterface.TCP_PROBES))
|
||||
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(TCPClientInterface.TCP_PROBE_AFTER))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(TCPClientInterface.TCP_PROBE_INTERVAL))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(TCPClientInterface.TCP_PROBES))
|
||||
else:
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(TCPClientInterface.I2P_USER_TIMEOUT * 1000))
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(TCPClientInterface.I2P_PROBE_AFTER))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(TCPClientInterface.I2P_PROBE_INTERVAL))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(TCPClientInterface.I2P_PROBES))
|
||||
|
||||
def set_timeouts_osx(self):
|
||||
if hasattr(socket, "TCP_KEEPALIVE"):
|
||||
@@ -97,9 +168,13 @@ class TCPClientInterface(Interface):
|
||||
else:
|
||||
TCP_KEEPIDLE = 0x10
|
||||
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(TCPClientInterface.TCP_PROBE_AFTER))
|
||||
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
|
||||
if not self.i2p_tunneled:
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(TCPClientInterface.TCP_PROBE_AFTER))
|
||||
else:
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(TCPClientInterface.I2P_PROBE_AFTER))
|
||||
|
||||
def detach(self):
|
||||
if self.socket != None:
|
||||
if hasattr(self.socket, "close"):
|
||||
@@ -121,9 +196,17 @@ class TCPClientInterface(Interface):
|
||||
|
||||
def connect(self, initial=False):
|
||||
try:
|
||||
if initial:
|
||||
RNS.log("Establishing TCP connection for "+str(self)+"...", RNS.LOG_DEBUG)
|
||||
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(TCPClientInterface.INITIAL_CONNECT_TIMEOUT)
|
||||
self.socket.connect((self.target_ip, self.target_port))
|
||||
self.socket.settimeout(None)
|
||||
self.online = True
|
||||
|
||||
if initial:
|
||||
RNS.log("TCP connection for "+str(self)+" established", RNS.LOG_DEBUG)
|
||||
|
||||
except Exception as e:
|
||||
if initial:
|
||||
@@ -167,13 +250,14 @@ class TCPClientInterface(Interface):
|
||||
RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
|
||||
if not self.never_connected:
|
||||
RNS.log("Reconnected TCP socket for "+str(self)+".", RNS.LOG_INFO)
|
||||
RNS.log("Reconnected socket for "+str(self)+".", RNS.LOG_INFO)
|
||||
|
||||
self.reconnecting = False
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
RNS.Transport.synthesize_tunnel(self)
|
||||
if not self.kiss_framing:
|
||||
RNS.Transport.synthesize_tunnel(self)
|
||||
|
||||
else:
|
||||
RNS.log("Attempt to reconnect on a non-initiator TCP interface. This should not happen.", RNS.LOG_ERROR)
|
||||
@@ -188,12 +272,17 @@ class TCPClientInterface(Interface):
|
||||
|
||||
def processOutgoing(self, data):
|
||||
if self.online:
|
||||
while self.writing:
|
||||
time.sleep(0.01)
|
||||
# while self.writing:
|
||||
# time.sleep(0.01)
|
||||
|
||||
try:
|
||||
self.writing = True
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
|
||||
if self.kiss_framing:
|
||||
data = bytes([KISS.FEND])+bytes([KISS.CMD_DATA])+KISS.escape(data)+bytes([KISS.FEND])
|
||||
else:
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
|
||||
self.socket.sendall(data)
|
||||
self.writing = False
|
||||
self.txb += len(data)
|
||||
@@ -211,6 +300,7 @@ class TCPClientInterface(Interface):
|
||||
in_frame = False
|
||||
escape = False
|
||||
data_buffer = b""
|
||||
command = KISS.CMD_UNKNOWN
|
||||
|
||||
while True:
|
||||
data_in = self.socket.recv(4096)
|
||||
@@ -219,30 +309,60 @@ class TCPClientInterface(Interface):
|
||||
while pointer < len(data_in):
|
||||
byte = data_in[pointer]
|
||||
pointer += 1
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < RNS.Reticulum.MTU):
|
||||
if (byte == HDLC.ESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.FLAG
|
||||
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.ESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
|
||||
if self.kiss_framing:
|
||||
# Read loop for KISS framing
|
||||
if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
elif (byte == KISS.FEND):
|
||||
in_frame = True
|
||||
command = KISS.CMD_UNKNOWN
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
|
||||
# We only support one HDLC port for now, so
|
||||
# strip off the port nibble
|
||||
byte = byte & 0x0F
|
||||
command = byte
|
||||
elif (command == KISS.CMD_DATA):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
|
||||
else:
|
||||
# Read loop for HDLC framing
|
||||
if (in_frame and byte == HDLC.FLAG):
|
||||
in_frame = False
|
||||
self.processIncoming(data_buffer)
|
||||
elif (byte == HDLC.FLAG):
|
||||
in_frame = True
|
||||
data_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < self.HW_MTU):
|
||||
if (byte == HDLC.ESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.FLAG
|
||||
if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
|
||||
byte = HDLC.ESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
else:
|
||||
self.online = False
|
||||
if self.initiator and not self.detached:
|
||||
RNS.log("TCP socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
|
||||
RNS.log("The socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
|
||||
self.reconnect()
|
||||
else:
|
||||
RNS.log("TCP socket for remote client "+str(self)+" was closed.", RNS.LOG_VERBOSE)
|
||||
RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_VERBOSE)
|
||||
self.teardown()
|
||||
|
||||
break
|
||||
@@ -284,22 +404,46 @@ class TCPClientInterface(Interface):
|
||||
|
||||
|
||||
class TCPServerInterface(Interface):
|
||||
BITRATE_GUESS = 10*1000*1000
|
||||
|
||||
@staticmethod
|
||||
def get_address_for_if(name):
|
||||
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['addr']
|
||||
import importlib
|
||||
if importlib.util.find_spec('netifaces') != None:
|
||||
import netifaces
|
||||
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['addr']
|
||||
else:
|
||||
RNS.log("Getting interface addresses from device names requires the netifaces module.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install it with the command: python3 -m pip install netifaces", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
@staticmethod
|
||||
def get_broadcast_for_if(name):
|
||||
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['broadcast']
|
||||
import importlib
|
||||
if importlib.util.find_spec('netifaces') != None:
|
||||
import netifaces
|
||||
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['broadcast']
|
||||
else:
|
||||
RNS.log("Getting interface addresses from device names requires the netifaces module.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install it with the command: python3 -m pip install netifaces", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
def __init__(self, owner, name, device=None, bindip=None, bindport=None):
|
||||
def __init__(self, owner, name, device=None, bindip=None, bindport=None, i2p_tunneled=False):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
self.online = False
|
||||
self.clients = 0
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.name = name
|
||||
self.detached = False
|
||||
|
||||
self.i2p_tunneled = i2p_tunneled
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
|
||||
if device != None:
|
||||
bindip = TCPServerInterface.get_address_for_if(device)
|
||||
@@ -320,8 +464,10 @@ class TCPServerInterface(Interface):
|
||||
ThreadingTCPServer.allow_reuse_address = True
|
||||
self.server = ThreadingTCPServer(address, handlerFactory(self.incoming_connection))
|
||||
|
||||
self.bitrate = TCPServerInterface.BITRATE_GUESS
|
||||
|
||||
thread = threading.Thread(target=self.server.serve_forever)
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
self.online = True
|
||||
@@ -330,12 +476,21 @@ class TCPServerInterface(Interface):
|
||||
def incoming_connection(self, handler):
|
||||
RNS.log("Accepting incoming TCP connection", RNS.LOG_VERBOSE)
|
||||
interface_name = "Client on "+self.name
|
||||
spawned_interface = TCPClientInterface(self.owner, interface_name, target_ip=None, target_port=None, connected_socket=handler.request)
|
||||
spawned_interface = TCPClientInterface(self.owner, interface_name, target_ip=None, target_port=None, connected_socket=handler.request, i2p_tunneled=self.i2p_tunneled)
|
||||
spawned_interface.OUT = self.OUT
|
||||
spawned_interface.IN = self.IN
|
||||
spawned_interface.target_ip = handler.client_address[0]
|
||||
spawned_interface.target_port = str(handler.client_address[1])
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
spawned_interface.ifac_size = self.ifac_size
|
||||
spawned_interface.ifac_netname = self.ifac_netname
|
||||
spawned_interface.ifac_netkey = self.ifac_netkey
|
||||
spawned_interface.announce_rate_target = self.announce_rate_target
|
||||
spawned_interface.announce_rate_grace = self.announce_rate_grace
|
||||
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.log("Spawned new TCPClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
@@ -345,13 +500,29 @@ class TCPServerInterface(Interface):
|
||||
def processOutgoing(self, data):
|
||||
pass
|
||||
|
||||
|
||||
def detach(self):
|
||||
if self.server != None:
|
||||
if hasattr(self.server, "shutdown"):
|
||||
if callable(self.server.shutdown):
|
||||
try:
|
||||
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
|
||||
self.server.shutdown()
|
||||
self.detached = True
|
||||
self.server = None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while shutting down server for "+str(self)+": "+str(e))
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "TCPServerInterface["+self.name+"/"+self.bind_ip+":"+str(self.bind_port)+"]"
|
||||
|
||||
|
||||
class TCPInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
def __init__(self, callback, *args, **keys):
|
||||
self.callback = callback
|
||||
socketserver.BaseRequestHandler.__init__(self, *args, **keys)
|
||||
|
||||
def handle(self):
|
||||
self.callback(handler=self)
|
||||
self.callback(handler=self)
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .Interface import Interface
|
||||
import socketserver
|
||||
import threading
|
||||
import netifaces
|
||||
import socket
|
||||
import time
|
||||
import sys
|
||||
@@ -9,21 +30,41 @@ import RNS
|
||||
|
||||
|
||||
class UDPInterface(Interface):
|
||||
BITRATE_GUESS = 10*1000*1000
|
||||
|
||||
@staticmethod
|
||||
def get_address_for_if(name):
|
||||
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['addr']
|
||||
import importlib
|
||||
if importlib.util.find_spec('netifaces') != None:
|
||||
import netifaces
|
||||
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['addr']
|
||||
else:
|
||||
RNS.log("Getting interface addresses from device names requires the netifaces module.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install it with the command: python3 -m pip install netifaces", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
@staticmethod
|
||||
def get_broadcast_for_if(name):
|
||||
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['broadcast']
|
||||
import importlib
|
||||
if importlib.util.find_spec('netifaces') != None:
|
||||
import netifaces
|
||||
return netifaces.ifaddresses(name)[netifaces.AF_INET][0]['broadcast']
|
||||
else:
|
||||
RNS.log("Getting interface addresses from device names requires the netifaces module.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install it with the command: python3 -m pip install netifaces", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
def __init__(self, owner, name, device=None, bindip=None, bindport=None, forwardip=None, forwardport=None):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
|
||||
self.HW_MTU = 1064
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.name = name
|
||||
self.online = False
|
||||
self.bitrate = UDPInterface.BITRATE_GUESS
|
||||
|
||||
if device != None:
|
||||
if bindip == None:
|
||||
@@ -44,10 +85,11 @@ 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))
|
||||
|
||||
thread = threading.Thread(target=self.server.serve_forever)
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
self.online = True
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
import glob
|
||||
import RNS.Interfaces.Android
|
||||
|
||||
modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
|
||||
__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')]
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.fernet import Fernet
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from RNS.Cryptography import X25519PrivateKey, X25519PublicKey, Ed25519PrivateKey, Ed25519PublicKey
|
||||
from RNS.Cryptography import Fernet
|
||||
|
||||
from time import sleep
|
||||
from .vendor import umsgpack as umsgpack
|
||||
import threading
|
||||
import base64
|
||||
import math
|
||||
import time
|
||||
import RNS
|
||||
|
||||
import traceback
|
||||
|
||||
class LinkCallbacks:
|
||||
def __init__(self):
|
||||
@@ -29,7 +45,7 @@ class Link:
|
||||
"""
|
||||
This class is used to establish and manage links to other peers. When a
|
||||
link instance is created, Reticulum will attempt to establish verified
|
||||
connectivity with the specified destination.
|
||||
and encrypted connectivity with the specified destination.
|
||||
|
||||
:param destination: A :ref:`RNS.Destination<api-destination>` instance which to establish a link to.
|
||||
:param established_callback: An optional function or method with the signature *callback(link)* to be called when the link has been established.
|
||||
@@ -43,24 +59,34 @@ class Link:
|
||||
ECPUBSIZE = 32+32
|
||||
KEYSIZE = 32
|
||||
|
||||
MDU = math.floor((RNS.Reticulum.MTU-RNS.Reticulum.HEADER_MINSIZE-RNS.Identity.FERNET_OVERHEAD)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
|
||||
MDU = math.floor((RNS.Reticulum.MTU-RNS.Reticulum.IFAC_MIN_SIZE-RNS.Reticulum.HEADER_MINSIZE-RNS.Identity.FERNET_OVERHEAD)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
|
||||
|
||||
# This value is set at a reasonable level for a 1 Kb/s channel.
|
||||
#
|
||||
# TODO: Find a way to automatically raise or lower this according to
|
||||
# channel bandwidth and utilisation.
|
||||
ESTABLISHMENT_TIMEOUT_PER_HOP = 5
|
||||
ESTABLISHMENT_TIMEOUT_PER_HOP = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT
|
||||
"""
|
||||
Default timeout for link establishment in seconds per hop to destination.
|
||||
Timeout for link establishment in seconds per hop to destination.
|
||||
"""
|
||||
|
||||
TRAFFIC_TIMEOUT_FACTOR = 6
|
||||
KEEPALIVE_TIMEOUT_FACTOR = 4
|
||||
"""
|
||||
RTT timeout factor used in link timeout calculation.
|
||||
"""
|
||||
STALE_GRACE = 2
|
||||
"""
|
||||
Grace period in seconds used in link timeout calculation.
|
||||
"""
|
||||
KEEPALIVE = 360
|
||||
"""
|
||||
Interval for sending keep-alive packets on established links in seconds.
|
||||
"""
|
||||
STALE_TIME = 2*KEEPALIVE
|
||||
"""
|
||||
If no traffic or keep-alive packets are received within this period, the
|
||||
link will be marked as stale, and a final keep-alive packet will be sent.
|
||||
If after this no traffic or keep-alive packets are received within ``RTT`` *
|
||||
``KEEPALIVE_TIMEOUT_FACTOR`` + ``STALE_GRACE``, the link is considered timed out,
|
||||
and will be torn down.
|
||||
"""
|
||||
|
||||
PENDING = 0x00
|
||||
HANDSHAKE = 0x01
|
||||
@@ -85,6 +111,7 @@ class Link:
|
||||
link.set_link_id(packet)
|
||||
link.destination = packet.destination
|
||||
link.establishment_timeout = Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, packet.hops)
|
||||
link.establishment_cost += len(packet.raw)
|
||||
RNS.log("Validating link request "+RNS.prettyhexrep(link.link_id), RNS.LOG_VERBOSE)
|
||||
link.handshake()
|
||||
link.attached_interface = packet.receiving_interface
|
||||
@@ -99,7 +126,7 @@ class Link:
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Validating link request failed", RNS.LOG_VERBOSE)
|
||||
traceback.print_exc()
|
||||
RNS.log("exc: "+str(e))
|
||||
return None
|
||||
|
||||
else:
|
||||
@@ -111,6 +138,7 @@ class Link:
|
||||
if destination != None and destination.type != RNS.Destination.SINGLE:
|
||||
raise TypeError("Links can only be established to the \"single\" destination type")
|
||||
self.rtt = None
|
||||
self.establishment_cost = 0
|
||||
self.callbacks = LinkCallbacks()
|
||||
self.resource_strategy = Link.ACCEPT_NONE
|
||||
self.outgoing_resources = []
|
||||
@@ -125,8 +153,10 @@ class Link:
|
||||
self.traffic_timeout_factor = Link.TRAFFIC_TIMEOUT_FACTOR
|
||||
self.keepalive_timeout_factor = Link.KEEPALIVE_TIMEOUT_FACTOR
|
||||
self.keepalive = Link.KEEPALIVE
|
||||
self.stale_time = Link.STALE_TIME
|
||||
self.watchdog_lock = False
|
||||
self.status = Link.PENDING
|
||||
self.activated_at = None
|
||||
self.type = RNS.Destination.LINK
|
||||
self.owner = owner
|
||||
self.destination = destination
|
||||
@@ -134,7 +164,7 @@ class Link:
|
||||
self.__remote_identity = None
|
||||
if self.destination == None:
|
||||
self.initiator = False
|
||||
self.prv = self.owner.identity.prv
|
||||
self.prv = X25519PrivateKey.generate()
|
||||
self.sig_prv = self.owner.identity.sig_prv
|
||||
else:
|
||||
self.initiator = True
|
||||
@@ -145,16 +175,10 @@ class Link:
|
||||
self.fernet = None
|
||||
|
||||
self.pub = self.prv.public_key()
|
||||
self.pub_bytes = self.pub.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
self.pub_bytes = self.pub.public_bytes()
|
||||
|
||||
self.sig_pub = self.sig_prv.public_key()
|
||||
self.sig_pub_bytes = self.sig_pub.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
self.sig_pub_bytes = self.sig_pub.public_bytes()
|
||||
|
||||
if peer_pub_bytes == None:
|
||||
self.peer_pub = None
|
||||
@@ -169,14 +193,11 @@ class Link:
|
||||
self.set_link_closed_callback(closed_callback)
|
||||
|
||||
if (self.initiator):
|
||||
peer_pub_bytes = self.destination.identity.get_public_key()[:Link.ECPUBSIZE//2]
|
||||
peer_sig_pub_bytes = self.destination.identity.get_public_key()[Link.ECPUBSIZE//2:Link.ECPUBSIZE]
|
||||
self.request_data = self.pub_bytes+self.sig_pub_bytes
|
||||
self.packet = RNS.Packet(destination, self.request_data, packet_type=RNS.Packet.LINKREQUEST)
|
||||
self.packet.pack()
|
||||
self.establishment_cost += len(self.packet.raw)
|
||||
self.set_link_id(self.packet)
|
||||
self.load_peer(peer_pub_bytes, peer_sig_pub_bytes)
|
||||
self.handshake()
|
||||
RNS.Transport.register_link(self)
|
||||
self.request_time = time.time()
|
||||
self.start_watchdog()
|
||||
@@ -202,22 +223,26 @@ class Link:
|
||||
def handshake(self):
|
||||
self.status = Link.HANDSHAKE
|
||||
self.shared_key = self.prv.exchange(self.peer_pub)
|
||||
self.derived_key = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
|
||||
self.derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
derive_from=self.shared_key,
|
||||
salt=self.get_salt(),
|
||||
info=self.get_context(),
|
||||
).derive(self.shared_key)
|
||||
context=self.get_context(),
|
||||
)
|
||||
|
||||
|
||||
def prove(self):
|
||||
signed_data = self.link_id+self.pub_bytes+self.sig_pub_bytes
|
||||
signature = self.owner.identity.sign(signed_data)
|
||||
|
||||
proof_data = signature
|
||||
proof_data = signature+self.pub_bytes
|
||||
proof = RNS.Packet(self, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.LRPROOF)
|
||||
proof.send()
|
||||
self.establishment_cost += len(proof.raw)
|
||||
self.had_outbound()
|
||||
|
||||
|
||||
def prove_packet(self, packet):
|
||||
signature = self.sign(packet.packet_hash)
|
||||
# TODO: Hardcoded as explicit proof for now
|
||||
@@ -232,8 +257,14 @@ class Link:
|
||||
self.had_outbound()
|
||||
|
||||
def validate_proof(self, packet):
|
||||
if self.status == Link.HANDSHAKE:
|
||||
if self.initiator and len(packet.data) == RNS.Identity.SIGLENGTH//8:
|
||||
if self.status == Link.PENDING:
|
||||
if self.initiator and len(packet.data) == RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2:
|
||||
peer_pub_bytes = packet.data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2]
|
||||
peer_sig_pub_bytes = self.destination.identity.get_public_key()[Link.ECPUBSIZE//2:Link.ECPUBSIZE]
|
||||
self.load_peer(peer_pub_bytes, peer_sig_pub_bytes)
|
||||
self.handshake()
|
||||
|
||||
self.establishment_cost += len(packet.raw)
|
||||
signed_data = self.link_id+self.peer_pub_bytes+self.peer_sig_pub_bytes
|
||||
signature = packet.data[:RNS.Identity.SIGLENGTH//8]
|
||||
|
||||
@@ -242,16 +273,17 @@ class Link:
|
||||
self.attached_interface = packet.receiving_interface
|
||||
self.__remote_identity = self.destination.identity
|
||||
RNS.Transport.activate_link(self)
|
||||
RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+str(self.rtt), RNS.LOG_VERBOSE)
|
||||
RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+str(round(self.rtt, 3))+"s", RNS.LOG_VERBOSE)
|
||||
rtt_data = umsgpack.packb(self.rtt)
|
||||
rtt_packet = RNS.Packet(self, rtt_data, context=RNS.Packet.LRRTT)
|
||||
rtt_packet.send()
|
||||
self.had_outbound()
|
||||
|
||||
self.status = Link.ACTIVE
|
||||
self.activated_at = time.time()
|
||||
if self.callbacks.link_established != None:
|
||||
thread = threading.Thread(target=self.callbacks.link_established, args=(self,))
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
else:
|
||||
RNS.log("Invalid link proof signature received by "+str(self)+". Ignoring.", RNS.LOG_DEBUG)
|
||||
@@ -292,7 +324,7 @@ class Link:
|
||||
packed_request = umsgpack.packb(unpacked_request)
|
||||
|
||||
if timeout == None:
|
||||
timeout = self.rtt * self.traffic_timeout_factor + RNS.Resource.RESPONSE_MAX_GRACE_TIME
|
||||
timeout = self.rtt * self.traffic_timeout_factor + RNS.Resource.RESPONSE_MAX_GRACE_TIME/4.0
|
||||
|
||||
if len(packed_request) <= Link.MDU:
|
||||
request_packet = RNS.Packet(self, packed_request, RNS.Packet.DATA, context = RNS.Packet.REQUEST)
|
||||
@@ -308,7 +340,8 @@ class Link:
|
||||
response_callback = response_callback,
|
||||
failed_callback = failed_callback,
|
||||
progress_callback = progress_callback,
|
||||
timeout = timeout
|
||||
timeout = timeout,
|
||||
request_size = len(packed_request),
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -322,7 +355,8 @@ class Link:
|
||||
response_callback = response_callback,
|
||||
failed_callback = failed_callback,
|
||||
progress_callback = progress_callback,
|
||||
timeout = timeout
|
||||
timeout = timeout,
|
||||
request_size = len(packed_request),
|
||||
)
|
||||
|
||||
|
||||
@@ -338,6 +372,8 @@ class Link:
|
||||
rtt = umsgpack.unpackb(plaintext)
|
||||
self.rtt = max(measured_rtt, rtt)
|
||||
self.status = Link.ACTIVE
|
||||
self.activated_at = time.time()
|
||||
|
||||
|
||||
if self.owner.callbacks.link_established != None:
|
||||
self.owner.callbacks.link_established(self)
|
||||
@@ -355,7 +391,9 @@ class Link:
|
||||
"""
|
||||
:returns: The time in seconds since last inbound packet on the link.
|
||||
"""
|
||||
return time.time() - self.last_inbound
|
||||
activated_at = self.activated_at if self.activated_at != None else 0
|
||||
last_inbound = max(self.last_inbound, activated_at)
|
||||
return time.time() - last_inbound
|
||||
|
||||
def no_outbound_for(self):
|
||||
"""
|
||||
@@ -371,7 +409,7 @@ class Link:
|
||||
|
||||
def get_remote_identity(self):
|
||||
"""
|
||||
:returns: The identity of the remote peer, if it is known
|
||||
:returns: The identity of the remote peer, if it is known. Calling this method will not query the remote initiator to reveal its identity. Returns ``None`` if the link initiator has not already independently called the ``identify(identity)`` method.
|
||||
"""
|
||||
return self.__remote_identity
|
||||
|
||||
@@ -425,11 +463,15 @@ class Link:
|
||||
self.destination.links.remove(self)
|
||||
|
||||
if self.callbacks.link_closed != None:
|
||||
self.callbacks.link_closed(self)
|
||||
try:
|
||||
self.callbacks.link_closed(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing link closed callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
def start_watchdog(self):
|
||||
thread = threading.Thread(target=self.__watchdog_job)
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def __watchdog_job(self):
|
||||
@@ -465,13 +507,21 @@ class Link:
|
||||
sleep_time = 0.001
|
||||
|
||||
elif self.status == Link.ACTIVE:
|
||||
if time.time() >= self.last_inbound + self.keepalive:
|
||||
sleep_time = self.rtt * self.keepalive_timeout_factor + Link.STALE_GRACE
|
||||
self.status = Link.STALE
|
||||
activated_at = self.activated_at if self.activated_at != None else 0
|
||||
last_inbound = max(self.last_inbound, activated_at)
|
||||
|
||||
if time.time() >= last_inbound + self.keepalive:
|
||||
if self.initiator:
|
||||
self.send_keepalive()
|
||||
|
||||
if time.time() >= last_inbound + self.stale_time:
|
||||
sleep_time = self.rtt * self.keepalive_timeout_factor + Link.STALE_GRACE
|
||||
self.status = Link.STALE
|
||||
else:
|
||||
sleep_time = self.keepalive
|
||||
|
||||
else:
|
||||
sleep_time = (self.last_inbound + self.keepalive) - time.time()
|
||||
sleep_time = (last_inbound + self.keepalive) - time.time()
|
||||
|
||||
elif self.status == Link.STALE:
|
||||
sleep_time = 0.001
|
||||
@@ -511,7 +561,7 @@ class Link:
|
||||
allowed = False
|
||||
if not allow == RNS.Destination.ALLOW_NONE:
|
||||
if allow == RNS.Destination.ALLOW_LIST:
|
||||
if self.__remote_identity.hash in allowed_list:
|
||||
if self.__remote_identity != None and self.__remote_identity.hash in allowed_list:
|
||||
allowed = True
|
||||
elif allow == RNS.Destination.ALLOW_ALL:
|
||||
allowed = True
|
||||
@@ -546,7 +596,8 @@ class Link:
|
||||
break
|
||||
|
||||
if remove != None:
|
||||
self.pending_requests.remove(remove)
|
||||
if remove in self.pending_requests:
|
||||
self.pending_requests.remove(remove)
|
||||
|
||||
def request_resource_concluded(self, resource):
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
@@ -590,7 +641,7 @@ class Link:
|
||||
plaintext = self.decrypt(packet.data)
|
||||
if self.callbacks.packet != None:
|
||||
thread = threading.Thread(target=self.callbacks.packet, args=(plaintext, packet))
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
if self.destination.proof_strategy == RNS.Destination.PROVE_ALL:
|
||||
@@ -598,7 +649,10 @@ class Link:
|
||||
|
||||
elif self.destination.proof_strategy == RNS.Destination.PROVE_APP:
|
||||
if self.destination.callbacks.proof_requested:
|
||||
self.destination.callbacks.proof_requested(packet)
|
||||
try:
|
||||
self.destination.callbacks.proof_requested(packet)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing proof request callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
elif packet.context == RNS.Packet.LINKIDENTIFY:
|
||||
plaintext = self.decrypt(packet.data)
|
||||
@@ -613,7 +667,10 @@ class Link:
|
||||
if identity.validate(signature, signed_data):
|
||||
self.__remote_identity = identity
|
||||
if self.callbacks.remote_identified != None:
|
||||
self.callbacks.remote_identified(self.__remote_identity)
|
||||
try:
|
||||
self.callbacks.remote_identified(self, self.__remote_identity)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing remote identified callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
elif packet.context == RNS.Packet.REQUEST:
|
||||
try:
|
||||
@@ -648,19 +705,24 @@ class Link:
|
||||
if RNS.ResourceAdvertisement.is_request(packet):
|
||||
RNS.Resource.accept(packet, callback=self.request_resource_concluded)
|
||||
elif RNS.ResourceAdvertisement.is_response(packet):
|
||||
request_id = RNS.ResourceAdvertisement.get_request_id(packet)
|
||||
request_id = RNS.ResourceAdvertisement.read_request_id(packet)
|
||||
for pending_request in self.pending_requests:
|
||||
if pending_request.request_id == request_id:
|
||||
RNS.Resource.accept(packet, callback=self.response_resource_concluded, progress_callback=pending_request.response_resource_progress, request_id = request_id)
|
||||
pending_request.response_size = RNS.ResourceAdvertisement.get_size(packet)
|
||||
pending_request.response_transfer_size = RNS.ResourceAdvertisement.get_transfer_size(packet)
|
||||
pending_request.response_size = RNS.ResourceAdvertisement.read_size(packet)
|
||||
pending_request.response_transfer_size = RNS.ResourceAdvertisement.read_transfer_size(packet)
|
||||
pending_request.started_at = time.time()
|
||||
elif self.resource_strategy == Link.ACCEPT_NONE:
|
||||
pass
|
||||
elif self.resource_strategy == Link.ACCEPT_APP:
|
||||
if self.callbacks.resource != None:
|
||||
if self.callbacks.resource(resource):
|
||||
RNS.Resource.accept(packet, self.callbacks.resource_concluded)
|
||||
try:
|
||||
resource_advertisement = RNS.ResourceAdvertisement.unpack(packet.plaintext)
|
||||
resource_advertisement.link = self
|
||||
if self.callbacks.resource(resource_advertisement):
|
||||
RNS.Resource.accept(packet, self.callbacks.resource_concluded)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing resource accept callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
elif self.resource_strategy == Link.ACCEPT_ALL:
|
||||
RNS.Resource.accept(packet, self.callbacks.resource_concluded)
|
||||
|
||||
@@ -670,6 +732,7 @@ class Link:
|
||||
resource_hash = plaintext[1+RNS.Resource.MAPHASH_LEN:RNS.Identity.HASHLENGTH//8+1+RNS.Resource.MAPHASH_LEN]
|
||||
else:
|
||||
resource_hash = plaintext[1:RNS.Identity.HASHLENGTH//8+1]
|
||||
|
||||
for resource in self.outgoing_resources:
|
||||
if resource.hash == resource_hash:
|
||||
# We need to check that this request has not been
|
||||
@@ -721,21 +784,12 @@ class Link:
|
||||
try:
|
||||
if not self.fernet:
|
||||
try:
|
||||
self.fernet = Fernet(base64.urlsafe_b64encode(self.derived_key))
|
||||
self.fernet = Fernet(self.derived_key)
|
||||
except Exception as e:
|
||||
RNS.log("Could not "+str(self)+" instantiate Fernet while performin encryption on link. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
# The fernet token VERSION field is stripped here and
|
||||
# reinserted on the receiving end, since it is always
|
||||
# set to 0x80.
|
||||
#
|
||||
# Since we're also quite content with supporting time-
|
||||
# stamps until the year 8921556 AD, we'll also strip 2
|
||||
# bytes from the timestamp field and reinsert those as
|
||||
# 0x00 when received.
|
||||
ciphertext = base64.urlsafe_b64decode(self.fernet.encrypt(plaintext))[3:]
|
||||
return ciphertext
|
||||
return self.fernet.encrypt(plaintext)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Encryption on link "+str(self)+" failed. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -745,15 +799,12 @@ class Link:
|
||||
def decrypt(self, ciphertext):
|
||||
try:
|
||||
if not self.fernet:
|
||||
self.fernet = Fernet(base64.urlsafe_b64encode(self.derived_key))
|
||||
self.fernet = Fernet(self.derived_key)
|
||||
|
||||
plaintext = self.fernet.decrypt(base64.urlsafe_b64encode(bytes([RNS.Identity.FERNET_VERSION, 0x00, 0x00])+ciphertext))
|
||||
return plaintext
|
||||
return self.fernet.decrypt(ciphertext)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Decryption failed on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log(traceback.format_exc(), RNS.LOG_ERROR)
|
||||
# TODO: Think long about implications here
|
||||
# self.teardown()
|
||||
|
||||
|
||||
def sign(self, message):
|
||||
@@ -770,6 +821,12 @@ class Link:
|
||||
self.callbacks.link_established = callback
|
||||
|
||||
def set_link_closed_callback(self, callback):
|
||||
"""
|
||||
Registers a function to be called when a link has been
|
||||
torn down.
|
||||
|
||||
:param callback: A function or method with the signature *callback(link)* to be called.
|
||||
"""
|
||||
self.callbacks.link_closed = callback
|
||||
|
||||
def set_packet_callback(self, callback):
|
||||
@@ -788,7 +845,7 @@ class Link:
|
||||
the resource will be accepted. If it returns *False* it will
|
||||
be ignored.
|
||||
|
||||
:param callback: A function or method with the signature *callback(resource)* to be called.
|
||||
:param callback: A function or method with the signature *callback(resource)* to be called. Please note that only the basic information of the resource is available at this time, such as *get_transfer_size()*, *get_data_size()*, *get_parts()* and *is_compressed()*.
|
||||
"""
|
||||
self.callbacks.resource = callback
|
||||
|
||||
@@ -815,7 +872,7 @@ class Link:
|
||||
Registers a function to be called when an initiating peer has
|
||||
identified over this link.
|
||||
|
||||
:param callback: A function or method with the signature *callback(identity)* to be called.
|
||||
:param callback: A function or method with the signature *callback(link, identity)* to be called.
|
||||
"""
|
||||
self.callbacks.remote_identified = callback
|
||||
|
||||
@@ -878,7 +935,7 @@ class RequestReceipt():
|
||||
RECEIVING = 0x03
|
||||
READY = 0x04
|
||||
|
||||
def __init__(self, link, packet_receipt = None, resource = None, response_callback = None, failed_callback = None, progress_callback = None, timeout = None):
|
||||
def __init__(self, link, packet_receipt = None, resource = None, response_callback = None, failed_callback = None, progress_callback = None, timeout = None, request_size = None):
|
||||
self.packet_receipt = packet_receipt
|
||||
self.resource = resource
|
||||
self.started_at = None
|
||||
@@ -894,6 +951,7 @@ class RequestReceipt():
|
||||
|
||||
self.link = link
|
||||
self.request_id = self.hash
|
||||
self.request_size = request_size
|
||||
|
||||
self.response = None
|
||||
self.response_transfer_size = None
|
||||
@@ -924,7 +982,7 @@ class RequestReceipt():
|
||||
self.status = RequestReceipt.DELIVERED
|
||||
self.__resource_response_timeout = time.time()+self.timeout
|
||||
response_timeout_thread = threading.Thread(target=self.__response_timeout_job)
|
||||
response_timeout_thread.setDaemon(True)
|
||||
response_timeout_thread.daemon = True
|
||||
response_timeout_thread.start()
|
||||
else:
|
||||
RNS.log("Sending request "+RNS.prettyhexrep(self.request_id)+" as resource failed with status: "+RNS.hexrep([resource.status]), RNS.LOG_DEBUG)
|
||||
@@ -933,7 +991,10 @@ class RequestReceipt():
|
||||
self.link.pending_requests.remove(self)
|
||||
|
||||
if self.callbacks.failed != None:
|
||||
self.callbacks.failed(self)
|
||||
try:
|
||||
self.callbacks.failed(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing request failed callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
def __response_timeout_job(self):
|
||||
@@ -951,7 +1012,10 @@ class RequestReceipt():
|
||||
self.link.pending_requests.remove(self)
|
||||
|
||||
if self.callbacks.failed != None:
|
||||
self.callbacks.failed(self)
|
||||
try:
|
||||
self.callbacks.failed(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing request timed out callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
def response_resource_progress(self, resource):
|
||||
@@ -967,7 +1031,10 @@ class RequestReceipt():
|
||||
self.progress = resource.get_progress()
|
||||
|
||||
if self.callbacks.progress != None:
|
||||
self.callbacks.progress(self)
|
||||
try:
|
||||
self.callbacks.progress(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing response progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
else:
|
||||
resource.cancel()
|
||||
|
||||
@@ -987,10 +1054,16 @@ class RequestReceipt():
|
||||
self.packet_receipt.callbacks.delivery(self.packet_receipt)
|
||||
|
||||
if self.callbacks.progress != None:
|
||||
self.callbacks.progress(self)
|
||||
try:
|
||||
self.callbacks.progress(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing response progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
if self.callbacks.response != None:
|
||||
self.callbacks.response(self)
|
||||
try:
|
||||
self.callbacks.response(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing response received callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def get_request_id(self):
|
||||
"""
|
||||
@@ -1034,4 +1107,4 @@ class RequestReceiptCallbacks:
|
||||
def __init__(self):
|
||||
self.response = None
|
||||
self.failed = None
|
||||
self.progress = None
|
||||
self.progress = None
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import threading
|
||||
import struct
|
||||
import math
|
||||
@@ -7,15 +29,19 @@ import RNS
|
||||
class Packet:
|
||||
"""
|
||||
The Packet class is used to create packet instances that can be sent
|
||||
over a Reticulum network. Packets to will automatically be encrypted if
|
||||
over a Reticulum network. Packets will automatically be encrypted if
|
||||
they are adressed to a ``RNS.Destination.SINGLE`` destination,
|
||||
``RNS.Destination.GROUP`` destination or a :ref:`RNS.Link<api-link>`.
|
||||
|
||||
For ``RNS.Destination.GROUP`` destinations, Reticulum will use the
|
||||
pre-shared key configured for the destination.
|
||||
pre-shared key configured for the destination. All packets to group
|
||||
destinations are encrypted with the same AES-128 key.
|
||||
|
||||
For ``RNS.Destination.SINGLE`` destinations and :ref:`RNS.Link<api-link>`
|
||||
destinations, reticulum will use ephemeral keys, and offers **Forward Secrecy**.
|
||||
For ``RNS.Destination.SINGLE`` destinations, Reticulum will use a newly
|
||||
derived ephemeral AES-128 key for every packet.
|
||||
|
||||
For :ref:`RNS.Link<api-link>` destinations, Reticulum will use per-link
|
||||
ephemeral keys, and offers **Forward Secrecy**.
|
||||
|
||||
:param destination: A :ref:`RNS.Destination<api-destination>` instance to which the packet will be sent.
|
||||
:param data: The data payload to be included in the packet as *bytes*.
|
||||
@@ -72,12 +98,10 @@ class Packet:
|
||||
"""
|
||||
PLAIN_MDU = MDU
|
||||
"""
|
||||
The maximum size of the payload data in a single unencrypted packet
|
||||
The maximum size of the payload data in a single unencrypted packet
|
||||
"""
|
||||
|
||||
# This value is set at a reasonable
|
||||
# level for a 1 Kb/s channel.
|
||||
TIMEOUT_PER_HOP = 5
|
||||
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):
|
||||
if destination != None:
|
||||
@@ -113,6 +137,8 @@ class Packet:
|
||||
|
||||
self.attached_interface = attached_interface
|
||||
self.receiving_interface = None
|
||||
self.rssi = None
|
||||
self.snr = None
|
||||
|
||||
def get_packed_flags(self):
|
||||
if self.context == Packet.LRPROOF:
|
||||
@@ -185,27 +211,35 @@ class Packet:
|
||||
|
||||
|
||||
def unpack(self):
|
||||
self.flags = self.raw[0]
|
||||
self.hops = self.raw[1]
|
||||
try:
|
||||
self.flags = self.raw[0]
|
||||
self.hops = self.raw[1]
|
||||
|
||||
self.header_type = (self.flags & 0b11000000) >> 6
|
||||
self.transport_type = (self.flags & 0b00110000) >> 4
|
||||
self.destination_type = (self.flags & 0b00001100) >> 2
|
||||
self.packet_type = (self.flags & 0b00000011)
|
||||
self.header_type = (self.flags & 0b11000000) >> 6
|
||||
self.transport_type = (self.flags & 0b00110000) >> 4
|
||||
self.destination_type = (self.flags & 0b00001100) >> 2
|
||||
self.packet_type = (self.flags & 0b00000011)
|
||||
|
||||
if self.header_type == Packet.HEADER_2:
|
||||
self.transport_id = self.raw[2:12]
|
||||
self.destination_hash = self.raw[12:22]
|
||||
self.context = ord(self.raw[22:23])
|
||||
self.data = self.raw[23:]
|
||||
else:
|
||||
self.transport_id = None
|
||||
self.destination_hash = self.raw[2:12]
|
||||
self.context = ord(self.raw[12:13])
|
||||
self.data = self.raw[13:]
|
||||
DST_LEN = RNS.Reticulum.TRUNCATED_HASHLENGTH//8
|
||||
|
||||
self.packed = False
|
||||
self.update_hash()
|
||||
if self.header_type == Packet.HEADER_2:
|
||||
self.transport_id = self.raw[2:DST_LEN+2]
|
||||
self.destination_hash = self.raw[DST_LEN+2:2*DST_LEN+2]
|
||||
self.context = ord(self.raw[2*DST_LEN+2:2*DST_LEN+3])
|
||||
self.data = self.raw[2*DST_LEN+3:]
|
||||
else:
|
||||
self.transport_id = None
|
||||
self.destination_hash = self.raw[2:DST_LEN+2]
|
||||
self.context = ord(self.raw[DST_LEN+2:DST_LEN+3])
|
||||
self.data = self.raw[DST_LEN+3:]
|
||||
|
||||
self.packed = False
|
||||
self.update_hash()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Received malformed packet, dropping it. The contained exception was: "+str(e), RNS.LOG_EXTREME)
|
||||
return False
|
||||
|
||||
def send(self):
|
||||
"""
|
||||
@@ -224,7 +258,7 @@ class Packet:
|
||||
|
||||
if not self.packed:
|
||||
self.pack()
|
||||
|
||||
|
||||
if RNS.Transport.outbound(self):
|
||||
return self.receipt
|
||||
else:
|
||||
@@ -285,7 +319,7 @@ class Packet:
|
||||
def get_hashable_part(self):
|
||||
hashable_part = bytes([self.raw[0] & 0b00001111])
|
||||
if self.header_type == Packet.HEADER_2:
|
||||
hashable_part += self.raw[12:]
|
||||
hashable_part += self.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:]
|
||||
else:
|
||||
hashable_part += self.raw[2:]
|
||||
|
||||
@@ -293,7 +327,7 @@ class Packet:
|
||||
|
||||
class ProofDestination:
|
||||
def __init__(self, packet):
|
||||
self.hash = packet.get_hash()[:10];
|
||||
self.hash = packet.get_hash()[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8];
|
||||
self.type = RNS.Destination.SINGLE
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
@@ -328,6 +362,7 @@ class PacketReceipt:
|
||||
self.destination = packet.destination
|
||||
self.callbacks = PacketReceiptCallbacks()
|
||||
self.concluded_at = None
|
||||
self.proof_packet = None
|
||||
|
||||
if packet.destination.type == RNS.Destination.LINK:
|
||||
self.timeout = packet.destination.rtt * packet.destination.traffic_timeout_factor
|
||||
@@ -344,12 +379,12 @@ class PacketReceipt:
|
||||
# Validate a proof packet
|
||||
def validate_proof_packet(self, proof_packet):
|
||||
if hasattr(proof_packet, "link") and proof_packet.link:
|
||||
return self.validate_link_proof(proof_packet.data, proof_packet.link)
|
||||
return self.validate_link_proof(proof_packet.data, proof_packet.link, proof_packet)
|
||||
else:
|
||||
return self.validate_proof(proof_packet.data)
|
||||
return self.validate_proof(proof_packet.data, proof_packet)
|
||||
|
||||
# Validate a raw proof for a link
|
||||
def validate_link_proof(self, proof, link):
|
||||
def validate_link_proof(self, proof, link, proof_packet=None):
|
||||
# TODO: Hardcoded as explicit proofs for now
|
||||
if True or len(proof) == PacketReceipt.EXPL_LENGTH:
|
||||
# This is an explicit proof
|
||||
@@ -361,6 +396,8 @@ class PacketReceipt:
|
||||
self.status = PacketReceipt.DELIVERED
|
||||
self.proved = True
|
||||
self.concluded_at = time.time()
|
||||
self.proof_packet = proof_packet
|
||||
|
||||
if self.callbacks.delivery != None:
|
||||
self.callbacks.delivery(self)
|
||||
return True
|
||||
@@ -388,7 +425,7 @@ class PacketReceipt:
|
||||
return False
|
||||
|
||||
# Validate a raw proof
|
||||
def validate_proof(self, proof):
|
||||
def validate_proof(self, proof, proof_packet=None):
|
||||
if len(proof) == PacketReceipt.EXPL_LENGTH:
|
||||
# This is an explicit proof
|
||||
proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
|
||||
@@ -399,8 +436,14 @@ class PacketReceipt:
|
||||
self.status = PacketReceipt.DELIVERED
|
||||
self.proved = True
|
||||
self.concluded_at = time.time()
|
||||
self.proof_packet = proof_packet
|
||||
|
||||
if self.callbacks.delivery != None:
|
||||
self.callbacks.delivery(self)
|
||||
try:
|
||||
self.callbacks.delivery(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing proof validated callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -417,8 +460,14 @@ class PacketReceipt:
|
||||
self.status = PacketReceipt.DELIVERED
|
||||
self.proved = True
|
||||
self.concluded_at = time.time()
|
||||
self.proof_packet = proof_packet
|
||||
|
||||
if self.callbacks.delivery != None:
|
||||
self.callbacks.delivery(self)
|
||||
try:
|
||||
self.callbacks.delivery(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing proof validated callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -445,7 +494,7 @@ class PacketReceipt:
|
||||
|
||||
if self.callbacks.timeout:
|
||||
thread = threading.Thread(target=self.callbacks.timeout, args=(self,))
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import os
|
||||
import bz2
|
||||
@@ -20,10 +42,39 @@ class Resource:
|
||||
:param callback: An optional *callable* with the signature *callback(resource)*. Will be called when the resource transfer concludes.
|
||||
:param progress_callback: An optional *callable* with the signature *callback(resource)*. Will be called whenever the resource transfer progress is updated.
|
||||
"""
|
||||
WINDOW_FLEXIBILITY = 4
|
||||
WINDOW_MIN = 1
|
||||
WINDOW_MAX = 10
|
||||
|
||||
# The initial window size at beginning of transfer
|
||||
WINDOW = 4
|
||||
|
||||
# Absolute minimum window size during transfer
|
||||
WINDOW_MIN = 1
|
||||
|
||||
# The maximum window size for transfers on slow links
|
||||
WINDOW_MAX_SLOW = 10
|
||||
|
||||
# The maximum window size for transfers on fast links
|
||||
WINDOW_MAX_FAST = 75
|
||||
|
||||
# For calculating maps and guard segments, this
|
||||
# must be set to the global maximum window.
|
||||
WINDOW_MAX = WINDOW_MAX_FAST
|
||||
|
||||
# If the fast rate is sustained for this many request
|
||||
# rounds, the fast link window size will be allowed.
|
||||
FAST_RATE_THRESHOLD = WINDOW_MAX_SLOW - WINDOW - 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
|
||||
|
||||
# The minimum allowed flexibility of the window size.
|
||||
# The difference between window_max and window_min
|
||||
# will never be smaller than this value.
|
||||
WINDOW_FLEXIBILITY = 4
|
||||
|
||||
# Number of bytes in a map hash
|
||||
MAPHASH_LEN = 4
|
||||
SDU = RNS.Packet.MDU
|
||||
RANDOM_HASH_SIZE = 4
|
||||
@@ -52,9 +103,11 @@ class Resource:
|
||||
|
||||
PART_TIMEOUT_FACTOR = 4
|
||||
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
|
||||
MAX_RETRIES = 5
|
||||
MAX_RETRIES = 8
|
||||
MAX_ADV_RETRIES = 4
|
||||
SENDER_GRACE_TIME = 10
|
||||
RETRY_GRACE_TIME = 0.25
|
||||
PER_RETRY_DELAY = 0.5
|
||||
|
||||
WATCHDOG_MAX_SLEEP = 1
|
||||
|
||||
@@ -98,7 +151,7 @@ class Resource:
|
||||
resource.outstanding_parts = 0
|
||||
resource.parts = [None] * resource.total_parts
|
||||
resource.window = Resource.WINDOW
|
||||
resource.window_max = Resource.WINDOW_MAX
|
||||
resource.window_max = Resource.WINDOW_MAX_SLOW
|
||||
resource.window_min = Resource.WINDOW_MIN
|
||||
resource.window_flexibility = Resource.WINDOW_FLEXIBILITY
|
||||
resource.last_activity = time.time()
|
||||
@@ -123,7 +176,10 @@ class Resource:
|
||||
|
||||
RNS.log("Accepting resource advertisement for "+RNS.prettyhexrep(resource.hash), RNS.LOG_DEBUG)
|
||||
if resource.link.callbacks.resource_started != None:
|
||||
resource.link.callbacks.resource_started(resource)
|
||||
try:
|
||||
resource.link.callbacks.resource_started(resource)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing resource started callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
resource.hashmap_update(0, resource.hashmap_raw)
|
||||
|
||||
@@ -140,6 +196,8 @@ class Resource:
|
||||
def __init__(self, data, link, advertise=True, auto_compress=True, callback=None, progress_callback=None, timeout = None, segment_index = 1, original_hash = None, request_id = None, is_response = False):
|
||||
data_size = None
|
||||
resource_data = None
|
||||
self.assembly_lock = False
|
||||
|
||||
if hasattr(data, "read"):
|
||||
data_size = os.stat(data.name).st_size
|
||||
self.total_size = data_size
|
||||
@@ -183,6 +241,7 @@ class Resource:
|
||||
self.status = Resource.NONE
|
||||
self.link = link
|
||||
self.max_retries = Resource.MAX_RETRIES
|
||||
self.max_adv_retries = Resource.MAX_ADV_RETRIES
|
||||
self.retries_left = self.max_retries
|
||||
self.timeout_factor = self.link.traffic_timeout_factor
|
||||
self.part_timeout_factor = Resource.PART_TIMEOUT_FACTOR
|
||||
@@ -192,6 +251,11 @@ class Resource:
|
||||
self.__watchdog_job_id = 0
|
||||
self.__progress_callback = progress_callback
|
||||
self.rtt = None
|
||||
self.rtt_rxd_bytes = 0
|
||||
self.req_sent = 0
|
||||
self.req_resp_rtt_rate = 0
|
||||
self.rtt_rxd_bytes_at_part_req = 0
|
||||
self.fast_rate_rounds = 0
|
||||
self.request_id = request_id
|
||||
self.is_response = is_response
|
||||
|
||||
@@ -321,10 +385,6 @@ class Resource:
|
||||
self.request_next()
|
||||
|
||||
def get_map_hash(self, data):
|
||||
# TODO: This will break if running unencrypted,
|
||||
# uncompressed transfers on streams with long blocks
|
||||
# of identical bytes. Doing so would be very silly
|
||||
# anyways but maybe it should be handled gracefully.
|
||||
return RNS.Identity.full_hash(data+self.random_hash)[:Resource.MAPHASH_LEN]
|
||||
|
||||
def advertise(self):
|
||||
@@ -333,7 +393,7 @@ class Resource:
|
||||
the resource advertisement it will begin transferring.
|
||||
"""
|
||||
thread = threading.Thread(target=self.__advertise_job)
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def __advertise_job(self):
|
||||
@@ -349,6 +409,7 @@ class Resource:
|
||||
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)
|
||||
except Exception as e:
|
||||
@@ -360,7 +421,7 @@ class Resource:
|
||||
|
||||
def watchdog_job(self):
|
||||
thread = threading.Thread(target=self.__watchdog_job)
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def __watchdog_job(self):
|
||||
@@ -403,17 +464,10 @@ class Resource:
|
||||
|
||||
window_remaining = self.outstanding_parts
|
||||
|
||||
sleep_time = self.last_activity + (rtt*(self.part_timeout_factor+window_remaining)) + Resource.RETRY_GRACE_TIME - time.time()
|
||||
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()
|
||||
|
||||
# TODO: Remove debug info
|
||||
# RNS.log("rtt "+str(rtt))
|
||||
# RNS.log("ptof "+str(self.part_timeout_factor))
|
||||
# RNS.log("wait "+str((rtt*self.part_timeout_factor) + Resource.RETRY_GRACE_TIME))
|
||||
# RNS.log("sleep "+str(sleep_time))
|
||||
# RNS.log("wndw "+str(self.window))
|
||||
# RNS.log("wndwr "+str(window_remaining))
|
||||
# RNS.log("")
|
||||
|
||||
if sleep_time < 0:
|
||||
if self.retries_left > 0:
|
||||
RNS.log("Timed out waiting for parts, requesting retry", RNS.LOG_DEBUG)
|
||||
@@ -432,7 +486,8 @@ class Resource:
|
||||
self.cancel()
|
||||
sleep_time = 0.001
|
||||
else:
|
||||
max_wait = self.rtt * self.timeout_factor * self.max_retries + self.sender_grace_time
|
||||
max_extra_wait = sum([(r+1) * Resource.PER_RETRY_DELAY for r in range(self.MAX_RETRIES)])
|
||||
max_wait = self.rtt * self.timeout_factor * self.max_retries + self.sender_grace_time + max_extra_wait
|
||||
sleep_time = self.last_activity + max_wait - time.time()
|
||||
if sleep_time < 0:
|
||||
RNS.log("Resource timed out waiting for part requests", RNS.LOG_DEBUG)
|
||||
@@ -506,11 +561,17 @@ class Resource:
|
||||
if self.segment_index == self.total_segments:
|
||||
if self.callback != None:
|
||||
self.data = open(self.storagepath, "rb")
|
||||
self.callback(self)
|
||||
try:
|
||||
self.callback(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing resource assembled callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
try:
|
||||
self.data.close()
|
||||
if hasattr(self.data, "close") and callable(self.data.close):
|
||||
self.data.close()
|
||||
|
||||
os.unlink(self.storagepath)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while cleaning up resource files, the contained exception was:", RNS.LOG_ERROR)
|
||||
RNS.log(str(e))
|
||||
@@ -540,11 +601,14 @@ class Resource:
|
||||
# If all segments were processed, we'll
|
||||
# signal that the resource sending concluded
|
||||
if self.callback != None:
|
||||
self.callback(self)
|
||||
try:
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
@@ -562,7 +626,7 @@ class Resource:
|
||||
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
|
||||
@@ -572,6 +636,16 @@ class Resource:
|
||||
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 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 not self.status == Resource.FAILED:
|
||||
self.status = Resource.TRANSFERRING
|
||||
part_data = packet.data
|
||||
@@ -583,6 +657,7 @@ class Resource:
|
||||
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
|
||||
|
||||
@@ -596,20 +671,17 @@ class Resource:
|
||||
cp += 1
|
||||
|
||||
if self.__progress_callback != None:
|
||||
self.__progress_callback(self)
|
||||
|
||||
# TODO: Remove debug info
|
||||
# RNS.log("outstanding_parts "+str(self.outstanding_parts))
|
||||
# RNS.log("total_parts "+str(self.total_parts))
|
||||
# RNS.log("received_count "+str(self.received_count))
|
||||
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)
|
||||
|
||||
i += 1
|
||||
|
||||
self.receiving_part = False
|
||||
|
||||
# TODO: Remove
|
||||
#if self.outstanding_parts == 0 and self.received_count == self.total_parts:
|
||||
if self.received_count == self.total_parts:
|
||||
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
|
||||
@@ -619,6 +691,20 @@ class Resource:
|
||||
if (self.window - self.window_min) > (self.window_flexibility-1):
|
||||
self.window_min += 1
|
||||
|
||||
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 rtt != 0:
|
||||
self.req_data_rtt_rate = req_transferred/rtt
|
||||
self.rtt_rxd_bytes_at_part_req = self.rtt_rxd_bytes
|
||||
|
||||
if self.req_data_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
|
||||
|
||||
self.request_next()
|
||||
else:
|
||||
self.receiving_part = False
|
||||
@@ -666,6 +752,7 @@ class Resource:
|
||||
request_packet.send()
|
||||
self.last_activity = time.time()
|
||||
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)
|
||||
@@ -690,27 +777,34 @@ class Resource:
|
||||
|
||||
requested_hashes = request_data[pad+RNS.Identity.HASHLENGTH//8:]
|
||||
|
||||
for i in range(0,len(requested_hashes)//Resource.MAPHASH_LEN):
|
||||
requested_hash = requested_hashes[i*Resource.MAPHASH_LEN:(i+1)*Resource.MAPHASH_LEN]
|
||||
|
||||
search_start = self.receiver_min_consecutive_height
|
||||
search_end = self.receiver_min_consecutive_height+ResourceAdvertisement.COLLISION_GUARD_SIZE
|
||||
for part in self.parts[search_start:search_end]:
|
||||
if part.map_hash == requested_hash:
|
||||
try:
|
||||
if not part.sent:
|
||||
part.send()
|
||||
self.sent_parts += 1
|
||||
else:
|
||||
part.resend()
|
||||
self.last_activity = time.time()
|
||||
self.last_part_sent = self.last_activity
|
||||
break
|
||||
except Exception as e:
|
||||
RNS.log("Resource could not send parts, cancelling transfer!", RNS.LOG_DEBUG)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
|
||||
self.cancel()
|
||||
# Define the search scope
|
||||
search_start = self.receiver_min_consecutive_height
|
||||
search_end = self.receiver_min_consecutive_height+ResourceAdvertisement.COLLISION_GUARD_SIZE
|
||||
|
||||
map_hashes = []
|
||||
for i in range(0,len(requested_hashes)//Resource.MAPHASH_LEN):
|
||||
map_hash = requested_hashes[i*Resource.MAPHASH_LEN:(i+1)*Resource.MAPHASH_LEN]
|
||||
map_hashes.append(map_hash)
|
||||
|
||||
search_scope = self.parts[search_start:search_end]
|
||||
requested_parts = list(filter(lambda part: part.map_hash in map_hashes, search_scope))
|
||||
|
||||
for part in requested_parts:
|
||||
try:
|
||||
if not part.sent:
|
||||
part.send()
|
||||
self.sent_parts += 1
|
||||
else:
|
||||
part.resend()
|
||||
|
||||
self.last_activity = time.time()
|
||||
self.last_part_sent = self.last_activity
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Resource could not send parts, cancelling transfer!", RNS.LOG_DEBUG)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
|
||||
self.cancel()
|
||||
|
||||
if wants_more_hashmap:
|
||||
last_map_hash = request_data[1:Resource.MAPHASH_LEN+1]
|
||||
|
||||
@@ -754,7 +848,10 @@ class Resource:
|
||||
self.status = Resource.AWAITING_PROOF
|
||||
|
||||
if self.__progress_callback != None:
|
||||
self.__progress_callback(self)
|
||||
try:
|
||||
self.__progress_callback(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def cancel(self):
|
||||
"""
|
||||
@@ -774,8 +871,11 @@ class Resource:
|
||||
self.link.cancel_incoming_resource(self)
|
||||
|
||||
if self.callback != None:
|
||||
self.link.resource_concluded(self)
|
||||
self.callback(self)
|
||||
try:
|
||||
self.link.resource_concluded(self)
|
||||
self.callback(self)
|
||||
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 set_callback(self, callback):
|
||||
self.callback = callback
|
||||
@@ -803,12 +903,48 @@ class Resource:
|
||||
progress = self.processed_parts / self.progress_total_parts
|
||||
return progress
|
||||
|
||||
def get_transfer_size(self):
|
||||
"""
|
||||
:returns: The number of bytes needed to transfer the resource.
|
||||
"""
|
||||
return self.size
|
||||
|
||||
def get_data_size(self):
|
||||
"""
|
||||
:returns: The total data size of the resource.
|
||||
"""
|
||||
return self.total_size
|
||||
|
||||
def get_parts(self):
|
||||
"""
|
||||
:returns: The number of parts the resource will be transferred in.
|
||||
"""
|
||||
return self.total_parts
|
||||
|
||||
def get_segments(self):
|
||||
"""
|
||||
:returns: The number of segments the resource is divided into.
|
||||
"""
|
||||
return self.total_segments
|
||||
|
||||
def get_hash(self):
|
||||
"""
|
||||
:returns: The hash of the resource.
|
||||
"""
|
||||
return self.hash
|
||||
|
||||
def is_compressed(self):
|
||||
"""
|
||||
:returns: Whether the resource is compressed.
|
||||
"""
|
||||
return self.compressed
|
||||
|
||||
def __str__(self):
|
||||
return RNS.prettyhexrep(self.hash)+str(self.link)
|
||||
return "<"+RNS.hexrep(self.hash,delimit=False)+"/"+RNS.hexrep(self.link.link_id,delimit=False)+">"
|
||||
|
||||
|
||||
class ResourceAdvertisement:
|
||||
OVERHEAD = 128
|
||||
OVERHEAD = 134
|
||||
HASHMAP_MAX_LEN = math.floor((RNS.Link.MDU-OVERHEAD)/Resource.MAPHASH_LEN)
|
||||
COLLISION_GUARD_SIZE = 2*Resource.WINDOW_MAX+HASHMAP_MAX_LEN
|
||||
|
||||
@@ -834,19 +970,19 @@ class ResourceAdvertisement:
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_request_id(advertisement_packet):
|
||||
def read_request_id(advertisement_packet):
|
||||
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
|
||||
return adv.q
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_transfer_size(advertisement_packet):
|
||||
def read_transfer_size(advertisement_packet):
|
||||
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
|
||||
return adv.t
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_size(advertisement_packet):
|
||||
def read_size(advertisement_packet):
|
||||
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
|
||||
return adv.d
|
||||
|
||||
@@ -880,6 +1016,23 @@ class ResourceAdvertisement:
|
||||
# Flags
|
||||
self.f = 0x00 | self.p << 4 | self.u << 3 | self.s << 2 | self.c << 1 | self.e
|
||||
|
||||
def get_transfer_size(self):
|
||||
return self.t
|
||||
|
||||
def get_data_size(self):
|
||||
return self.d
|
||||
|
||||
def get_parts(self):
|
||||
return self.n
|
||||
|
||||
def get_segments(self):
|
||||
return self.l
|
||||
|
||||
def get_hash(self):
|
||||
return self.h
|
||||
|
||||
def is_compressed(self):
|
||||
return self.c
|
||||
|
||||
def pack(self, segment=0):
|
||||
hashmap_start = segment*ResourceAdvertisement.HASHMAP_MAX_LEN
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
import glob
|
||||
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import argparse
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
from RNS._version import __version__
|
||||
|
||||
APP_NAME = "rncp"
|
||||
allow_all = False
|
||||
allowed_identity_hashes = []
|
||||
|
||||
def receive(configdir, verbosity = 0, quietness = 0, allowed = [], display_identity = False, limit = None, disable_auth = None, disable_announce = False):
|
||||
global allow_all, allowed_identity_hashes
|
||||
identity = None
|
||||
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
|
||||
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
|
||||
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "receive")
|
||||
|
||||
if display_identity:
|
||||
print("Identity : "+str(identity))
|
||||
print("Receiving on : "+RNS.prettyhexrep(destination.hash))
|
||||
exit(0)
|
||||
|
||||
if disable_auth:
|
||||
allow_all = True
|
||||
else:
|
||||
if allowed != None:
|
||||
for a in allowed:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(a) != dest_len:
|
||||
raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(a)
|
||||
allowed_identity_hashes.append(destination_hash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(1)
|
||||
|
||||
if len(allowed_identity_hashes) < 1 and not disable_auth:
|
||||
print("Warning: No allowed identities configured, rncp will not accept any files!")
|
||||
|
||||
destination.set_link_established_callback(receive_link_established)
|
||||
print("rncp ready to receive on "+RNS.prettyhexrep(destination.hash))
|
||||
|
||||
if not disable_announce:
|
||||
destination.announce()
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
def receive_link_established(link):
|
||||
RNS.log("Incoming link established", RNS.LOG_VERBOSE)
|
||||
link.set_remote_identified_callback(receive_sender_identified)
|
||||
link.set_resource_strategy(RNS.Link.ACCEPT_APP)
|
||||
link.set_resource_callback(receive_resource_callback)
|
||||
link.set_resource_started_callback(receive_resource_started)
|
||||
link.set_resource_concluded_callback(receive_resource_concluded)
|
||||
|
||||
def receive_sender_identified(link, identity):
|
||||
global allow_all
|
||||
|
||||
if identity.hash in allowed_identity_hashes:
|
||||
RNS.log("Authenticated sender", RNS.LOG_VERBOSE)
|
||||
else:
|
||||
if not allow_all:
|
||||
RNS.log("Sender not allowed, tearing down link", RNS.LOG_VERBOSE)
|
||||
link.teardown()
|
||||
else:
|
||||
pass
|
||||
|
||||
def receive_resource_callback(resource):
|
||||
global allow_all
|
||||
|
||||
sender_identity = resource.link.get_remote_identity()
|
||||
|
||||
if sender_identity != None:
|
||||
if sender_identity.hash in allowed_identity_hashes:
|
||||
return True
|
||||
|
||||
if allow_all:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def receive_resource_started(resource):
|
||||
if resource.link.get_remote_identity():
|
||||
id_str = " from "+RNS.prettyhexrep(resource.link.get_remote_identity().hash)
|
||||
else:
|
||||
id_str = ""
|
||||
|
||||
print("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str)
|
||||
|
||||
def receive_resource_concluded(resource):
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
print(str(resource)+" completed")
|
||||
|
||||
if resource.total_size > 4:
|
||||
filename_len = int.from_bytes(resource.data.read(2), "big")
|
||||
filename = resource.data.read(filename_len).decode("utf-8")
|
||||
|
||||
counter = 0
|
||||
saved_filename = filename
|
||||
while os.path.isfile(saved_filename):
|
||||
counter += 1
|
||||
saved_filename = filename+"."+str(counter)
|
||||
|
||||
file = open(saved_filename, "wb")
|
||||
file.write(resource.data.read())
|
||||
file.close()
|
||||
|
||||
else:
|
||||
print("Invalid data received, ignoring resource")
|
||||
|
||||
else:
|
||||
print("Resource failed")
|
||||
|
||||
resource_done = False
|
||||
current_resource = None
|
||||
stats = []
|
||||
speed = 0.0
|
||||
def sender_progress(resource):
|
||||
stats_max = 32
|
||||
global current_resource, stats, speed, resource_done
|
||||
current_resource = resource
|
||||
now = time.time()
|
||||
got = current_resource.get_progress()*current_resource.total_size
|
||||
entry = [now, got]
|
||||
stats.append(entry)
|
||||
while len(stats) > stats_max:
|
||||
stats.pop(0)
|
||||
|
||||
span = now - stats[0][0]
|
||||
if span == 0:
|
||||
speed = 0
|
||||
else:
|
||||
diff = got - stats[0][1]
|
||||
speed = diff/span
|
||||
|
||||
if resource.status < RNS.Resource.COMPLETE:
|
||||
resource_done = False
|
||||
else:
|
||||
resource_done = True
|
||||
|
||||
link = None
|
||||
def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT):
|
||||
global current_resource, resource_done, link, speed
|
||||
from tempfile import TemporaryFile
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination) != dest_len:
|
||||
raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(1)
|
||||
|
||||
|
||||
file_path = os.path.expanduser(file)
|
||||
if not os.path.isfile(file_path):
|
||||
print("File not found")
|
||||
exit(1)
|
||||
|
||||
temp_file = TemporaryFile()
|
||||
real_file = open(file_path, "rb")
|
||||
filename_bytes = os.path.basename(file_path).encode("utf-8")
|
||||
filename_len = len(filename_bytes)
|
||||
|
||||
if filename_len > 0xFFFF:
|
||||
print("Filename exceeds max size, cannot send")
|
||||
exit(1)
|
||||
else:
|
||||
print("Preparing file...", end=" ")
|
||||
|
||||
temp_file.write(filename_len.to_bytes(2, "big"))
|
||||
temp_file.write(filename_bytes)
|
||||
temp_file.write(real_file.read())
|
||||
temp_file.seek(0)
|
||||
|
||||
print("\r \r", end="")
|
||||
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
|
||||
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
|
||||
sys.stdout.flush()
|
||||
|
||||
i = 0
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
estab_timeout = time.time()+timeout
|
||||
while not RNS.Transport.has_path(destination_hash) and time.time() < estab_timeout:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
print("\r \rPath not found")
|
||||
exit(1)
|
||||
else:
|
||||
print("\r \rEstablishing link with "+RNS.prettyhexrep(destination_hash)+" ", end=" ")
|
||||
|
||||
receiver_identity = RNS.Identity.recall(destination_hash)
|
||||
receiver_destination = RNS.Destination(
|
||||
receiver_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
APP_NAME,
|
||||
"receive"
|
||||
)
|
||||
|
||||
link = RNS.Link(receiver_destination)
|
||||
while link.status != RNS.Link.ACTIVE and time.time() < estab_timeout:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
print("\r \rCould not establish link with "+RNS.prettyhexrep(destination_hash))
|
||||
exit(1)
|
||||
else:
|
||||
print("\r \rAdvertising file resource ", end=" ")
|
||||
|
||||
link.identify(identity)
|
||||
resource = RNS.Resource(temp_file, link, callback = sender_progress, progress_callback = sender_progress)
|
||||
current_resource = resource
|
||||
|
||||
while resource.status < RNS.Resource.TRANSFERRING:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
|
||||
if resource.status > RNS.Resource.COMPLETE:
|
||||
print("\r \rFile was not accepted by "+RNS.prettyhexrep(destination_hash))
|
||||
exit(1)
|
||||
else:
|
||||
print("\r \rTransferring file ", end=" ")
|
||||
|
||||
while not resource_done:
|
||||
time.sleep(0.1)
|
||||
prg = current_resource.get_progress()
|
||||
percent = round(prg * 100.0, 1)
|
||||
stat_str = str(percent)+"% - " + size_str(int(prg*current_resource.total_size)) + " of " + size_str(current_resource.total_size) + " - " +size_str(speed, "b")+"ps"
|
||||
print("\r \rTransferring file "+syms[i]+" "+stat_str, end=" ")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if current_resource.status != RNS.Resource.COMPLETE:
|
||||
print("\r \rThe transfer failed")
|
||||
exit(1)
|
||||
else:
|
||||
print("\r \r"+str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
|
||||
link.teardown()
|
||||
time.sleep(0.25)
|
||||
real_file.close()
|
||||
temp_file.close()
|
||||
exit(0)
|
||||
|
||||
def main():
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description="Reticulum File Transfer Utility")
|
||||
parser.add_argument("file", nargs="?", default=None, help="file to be transferred", type=str)
|
||||
parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the receiver", type=str)
|
||||
parser.add_argument("--config", metavar="path", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0, help="increase verbosity")
|
||||
parser.add_argument('-q', '--quiet', action='count', default=0, help="decrease verbosity")
|
||||
parser.add_argument('-p', '--print-identity', action='store_true', default=False, help="print identity and destination info and exit")
|
||||
parser.add_argument("-r", '--receive', action='store_true', default=False, help="wait for incoming files")
|
||||
parser.add_argument("-b", '--no-announce', action='store_true', default=False, help="don't announce at program start")
|
||||
parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="accept from this identity", type=str)
|
||||
parser.add_argument('-n', '--no-auth', action='store_true', default=False, help="accept files from anyone")
|
||||
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="sender timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
|
||||
# parser.add_argument("--limit", action="store", metavar="files", type=float, help="maximum number of files to accept", default=None)
|
||||
parser.add_argument("--version", action="version", version="rncp {version}".format(version=__version__))
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.receive or args.print_identity:
|
||||
receive(
|
||||
configdir = args.config,
|
||||
verbosity=args.verbose,
|
||||
quietness=args.quiet,
|
||||
allowed = args.allowed,
|
||||
display_identity=args.print_identity,
|
||||
# limit=args.limit,
|
||||
disable_auth=args.no_auth,
|
||||
disable_announce=args.no_announce,
|
||||
)
|
||||
|
||||
elif args.destination != None and args.file != None:
|
||||
send(
|
||||
configdir = args.config,
|
||||
verbosity = args.verbose,
|
||||
quietness = args.quiet,
|
||||
destination = args.destination,
|
||||
file = args.file,
|
||||
timeout = args.w,
|
||||
)
|
||||
|
||||
else:
|
||||
print("")
|
||||
parser.print_help()
|
||||
print("")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
if resource != None:
|
||||
resource.cancel()
|
||||
if link != None:
|
||||
link.teardown()
|
||||
exit()
|
||||
|
||||
def size_str(num, suffix='B'):
|
||||
units = ['','K','M','G','T','P','E','Z']
|
||||
last_unit = 'Y'
|
||||
|
||||
if suffix == 'b':
|
||||
num *= 8
|
||||
units = ['','K','M','G','T','P','E','Z']
|
||||
last_unit = 'Y'
|
||||
|
||||
for unit in units:
|
||||
if abs(num) < 1000.0:
|
||||
if unit == "":
|
||||
return "%.0f %s%s" % (num, unit, suffix)
|
||||
else:
|
||||
return "%.2f %s%s" % (num, unit, suffix)
|
||||
num /= 1000.0
|
||||
|
||||
return "%.2f%s%s" % (num, last_unit, suffix)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import sys
|
||||
import time
|
||||
@@ -8,44 +30,176 @@ import argparse
|
||||
from RNS._version import __version__
|
||||
|
||||
|
||||
def program_setup(configdir, destination_hexhash, verbosity):
|
||||
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))
|
||||
def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity, timeout, drop_queues):
|
||||
if table:
|
||||
destination_hash = None
|
||||
if destination_hexhash != None:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
table = sorted(reticulum.get_path_table(), key=lambda e: (e["interface"], e["hops"]) )
|
||||
|
||||
displayed = 0
|
||||
for path in table:
|
||||
if destination_hash == None or destination_hash == path["hash"]:
|
||||
displayed += 1
|
||||
exp_str = RNS.timestamp_str(path["expires"])
|
||||
if path["hops"] == 1:
|
||||
m_str = " "
|
||||
else:
|
||||
m_str = "s"
|
||||
print(RNS.prettyhexrep(path["hash"])+" is "+str(path["hops"])+" hop"+m_str+" away via "+RNS.prettyhexrep(path["via"])+" on "+path["interface"]+" expires "+RNS.timestamp_str(path["expires"]))
|
||||
|
||||
if destination_hash != None and displayed == 0:
|
||||
print("No path known")
|
||||
sys.exit(1)
|
||||
|
||||
elif rates:
|
||||
destination_hash = None
|
||||
if destination_hexhash != None:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
table = sorted(reticulum.get_rate_table(), key=lambda e: e["last"] )
|
||||
|
||||
if len(table) == 0:
|
||||
print("No information available")
|
||||
|
||||
else:
|
||||
displayed = 0
|
||||
for entry in table:
|
||||
if destination_hash == None or destination_hash == entry["hash"]:
|
||||
displayed += 1
|
||||
try:
|
||||
last_str = pretty_date(int(entry["last"]))
|
||||
start_ts = entry["timestamps"][0]
|
||||
span = max(time.time() - start_ts, 3600.0)
|
||||
span_hours = span/3600.0
|
||||
span_str = pretty_date(int(entry["timestamps"][0]))
|
||||
hour_rate = round(len(entry["timestamps"])/span_hours, 3)
|
||||
if hour_rate-int(hour_rate) == 0:
|
||||
hour_rate = int(hour_rate)
|
||||
|
||||
if entry["rate_violations"] > 0:
|
||||
if entry["rate_violations"] == 1:
|
||||
s_str = ""
|
||||
else:
|
||||
s_str = "s"
|
||||
|
||||
rv_str = ", "+str(entry["rate_violations"])+" active rate violation"+s_str
|
||||
else:
|
||||
rv_str = ""
|
||||
|
||||
if entry["blocked_until"] > time.time():
|
||||
bli = time.time()-(int(entry["blocked_until"])-time.time())
|
||||
bl_str = ", new announces allowed in "+pretty_date(int(bli))
|
||||
else:
|
||||
bl_str = ""
|
||||
|
||||
|
||||
print(RNS.prettyhexrep(entry["hash"])+" last heard "+last_str+" ago, "+str(hour_rate)+" announces/hour in the last "+span_str+rv_str+bl_str)
|
||||
|
||||
except Exception as e:
|
||||
print("Error while processing entry for "+RNS.prettyhexrep(entry["hash"]))
|
||||
print(str(e))
|
||||
|
||||
if destination_hash != None and displayed == 0:
|
||||
print("No information available")
|
||||
sys.exit(1)
|
||||
|
||||
elif drop_queues:
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
RNS.log("Dropping announce queues on all interfaces...")
|
||||
reticulum.drop_announce_queues()
|
||||
|
||||
elif drop:
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
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:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit()
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
|
||||
sys.stdout.flush()
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
|
||||
i = 0
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
while not RNS.Transport.has_path(destination_hash):
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
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)
|
||||
|
||||
hops = RNS.Transport.hops_to(destination_hash)
|
||||
next_hop = RNS.prettyhexrep(reticulum.get_next_hop(destination_hash))
|
||||
next_hop_interface = reticulum.get_next_hop_if_name(destination_hash)
|
||||
|
||||
if hops != 1:
|
||||
ms = "s"
|
||||
else:
|
||||
ms = ""
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
|
||||
sys.stdout.flush()
|
||||
|
||||
i = 0
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
limit = time.time()+timeout
|
||||
while not RNS.Transport.has_path(destination_hash) and time.time()<limit:
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if RNS.Transport.has_path(destination_hash):
|
||||
hops = RNS.Transport.hops_to(destination_hash)
|
||||
next_hop = RNS.prettyhexrep(reticulum.get_next_hop(destination_hash))
|
||||
next_hop_interface = reticulum.get_next_hop_if_name(destination_hash)
|
||||
|
||||
if hops != 1:
|
||||
ms = "s"
|
||||
else:
|
||||
ms = ""
|
||||
|
||||
print("\rPath found, destination "+RNS.prettyhexrep(destination_hash)+" is "+str(hops)+" hop"+ms+" away via "+next_hop+" on "+next_hop_interface)
|
||||
else:
|
||||
print("\r \rPath not found")
|
||||
sys.exit(1)
|
||||
|
||||
print("\rPath found, destination "+RNS.prettyhexrep(destination_hash)+" is "+str(hops)+" hop"+ms+" away via "+next_hop+" on "+next_hop_interface)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -65,6 +219,47 @@ def main():
|
||||
version="rnpath {version}".format(version=__version__)
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--table",
|
||||
action="store_true",
|
||||
help="show all known paths",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--rates",
|
||||
action="store_true",
|
||||
help="show announce rate info",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--drop",
|
||||
action="store_true",
|
||||
help="remove the path to a destination",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-D",
|
||||
"--drop-announces",
|
||||
action="store_true",
|
||||
help="drop all queued announces",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
action="store",
|
||||
metavar="seconds",
|
||||
type=float,
|
||||
help="timeout before giving up",
|
||||
default=RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"destination",
|
||||
nargs="?",
|
||||
@@ -82,16 +277,62 @@ def main():
|
||||
else:
|
||||
configarg = None
|
||||
|
||||
if not args.destination:
|
||||
if not args.drop_announces and not args.table and not args.rates and not args.destination:
|
||||
print("")
|
||||
parser.print_help()
|
||||
print("")
|
||||
else:
|
||||
program_setup(configdir = configarg, destination_hexhash = args.destination, verbosity = args.verbose)
|
||||
program_setup(
|
||||
configdir = configarg,
|
||||
table = args.table,
|
||||
rates = args.rates,
|
||||
drop = args.drop,
|
||||
destination_hexhash = args.destination,
|
||||
verbosity = args.verbose,
|
||||
timeout = args.w,
|
||||
drop_queues = args.drop_announces,
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
|
||||
def pretty_date(time=False):
|
||||
from datetime import datetime
|
||||
now = datetime.now()
|
||||
if type(time) is int:
|
||||
diff = now - datetime.fromtimestamp(time)
|
||||
elif isinstance(time,datetime):
|
||||
diff = now - time
|
||||
elif not time:
|
||||
diff = now - now
|
||||
second_diff = diff.seconds
|
||||
day_diff = diff.days
|
||||
if day_diff < 0:
|
||||
return ''
|
||||
if day_diff == 0:
|
||||
if second_diff < 10:
|
||||
return str(second_diff) + " seconds"
|
||||
if second_diff < 60:
|
||||
return str(second_diff) + " seconds"
|
||||
if second_diff < 120:
|
||||
return "1 minute"
|
||||
if second_diff < 3600:
|
||||
return str(int(second_diff / 60)) + " minutes"
|
||||
if second_diff < 7200:
|
||||
return "an hour"
|
||||
if second_diff < 86400:
|
||||
return str(int(second_diff / 3600)) + " hours"
|
||||
if day_diff == 1:
|
||||
return "1 day"
|
||||
if day_diff < 7:
|
||||
return str(day_diff) + " days"
|
||||
if day_diff < 31:
|
||||
return str(int(day_diff / 7)) + " weeks"
|
||||
if day_diff < 365:
|
||||
return str(int(day_diff / 30)) + " months"
|
||||
return str(int(day_diff / 365)) + " years"
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import os
|
||||
import sys
|
||||
@@ -101,11 +123,31 @@ def program_setup(configdir, destination_hexhash, size=DEFAULT_PROBE_SIZE, full_
|
||||
rtt = round(rtt*1000, 3)
|
||||
rttstring = str(rtt)+" milliseconds"
|
||||
|
||||
reception_stats = ""
|
||||
if reticulum.is_connected_to_shared_instance:
|
||||
reception_rssi = reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
|
||||
reception_snr = reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
|
||||
|
||||
if reception_rssi != None:
|
||||
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
|
||||
|
||||
if reception_snr != None:
|
||||
reception_stats += " [SNR "+str(reception_snr)+" dB]"
|
||||
|
||||
else:
|
||||
if receipt.proof_packet != None:
|
||||
if receipt.proof_packet.rssi != None:
|
||||
reception_stats += " [RSSI "+str(receipt.proof_packet.rssi)+" dBm]"
|
||||
|
||||
if receipt.proof_packet.snr != None:
|
||||
reception_stats += " [SNR "+str(receipt.proof_packet.snr)+" dB]"
|
||||
|
||||
print(
|
||||
"Valid reply received from "+
|
||||
RNS.prettyhexrep(receipt.destination.hash)+
|
||||
"\nRound-trip time is "+rttstring+
|
||||
" over "+str(hops)+" hop"+ms
|
||||
" over "+str(hops)+" hop"+ms+
|
||||
reception_stats
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import argparse
|
||||
import time
|
||||
|
||||
from RNS._version import __version__
|
||||
|
||||
|
||||
def program_setup(configdir, verbosity = 0, quietness = 0):
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity-quietness)
|
||||
RNS.log("Started rnsd version {version}".format(version=__version__), RNS.LOG_NOTICE)
|
||||
def program_setup(configdir, verbosity = 0, quietness = 0, service = False):
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
|
||||
if service:
|
||||
targetlogdest = RNS.LOG_FILE
|
||||
targetloglevel = None
|
||||
else:
|
||||
targetlogdest = RNS.LOG_STDOUT
|
||||
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel, logdest=targetlogdest)
|
||||
if reticulum.is_connected_to_shared_instance:
|
||||
RNS.log("Started rnsd version {version} connected to another shared local instance, this is probably NOT what you want!".format(version=__version__), RNS.LOG_WARNING)
|
||||
else:
|
||||
RNS.log("Started rnsd version {version}".format(version=__version__), RNS.LOG_NOTICE)
|
||||
|
||||
while True:
|
||||
input()
|
||||
time.sleep(1)
|
||||
|
||||
def main():
|
||||
try:
|
||||
@@ -18,20 +53,366 @@ def main():
|
||||
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0)
|
||||
parser.add_argument('-q', '--quiet', action='count', default=0)
|
||||
parser.add_argument('-s', '--service', action='store_true', default=False, help="rnsd is running as a service and should log to file")
|
||||
parser.add_argument("--exampleconfig", action='store_true', default=False, help="print verbose configuration example to stdout and exit")
|
||||
parser.add_argument("--version", action="version", version="rnsd {version}".format(version=__version__))
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.exampleconfig:
|
||||
print(__example_rns_config__)
|
||||
exit()
|
||||
|
||||
if args.config:
|
||||
configarg = args.config
|
||||
else:
|
||||
configarg = None
|
||||
|
||||
program_setup(configdir = configarg, verbosity=args.verbose, quietness=args.quiet)
|
||||
program_setup(configdir = configarg, verbosity=args.verbose, quietness=args.quiet, service=args.service)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
|
||||
__example_rns_config__ = '''# This is an example Reticulum config file.
|
||||
# You should probably edit it to include any additional,
|
||||
# interfaces and settings you might need.
|
||||
|
||||
[reticulum]
|
||||
|
||||
# If you enable Transport, your system will route traffic
|
||||
# for other peers, pass announces and serve path requests.
|
||||
# This should be done for systems that are suited to act
|
||||
# as transport nodes, ie. if they are stationary and
|
||||
# always-on. This directive is optional and can be removed
|
||||
# for brevity.
|
||||
|
||||
enable_transport = False
|
||||
|
||||
|
||||
# By default, the first program to launch the Reticulum
|
||||
# Network Stack will create a shared instance, that other
|
||||
# programs can communicate with. Only the shared instance
|
||||
# opens all the configured interfaces directly, and other
|
||||
# local programs communicate with the shared instance over
|
||||
# a local socket. This is completely transparent to the
|
||||
# user, and should generally be turned on. This directive
|
||||
# is optional and can be removed for brevity.
|
||||
|
||||
share_instance = Yes
|
||||
|
||||
|
||||
# If you want to run multiple *different* shared instances
|
||||
# on the same system, you will need to specify different
|
||||
# shared instance ports for each. The defaults are given
|
||||
# below, and again, these options can be left out if you
|
||||
# don't need them.
|
||||
|
||||
shared_instance_port = 37428
|
||||
instance_control_port = 37429
|
||||
|
||||
# You can configure Reticulum to panic and forcibly close
|
||||
# if an unrecoverable interface error occurs, such as the
|
||||
# hardware device for an interface disappearing. This is
|
||||
# an optional directive, and can be left out for brevity.
|
||||
# This behaviour is disabled by default.
|
||||
|
||||
panic_on_interface_error = No
|
||||
|
||||
|
||||
[logging]
|
||||
# Valid log levels are 0 through 7:
|
||||
# 0: Log only critical information
|
||||
# 1: Log errors and lower log levels
|
||||
# 2: Log warnings and lower log levels
|
||||
# 3: Log notices and lower log levels
|
||||
# 4: Log info and lower (this is the default)
|
||||
# 5: Verbose logging
|
||||
# 6: Debug logging
|
||||
# 7: Extreme logging
|
||||
|
||||
loglevel = 4
|
||||
|
||||
|
||||
# The interfaces section defines the physical and virtual
|
||||
# interfaces Reticulum will use to communicate on. This
|
||||
# section will contain examples for a variety of interface
|
||||
# types. You can modify these or use them as a basis for
|
||||
# your own config, or simply remove the unused ones.
|
||||
|
||||
[interfaces]
|
||||
|
||||
# This interface enables communication with other
|
||||
# link-local Reticulum nodes over UDP. It does not
|
||||
# need any functional IP infrastructure like routers
|
||||
# or DHCP servers, but will require that at least link-
|
||||
# local IPv6 is enabled in your operating system, which
|
||||
# should be enabled by default in almost any OS. See
|
||||
# the Reticulum Manual for more configuration options.
|
||||
|
||||
[[Default Interface]]
|
||||
type = AutoInterface
|
||||
enabled = yes
|
||||
|
||||
|
||||
# The following example enables communication with other
|
||||
# local Reticulum peers using UDP broadcasts.
|
||||
|
||||
[[UDP Interface]]
|
||||
type = UDPInterface
|
||||
enabled = no
|
||||
listen_ip = 0.0.0.0
|
||||
listen_port = 4242
|
||||
forward_ip = 255.255.255.255
|
||||
forward_port = 4242
|
||||
|
||||
# The above configuration will allow communication
|
||||
# within the local broadcast domains of all local
|
||||
# IP interfaces.
|
||||
|
||||
# Instead of specifying listen_ip, listen_port,
|
||||
# forward_ip and forward_port, you can also bind
|
||||
# to a specific network device like below.
|
||||
|
||||
# device = eth0
|
||||
# port = 4242
|
||||
|
||||
# Assuming the eth0 device has the address
|
||||
# 10.55.0.72/24, the above configuration would
|
||||
# be equivalent to the following manual setup.
|
||||
# Note that we are both listening and forwarding to
|
||||
# the broadcast address of the network segments.
|
||||
|
||||
# listen_ip = 10.55.0.255
|
||||
# listen_port = 4242
|
||||
# forward_ip = 10.55.0.255
|
||||
# forward_port = 4242
|
||||
|
||||
# You can of course also communicate only with
|
||||
# a single IP address
|
||||
|
||||
# listen_ip = 10.55.0.15
|
||||
# listen_port = 4242
|
||||
# forward_ip = 10.55.0.16
|
||||
# forward_port = 4242
|
||||
|
||||
|
||||
# This example demonstrates a TCP server interface.
|
||||
# It will listen for incoming connections on the
|
||||
# specified IP address and port number.
|
||||
|
||||
[[TCP Server Interface]]
|
||||
type = TCPServerInterface
|
||||
enabled = no
|
||||
|
||||
# This configuration will listen on all IP
|
||||
# interfaces on port 4242
|
||||
|
||||
listen_ip = 0.0.0.0
|
||||
listen_port = 4242
|
||||
|
||||
# Alternatively you can bind to a specific IP
|
||||
|
||||
# listen_ip = 10.0.0.88
|
||||
# listen_port = 4242
|
||||
|
||||
# Or a specific network device
|
||||
|
||||
# device = eth0
|
||||
# port = 4242
|
||||
|
||||
|
||||
# To connect to a TCP server interface, you would
|
||||
# naturally use the TCP client interface. Here's
|
||||
# an example. The target_host can either be an IP
|
||||
# address or a hostname
|
||||
|
||||
[[TCP Client Interface]]
|
||||
type = TCPClientInterface
|
||||
enabled = no
|
||||
target_host = 127.0.0.1
|
||||
target_port = 4242
|
||||
|
||||
|
||||
# This example shows how to make your Reticulum
|
||||
# instance available over I2P, and connect to
|
||||
# another I2P peer. Please be aware that you
|
||||
# must have an I2P router running on your system
|
||||
# with the SAMv3 API enabled for this to work.
|
||||
|
||||
[[I2P]]
|
||||
type = I2PInterface
|
||||
enabled = no
|
||||
connectable = yes
|
||||
peers = ykzlw5ujbaqc2xkec4cpvgyxj257wcrmmgkuxqmqcur7cq3w3lha.b32.i2p
|
||||
|
||||
|
||||
# Here's an example of how to add a LoRa interface
|
||||
# using the RNode LoRa transceiver.
|
||||
|
||||
[[RNode LoRa Interface]]
|
||||
type = RNodeInterface
|
||||
|
||||
# Enable interface if you want use it!
|
||||
enabled = no
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB0
|
||||
|
||||
# Set frequency to 867.2 MHz
|
||||
frequency = 867200000
|
||||
|
||||
# Set LoRa bandwidth to 125 KHz
|
||||
bandwidth = 125000
|
||||
|
||||
# Set TX power to 7 dBm (5 mW)
|
||||
txpower = 7
|
||||
|
||||
# Select spreading factor 8. Valid
|
||||
# range is 7 through 12, with 7
|
||||
# being the fastest and 12 having
|
||||
# the longest range.
|
||||
spreadingfactor = 8
|
||||
|
||||
# Select coding rate 5. Valid range
|
||||
# is 5 throough 8, with 5 being the
|
||||
# fastest, and 8 the longest range.
|
||||
codingrate = 5
|
||||
|
||||
# You can configure the RNode to send
|
||||
# out identification on the channel with
|
||||
# a set interval by configuring the
|
||||
# following two parameters. The trans-
|
||||
# ceiver will only ID if the set
|
||||
# interval has elapsed since it's last
|
||||
# actual transmission. The interval is
|
||||
# configured in seconds.
|
||||
# This option is commented out and not
|
||||
# used by default.
|
||||
# id_callsign = MYCALL-0
|
||||
# id_interval = 600
|
||||
|
||||
# For certain homebrew RNode interfaces
|
||||
# with low amounts of RAM, using packet
|
||||
# flow control can be useful. By default
|
||||
# it is disabled.
|
||||
flow_control = False
|
||||
|
||||
|
||||
# An example KISS modem interface. Useful for running
|
||||
# Reticulum over packet radio hardware.
|
||||
|
||||
[[Packet Radio KISS Interface]]
|
||||
type = KISSInterface
|
||||
|
||||
# Enable interface if you want use it!
|
||||
enabled = no
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB1
|
||||
|
||||
# Set the serial baud-rate and other
|
||||
# configuration parameters.
|
||||
speed = 115200
|
||||
databits = 8
|
||||
parity = none
|
||||
stopbits = 1
|
||||
|
||||
# Set the modem preamble. A 150ms
|
||||
# preamble should be a reasonable
|
||||
# default, but may need to be
|
||||
# increased for radios with slow-
|
||||
# opening squelch and long TX/RX
|
||||
# turnaround
|
||||
preamble = 150
|
||||
|
||||
# Set the modem TX tail. In most
|
||||
# cases this should be kept as low
|
||||
# as possible to not waste airtime.
|
||||
txtail = 10
|
||||
|
||||
# Configure CDMA parameters. These
|
||||
# settings are reasonable defaults.
|
||||
persistence = 200
|
||||
slottime = 20
|
||||
|
||||
# You can configure the interface to send
|
||||
# out identification on the channel with
|
||||
# a set interval by configuring the
|
||||
# following two parameters. The KISS
|
||||
# interface will only ID if the set
|
||||
# interval has elapsed since it's last
|
||||
# actual transmission. The interval is
|
||||
# configured in seconds.
|
||||
# This option is commented out and not
|
||||
# used by default.
|
||||
# id_callsign = MYCALL-0
|
||||
# id_interval = 600
|
||||
|
||||
# Whether to use KISS flow-control.
|
||||
# This is useful for modems that have
|
||||
# a small internal packet buffer, but
|
||||
# support packet flow control instead.
|
||||
flow_control = false
|
||||
|
||||
|
||||
# If you're using Reticulum on amateur radio spectrum,
|
||||
# you might want to use the AX.25 KISS interface. This
|
||||
# way, Reticulum will automatically encapsulate it's
|
||||
# traffic in AX.25 and also identify your stations
|
||||
# transmissions with your callsign and SSID.
|
||||
#
|
||||
# Only do this if you really need to! Reticulum doesn't
|
||||
# need the AX.25 layer for anything, and it incurs extra
|
||||
# overhead on every packet to encapsulate in AX.25.
|
||||
#
|
||||
# A more efficient way is to use the plain KISS interface
|
||||
# with the beaconing functionality described above.
|
||||
|
||||
[[Packet Radio AX.25 KISS Interface]]
|
||||
type = AX25KISSInterface
|
||||
|
||||
# Set the station callsign and SSID
|
||||
callsign = NO1CLL
|
||||
ssid = 0
|
||||
|
||||
# Enable interface if you want use it!
|
||||
enabled = no
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB2
|
||||
|
||||
# Set the serial baud-rate and other
|
||||
# configuration parameters.
|
||||
speed = 115200
|
||||
databits = 8
|
||||
parity = none
|
||||
stopbits = 1
|
||||
|
||||
# Whether to use KISS flow-control.
|
||||
# This is useful for modems with a
|
||||
# small internal packet buffer.
|
||||
flow_control = false
|
||||
|
||||
# Set the modem preamble. A 150ms
|
||||
# preamble should be a reasonable
|
||||
# default, but may need to be
|
||||
# increased for radios with slow-
|
||||
# opening squelch and long TX/RX
|
||||
# turnaround
|
||||
preamble = 150
|
||||
|
||||
# Set the modem TX tail. In most
|
||||
# cases this should be kept as low
|
||||
# as possible to not waste airtime.
|
||||
txtail = 10
|
||||
|
||||
# Configure CDMA parameters. These
|
||||
# settings are reasonable defaults.
|
||||
persistence = 200
|
||||
slottime = 20
|
||||
|
||||
'''
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import argparse
|
||||
|
||||
@@ -24,36 +46,113 @@ def size_str(num, suffix='B'):
|
||||
|
||||
return "%.2f%s%s" % (num, last_unit, suffix)
|
||||
|
||||
def program_setup(configdir, dispall=False, verbosity = 0):
|
||||
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None):
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
|
||||
ifstats = reticulum.get_interface_stats()
|
||||
if ifstats != None:
|
||||
for ifstat in ifstats:
|
||||
|
||||
stats = None
|
||||
try:
|
||||
stats = reticulum.get_interface_stats()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if stats != None:
|
||||
for ifstat in stats["interfaces"]:
|
||||
name = ifstat["name"]
|
||||
|
||||
if dispall or not (name.startswith("LocalInterface[") or name.startswith("TCPInterface[Client")):
|
||||
print("")
|
||||
if ifstat["status"]:
|
||||
ss = "Up"
|
||||
else:
|
||||
ss = "Down"
|
||||
if dispall or not (
|
||||
name.startswith("LocalInterface[") or
|
||||
name.startswith("TCPInterface[Client") or
|
||||
name.startswith("I2PInterfacePeer[Connected peer") or
|
||||
(name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False))
|
||||
):
|
||||
|
||||
if ifstat["clients"] != None:
|
||||
clients = ifstat["clients"]
|
||||
if name.startswith("Shared Instance["):
|
||||
clients_string = "Connected applications: "+str(max(clients-1,0))
|
||||
else:
|
||||
clients_string = "Connected clients: "+str(clients)
|
||||
if not (name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False)):
|
||||
if name_filter == None or name_filter.lower() in name.lower():
|
||||
print("")
|
||||
|
||||
else:
|
||||
clients = None
|
||||
if ifstat["status"]:
|
||||
ss = "Up"
|
||||
else:
|
||||
ss = "Down"
|
||||
|
||||
print(" {n}".format(n=ifstat["name"]))
|
||||
print("\tStatus: {ss}".format(ss=ss))
|
||||
if clients != None:
|
||||
print("\t"+clients_string)
|
||||
print("\tRX: {rxb}\n\tTX: {txb}".format(rxb=size_str(ifstat["rxb"]), txb=size_str(ifstat["txb"])))
|
||||
if ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ACCESS_POINT:
|
||||
modestr = "Access Point"
|
||||
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_POINT_TO_POINT:
|
||||
modestr = "Point-to-Point"
|
||||
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ROAMING:
|
||||
modestr = "Roaming"
|
||||
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_BOUNDARY:
|
||||
modestr = "Boundary"
|
||||
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_GATEWAY:
|
||||
modestr = "Gateway"
|
||||
else:
|
||||
modestr = "Full"
|
||||
|
||||
|
||||
if ifstat["clients"] != None:
|
||||
clients = ifstat["clients"]
|
||||
if name.startswith("Shared Instance["):
|
||||
cnum = max(clients-1,0)
|
||||
if cnum == 1:
|
||||
spec_str = " program"
|
||||
else:
|
||||
spec_str = " programs"
|
||||
|
||||
clients_string = "Serving : "+str(cnum)+spec_str
|
||||
elif name.startswith("I2PInterface["):
|
||||
if "i2p_connectable" in ifstat and ifstat["i2p_connectable"] == True:
|
||||
cnum = clients
|
||||
if cnum == 1:
|
||||
spec_str = " connected I2P endpoint"
|
||||
else:
|
||||
spec_str = " connected I2P endpoints"
|
||||
|
||||
clients_string = "Peers : "+str(cnum)+spec_str
|
||||
else:
|
||||
clients_string = ""
|
||||
else:
|
||||
clients_string = "Clients : "+str(clients)
|
||||
|
||||
else:
|
||||
clients = None
|
||||
|
||||
print(" {n}".format(n=ifstat["name"]))
|
||||
|
||||
if "ifac_netname" in ifstat and ifstat["ifac_netname"] != None:
|
||||
print(" Network : {nn}".format(nn=ifstat["ifac_netname"]))
|
||||
|
||||
print(" Status : {ss}".format(ss=ss))
|
||||
|
||||
if clients != None and clients_string != "":
|
||||
print(" "+clients_string)
|
||||
|
||||
if not (name.startswith("Shared Instance[") or name.startswith("TCPInterface[Client") or name.startswith("LocalInterface[")):
|
||||
print(" Mode : {mode}".format(mode=modestr))
|
||||
|
||||
if "bitrate" in ifstat and ifstat["bitrate"] != None:
|
||||
print(" Rate : {ss}".format(ss=speed_str(ifstat["bitrate"])))
|
||||
|
||||
if "peers" in ifstat and ifstat["peers"] != None:
|
||||
print(" Peers : {np} reachable".format(np=ifstat["peers"]))
|
||||
|
||||
if "ifac_signature" in ifstat and ifstat["ifac_signature"] != None:
|
||||
sigstr = "<…"+RNS.hexrep(ifstat["ifac_signature"][-5:], delimit=False)+">"
|
||||
print(" Access : {nb}-bit IFAC by {sig}".format(nb=ifstat["ifac_size"]*8, sig=sigstr))
|
||||
|
||||
if "i2p_b32" in ifstat and ifstat["i2p_b32"] != None:
|
||||
print(" I2P B32 : {ep}".format(ep=str(ifstat["i2p_b32"])))
|
||||
|
||||
if "announce_queue" in ifstat and ifstat["announce_queue"] != None and ifstat["announce_queue"] > 0:
|
||||
aqn = ifstat["announce_queue"]
|
||||
if aqn == 1:
|
||||
print(" Queued : {np} announce".format(np=aqn))
|
||||
else:
|
||||
print(" Queued : {np} announces".format(np=aqn))
|
||||
|
||||
print(" Traffic : {txb}↑\n {rxb}↓".format(rxb=size_str(ifstat["rxb"]), txb=size_str(ifstat["txb"])))
|
||||
|
||||
if "transport_id" in stats and stats["transport_id"] != None:
|
||||
print("\n Reticulum Transport Instance "+RNS.prettyhexrep(stats["transport_id"])+" is running")
|
||||
|
||||
print("")
|
||||
|
||||
@@ -75,6 +174,8 @@ def main():
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -83,11 +184,27 @@ def main():
|
||||
else:
|
||||
configarg = None
|
||||
|
||||
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose)
|
||||
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
exit()
|
||||
|
||||
def speed_str(num, suffix='bps'):
|
||||
units = ['','k','M','G','T','P','E','Z']
|
||||
last_unit = 'Y'
|
||||
|
||||
if suffix == 'Bps':
|
||||
num /= 8
|
||||
units = ['','K','M','G','T','P','E','Z']
|
||||
last_unit = 'Y'
|
||||
|
||||
for unit in units:
|
||||
if abs(num) < 1000.0:
|
||||
return "%3.2f %s%s" % (num, unit, suffix)
|
||||
num /= 1000.0
|
||||
|
||||
return "%.2f %s%s" % (num, last_unit, suffix)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,714 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import subprocess
|
||||
import argparse
|
||||
import shlex
|
||||
import time
|
||||
import sys
|
||||
import tty
|
||||
import os
|
||||
|
||||
from RNS._version import __version__
|
||||
|
||||
APP_NAME = "rnx"
|
||||
identity = None
|
||||
reticulum = None
|
||||
allow_all = False
|
||||
allowed_identity_hashes = []
|
||||
|
||||
def prepare_identity(identity_path):
|
||||
global identity
|
||||
if identity_path == None:
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
|
||||
def listen(configdir, identitypath = None, verbosity = 0, quietness = 0, allowed = [], print_identity = False, disable_auth = None, disable_announce=False):
|
||||
global identity, allow_all, allowed_identity_hashes, reticulum
|
||||
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
|
||||
|
||||
prepare_identity(identitypath)
|
||||
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "execute")
|
||||
|
||||
if print_identity:
|
||||
print("Identity : "+str(identity))
|
||||
print("Listening on : "+RNS.prettyhexrep(destination.hash))
|
||||
exit(0)
|
||||
|
||||
if disable_auth:
|
||||
allow_all = True
|
||||
else:
|
||||
if allowed != None:
|
||||
for a in allowed:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(a) != dest_len:
|
||||
raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(a)
|
||||
allowed_identity_hashes.append(destination_hash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(1)
|
||||
|
||||
if len(allowed_identity_hashes) < 1 and not disable_auth:
|
||||
print("Warning: No allowed identities configured, rncx will not accept any commands!")
|
||||
|
||||
destination.set_link_established_callback(command_link_established)
|
||||
|
||||
if not allow_all:
|
||||
destination.register_request_handler(
|
||||
path = "command",
|
||||
response_generator = execute_received_command,
|
||||
allow = RNS.Destination.ALLOW_LIST,
|
||||
allowed_list = allowed_identity_hashes
|
||||
)
|
||||
else:
|
||||
destination.register_request_handler(
|
||||
path = "command",
|
||||
response_generator = execute_received_command,
|
||||
allow = RNS.Destination.ALLOW_ALL,
|
||||
)
|
||||
|
||||
RNS.log("rnx listening for commands on "+RNS.prettyhexrep(destination.hash))
|
||||
|
||||
if not disable_announce:
|
||||
destination.announce()
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
def command_link_established(link):
|
||||
link.set_remote_identified_callback(initiator_identified)
|
||||
link.set_link_closed_callback(command_link_closed)
|
||||
RNS.log("Command link "+str(link)+" established")
|
||||
|
||||
def command_link_closed(link):
|
||||
RNS.log("Command link "+str(link)+" closed")
|
||||
|
||||
def initiator_identified(link, identity):
|
||||
global allow_all
|
||||
RNS.log("Initiator of link "+str(link)+" identified as "+RNS.prettyhexrep(identity.hash))
|
||||
if not allow_all and not identity.hash in allowed_identity_hashes:
|
||||
RNS.log("Identity "+RNS.prettyhexrep(identity.hash)+" not allowed, tearing down link")
|
||||
link.teardown()
|
||||
|
||||
def execute_received_command(path, data, request_id, remote_identity, requested_at):
|
||||
command = data[0].decode("utf-8") # Command to execute
|
||||
timeout = data[1] # Timeout in seconds
|
||||
o_limit = data[2] # Size limit for stdout
|
||||
e_limit = data[3] # Size limit for stderr
|
||||
stdin = data[4] # Data passed to stdin
|
||||
|
||||
if remote_identity != None:
|
||||
RNS.log("Executing command ["+command+"] for "+RNS.prettyhexrep(remote_identity.hash))
|
||||
else:
|
||||
RNS.log("Executing command ["+command+"] for unknown requestor")
|
||||
|
||||
result = [
|
||||
False, # 0: Command was executed
|
||||
None, # 1: Return value
|
||||
None, # 2: Stdout
|
||||
None, # 3: Stderr
|
||||
None, # 4: Total stdout length
|
||||
None, # 5: Total stderr length
|
||||
time.time(), # 6: Started
|
||||
None, # 7: Concluded
|
||||
]
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(shlex.split(command), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
result[0] = True
|
||||
|
||||
except Exception as e:
|
||||
result[0] = False
|
||||
return result
|
||||
|
||||
stdout = b""
|
||||
stderr = b""
|
||||
timed_out = False
|
||||
|
||||
if stdin != None:
|
||||
process.stdin.write(stdin)
|
||||
|
||||
while True:
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=1)
|
||||
if process.poll() != None:
|
||||
break
|
||||
|
||||
if len(stdout) > 0:
|
||||
print(str(stdout))
|
||||
sys.stdout.flush()
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
if timeout != None and time.time() > result[6]+timeout:
|
||||
RNS.log("Command ["+command+"] timed out and is being killed...")
|
||||
process.terminate()
|
||||
process.wait()
|
||||
if process.poll() != None:
|
||||
stdout, stderr = process.communicate()
|
||||
else:
|
||||
stdout = None
|
||||
stderr = None
|
||||
|
||||
break
|
||||
|
||||
if timeout != None and time.time() < result[6]+timeout:
|
||||
result[7] = time.time()
|
||||
|
||||
# Deliver result
|
||||
result[1] = process.returncode
|
||||
|
||||
if o_limit != None and len(stdout) > o_limit:
|
||||
if o_limit == 0:
|
||||
result[2] = b""
|
||||
else:
|
||||
result[2] = stdout[0:o_limit]
|
||||
else:
|
||||
result[2] = stdout
|
||||
|
||||
if e_limit != None and len(stderr) > e_limit:
|
||||
if e_limit == 0:
|
||||
result[3] = b""
|
||||
else:
|
||||
result[3] = stderr[0:e_limit]
|
||||
else:
|
||||
result[3] = stderr
|
||||
|
||||
result[4] = len(stdout)
|
||||
result[5] = len(stderr)
|
||||
|
||||
if timed_out:
|
||||
RNS.log("Command timed out")
|
||||
return result
|
||||
|
||||
if remote_identity != None:
|
||||
RNS.log("Delivering result of command ["+str(command)+"] to "+RNS.prettyhexrep(remote_identity.hash))
|
||||
else:
|
||||
RNS.log("Delivering result of command ["+str(command)+"] to unknown requestor")
|
||||
|
||||
return result
|
||||
|
||||
def spin(until=None, msg=None, timeout=None):
|
||||
i = 0
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
if timeout != None:
|
||||
timeout = time.time()+timeout
|
||||
|
||||
print(msg+" ", end=" ")
|
||||
while (timeout == None or time.time()<timeout) and not until():
|
||||
time.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
print("\r"+" "*len(msg)+" \r", end="")
|
||||
|
||||
if timeout != None and time.time() > timeout:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
current_progress = 0.0
|
||||
stats = []
|
||||
speed = 0.0
|
||||
def spin_stat(until=None, timeout=None):
|
||||
global current_progress, response_transfer_size, speed
|
||||
i = 0
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
if timeout != None:
|
||||
timeout = time.time()+timeout
|
||||
|
||||
while (timeout == None or time.time()<timeout) and not until():
|
||||
time.sleep(0.1)
|
||||
prg = current_progress
|
||||
percent = round(prg * 100.0, 1)
|
||||
stat_str = str(percent)+"% - " + size_str(int(prg*response_transfer_size)) + " of " + size_str(response_transfer_size) + " - " +size_str(speed, "b")+"ps"
|
||||
print("\r \rReceiving result "+syms[i]+" "+stat_str, end=" ")
|
||||
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
print("\r \r", end="")
|
||||
|
||||
if timeout != None and time.time() > timeout:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def remote_execution_done(request_receipt):
|
||||
pass
|
||||
|
||||
def remote_execution_progress(request_receipt):
|
||||
stats_max = 32
|
||||
global current_progress, response_transfer_size, speed
|
||||
current_progress = request_receipt.progress
|
||||
response_transfer_size = request_receipt.response_transfer_size
|
||||
now = time.time()
|
||||
got = current_progress*response_transfer_size
|
||||
entry = [now, got]
|
||||
stats.append(entry)
|
||||
while len(stats) > stats_max:
|
||||
stats.pop(0)
|
||||
|
||||
span = now - stats[0][0]
|
||||
if span == 0:
|
||||
speed = 0
|
||||
else:
|
||||
diff = got - stats[0][1]
|
||||
speed = diff/span
|
||||
|
||||
link = None
|
||||
listener_destination = None
|
||||
remote_exec_grace = 2.0
|
||||
def execute(configdir, identitypath = None, verbosity = 0, quietness = 0, detailed = False, mirror = False, noid = False, destination = None, command = None, stdin = None, stdoutl = None, stderrl = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, result_timeout = None, interactive = False):
|
||||
global identity, reticulum, link, listener_destination, remote_exec_grace
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination) != dest_len:
|
||||
raise ValueError("Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(241)
|
||||
|
||||
if reticulum == None:
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
|
||||
|
||||
if identity == None:
|
||||
prepare_identity(identitypath)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
if not spin(until=lambda: RNS.Transport.has_path(destination_hash), msg="Path to "+RNS.prettyhexrep(destination_hash)+" requested", timeout=timeout):
|
||||
print("Path not found")
|
||||
exit(242)
|
||||
|
||||
if listener_destination == None:
|
||||
listener_identity = RNS.Identity.recall(destination_hash)
|
||||
listener_destination = RNS.Destination(
|
||||
listener_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
APP_NAME,
|
||||
"execute"
|
||||
)
|
||||
|
||||
if link == None or link.status == RNS.Link.CLOSED or link.status == RNS.Link.PENDING:
|
||||
link = RNS.Link(listener_destination)
|
||||
link.did_identify = False
|
||||
|
||||
if not spin(until=lambda: link.status == RNS.Link.ACTIVE, msg="Establishing link with "+RNS.prettyhexrep(destination_hash), timeout=timeout):
|
||||
print("Could not establish link with "+RNS.prettyhexrep(destination_hash))
|
||||
exit(243)
|
||||
|
||||
if not noid and not link.did_identify:
|
||||
link.identify(identity)
|
||||
link.did_identify = True
|
||||
|
||||
if stdin != None:
|
||||
stdin = stdin.encode("utf-8")
|
||||
|
||||
request_data = [
|
||||
command.encode("utf-8"), # Command to execute
|
||||
timeout, # Timeout in seconds
|
||||
stdoutl, # Size limit for stdout
|
||||
stderrl, # Size limit for stderr
|
||||
stdin, # Data passed to stdin
|
||||
]
|
||||
|
||||
# TODO: Tune
|
||||
rexec_timeout = timeout+link.rtt*4+remote_exec_grace
|
||||
|
||||
request_receipt = link.request(
|
||||
path="command",
|
||||
data=request_data,
|
||||
response_callback=remote_execution_done,
|
||||
failed_callback=remote_execution_done,
|
||||
progress_callback=remote_execution_progress,
|
||||
timeout=rexec_timeout
|
||||
)
|
||||
|
||||
spin(
|
||||
until=lambda:link.status == RNS.Link.CLOSED or (request_receipt.status != RNS.RequestReceipt.FAILED and request_receipt.status != RNS.RequestReceipt.SENT),
|
||||
msg="Sending execution request",
|
||||
timeout=rexec_timeout+0.5
|
||||
)
|
||||
|
||||
if link.status == RNS.Link.CLOSED:
|
||||
print("Could not request remote execution, link was closed")
|
||||
exit(244)
|
||||
|
||||
if request_receipt.status == RNS.RequestReceipt.FAILED:
|
||||
print("Could not request remote execution")
|
||||
if interactive:
|
||||
return
|
||||
else:
|
||||
exit(244)
|
||||
|
||||
spin(
|
||||
until=lambda:request_receipt.status != RNS.RequestReceipt.DELIVERED,
|
||||
msg="Command delivered, awaiting result",
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
if request_receipt.status == RNS.RequestReceipt.FAILED:
|
||||
print("No result was received")
|
||||
if interactive:
|
||||
return
|
||||
else:
|
||||
exit(245)
|
||||
|
||||
spin_stat(
|
||||
until=lambda:request_receipt.status != RNS.RequestReceipt.RECEIVING,
|
||||
timeout=result_timeout
|
||||
)
|
||||
|
||||
if request_receipt.status == RNS.RequestReceipt.FAILED:
|
||||
print("Receiving result failed")
|
||||
if interactive:
|
||||
return
|
||||
else:
|
||||
exit(246)
|
||||
|
||||
if request_receipt.response != None:
|
||||
try:
|
||||
executed = request_receipt.response[0]
|
||||
retval = request_receipt.response[1]
|
||||
stdout = request_receipt.response[2]
|
||||
stderr = request_receipt.response[3]
|
||||
outlen = request_receipt.response[4]
|
||||
errlen = request_receipt.response[5]
|
||||
started = request_receipt.response[6]
|
||||
concluded = request_receipt.response[7]
|
||||
|
||||
except Exception as e:
|
||||
print("Received invalid result")
|
||||
if interactive:
|
||||
return
|
||||
else:
|
||||
exit(247)
|
||||
|
||||
if executed:
|
||||
if detailed:
|
||||
if stdout != None and len(stdout) > 0:
|
||||
print(stdout.decode("utf-8"), end="")
|
||||
if stderr != None and len(stderr) > 0:
|
||||
print(stderr.decode("utf-8"), file=sys.stderr, end="")
|
||||
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
print("\n--- End of remote output, rnx done ---")
|
||||
if started != None and concluded != None:
|
||||
cmd_duration = round(concluded - started, 3)
|
||||
print("Remote command execution took "+str(cmd_duration)+" seconds")
|
||||
|
||||
total_size = request_receipt.response_size
|
||||
if request_receipt.request_size != None:
|
||||
total_size += request_receipt.request_size
|
||||
|
||||
transfer_duration = round(request_receipt.response_concluded_at - request_receipt.sent_at - cmd_duration, 3)
|
||||
if transfer_duration == 1:
|
||||
tdstr = " in 1 second"
|
||||
elif transfer_duration < 10:
|
||||
tdstr = " in "+str(transfer_duration)+" seconds"
|
||||
else:
|
||||
tdstr = " in "+pretty_time(transfer_duration)
|
||||
|
||||
spdstr = ", effective rate "+size_str(total_size/transfer_duration, "b")+"ps"
|
||||
|
||||
print("Transferred "+size_str(total_size)+tdstr+spdstr)
|
||||
|
||||
if outlen != None and stdout != None:
|
||||
if len(stdout) < outlen:
|
||||
tstr = ", "+str(len(stdout))+" bytes displayed"
|
||||
else:
|
||||
tstr = ""
|
||||
print("Remote wrote "+str(outlen)+" bytes to stdout"+tstr)
|
||||
|
||||
if errlen != None and stderr != None:
|
||||
if len(stderr) < errlen:
|
||||
tstr = ", "+str(len(stderr))+" bytes displayed"
|
||||
else:
|
||||
tstr = ""
|
||||
print("Remote wrote "+str(errlen)+" bytes to stderr"+tstr)
|
||||
|
||||
else:
|
||||
if stdout != None and len(stdout) > 0:
|
||||
print(stdout.decode("utf-8"), end="")
|
||||
if stderr != None and len(stderr) > 0:
|
||||
print(stderr.decode("utf-8"), file=sys.stderr, end="")
|
||||
|
||||
|
||||
if (stdoutl != 0 and len(stdout) < outlen) or (stderrl != 0 and len(stderr) < errlen):
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
print("\nOutput truncated before being returned:")
|
||||
if len(stdout) != 0 and len(stdout) < outlen:
|
||||
print(" stdout truncated to "+str(len(stdout))+" bytes")
|
||||
if len(stderr) != 0 and len(stderr) < errlen:
|
||||
print(" stderr truncated to "+str(len(stderr))+" bytes")
|
||||
else:
|
||||
print("Remote could not execute command")
|
||||
if interactive:
|
||||
return
|
||||
else:
|
||||
exit(248)
|
||||
else:
|
||||
print("No response")
|
||||
if interactive:
|
||||
return
|
||||
else:
|
||||
exit(249)
|
||||
|
||||
try:
|
||||
if not interactive:
|
||||
link.teardown()
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if not interactive and mirror:
|
||||
if request_receipt.response[1] != None:
|
||||
exit(request_receipt.response[1])
|
||||
else:
|
||||
exit(240)
|
||||
else:
|
||||
if interactive:
|
||||
if mirror:
|
||||
return request_receipt.response[1]
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
exit(0)
|
||||
|
||||
def main():
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description="Reticulum Remote Execution Utility")
|
||||
parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the listener", type=str)
|
||||
parser.add_argument("command", nargs="?", default=None, help="command to be execute", type=str)
|
||||
parser.add_argument("--config", metavar="path", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0, help="increase verbosity")
|
||||
parser.add_argument('-q', '--quiet', action='count', default=0, help="decrease verbosity")
|
||||
parser.add_argument('-p', '--print-identity', action='store_true', default=False, help="print identity and destination info and exit")
|
||||
parser.add_argument("-l", '--listen', action='store_true', default=False, help="listen for incoming commands")
|
||||
parser.add_argument('-i', metavar="identity", action='store', dest="identity", default=None, help="path to identity to use", type=str)
|
||||
parser.add_argument("-x", '--interactive', action='store_true', default=False, help="enter interactive mode")
|
||||
parser.add_argument("-b", '--no-announce', action='store_true', default=False, help="don't announce at program start")
|
||||
parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="accept from this identity", type=str)
|
||||
parser.add_argument('-n', '--noauth', action='store_true', default=False, help="accept commands from anyone")
|
||||
parser.add_argument('-N', '--noid', action='store_true', default=False, help="don't identify to listener")
|
||||
parser.add_argument("-d", '--detailed', action='store_true', default=False, help="show detailed result output")
|
||||
parser.add_argument("-m", action='store_true', dest="mirror", default=False, help="mirror exit code of remote command")
|
||||
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="connect and request timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
|
||||
parser.add_argument("-W", action="store", metavar="seconds", type=float, help="max result download time", default=None)
|
||||
parser.add_argument("--stdin", action='store', default=None, help="pass input to stdin", type=str)
|
||||
parser.add_argument("--stdout", action='store', default=None, help="max size in bytes of returned stdout", type=int)
|
||||
parser.add_argument("--stderr", action='store', default=None, help="max size in bytes of returned stderr", type=int)
|
||||
parser.add_argument("--version", action="version", version="rnx {version}".format(version=__version__))
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.listen or args.print_identity:
|
||||
listen(
|
||||
configdir = args.config,
|
||||
identitypath = args.identity,
|
||||
verbosity=args.verbose,
|
||||
quietness=args.quiet,
|
||||
allowed = args.allowed,
|
||||
print_identity=args.print_identity,
|
||||
disable_auth=args.noauth,
|
||||
disable_announce=args.no_announce,
|
||||
)
|
||||
|
||||
elif args.destination != None and args.command != None:
|
||||
execute(
|
||||
configdir = args.config,
|
||||
identitypath = args.identity,
|
||||
verbosity = args.verbose,
|
||||
quietness = args.quiet,
|
||||
detailed = args.detailed,
|
||||
mirror = args.mirror,
|
||||
noid = args.noid,
|
||||
destination = args.destination,
|
||||
command = args.command,
|
||||
stdin = args.stdin,
|
||||
stdoutl = args.stdout,
|
||||
stderrl = args.stderr,
|
||||
timeout = args.w,
|
||||
result_timeout = args.W,
|
||||
interactive = args.interactive,
|
||||
)
|
||||
|
||||
if args.destination != None and args.interactive:
|
||||
# command_history_max = 5000
|
||||
# command_history = []
|
||||
# command_current = ""
|
||||
# history_idx = 0
|
||||
# tty.setcbreak(sys.stdin.fileno())
|
||||
|
||||
code = None
|
||||
while True:
|
||||
try:
|
||||
cstr = str(code) if code and code != 0 else ""
|
||||
prompt = cstr+"> "
|
||||
print(prompt,end="")
|
||||
|
||||
# cmdbuf = b""
|
||||
# while True:
|
||||
# ch = sys.stdin.read(1)
|
||||
# cmdbuf += ch.encode("utf-8")
|
||||
# print("\r"+prompt+cmdbuf.decode("utf-8"), end="")
|
||||
|
||||
command = input()
|
||||
if command.lower() == "exit" or command.lower() == "quit":
|
||||
exit(0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
exit(0)
|
||||
except EOFError:
|
||||
exit(0)
|
||||
|
||||
if command.lower() == "clear":
|
||||
print('\033c', end='')
|
||||
|
||||
# command_history.append(command)
|
||||
# while len(command_history) > command_history_max:
|
||||
# command_history.pop(0)
|
||||
|
||||
else:
|
||||
code = execute(
|
||||
configdir = args.config,
|
||||
identitypath = args.identity,
|
||||
verbosity = args.verbose,
|
||||
quietness = args.quiet,
|
||||
detailed = args.detailed,
|
||||
mirror = args.mirror,
|
||||
noid = args.noid,
|
||||
destination = args.destination,
|
||||
command = command,
|
||||
stdin = None,
|
||||
stdoutl = args.stdout,
|
||||
stderrl = args.stderr,
|
||||
timeout = args.w,
|
||||
result_timeout = args.W,
|
||||
interactive = True,
|
||||
)
|
||||
|
||||
else:
|
||||
print("")
|
||||
parser.print_help()
|
||||
print("")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
# tty.setnocbreak(sys.stdin.fileno())
|
||||
print("")
|
||||
if link != None:
|
||||
link.teardown()
|
||||
exit()
|
||||
|
||||
def size_str(num, suffix='B'):
|
||||
units = ['','K','M','G','T','P','E','Z']
|
||||
last_unit = 'Y'
|
||||
|
||||
if suffix == 'b':
|
||||
num *= 8
|
||||
units = ['','K','M','G','T','P','E','Z']
|
||||
last_unit = 'Y'
|
||||
|
||||
for unit in units:
|
||||
if abs(num) < 1000.0:
|
||||
if unit == "":
|
||||
return "%.0f %s%s" % (num, unit, suffix)
|
||||
else:
|
||||
return "%.2f %s%s" % (num, unit, suffix)
|
||||
num /= 1000.0
|
||||
|
||||
return "%.2f%s%s" % (num, last_unit, suffix)
|
||||
|
||||
def pretty_time(time, verbose=False):
|
||||
days = int(time // (24 * 3600))
|
||||
time = time % (24 * 3600)
|
||||
hours = int(time // 3600)
|
||||
time %= 3600
|
||||
minutes = int(time // 60)
|
||||
time %= 60
|
||||
seconds = round(time, 2)
|
||||
|
||||
ss = "" if seconds == 1 else "s"
|
||||
sm = "" if minutes == 1 else "s"
|
||||
sh = "" if hours == 1 else "s"
|
||||
sd = "" if days == 1 else "s"
|
||||
|
||||
components = []
|
||||
if days > 0:
|
||||
components.append(str(days)+" day"+sd if verbose else str(days)+"d")
|
||||
|
||||
if hours > 0:
|
||||
components.append(str(hours)+" hour"+sh if verbose else str(hours)+"h")
|
||||
|
||||
if minutes > 0:
|
||||
components.append(str(minutes)+" minute"+sm if verbose else str(minutes)+"m")
|
||||
|
||||
if seconds > 0:
|
||||
components.append(str(seconds)+" second"+ss if verbose else str(seconds)+"s")
|
||||
|
||||
i = 0
|
||||
tstr = ""
|
||||
for c in components:
|
||||
i += 1
|
||||
if i == 1:
|
||||
pass
|
||||
elif i < len(components):
|
||||
tstr += ", "
|
||||
elif i == len(components):
|
||||
tstr += " and "
|
||||
|
||||
tstr += c
|
||||
|
||||
return tstr
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,3 +1,25 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
@@ -15,6 +37,8 @@ from .Destination import Destination
|
||||
from .Packet import Packet
|
||||
from .Packet import PacketReceipt
|
||||
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')]
|
||||
@@ -33,12 +57,14 @@ LOG_FILE = 0x92
|
||||
|
||||
LOG_MAXSIZE = 5*1024*1024
|
||||
|
||||
loglevel = LOG_NOTICE
|
||||
logfile = None
|
||||
logdest = LOG_STDOUT
|
||||
logtimefmt = "%Y-%m-%d %H:%M:%S"
|
||||
loglevel = LOG_NOTICE
|
||||
logfile = None
|
||||
logdest = LOG_STDOUT
|
||||
logtimefmt = "%Y-%m-%d %H:%M:%S"
|
||||
compact_log_fmt = False
|
||||
|
||||
random.seed(os.urandom(10))
|
||||
instance_random = random.Random()
|
||||
instance_random.seed(os.urandom(10))
|
||||
|
||||
_always_override_destination = False
|
||||
|
||||
@@ -67,12 +93,23 @@ def loglevelname(level):
|
||||
def version():
|
||||
return __version__
|
||||
|
||||
def host_os():
|
||||
from .vendor.platformutils import get_platform
|
||||
return get_platform()
|
||||
|
||||
def timestamp_str(time_s):
|
||||
timestamp = time.localtime(time_s)
|
||||
return time.strftime(logtimefmt, timestamp)
|
||||
|
||||
def log(msg, level=3, _override_destination = False):
|
||||
global _always_override_destination
|
||||
global _always_override_destination, compact_log_fmt
|
||||
|
||||
if loglevel >= level:
|
||||
timestamp = time.time()
|
||||
logstring = "["+time.strftime(logtimefmt)+"] ["+loglevelname(level)+"] "+msg
|
||||
if not compact_log_fmt:
|
||||
logstring = "["+timestamp_str(time.time())+"] ["+loglevelname(level)+"] "+msg
|
||||
else:
|
||||
logstring = "["+timestamp_str(time.time())+"] "+msg
|
||||
|
||||
logging_lock.acquire()
|
||||
|
||||
if (logdest == LOG_STDOUT or _always_override_destination or _override_destination):
|
||||
@@ -101,10 +138,15 @@ def log(msg, level=3, _override_destination = False):
|
||||
|
||||
|
||||
def rand():
|
||||
result = random.random()
|
||||
result = instance_random.random()
|
||||
return result
|
||||
|
||||
def hexrep(data, delimit=True):
|
||||
try:
|
||||
iter(data)
|
||||
except TypeError:
|
||||
data = [data]
|
||||
|
||||
delimiter = ":"
|
||||
if not delimit:
|
||||
delimiter = ""
|
||||
@@ -116,8 +158,79 @@ def prettyhexrep(data):
|
||||
hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">"
|
||||
return hexrep
|
||||
|
||||
def prettysize(num, suffix='B'):
|
||||
units = ['','K','M','G','T','P','E','Z']
|
||||
last_unit = 'Y'
|
||||
|
||||
if suffix == 'b':
|
||||
num *= 8
|
||||
units = ['','K','M','G','T','P','E','Z']
|
||||
last_unit = 'Y'
|
||||
|
||||
for unit in units:
|
||||
if abs(num) < 1000.0:
|
||||
if unit == "":
|
||||
return "%.0f %s%s" % (num, unit, suffix)
|
||||
else:
|
||||
return "%.2f %s%s" % (num, unit, suffix)
|
||||
num /= 1000.0
|
||||
|
||||
return "%.2f%s%s" % (num, last_unit, suffix)
|
||||
|
||||
def prettytime(time, verbose=False):
|
||||
days = int(time // (24 * 3600))
|
||||
time = time % (24 * 3600)
|
||||
hours = int(time // 3600)
|
||||
time %= 3600
|
||||
minutes = int(time // 60)
|
||||
time %= 60
|
||||
seconds = round(time, 2)
|
||||
|
||||
ss = "" if seconds == 1 else "s"
|
||||
sm = "" if minutes == 1 else "s"
|
||||
sh = "" if hours == 1 else "s"
|
||||
sd = "" if days == 1 else "s"
|
||||
|
||||
components = []
|
||||
if days > 0:
|
||||
components.append(str(days)+" day"+sd if verbose else str(days)+"d")
|
||||
|
||||
if hours > 0:
|
||||
components.append(str(hours)+" hour"+sh if verbose else str(hours)+"h")
|
||||
|
||||
if minutes > 0:
|
||||
components.append(str(minutes)+" minute"+sm if verbose else str(minutes)+"m")
|
||||
|
||||
if seconds > 0:
|
||||
components.append(str(seconds)+" second"+ss if verbose else str(seconds)+"s")
|
||||
|
||||
i = 0
|
||||
tstr = ""
|
||||
for c in components:
|
||||
i += 1
|
||||
if i == 1:
|
||||
pass
|
||||
elif i < len(components):
|
||||
tstr += ", "
|
||||
elif i == len(components):
|
||||
tstr += " and "
|
||||
|
||||
tstr += c
|
||||
|
||||
return tstr
|
||||
|
||||
def phyparams():
|
||||
print("Required Physical Layer MTU : "+str(Reticulum.MTU)+" bytes")
|
||||
print("Plaintext Packet MDU : "+str(Packet.PLAIN_MDU)+" bytes")
|
||||
print("Encrypted Packet MDU : "+str(Packet.ENCRYPTED_MDU)+" bytes")
|
||||
print("Link Curve : "+str(Link.CURVE))
|
||||
print("Link Packet MDU : "+str(Packet.ENCRYPTED_MDU)+" bytes")
|
||||
print("Link Public Key Size : "+str(Link.ECPUBSIZE*8)+" bits")
|
||||
print("Link Private Key Size : "+str(Link.KEYSIZE*8)+" bits")
|
||||
|
||||
def panic():
|
||||
os._exit(255)
|
||||
|
||||
def exit():
|
||||
print("")
|
||||
sys.exit(0)
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.2.7"
|
||||
__version__ = "0.3.19"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2018 Viktor Villainov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
A modern asynchronous library for building I2P applications.
|
||||
"""
|
||||
|
||||
from .__version__ import (
|
||||
__title__, __description__, __url__, __version__,
|
||||
__author__, __author_email__, __license__, __copyright__
|
||||
)
|
||||
|
||||
from .sam import Destination, PrivateKey
|
||||
|
||||
from .aiosam import (
|
||||
get_sam_socket, dest_lookup, new_destination,
|
||||
create_session, stream_connect, stream_accept,
|
||||
Session, StreamConnection, StreamAcceptor
|
||||
)
|
||||
|
||||
from .tunnel import ClientTunnel, ServerTunnel
|
||||
|
||||
from .utils import get_sam_address
|
||||
|
||||
from .exceptions import (
|
||||
CantReachPeer, DuplicatedDest, DuplicatedId, I2PError,
|
||||
InvalidId, InvalidKey, KeyNotFound, PeerNotFound, Timeout,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
__title__ = 'i2plib'
|
||||
__description__ = 'A modern asynchronous library for building I2P applications.'
|
||||
__url__ = 'https://github.com/l-n-s/i2plib'
|
||||
__version__ = '0.0.14'
|
||||
__author__ = 'Viktor Villainov'
|
||||
__author_email__ = 'supervillain@riseup.net'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright 2018 Viktor Villainov'
|
||||
@@ -0,0 +1,258 @@
|
||||
import asyncio
|
||||
|
||||
from . import sam
|
||||
from . import exceptions
|
||||
from . import utils
|
||||
from .log import logger
|
||||
|
||||
def parse_reply(data):
|
||||
if not data:
|
||||
raise ConnectionAbortedError("Empty response: SAM API went offline")
|
||||
|
||||
try:
|
||||
msg = sam.Message(data.decode().strip())
|
||||
logger.debug("SAM reply: "+str(msg))
|
||||
except:
|
||||
raise ConnectionAbortedError("Invalid SAM response")
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
async def get_sam_socket(sam_address=sam.DEFAULT_ADDRESS, loop=None):
|
||||
"""A couroutine used to create a new SAM socket.
|
||||
|
||||
:param sam_address: (optional) SAM API address
|
||||
:param loop: (optional) event loop instance
|
||||
:return: A (reader, writer) pair
|
||||
"""
|
||||
reader, writer = await asyncio.open_connection(*sam_address)
|
||||
writer.write(sam.hello("3.1", "3.1"))
|
||||
reply = parse_reply(await reader.readline())
|
||||
if reply.ok:
|
||||
return (reader, writer)
|
||||
else:
|
||||
writer.close()
|
||||
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
|
||||
|
||||
async def dest_lookup(domain, sam_address=sam.DEFAULT_ADDRESS,
|
||||
loop=None):
|
||||
"""A coroutine used to lookup a full I2P destination by .i2p domain or
|
||||
.b32.i2p address.
|
||||
|
||||
:param domain: Address to be resolved, can be a .i2p domain or a .b32.i2p
|
||||
address.
|
||||
:param sam_address: (optional) SAM API address
|
||||
:param loop: (optional) Event loop instance
|
||||
:return: An instance of :class:`Destination`
|
||||
"""
|
||||
reader, writer = await get_sam_socket(sam_address, loop)
|
||||
writer.write(sam.naming_lookup(domain))
|
||||
reply = parse_reply(await reader.readline())
|
||||
writer.close()
|
||||
if reply.ok:
|
||||
return sam.Destination(reply["VALUE"])
|
||||
else:
|
||||
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
|
||||
|
||||
async def new_destination(sam_address=sam.DEFAULT_ADDRESS, loop=None,
|
||||
sig_type=sam.Destination.default_sig_type):
|
||||
"""A coroutine used to generate a new destination with a private key of a
|
||||
chosen signature type.
|
||||
|
||||
:param sam_address: (optional) SAM API address
|
||||
:param loop: (optional) Event loop instance
|
||||
:param sig_type: (optional) Signature type
|
||||
:return: An instance of :class:`Destination`
|
||||
"""
|
||||
reader, writer = await get_sam_socket(sam_address, loop)
|
||||
writer.write(sam.dest_generate(sig_type))
|
||||
reply = parse_reply(await reader.readline())
|
||||
writer.close()
|
||||
return sam.Destination(reply["PRIV"], has_private_key=True)
|
||||
|
||||
async def create_session(session_name, sam_address=sam.DEFAULT_ADDRESS,
|
||||
loop=None, style="STREAM",
|
||||
signature_type=sam.Destination.default_sig_type,
|
||||
destination=None, options={}):
|
||||
"""A coroutine used to create a new SAM session.
|
||||
|
||||
:param session_name: Session nick name
|
||||
:param sam_address: (optional) SAM API address
|
||||
:param loop: (optional) Event loop instance
|
||||
:param style: (optional) Session style, can be STREAM, DATAGRAM, RAW
|
||||
:param signature_type: (optional) If the destination is TRANSIENT, this
|
||||
signature type is used
|
||||
:param destination: (optional) Destination to use in this session. Can be
|
||||
a base64 encoded string, :class:`Destination`
|
||||
instance or None. TRANSIENT destination is used when it
|
||||
is None.
|
||||
:param options: (optional) A dict object with i2cp options
|
||||
:return: A (reader, writer) pair
|
||||
"""
|
||||
logger.debug("Creating session {}".format(session_name))
|
||||
if destination:
|
||||
if type(destination) == sam.Destination:
|
||||
destination = destination
|
||||
else:
|
||||
destination = sam.Destination(
|
||||
destination, has_private_key=True)
|
||||
|
||||
dest_string = destination.private_key.base64
|
||||
else:
|
||||
dest_string = sam.TRANSIENT_DESTINATION
|
||||
|
||||
options = " ".join(["{}={}".format(k, v) for k, v in options.items()])
|
||||
|
||||
reader, writer = await get_sam_socket(sam_address, loop)
|
||||
writer.write(sam.session_create(
|
||||
style, session_name, dest_string, options))
|
||||
|
||||
reply = parse_reply(await reader.readline())
|
||||
if reply.ok:
|
||||
if not destination:
|
||||
destination = sam.Destination(
|
||||
reply["DESTINATION"], has_private_key=True)
|
||||
logger.debug(destination.base32)
|
||||
logger.debug("Session created {}".format(session_name))
|
||||
return (reader, writer)
|
||||
else:
|
||||
writer.close()
|
||||
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
|
||||
|
||||
async def stream_connect(session_name, destination,
|
||||
sam_address=sam.DEFAULT_ADDRESS, loop=None):
|
||||
"""A coroutine used to connect to a remote I2P destination.
|
||||
|
||||
:param session_name: Session nick name
|
||||
:param destination: I2P destination to connect to
|
||||
:param sam_address: (optional) SAM API address
|
||||
:param loop: (optional) Event loop instance
|
||||
:return: A (reader, writer) pair
|
||||
"""
|
||||
logger.debug("Connecting stream {}".format(session_name))
|
||||
if isinstance(destination, str) and not destination.endswith(".i2p"):
|
||||
destination = sam.Destination(destination)
|
||||
elif isinstance(destination, str):
|
||||
destination = await dest_lookup(destination, sam_address, loop)
|
||||
|
||||
reader, writer = await get_sam_socket(sam_address, loop)
|
||||
writer.write(sam.stream_connect(session_name, destination.base64,
|
||||
silent="false"))
|
||||
reply = parse_reply(await reader.readline())
|
||||
if reply.ok:
|
||||
logger.debug("Stream connected {}".format(session_name))
|
||||
return (reader, writer)
|
||||
else:
|
||||
writer.close()
|
||||
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
|
||||
|
||||
async def stream_accept(session_name, sam_address=sam.DEFAULT_ADDRESS,
|
||||
loop=None):
|
||||
"""A coroutine used to accept a connection from the I2P network.
|
||||
|
||||
:param session_name: Session nick name
|
||||
:param sam_address: (optional) SAM API address
|
||||
:param loop: (optional) Event loop instance
|
||||
:return: A (reader, writer) pair
|
||||
"""
|
||||
reader, writer = await get_sam_socket(sam_address, loop)
|
||||
writer.write(sam.stream_accept(session_name, silent="false"))
|
||||
reply = parse_reply(await reader.readline())
|
||||
if reply.ok:
|
||||
return (reader, writer)
|
||||
else:
|
||||
writer.close()
|
||||
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
|
||||
|
||||
### Context managers
|
||||
|
||||
class Session:
|
||||
"""Async SAM session context manager.
|
||||
|
||||
:param session_name: Session nick name
|
||||
:param sam_address: (optional) SAM API address
|
||||
:param loop: (optional) Event loop instance
|
||||
:param style: (optional) Session style, can be STREAM, DATAGRAM, RAW
|
||||
:param signature_type: (optional) If the destination is TRANSIENT, this
|
||||
signature type is used
|
||||
:param destination: (optional) Destination to use in this session. Can be
|
||||
a base64 encoded string, :class:`Destination`
|
||||
instance or None. TRANSIENT destination is used when it
|
||||
is None.
|
||||
:param options: (optional) A dict object with i2cp options
|
||||
:return: :class:`Session` object
|
||||
"""
|
||||
def __init__(self, session_name, sam_address=sam.DEFAULT_ADDRESS,
|
||||
loop=None, style="STREAM",
|
||||
signature_type=sam.Destination.default_sig_type,
|
||||
destination=None, options={}):
|
||||
self.session_name = session_name
|
||||
self.sam_address = sam_address
|
||||
self.loop = loop
|
||||
self.style = style
|
||||
self.signature_type = signature_type
|
||||
self.destination = destination
|
||||
self.options = options
|
||||
|
||||
async def __aenter__(self):
|
||||
self.reader, self.writer = await create_session(self.session_name,
|
||||
sam_address=self.sam_address, loop=self.loop, style=self.style,
|
||||
signature_type=self.signature_type,
|
||||
destination=self.destination, options=self.options)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
### TODO handle exceptions
|
||||
self.writer.close()
|
||||
|
||||
class StreamConnection:
|
||||
"""Async stream connection context manager.
|
||||
|
||||
:param session_name: Session nick name
|
||||
:param destination: I2P destination to connect to
|
||||
:param sam_address: (optional) SAM API address
|
||||
:param loop: (optional) Event loop instance
|
||||
:return: :class:`StreamConnection` object
|
||||
"""
|
||||
def __init__(self, session_name, destination,
|
||||
sam_address=sam.DEFAULT_ADDRESS, loop=None):
|
||||
self.session_name = session_name
|
||||
self.sam_address = sam_address
|
||||
self.loop = loop
|
||||
self.destination = destination
|
||||
|
||||
async def __aenter__(self):
|
||||
self.reader, self.writer = await stream_connect(self.session_name,
|
||||
self.destination, sam_address=self.sam_address, loop=self.loop)
|
||||
self.read = self.reader.read
|
||||
self.write = self.writer.write
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
### TODO handle exceptions
|
||||
self.writer.close()
|
||||
|
||||
class StreamAcceptor:
|
||||
"""Async stream acceptor context manager.
|
||||
|
||||
:param session_name: Session nick name
|
||||
:param sam_address: (optional) SAM API address
|
||||
:param loop: (optional) Event loop instance
|
||||
:return: :class:`StreamAcceptor` object
|
||||
"""
|
||||
def __init__(self, session_name, sam_address=sam.DEFAULT_ADDRESS,
|
||||
loop=None):
|
||||
self.session_name = session_name
|
||||
self.sam_address = sam_address
|
||||
self.loop = loop
|
||||
|
||||
async def __aenter__(self):
|
||||
self.reader, self.writer = await stream_accept(self.session_name,
|
||||
sam_address=self.sam_address, loop=self.loop)
|
||||
self.read = self.reader.read
|
||||
self.write = self.writer.write
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
### TODO handle exceptions
|
||||
self.writer.close()
|
||||
@@ -0,0 +1,44 @@
|
||||
# SAM exceptions
|
||||
|
||||
class SAMException(IOError):
|
||||
"""Base class for SAM exceptions"""
|
||||
|
||||
class CantReachPeer(SAMException):
|
||||
"""The peer exists, but cannot be reached"""
|
||||
|
||||
class DuplicatedDest(SAMException):
|
||||
"""The specified Destination is already in use"""
|
||||
|
||||
class DuplicatedId(SAMException):
|
||||
"""The nickname is already associated with a session"""
|
||||
|
||||
class I2PError(SAMException):
|
||||
"""A generic I2P error"""
|
||||
|
||||
class InvalidId(SAMException):
|
||||
"""STREAM SESSION ID doesn't exist"""
|
||||
|
||||
class InvalidKey(SAMException):
|
||||
"""The specified key is not valid (bad format, etc.)"""
|
||||
|
||||
class KeyNotFound(SAMException):
|
||||
"""The naming system can't resolve the given name"""
|
||||
|
||||
class PeerNotFound(SAMException):
|
||||
"""The peer cannot be found on the network"""
|
||||
|
||||
class Timeout(SAMException):
|
||||
"""The peer cannot be found on the network"""
|
||||
|
||||
SAM_EXCEPTIONS = {
|
||||
"CANT_REACH_PEER": CantReachPeer,
|
||||
"DUPLICATED_DEST": DuplicatedDest,
|
||||
"DUPLICATED_ID": DuplicatedId,
|
||||
"I2P_ERROR": I2PError,
|
||||
"INVALID_ID": InvalidId,
|
||||
"INVALID_KEY": InvalidKey,
|
||||
"KEY_NOT_FOUND": KeyNotFound,
|
||||
"PEER_NOT_FOUND": PeerNotFound,
|
||||
"TIMEOUT": Timeout,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Logging configuration."""
|
||||
import logging
|
||||
|
||||
# Name the logger after the package.
|
||||
logger = logging.getLogger(__package__)
|
||||
@@ -0,0 +1,147 @@
|
||||
from base64 import b64decode, b64encode, b32encode
|
||||
from hashlib import sha256
|
||||
import struct
|
||||
import re
|
||||
|
||||
|
||||
I2P_B64_CHARS = "-~"
|
||||
|
||||
def i2p_b64encode(x):
|
||||
"""Encode I2P destination"""
|
||||
return b64encode(x, altchars=I2P_B64_CHARS.encode()).decode()
|
||||
|
||||
def i2p_b64decode(x):
|
||||
"""Decode I2P destination"""
|
||||
return b64decode(x, altchars=I2P_B64_CHARS, validate=True)
|
||||
|
||||
SAM_BUFSIZE = 4096
|
||||
DEFAULT_ADDRESS = ("127.0.0.1", 7656)
|
||||
DEFAULT_MIN_VER = "3.1"
|
||||
DEFAULT_MAX_VER = "3.1"
|
||||
TRANSIENT_DESTINATION = "TRANSIENT"
|
||||
|
||||
VALID_BASE32_ADDRESS = re.compile(r"^([a-zA-Z0-9]{52}).b32.i2p$")
|
||||
VALID_BASE64_ADDRESS = re.compile(r"^([a-zA-Z0-9-~=]{516,528})$")
|
||||
|
||||
class Message(object):
|
||||
"""Parse SAM message to an object"""
|
||||
def __init__(self, s):
|
||||
self.opts = {}
|
||||
if type(s) != str:
|
||||
self._reply_string = s.decode().strip()
|
||||
else:
|
||||
self._reply_string = s
|
||||
|
||||
self.cmd, self.action, opts = self._reply_string.split(" ", 2)
|
||||
for v in opts.split(" "):
|
||||
data = v.split("=", 1) if "=" in v else (v, True)
|
||||
self.opts[data[0]] = data[1]
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.opts[key]
|
||||
|
||||
@property
|
||||
def ok(self):
|
||||
return self["RESULT"] == "OK"
|
||||
|
||||
def __repr__(self):
|
||||
return self._reply_string
|
||||
|
||||
|
||||
# SAM request messages
|
||||
|
||||
def hello(min_version, max_version):
|
||||
return "HELLO VERSION MIN={} MAX={}\n".format(min_version,
|
||||
max_version).encode()
|
||||
|
||||
def session_create(style, session_id, destination, options=""):
|
||||
return "SESSION CREATE STYLE={} ID={} DESTINATION={} {}\n".format(
|
||||
style, session_id, destination, options).encode()
|
||||
|
||||
|
||||
def stream_connect(session_id, destination, silent="false"):
|
||||
return "STREAM CONNECT ID={} DESTINATION={} SILENT={}\n".format(
|
||||
session_id, destination, silent).encode()
|
||||
|
||||
def stream_accept(session_id, silent="false"):
|
||||
return "STREAM ACCEPT ID={} SILENT={}\n".format(session_id, silent).encode()
|
||||
|
||||
def stream_forward(session_id, port, options=""):
|
||||
return "STREAM FORWARD ID={} PORT={} {}\n".format(
|
||||
session_id, port, options).encode()
|
||||
|
||||
|
||||
|
||||
def naming_lookup(name):
|
||||
return "NAMING LOOKUP NAME={}\n".format(name).encode()
|
||||
|
||||
def dest_generate(signature_type):
|
||||
return "DEST GENERATE SIGNATURE_TYPE={}\n".format(signature_type).encode()
|
||||
|
||||
class Destination(object):
|
||||
"""I2P destination
|
||||
|
||||
https://geti2p.net/spec/common-structures#destination
|
||||
|
||||
:param data: (optional) Base64 encoded data or binary data
|
||||
:param path: (optional) A path to a file with binary data
|
||||
:param has_private_key: (optional) Does data have a private key?
|
||||
"""
|
||||
|
||||
ECDSA_SHA256_P256 = 1
|
||||
ECDSA_SHA384_P384 = 2
|
||||
ECDSA_SHA512_P521 = 3
|
||||
EdDSA_SHA512_Ed25519 = 7
|
||||
|
||||
default_sig_type = EdDSA_SHA512_Ed25519
|
||||
|
||||
_pubkey_size = 256
|
||||
_signkey_size = 128
|
||||
_min_cert_size = 3
|
||||
|
||||
def __init__(self, data=None, path=None, has_private_key=False):
|
||||
#: Binary destination
|
||||
self.data = bytes()
|
||||
#: Base64 encoded destination
|
||||
self.base64 = ""
|
||||
#: :class:`RNS.vendor.i2plib.PrivateKey` instance or None
|
||||
self.private_key = None
|
||||
|
||||
if path:
|
||||
with open(path, "rb") as f: data = f.read()
|
||||
|
||||
if data and has_private_key:
|
||||
self.private_key = PrivateKey(data)
|
||||
|
||||
cert_len = struct.unpack("!H", self.private_key.data[385:387])[0]
|
||||
data = self.private_key.data[:387+cert_len]
|
||||
|
||||
if not data:
|
||||
raise Exception("Can't create a destination with no data")
|
||||
|
||||
self.data = data if type(data) == bytes else i2p_b64decode(data)
|
||||
self.base64 = data if type(data) == str else i2p_b64encode(data)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Destination: {}>".format(self.base32)
|
||||
|
||||
@property
|
||||
def base32(self):
|
||||
"""Base32 destination hash of this destination"""
|
||||
desthash = sha256(self.data).digest()
|
||||
return b32encode(desthash).decode()[:52].lower()
|
||||
|
||||
class PrivateKey(object):
|
||||
"""I2P private key
|
||||
|
||||
https://geti2p.net/spec/common-structures#keysandcert
|
||||
|
||||
:param data: Base64 encoded data or binary data
|
||||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
#: Binary private key
|
||||
self.data = data if type(data) == bytes else i2p_b64decode(data)
|
||||
#: Base64 encoded private key
|
||||
self.base64 = data if type(data) == str else i2p_b64encode(data)
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import argparse
|
||||
|
||||
from . import sam
|
||||
from . import aiosam
|
||||
from . import utils
|
||||
from .log import logger
|
||||
|
||||
BUFFER_SIZE = 65536
|
||||
|
||||
async def proxy_data(reader, writer):
|
||||
"""Proxy data from reader to writer"""
|
||||
try:
|
||||
while True:
|
||||
data = await reader.read(BUFFER_SIZE)
|
||||
if not data:
|
||||
break
|
||||
writer.write(data)
|
||||
except Exception as e:
|
||||
logger.debug('proxy_data_task exception {}'.format(e))
|
||||
finally:
|
||||
try:
|
||||
writer.close()
|
||||
except RuntimeError:
|
||||
pass
|
||||
logger.debug('close connection')
|
||||
|
||||
class I2PTunnel(object):
|
||||
"""Base I2P Tunnel object, not to be used directly
|
||||
|
||||
:param local_address: A local address to use for a tunnel.
|
||||
E.g. ("127.0.0.1", 6668)
|
||||
:param destination: (optional) Destination to use for this tunnel. Can be
|
||||
a base64 encoded string, :class:`Destination`
|
||||
instance or None. A new destination is created when it
|
||||
is None.
|
||||
:param session_name: (optional) Session nick name. A new session nickname is
|
||||
generated if not specified.
|
||||
:param options: (optional) A dict object with i2cp options
|
||||
:param loop: (optional) Event loop instance
|
||||
:param sam_address: (optional) SAM API address
|
||||
"""
|
||||
|
||||
def __init__(self, local_address, destination=None, session_name=None,
|
||||
options={}, loop=None, sam_address=sam.DEFAULT_ADDRESS):
|
||||
self.local_address = local_address
|
||||
self.destination = destination
|
||||
self.session_name = session_name or utils.generate_session_id()
|
||||
self.options = options
|
||||
self.loop = loop
|
||||
self.sam_address = sam_address
|
||||
|
||||
async def _pre_run(self):
|
||||
if not self.destination:
|
||||
self.destination = await aiosam.new_destination(
|
||||
sam_address=self.sam_address, loop=self.loop)
|
||||
_, self.session_writer = await aiosam.create_session(
|
||||
self.session_name, style=self.style, options=self.options,
|
||||
sam_address=self.sam_address,
|
||||
loop=self.loop, destination=self.destination)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the tunnel"""
|
||||
self.session_writer.close()
|
||||
|
||||
class ClientTunnel(I2PTunnel):
|
||||
"""Client tunnel, a subclass of tunnel.I2PTunnel
|
||||
|
||||
If you run a client tunnel with a local address ("127.0.0.1", 6668) and
|
||||
a remote destination "irc.echelon.i2p", all connections to 127.0.0.1:6668
|
||||
will be proxied to irc.echelon.i2p.
|
||||
|
||||
:param remote_destination: Remote I2P destination, can be either .i2p
|
||||
domain, .b32.i2p address, base64 destination or
|
||||
:class:`Destination` instance
|
||||
"""
|
||||
|
||||
def __init__(self, remote_destination, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.style = "STREAM"
|
||||
self.remote_destination = remote_destination
|
||||
|
||||
async def run(self):
|
||||
"""A coroutine used to run the tunnel"""
|
||||
await self._pre_run()
|
||||
|
||||
self.status = { "setup_ran": False, "setup_failed": False, "exception": None, "connect_tasks": [] }
|
||||
async def handle_client(client_reader, client_writer):
|
||||
"""Handle local client connection"""
|
||||
try:
|
||||
sc_task = aiosam.stream_connect(
|
||||
self.session_name, self.remote_destination,
|
||||
sam_address=self.sam_address, loop=self.loop)
|
||||
self.status["connect_tasks"].append(sc_task)
|
||||
|
||||
remote_reader, remote_writer = await sc_task
|
||||
asyncio.ensure_future(proxy_data(remote_reader, client_writer),
|
||||
loop=self.loop)
|
||||
asyncio.ensure_future(proxy_data(client_reader, remote_writer),
|
||||
loop=self.loop)
|
||||
|
||||
except Exception as e:
|
||||
self.status["setup_ran"] = True
|
||||
self.status["setup_failed"] = True
|
||||
self.status["exception"] = e
|
||||
|
||||
|
||||
try:
|
||||
self.server = await asyncio.start_server(handle_client, *self.local_address)
|
||||
self.status["setup_ran"] = True
|
||||
|
||||
except Exception as e:
|
||||
self.status["setup_ran"] = True
|
||||
self.status["setup_failed"] = True
|
||||
self.status["exception"] = e
|
||||
|
||||
def stop(self):
|
||||
super().stop()
|
||||
self.server.close()
|
||||
|
||||
class ServerTunnel(I2PTunnel):
|
||||
"""Server tunnel, a subclass of tunnel.I2PTunnel
|
||||
|
||||
If you want to expose a local service 127.0.0.1:80 to the I2P network, run
|
||||
a server tunnel with a local address ("127.0.0.1", 80). If you don't
|
||||
provide a private key or a session name, it will use a TRANSIENT
|
||||
destination.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.style = "STREAM"
|
||||
|
||||
async def run(self):
|
||||
"""A coroutine used to run the tunnel"""
|
||||
await self._pre_run()
|
||||
|
||||
self.status = { "setup_ran": False, "setup_failed": False, "exception": None, "connect_tasks": [] }
|
||||
async def handle_client(incoming, client_reader, client_writer):
|
||||
try:
|
||||
# data and dest may come in one chunk
|
||||
dest, data = incoming.split(b"\n", 1)
|
||||
remote_destination = sam.Destination(dest.decode())
|
||||
logger.debug("{} client connected: {}.b32.i2p".format(
|
||||
self.session_name, remote_destination.base32))
|
||||
|
||||
except Exception as e:
|
||||
self.status["exception"] = e
|
||||
self.status["setup_failed"] = True
|
||||
data = None
|
||||
|
||||
try:
|
||||
sc_task = asyncio.wait_for(
|
||||
asyncio.open_connection(
|
||||
host=self.local_address[0],
|
||||
port=self.local_address[1]),
|
||||
timeout=5)
|
||||
self.status["connect_tasks"].append(sc_task)
|
||||
|
||||
remote_reader, remote_writer = await sc_task
|
||||
if data: remote_writer.write(data)
|
||||
asyncio.ensure_future(proxy_data(remote_reader, client_writer),
|
||||
loop=self.loop)
|
||||
asyncio.ensure_future(proxy_data(client_reader, remote_writer),
|
||||
loop=self.loop)
|
||||
|
||||
except ConnectionRefusedError:
|
||||
client_writer.close()
|
||||
self.status["exception"] = e
|
||||
self.status["setup_failed"] = True
|
||||
|
||||
async def server_loop():
|
||||
try:
|
||||
while True:
|
||||
client_reader, client_writer = await aiosam.stream_accept(
|
||||
self.session_name, sam_address=self.sam_address,
|
||||
loop=self.loop)
|
||||
incoming = await client_reader.read(BUFFER_SIZE)
|
||||
asyncio.ensure_future(handle_client(
|
||||
incoming, client_reader, client_writer), loop=self.loop)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
self.server_loop = asyncio.ensure_future(server_loop(), loop=self.loop)
|
||||
self.status["setup_ran"] = True
|
||||
|
||||
def stop(self):
|
||||
super().stop()
|
||||
self.server_loop.cancel()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('type', metavar="TYPE", choices=('server', 'client'),
|
||||
help="Tunnel type (server or client)")
|
||||
parser.add_argument('address', metavar="ADDRESS",
|
||||
help="Local address (e.g. 127.0.0.1:8000)")
|
||||
parser.add_argument('--debug', '-d', action='store_true',
|
||||
help='Debugging')
|
||||
parser.add_argument('--key', '-k', default='', metavar='PRIVATE_KEY',
|
||||
help='Path to private key file')
|
||||
parser.add_argument('--destination', '-D', default='',
|
||||
metavar='DESTINATION', help='Remote destination')
|
||||
args = parser.parse_args()
|
||||
|
||||
SAM_ADDRESS = utils.get_sam_address()
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.set_debug(args.debug)
|
||||
|
||||
if args.key:
|
||||
destination = sam.Destination(path=args.key, has_private_key=True)
|
||||
else:
|
||||
destination = None
|
||||
|
||||
local_address = utils.address_from_string(args.address)
|
||||
|
||||
if args.type == "client":
|
||||
tunnel = ClientTunnel(args.destination, local_address, loop=loop,
|
||||
destination=destination, sam_address=SAM_ADDRESS)
|
||||
elif args.type == "server":
|
||||
tunnel = ServerTunnel(local_address, loop=loop, destination=destination,
|
||||
sam_address=SAM_ADDRESS)
|
||||
|
||||
asyncio.ensure_future(tunnel.run(), loop=loop)
|
||||
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
tunnel.stop()
|
||||
finally:
|
||||
loop.stop()
|
||||
loop.close()
|
||||
@@ -0,0 +1,42 @@
|
||||
import socket
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
|
||||
from . import sam
|
||||
|
||||
def get_free_port():
|
||||
"""Get a free port on your local host"""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.bind(('', 0))
|
||||
free_port = s.getsockname()[1]
|
||||
s.close()
|
||||
return free_port
|
||||
|
||||
def is_address_accessible(address):
|
||||
"""Check if address is accessible or down"""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
is_accessible = s.connect_ex(address) == 0
|
||||
s.close()
|
||||
return is_accessible
|
||||
|
||||
def address_from_string(address_string):
|
||||
"""Address tuple from host:port string"""
|
||||
address = address_string.split(":")
|
||||
return (address[0], int(address[1]))
|
||||
|
||||
def get_sam_address():
|
||||
"""
|
||||
Get SAM address from environment variable I2P_SAM_ADDRESS, or use a default
|
||||
value
|
||||
"""
|
||||
value = os.getenv("I2P_SAM_ADDRESS")
|
||||
return address_from_string(value) if value else sam.DEFAULT_ADDRESS
|
||||
|
||||
def generate_session_id(length=6):
|
||||
"""Generate random session id"""
|
||||
rand = random.SystemRandom()
|
||||
sid = [rand.choice(string.ascii_letters) for _ in range(length)]
|
||||
return "reticulum-" + "".join(sid)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
def get_platform():
|
||||
from os import environ
|
||||
if "ANDROID_ARGUMENT" in environ:
|
||||
return "android"
|
||||
elif "ANDROID_ROOT" in environ:
|
||||
return "android"
|
||||
else:
|
||||
import sys
|
||||
return sys.platform
|
||||
|
||||
def is_linux():
|
||||
if get_platform() == "linux":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_darwin():
|
||||
if get_platform() == "darwin":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_android():
|
||||
if get_platform() == "android":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def is_windows():
|
||||
if str(get_platform()).startswith("win"):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def platform_checks():
|
||||
if is_windows():
|
||||
import sys
|
||||
if sys.version_info.major >= 3 and sys.version_info.minor >= 8:
|
||||
pass
|
||||
else:
|
||||
import RNS
|
||||
RNS.log("On Windows, Reticulum requires Python 3.8 or higher.", RNS.LOG_ERROR)
|
||||
RNS.log("Please update Python to run Reticulum.", RNS.LOG_ERROR)
|
||||
RNS.panic()
|
||||
|
||||
def cryptography_old_api():
|
||||
import cryptography
|
||||
if cryptography.__version__ == "2.8":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -1,4 +1,4 @@
|
||||
# u-msgpack-python v2.5.0 - v at sergeev.io
|
||||
# u-msgpack-python v2.7.1 - v at sergeev.io
|
||||
# https://github.com/vsergeev/u-msgpack-python
|
||||
#
|
||||
# u-msgpack-python is a lightweight MessagePack serializer and deserializer
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2013-2016 vsergeev / Ivan (Vanya) A. Sergeev
|
||||
# Copyright (c) 2013-2020 vsergeev / Ivan (Vanya) A. Sergeev
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -31,7 +31,7 @@
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
"""
|
||||
u-msgpack-python v2.5.0 - v at sergeev.io
|
||||
u-msgpack-python v2.7.1 - v at sergeev.io
|
||||
https://github.com/vsergeev/u-msgpack-python
|
||||
|
||||
u-msgpack-python is a lightweight MessagePack serializer and deserializer
|
||||
@@ -49,10 +49,15 @@ import datetime
|
||||
import sys
|
||||
import io
|
||||
|
||||
__version__ = "2.5.0"
|
||||
if sys.version_info[0:2] >= (3, 3):
|
||||
from collections.abc import Hashable
|
||||
else:
|
||||
from collections import Hashable
|
||||
|
||||
__version__ = "2.7.1"
|
||||
"Module version string"
|
||||
|
||||
version = (2, 5, 0)
|
||||
version = (2, 7, 1)
|
||||
"Module version tuple"
|
||||
|
||||
|
||||
@@ -61,7 +66,7 @@ version = (2, 5, 0)
|
||||
##############################################################################
|
||||
|
||||
# Extension type for application-defined types and data
|
||||
class Ext:
|
||||
class Ext(object):
|
||||
"""
|
||||
The Ext class facilitates creating a serializable extension object to store
|
||||
an application-defined type and data byte array.
|
||||
@@ -75,23 +80,33 @@ class Ext:
|
||||
type: application-defined type integer
|
||||
data: application-defined data byte array
|
||||
|
||||
TypeError:
|
||||
Type is not an integer.
|
||||
ValueError:
|
||||
Type is out of range of -128 to 127.
|
||||
TypeError::
|
||||
Data is not type 'bytes' (Python 3) or not type 'str' (Python 2).
|
||||
|
||||
Example:
|
||||
>>> foo = umsgpack.Ext(0x05, b"\x01\x02\x03")
|
||||
>>> foo = umsgpack.Ext(5, b"\x01\x02\x03")
|
||||
>>> umsgpack.packb({u"special stuff": foo, u"awesome": True})
|
||||
'\x82\xa7awesome\xc3\xadspecial stuff\xc7\x03\x05\x01\x02\x03'
|
||||
>>> bar = umsgpack.unpackb(_)
|
||||
>>> print(bar["special stuff"])
|
||||
Ext Object (Type: 0x05, Data: 01 02 03)
|
||||
Ext Object (Type: 5, Data: 01 02 03)
|
||||
>>>
|
||||
"""
|
||||
# Check type is type int
|
||||
# Check type is type int and in range
|
||||
if not isinstance(type, int):
|
||||
raise TypeError("ext type is not type integer")
|
||||
# Check data is type bytes
|
||||
elif not (-2**7 <= type <= 2**7 - 1):
|
||||
raise ValueError("ext type value {:d} is out of range (-128 to 127)".format(type))
|
||||
# Check data is type bytes or str
|
||||
elif sys.version_info[0] == 3 and not isinstance(data, bytes):
|
||||
raise TypeError("ext data is not type \'bytes\'")
|
||||
elif sys.version_info[0] == 2 and not isinstance(data, str):
|
||||
raise TypeError("ext data is not type \'str\'")
|
||||
|
||||
self.type = type
|
||||
self.data = data
|
||||
|
||||
@@ -99,9 +114,8 @@ class Ext:
|
||||
"""
|
||||
Compare this Ext object with another for equality.
|
||||
"""
|
||||
return (isinstance(other, self.__class__) and
|
||||
self.type == other.type and
|
||||
self.data == other.data)
|
||||
return isinstance(other, self.__class__) \
|
||||
and self.type == other.type and self.data == other.data
|
||||
|
||||
def __ne__(self, other):
|
||||
"""
|
||||
@@ -113,8 +127,8 @@ class Ext:
|
||||
"""
|
||||
String representation of this Ext object.
|
||||
"""
|
||||
s = "Ext Object (Type: 0x%02x, Data: " % self.type
|
||||
s += " ".join(["0x%02x" % ord(self.data[i:i + 1])
|
||||
s = "Ext Object (Type: {:d}, Data: ".format(self.type)
|
||||
s += " ".join(["0x{:02}".format(ord(self.data[i:i + 1]))
|
||||
for i in xrange(min(len(self.data), 8))])
|
||||
if len(self.data) > 8:
|
||||
s += " ..."
|
||||
@@ -130,7 +144,52 @@ class Ext:
|
||||
|
||||
class InvalidString(bytes):
|
||||
"""Subclass of bytes to hold invalid UTF-8 strings."""
|
||||
pass
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Ext Serializable Decorator
|
||||
##############################################################################
|
||||
|
||||
_ext_class_to_type = {}
|
||||
_ext_type_to_class = {}
|
||||
|
||||
|
||||
def ext_serializable(ext_type):
|
||||
"""
|
||||
Return a decorator to register a class for automatic packing and unpacking
|
||||
with the specified Ext type code. The application class should implement a
|
||||
`packb()` method that returns serialized bytes, and an `unpackb()` class
|
||||
method or static method that accepts serialized bytes and returns an
|
||||
instance of the application class.
|
||||
|
||||
Args:
|
||||
ext_type: application-defined Ext type code
|
||||
|
||||
Raises:
|
||||
TypeError:
|
||||
Ext type is not an integer.
|
||||
ValueError:
|
||||
Ext type is out of range of -128 to 127.
|
||||
ValueError:
|
||||
Ext type or class already registered.
|
||||
"""
|
||||
def wrapper(cls):
|
||||
if not isinstance(ext_type, int):
|
||||
raise TypeError("Ext type is not type integer")
|
||||
elif not (-2**7 <= ext_type <= 2**7 - 1):
|
||||
raise ValueError("Ext type value {:d} is out of range of -128 to 127".format(ext_type))
|
||||
elif ext_type in _ext_type_to_class:
|
||||
raise ValueError("Ext type {:d} already registered with class {:s}".format(ext_type, repr(_ext_type_to_class[ext_type])))
|
||||
elif cls in _ext_class_to_type:
|
||||
raise ValueError("Class {:s} already registered with Ext type {:d}".format(repr(cls), ext_type))
|
||||
|
||||
_ext_type_to_class[ext_type] = cls
|
||||
_ext_class_to_type[cls] = ext_type
|
||||
|
||||
return cls
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Exceptions
|
||||
@@ -140,39 +199,32 @@ class InvalidString(bytes):
|
||||
# Base Exception classes
|
||||
class PackException(Exception):
|
||||
"Base class for exceptions encountered during packing."
|
||||
pass
|
||||
|
||||
|
||||
class UnpackException(Exception):
|
||||
"Base class for exceptions encountered during unpacking."
|
||||
pass
|
||||
|
||||
|
||||
# Packing error
|
||||
class UnsupportedTypeException(PackException):
|
||||
"Object type not supported for packing."
|
||||
pass
|
||||
|
||||
|
||||
# Unpacking error
|
||||
class InsufficientDataException(UnpackException):
|
||||
"Insufficient data to unpack the serialized object."
|
||||
pass
|
||||
|
||||
|
||||
class InvalidStringException(UnpackException):
|
||||
"Invalid UTF-8 string encountered during unpacking."
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedTimestampException(UnpackException):
|
||||
"Unsupported timestamp format encountered during unpacking."
|
||||
pass
|
||||
|
||||
|
||||
class ReservedCodeException(UnpackException):
|
||||
"Reserved code encountered during unpacking."
|
||||
pass
|
||||
|
||||
|
||||
class UnhashableKeyException(UnpackException):
|
||||
@@ -180,12 +232,10 @@ class UnhashableKeyException(UnpackException):
|
||||
Unhashable key encountered during map unpacking.
|
||||
The serialized map cannot be deserialized into a Python dictionary.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class DuplicateKeyException(UnpackException):
|
||||
"Duplicate key encountered during map unpacking."
|
||||
pass
|
||||
|
||||
|
||||
# Backwards compatibility
|
||||
@@ -250,15 +300,15 @@ def _pack_integer(obj, fp, options):
|
||||
else:
|
||||
raise UnsupportedTypeException("huge signed int")
|
||||
else:
|
||||
if obj <= 127:
|
||||
if obj < 128:
|
||||
fp.write(struct.pack("B", obj))
|
||||
elif obj <= 2**8 - 1:
|
||||
elif obj < 2**8:
|
||||
fp.write(b"\xcc" + struct.pack("B", obj))
|
||||
elif obj <= 2**16 - 1:
|
||||
elif obj < 2**16:
|
||||
fp.write(b"\xcd" + struct.pack(">H", obj))
|
||||
elif obj <= 2**32 - 1:
|
||||
elif obj < 2**32:
|
||||
fp.write(b"\xce" + struct.pack(">I", obj))
|
||||
elif obj <= 2**64 - 1:
|
||||
elif obj < 2**64:
|
||||
fp.write(b"\xcf" + struct.pack(">Q", obj))
|
||||
else:
|
||||
raise UnsupportedTypeException("huge unsigned int")
|
||||
@@ -285,94 +335,99 @@ def _pack_float(obj, fp, options):
|
||||
|
||||
def _pack_string(obj, fp, options):
|
||||
obj = obj.encode('utf-8')
|
||||
if len(obj) <= 31:
|
||||
fp.write(struct.pack("B", 0xa0 | len(obj)) + obj)
|
||||
elif len(obj) <= 2**8 - 1:
|
||||
fp.write(b"\xd9" + struct.pack("B", len(obj)) + obj)
|
||||
elif len(obj) <= 2**16 - 1:
|
||||
fp.write(b"\xda" + struct.pack(">H", len(obj)) + obj)
|
||||
elif len(obj) <= 2**32 - 1:
|
||||
fp.write(b"\xdb" + struct.pack(">I", len(obj)) + obj)
|
||||
obj_len = len(obj)
|
||||
if obj_len < 32:
|
||||
fp.write(struct.pack("B", 0xa0 | obj_len) + obj)
|
||||
elif obj_len < 2**8:
|
||||
fp.write(b"\xd9" + struct.pack("B", obj_len) + obj)
|
||||
elif obj_len < 2**16:
|
||||
fp.write(b"\xda" + struct.pack(">H", obj_len) + obj)
|
||||
elif obj_len < 2**32:
|
||||
fp.write(b"\xdb" + struct.pack(">I", obj_len) + obj)
|
||||
else:
|
||||
raise UnsupportedTypeException("huge string")
|
||||
|
||||
|
||||
def _pack_binary(obj, fp, options):
|
||||
if len(obj) <= 2**8 - 1:
|
||||
fp.write(b"\xc4" + struct.pack("B", len(obj)) + obj)
|
||||
elif len(obj) <= 2**16 - 1:
|
||||
fp.write(b"\xc5" + struct.pack(">H", len(obj)) + obj)
|
||||
elif len(obj) <= 2**32 - 1:
|
||||
fp.write(b"\xc6" + struct.pack(">I", len(obj)) + obj)
|
||||
obj_len = len(obj)
|
||||
if obj_len < 2**8:
|
||||
fp.write(b"\xc4" + struct.pack("B", obj_len) + obj)
|
||||
elif obj_len < 2**16:
|
||||
fp.write(b"\xc5" + struct.pack(">H", obj_len) + obj)
|
||||
elif obj_len < 2**32:
|
||||
fp.write(b"\xc6" + struct.pack(">I", obj_len) + obj)
|
||||
else:
|
||||
raise UnsupportedTypeException("huge binary string")
|
||||
|
||||
|
||||
def _pack_oldspec_raw(obj, fp, options):
|
||||
if len(obj) <= 31:
|
||||
fp.write(struct.pack("B", 0xa0 | len(obj)) + obj)
|
||||
elif len(obj) <= 2**16 - 1:
|
||||
fp.write(b"\xda" + struct.pack(">H", len(obj)) + obj)
|
||||
elif len(obj) <= 2**32 - 1:
|
||||
fp.write(b"\xdb" + struct.pack(">I", len(obj)) + obj)
|
||||
obj_len = len(obj)
|
||||
if obj_len < 32:
|
||||
fp.write(struct.pack("B", 0xa0 | obj_len) + obj)
|
||||
elif obj_len < 2**16:
|
||||
fp.write(b"\xda" + struct.pack(">H", obj_len) + obj)
|
||||
elif obj_len < 2**32:
|
||||
fp.write(b"\xdb" + struct.pack(">I", obj_len) + obj)
|
||||
else:
|
||||
raise UnsupportedTypeException("huge raw string")
|
||||
|
||||
|
||||
def _pack_ext(obj, fp, options):
|
||||
if len(obj.data) == 1:
|
||||
obj_len = len(obj.data)
|
||||
if obj_len == 1:
|
||||
fp.write(b"\xd4" + struct.pack("B", obj.type & 0xff) + obj.data)
|
||||
elif len(obj.data) == 2:
|
||||
elif obj_len == 2:
|
||||
fp.write(b"\xd5" + struct.pack("B", obj.type & 0xff) + obj.data)
|
||||
elif len(obj.data) == 4:
|
||||
elif obj_len == 4:
|
||||
fp.write(b"\xd6" + struct.pack("B", obj.type & 0xff) + obj.data)
|
||||
elif len(obj.data) == 8:
|
||||
elif obj_len == 8:
|
||||
fp.write(b"\xd7" + struct.pack("B", obj.type & 0xff) + obj.data)
|
||||
elif len(obj.data) == 16:
|
||||
elif obj_len == 16:
|
||||
fp.write(b"\xd8" + struct.pack("B", obj.type & 0xff) + obj.data)
|
||||
elif len(obj.data) <= 2**8 - 1:
|
||||
fp.write(b"\xc7" +
|
||||
struct.pack("BB", len(obj.data), obj.type & 0xff) + obj.data)
|
||||
elif len(obj.data) <= 2**16 - 1:
|
||||
fp.write(b"\xc8" +
|
||||
struct.pack(">HB", len(obj.data), obj.type & 0xff) + obj.data)
|
||||
elif len(obj.data) <= 2**32 - 1:
|
||||
fp.write(b"\xc9" +
|
||||
struct.pack(">IB", len(obj.data), obj.type & 0xff) + obj.data)
|
||||
elif obj_len < 2**8:
|
||||
fp.write(b"\xc7" + struct.pack("BB", obj_len, obj.type & 0xff) + obj.data)
|
||||
elif obj_len < 2**16:
|
||||
fp.write(b"\xc8" + struct.pack(">HB", obj_len, obj.type & 0xff) + obj.data)
|
||||
elif obj_len < 2**32:
|
||||
fp.write(b"\xc9" + struct.pack(">IB", obj_len, obj.type & 0xff) + obj.data)
|
||||
else:
|
||||
raise UnsupportedTypeException("huge ext data")
|
||||
|
||||
|
||||
def _pack_ext_timestamp(obj, fp, options):
|
||||
delta = obj - _epoch
|
||||
if not obj.tzinfo:
|
||||
# Object is naive datetime, convert to aware date time,
|
||||
# assuming UTC timezone
|
||||
delta = obj.replace(tzinfo=_utc_tzinfo) - _epoch
|
||||
else:
|
||||
# Object is aware datetime
|
||||
delta = obj - _epoch
|
||||
|
||||
seconds = delta.seconds + delta.days * 86400
|
||||
microseconds = delta.microseconds
|
||||
|
||||
if microseconds == 0 and 0 <= seconds <= 2**32 - 1:
|
||||
# 32-bit timestamp
|
||||
fp.write(b"\xd6\xff" +
|
||||
struct.pack(">I", seconds))
|
||||
fp.write(b"\xd6\xff" + struct.pack(">I", seconds))
|
||||
elif 0 <= seconds <= 2**34 - 1:
|
||||
# 64-bit timestamp
|
||||
value = ((microseconds * 1000) << 34) | seconds
|
||||
fp.write(b"\xd7\xff" +
|
||||
struct.pack(">Q", value))
|
||||
fp.write(b"\xd7\xff" + struct.pack(">Q", value))
|
||||
elif -2**63 <= abs(seconds) <= 2**63 - 1:
|
||||
# 96-bit timestamp
|
||||
fp.write(b"\xc7\x0c\xff" +
|
||||
struct.pack(">I", microseconds * 1000) +
|
||||
struct.pack(">q", seconds))
|
||||
fp.write(b"\xc7\x0c\xff" + struct.pack(">Iq", microseconds * 1000, seconds))
|
||||
else:
|
||||
raise UnsupportedTypeException("huge timestamp")
|
||||
|
||||
|
||||
def _pack_array(obj, fp, options):
|
||||
if len(obj) <= 15:
|
||||
fp.write(struct.pack("B", 0x90 | len(obj)))
|
||||
elif len(obj) <= 2**16 - 1:
|
||||
fp.write(b"\xdc" + struct.pack(">H", len(obj)))
|
||||
elif len(obj) <= 2**32 - 1:
|
||||
fp.write(b"\xdd" + struct.pack(">I", len(obj)))
|
||||
obj_len = len(obj)
|
||||
if obj_len < 16:
|
||||
fp.write(struct.pack("B", 0x90 | obj_len))
|
||||
elif obj_len < 2**16:
|
||||
fp.write(b"\xdc" + struct.pack(">H", obj_len))
|
||||
elif obj_len < 2**32:
|
||||
fp.write(b"\xdd" + struct.pack(">I", obj_len))
|
||||
else:
|
||||
raise UnsupportedTypeException("huge array")
|
||||
|
||||
@@ -381,12 +436,13 @@ def _pack_array(obj, fp, options):
|
||||
|
||||
|
||||
def _pack_map(obj, fp, options):
|
||||
if len(obj) <= 15:
|
||||
fp.write(struct.pack("B", 0x80 | len(obj)))
|
||||
elif len(obj) <= 2**16 - 1:
|
||||
fp.write(b"\xde" + struct.pack(">H", len(obj)))
|
||||
elif len(obj) <= 2**32 - 1:
|
||||
fp.write(b"\xdf" + struct.pack(">I", len(obj)))
|
||||
obj_len = len(obj)
|
||||
if obj_len < 16:
|
||||
fp.write(struct.pack("B", 0x80 | obj_len))
|
||||
elif obj_len < 2**16:
|
||||
fp.write(b"\xde" + struct.pack(">H", obj_len))
|
||||
elif obj_len < 2**32:
|
||||
fp.write(b"\xdf" + struct.pack(">I", obj_len))
|
||||
else:
|
||||
raise UnsupportedTypeException("huge array")
|
||||
|
||||
@@ -435,9 +491,14 @@ def _pack2(obj, fp, **options):
|
||||
_pack_nil(obj, fp, options)
|
||||
elif ext_handlers and obj.__class__ in ext_handlers:
|
||||
_pack_ext(ext_handlers[obj.__class__](obj), fp, options)
|
||||
elif obj.__class__ in _ext_class_to_type:
|
||||
try:
|
||||
_pack_ext(Ext(_ext_class_to_type[obj.__class__], obj.packb()), fp, options)
|
||||
except AttributeError:
|
||||
raise NotImplementedError("Ext serializable class {:s} is missing implementation of packb()".format(repr(obj.__class__)))
|
||||
elif isinstance(obj, bool):
|
||||
_pack_boolean(obj, fp, options)
|
||||
elif isinstance(obj, int) or isinstance(obj, long):
|
||||
elif isinstance(obj, (int, long)):
|
||||
_pack_integer(obj, fp, options)
|
||||
elif isinstance(obj, float):
|
||||
_pack_float(obj, fp, options)
|
||||
@@ -449,7 +510,7 @@ def _pack2(obj, fp, **options):
|
||||
_pack_string(obj, fp, options)
|
||||
elif isinstance(obj, str):
|
||||
_pack_binary(obj, fp, options)
|
||||
elif isinstance(obj, list) or isinstance(obj, tuple):
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
_pack_array(obj, fp, options)
|
||||
elif isinstance(obj, dict):
|
||||
_pack_map(obj, fp, options)
|
||||
@@ -464,9 +525,19 @@ def _pack2(obj, fp, **options):
|
||||
_pack_ext(ext_handlers[t](obj), fp, options)
|
||||
else:
|
||||
raise UnsupportedTypeException(
|
||||
"unsupported type: %s" % str(type(obj)))
|
||||
"unsupported type: {:s}".format(str(type(obj))))
|
||||
elif _ext_class_to_type:
|
||||
# Linear search for superclass
|
||||
t = next((t for t in _ext_class_to_type if isinstance(obj, t)), None)
|
||||
if t:
|
||||
try:
|
||||
_pack_ext(Ext(_ext_class_to_type[t], obj.packb()), fp, options)
|
||||
except AttributeError:
|
||||
raise NotImplementedError("Ext serializable class {:s} is missing implementation of packb()".format(repr(t)))
|
||||
else:
|
||||
raise UnsupportedTypeException("unsupported type: {:s}".format(str(type(obj))))
|
||||
else:
|
||||
raise UnsupportedTypeException("unsupported type: %s" % str(type(obj)))
|
||||
raise UnsupportedTypeException("unsupported type: {:s}".format(str(type(obj))))
|
||||
|
||||
|
||||
# Pack for Python 3, with unicode 'str' type, 'bytes' type, and no 'long' type
|
||||
@@ -507,6 +578,11 @@ def _pack3(obj, fp, **options):
|
||||
_pack_nil(obj, fp, options)
|
||||
elif ext_handlers and obj.__class__ in ext_handlers:
|
||||
_pack_ext(ext_handlers[obj.__class__](obj), fp, options)
|
||||
elif obj.__class__ in _ext_class_to_type:
|
||||
try:
|
||||
_pack_ext(Ext(_ext_class_to_type[obj.__class__], obj.packb()), fp, options)
|
||||
except AttributeError:
|
||||
raise NotImplementedError("Ext serializable class {:s} is missing implementation of packb()".format(repr(obj.__class__)))
|
||||
elif isinstance(obj, bool):
|
||||
_pack_boolean(obj, fp, options)
|
||||
elif isinstance(obj, int):
|
||||
@@ -521,7 +597,7 @@ def _pack3(obj, fp, **options):
|
||||
_pack_string(obj, fp, options)
|
||||
elif isinstance(obj, bytes):
|
||||
_pack_binary(obj, fp, options)
|
||||
elif isinstance(obj, list) or isinstance(obj, tuple):
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
_pack_array(obj, fp, options)
|
||||
elif isinstance(obj, dict):
|
||||
_pack_map(obj, fp, options)
|
||||
@@ -536,10 +612,20 @@ def _pack3(obj, fp, **options):
|
||||
_pack_ext(ext_handlers[t](obj), fp, options)
|
||||
else:
|
||||
raise UnsupportedTypeException(
|
||||
"unsupported type: %s" % str(type(obj)))
|
||||
"unsupported type: {:s}".format(str(type(obj))))
|
||||
elif _ext_class_to_type:
|
||||
# Linear search for superclass
|
||||
t = next((t for t in _ext_class_to_type if isinstance(obj, t)), None)
|
||||
if t:
|
||||
try:
|
||||
_pack_ext(Ext(_ext_class_to_type[t], obj.packb()), fp, options)
|
||||
except AttributeError:
|
||||
raise NotImplementedError("Ext serializable class {:s} is missing implementation of packb()".format(repr(t)))
|
||||
else:
|
||||
raise UnsupportedTypeException("unsupported type: {:s}".format(str(type(obj))))
|
||||
else:
|
||||
raise UnsupportedTypeException(
|
||||
"unsupported type: %s" % str(type(obj)))
|
||||
"unsupported type: {:s}".format(str(type(obj))))
|
||||
|
||||
|
||||
def _packb2(obj, **options):
|
||||
@@ -613,9 +699,20 @@ def _packb3(obj, **options):
|
||||
|
||||
|
||||
def _read_except(fp, n):
|
||||
if n == 0:
|
||||
return b""
|
||||
|
||||
data = fp.read(n)
|
||||
if len(data) < n:
|
||||
if len(data) == 0:
|
||||
raise InsufficientDataException()
|
||||
|
||||
while len(data) < n:
|
||||
chunk = fp.read(n - len(data))
|
||||
if len(chunk) == 0:
|
||||
raise InsufficientDataException()
|
||||
|
||||
data += chunk
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -640,21 +737,21 @@ def _unpack_integer(code, fp, options):
|
||||
return struct.unpack(">I", _read_except(fp, 4))[0]
|
||||
elif code == b'\xcf':
|
||||
return struct.unpack(">Q", _read_except(fp, 8))[0]
|
||||
raise Exception("logic error, not int: 0x%02x" % ord(code))
|
||||
raise Exception("logic error, not int: 0x{:02x}".format(ord(code)))
|
||||
|
||||
|
||||
def _unpack_reserved(code, fp, options):
|
||||
if code == b'\xc1':
|
||||
raise ReservedCodeException(
|
||||
"encountered reserved code: 0x%02x" % ord(code))
|
||||
"encountered reserved code: 0x{:02x}".format(ord(code)))
|
||||
raise Exception(
|
||||
"logic error, not reserved code: 0x%02x" % ord(code))
|
||||
"logic error, not reserved code: 0x{:02x}".format(ord(code)))
|
||||
|
||||
|
||||
def _unpack_nil(code, fp, options):
|
||||
if code == b'\xc0':
|
||||
return None
|
||||
raise Exception("logic error, not nil: 0x%02x" % ord(code))
|
||||
raise Exception("logic error, not nil: 0x{:02x}".format(ord(code)))
|
||||
|
||||
|
||||
def _unpack_boolean(code, fp, options):
|
||||
@@ -662,7 +759,7 @@ def _unpack_boolean(code, fp, options):
|
||||
return False
|
||||
elif code == b'\xc3':
|
||||
return True
|
||||
raise Exception("logic error, not boolean: 0x%02x" % ord(code))
|
||||
raise Exception("logic error, not boolean: 0x{:02x}".format(ord(code)))
|
||||
|
||||
|
||||
def _unpack_float(code, fp, options):
|
||||
@@ -670,7 +767,7 @@ def _unpack_float(code, fp, options):
|
||||
return struct.unpack(">f", _read_except(fp, 4))[0]
|
||||
elif code == b'\xcb':
|
||||
return struct.unpack(">d", _read_except(fp, 8))[0]
|
||||
raise Exception("logic error, not float: 0x%02x" % ord(code))
|
||||
raise Exception("logic error, not float: 0x{:02x}".format(ord(code)))
|
||||
|
||||
|
||||
def _unpack_string(code, fp, options):
|
||||
@@ -683,7 +780,7 @@ def _unpack_string(code, fp, options):
|
||||
elif code == b'\xdb':
|
||||
length = struct.unpack(">I", _read_except(fp, 4))[0]
|
||||
else:
|
||||
raise Exception("logic error, not string: 0x%02x" % ord(code))
|
||||
raise Exception("logic error, not string: 0x{:02x}".format(ord(code)))
|
||||
|
||||
# Always return raw bytes in compatibility mode
|
||||
global compatibility
|
||||
@@ -707,7 +804,7 @@ def _unpack_binary(code, fp, options):
|
||||
elif code == b'\xc6':
|
||||
length = struct.unpack(">I", _read_except(fp, 4))[0]
|
||||
else:
|
||||
raise Exception("logic error, not binary: 0x%02x" % ord(code))
|
||||
raise Exception("logic error, not binary: 0x{:02x}".format(ord(code)))
|
||||
|
||||
return _read_except(fp, length)
|
||||
|
||||
@@ -730,43 +827,48 @@ def _unpack_ext(code, fp, options):
|
||||
elif code == b'\xc9':
|
||||
length = struct.unpack(">I", _read_except(fp, 4))[0]
|
||||
else:
|
||||
raise Exception("logic error, not ext: 0x%02x" % ord(code))
|
||||
raise Exception("logic error, not ext: 0x{:02x}".format(ord(code)))
|
||||
|
||||
ext_type = struct.unpack("b", _read_except(fp, 1))[0]
|
||||
ext_data = _read_except(fp, length)
|
||||
|
||||
# Create extension object
|
||||
ext = Ext(ext_type, ext_data)
|
||||
|
||||
# Unpack with ext handler, if we have one
|
||||
ext_handlers = options.get("ext_handlers")
|
||||
if ext_handlers and ext.type in ext_handlers:
|
||||
return ext_handlers[ext.type](ext)
|
||||
if ext_handlers and ext_type in ext_handlers:
|
||||
return ext_handlers[ext_type](Ext(ext_type, ext_data))
|
||||
|
||||
# Unpack with ext classes, if type is registered
|
||||
if ext_type in _ext_type_to_class:
|
||||
try:
|
||||
return _ext_type_to_class[ext_type].unpackb(ext_data)
|
||||
except AttributeError:
|
||||
raise NotImplementedError("Ext serializable class {:s} is missing implementation of unpackb()".format(repr(_ext_type_to_class[ext_type])))
|
||||
|
||||
# Timestamp extension
|
||||
if ext.type == -1:
|
||||
return _unpack_ext_timestamp(ext, options)
|
||||
if ext_type == -1:
|
||||
return _unpack_ext_timestamp(ext_data, options)
|
||||
|
||||
return ext
|
||||
return Ext(ext_type, ext_data)
|
||||
|
||||
|
||||
def _unpack_ext_timestamp(ext, options):
|
||||
if len(ext.data) == 4:
|
||||
def _unpack_ext_timestamp(ext_data, options):
|
||||
obj_len = len(ext_data)
|
||||
if obj_len == 4:
|
||||
# 32-bit timestamp
|
||||
seconds = struct.unpack(">I", ext.data)[0]
|
||||
seconds = struct.unpack(">I", ext_data)[0]
|
||||
microseconds = 0
|
||||
elif len(ext.data) == 8:
|
||||
elif obj_len == 8:
|
||||
# 64-bit timestamp
|
||||
value = struct.unpack(">Q", ext.data)[0]
|
||||
value = struct.unpack(">Q", ext_data)[0]
|
||||
seconds = value & 0x3ffffffff
|
||||
microseconds = (value >> 34) // 1000
|
||||
elif len(ext.data) == 12:
|
||||
elif obj_len == 12:
|
||||
# 96-bit timestamp
|
||||
seconds = struct.unpack(">q", ext.data[4:12])[0]
|
||||
microseconds = struct.unpack(">I", ext.data[0:4])[0] // 1000
|
||||
seconds = struct.unpack(">q", ext_data[4:12])[0]
|
||||
microseconds = struct.unpack(">I", ext_data[0:4])[0] // 1000
|
||||
else:
|
||||
raise UnsupportedTimestampException(
|
||||
"unsupported timestamp with data length %d" % len(ext.data))
|
||||
"unsupported timestamp with data length {:d}".format(len(ext_data)))
|
||||
|
||||
return _epoch + datetime.timedelta(seconds=seconds,
|
||||
microseconds=microseconds)
|
||||
@@ -780,7 +882,10 @@ def _unpack_array(code, fp, options):
|
||||
elif code == b'\xdd':
|
||||
length = struct.unpack(">I", _read_except(fp, 4))[0]
|
||||
else:
|
||||
raise Exception("logic error, not array: 0x%02x" % ord(code))
|
||||
raise Exception("logic error, not array: 0x{:02x}".format(ord(code)))
|
||||
|
||||
if options.get('use_tuple'):
|
||||
return tuple((_unpack(fp, options) for i in xrange(length)))
|
||||
|
||||
return [_unpack(fp, options) for i in xrange(length)]
|
||||
|
||||
@@ -799,10 +904,9 @@ def _unpack_map(code, fp, options):
|
||||
elif code == b'\xdf':
|
||||
length = struct.unpack(">I", _read_except(fp, 4))[0]
|
||||
else:
|
||||
raise Exception("logic error, not map: 0x%02x" % ord(code))
|
||||
raise Exception("logic error, not map: 0x{:02x}".format(ord(code)))
|
||||
|
||||
d = {} if not options.get('use_ordered_dict') \
|
||||
else collections.OrderedDict()
|
||||
d = {} if not options.get('use_ordered_dict') else collections.OrderedDict()
|
||||
for _ in xrange(length):
|
||||
# Unpack key
|
||||
k = _unpack(fp, options)
|
||||
@@ -810,12 +914,12 @@ def _unpack_map(code, fp, options):
|
||||
if isinstance(k, list):
|
||||
# Attempt to convert list into a hashable tuple
|
||||
k = _deep_list_to_tuple(k)
|
||||
elif not isinstance(k, collections.Hashable):
|
||||
elif not isinstance(k, Hashable):
|
||||
raise UnhashableKeyException(
|
||||
"encountered unhashable key: %s, %s" % (str(k), str(type(k))))
|
||||
"encountered unhashable key: \"{:s}\" ({:s})".format(str(k), str(type(k))))
|
||||
elif k in d:
|
||||
raise DuplicateKeyException(
|
||||
"encountered duplicate key: %s, %s" % (str(k), str(type(k))))
|
||||
"encountered duplicate key: \"{:s}\" ({:s})".format(str(k), str(type(k))))
|
||||
|
||||
# Unpack value
|
||||
v = _unpack(fp, options)
|
||||
@@ -824,7 +928,7 @@ def _unpack_map(code, fp, options):
|
||||
d[k] = v
|
||||
except TypeError:
|
||||
raise UnhashableKeyException(
|
||||
"encountered unhashable key: %s" % str(k))
|
||||
"encountered unhashable key: \"{:s}\"".format(str(k)))
|
||||
return d
|
||||
|
||||
|
||||
@@ -848,6 +952,8 @@ def _unpack2(fp, **options):
|
||||
Ext into an object
|
||||
use_ordered_dict (bool): unpack maps into OrderedDict, instead of
|
||||
unordered dict (default False)
|
||||
use_tuple (bool): unpacks arrays into tuples, instead of lists (default
|
||||
False)
|
||||
allow_invalid_utf8 (bool): unpack invalid strings into instances of
|
||||
InvalidString, for access to the bytes
|
||||
(default False)
|
||||
@@ -892,6 +998,8 @@ def _unpack3(fp, **options):
|
||||
Ext into an object
|
||||
use_ordered_dict (bool): unpack maps into OrderedDict, instead of
|
||||
unordered dict (default False)
|
||||
use_tuple (bool): unpacks arrays into tuples, instead of lists (default
|
||||
False)
|
||||
allow_invalid_utf8 (bool): unpack invalid strings into instances of
|
||||
InvalidString, for access to the bytes
|
||||
(default False)
|
||||
@@ -937,6 +1045,8 @@ def _unpackb2(s, **options):
|
||||
Ext into an object
|
||||
use_ordered_dict (bool): unpack maps into OrderedDict, instead of
|
||||
unordered dict (default False)
|
||||
use_tuple (bool): unpacks arrays into tuples, instead of lists (default
|
||||
False)
|
||||
allow_invalid_utf8 (bool): unpack invalid strings into instances of
|
||||
InvalidString, for access to the bytes
|
||||
(default False)
|
||||
@@ -985,6 +1095,8 @@ def _unpackb3(s, **options):
|
||||
Ext into an object
|
||||
use_ordered_dict (bool): unpack maps into OrderedDict, instead of
|
||||
unordered dict (default False)
|
||||
use_tuple (bool): unpacks arrays into tuples, instead of lists (default
|
||||
False)
|
||||
allow_invalid_utf8 (bool): unpack invalid strings into instances of
|
||||
InvalidString, for access to the bytes
|
||||
(default False)
|
||||
@@ -1045,9 +1157,21 @@ def __init():
|
||||
if sys.version_info[0] == 3:
|
||||
_utc_tzinfo = datetime.timezone.utc
|
||||
else:
|
||||
_utc_tzinfo = None
|
||||
class UTC(datetime.tzinfo):
|
||||
ZERO = datetime.timedelta(0)
|
||||
|
||||
# Calculate epoch datetime
|
||||
def utcoffset(self, dt):
|
||||
return UTC.ZERO
|
||||
|
||||
def tzname(self, dt):
|
||||
return "UTC"
|
||||
|
||||
def dst(self, dt):
|
||||
return UTC.ZERO
|
||||
|
||||
_utc_tzinfo = UTC()
|
||||
|
||||
# Calculate an aware epoch datetime
|
||||
_epoch = datetime.datetime(1970, 1, 1, tzinfo=_utc_tzinfo)
|
||||
|
||||
# Auto-detect system float precision
|
||||
|
||||
@@ -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: 55e1a358a797ca9390d1b4f0e18f7ba3
|
||||
config: 19fea9318285890fd65096600dbd8f71
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 562 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 63 KiB |
@@ -6,17 +6,37 @@ The best way to get started with the Reticulum Network Stack depends on what
|
||||
you want to do. This guide will outline sensible starting paths for different
|
||||
scenarios.
|
||||
|
||||
|
||||
Try Using a Reticulum-based Program
|
||||
=============================================
|
||||
If you simply want to try using a program built with Reticulum, you can take
|
||||
a look at `Nomad Network <https://github.com/markqvist/nomadnet>`_, which
|
||||
provides a complete encrypted communications suite built with Reticulum.
|
||||
|
||||
If you simply want to try using a program built with Reticulum, a few different
|
||||
programs exist that allow basic communication and a range of other useful functions
|
||||
over even extremely low-bandwidth Reticulum networks.
|
||||
|
||||
These programs will let you get a feel for how Reticulum works. They have been designed
|
||||
to run well over networks based on LoRa or packet radio, but can also be used completely
|
||||
over local WiFi, wired Ethernet, the Internet, or any combination.
|
||||
|
||||
As such, it is easy to get started experimenting, without having to set up any radio
|
||||
transceivers or infrastructure just to try it out. Launching the programs on separate
|
||||
devices connected to the same WiFi network is enough to get started, and physical
|
||||
radio interfaces can then be added later.
|
||||
|
||||
Nomad Network
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
The terminal-based program `Nomad Network <https://github.com/markqvist/nomadnet>`_
|
||||
provides a complete encrypted communications suite built with Reticulum. It features
|
||||
encrypted messaging (both direct and delayed-delivery for offline users), file sharing,
|
||||
and has a built-in text-browser and page server with support for dynamically rendered pages,
|
||||
user authentication and more.
|
||||
|
||||
.. image:: screenshots/nomadnet_3.png
|
||||
:target: _images/nomadnet_3.png
|
||||
|
||||
`Nomad Network <https://github.com/markqvist/nomadnet>`_ is a user-facing client
|
||||
in the development for the messaging and information-sharing protocol
|
||||
for the messaging and information-sharing protocol
|
||||
`LXMF <https://github.com/markqvist/lxmf>`_, another project built with Reticulum.
|
||||
|
||||
You can install Nomad Network via pip:
|
||||
@@ -29,7 +49,24 @@ 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.
|
||||
|
||||
Sideband
|
||||
^^^^^^^^
|
||||
|
||||
If you would rather use a program with a graphical user interface, you can take
|
||||
a look at `Sideband <https://unsigned.io/sideband>`_, which is available for Android,
|
||||
Linux and macOS.
|
||||
|
||||
.. image:: screenshots/sideband_1.png
|
||||
:align: center
|
||||
:target: _images/sideband_1.png
|
||||
|
||||
Sideband is currently in the early stages of development, but already provides basic
|
||||
communication features, and interoperates with Nomad Network, or any other LXMF client.
|
||||
|
||||
Using the Included Utilities
|
||||
=============================================
|
||||
@@ -44,25 +81,139 @@ network status and connectivity.
|
||||
To learn more about these utility programs, have a look at the
|
||||
:ref:`Using Reticulum on Your System<using-main>` chapter of this manual.
|
||||
|
||||
|
||||
Creating a Network With Reticulum
|
||||
=============================================
|
||||
To create a network, you will need to specify one or more *interfaces* for
|
||||
Reticulum to use. This is done in the Reticulum configuration file, which by
|
||||
default is located at ``~/.reticulum/config``.
|
||||
default is located at ``~/.reticulum/config``. You can edit this file by hand,
|
||||
or use the interactive ``rnsconfig`` utility.
|
||||
|
||||
When Reticulum is started for the first time, it will create a default
|
||||
configuration file, with one active interface. This default interface uses
|
||||
your existing ethernet network (if there is one), and only allows you to
|
||||
communicate with other Reticulum peers within your local broadcast domain.
|
||||
your existing Ethernet and WiFi networks (if any), and only allows you to
|
||||
communicate with other Reticulum peers within your local broadcast domains.
|
||||
|
||||
To communicate further, you will have to add one or more interfaces. The default
|
||||
configuration includes a number of examples, ranging from using TCP over the
|
||||
internet, to LoRa and Packet Radio interfaces.
|
||||
|
||||
With Reticulum, you only need to configure what interfaces you want to communicate
|
||||
over. There is no need to configure address spaces, subnets, routing tables,
|
||||
or other things you might be used to from other network types.
|
||||
|
||||
Once Reticulum knows which interfaces it should use, it will automatically
|
||||
discover topography and configure transport of data to any destinations it
|
||||
knows about.
|
||||
|
||||
In situations where you already have an established WiFi or Ethernet network, and
|
||||
many devices that want to utilise the same external Reticulum network paths (for example over
|
||||
LoRa), it will often be sufficient to let one system act as a Reticulum gateway, by
|
||||
adding any external interfaces to the configuration of this system, and then enabling transport on it. Any
|
||||
other device on your local WiFi will then be able to connect to this wider Reticulum
|
||||
network just using the default (:ref:`AutoInterface<interfaces-auto>`) configuration.
|
||||
|
||||
Possibly, the examples in the config file are enough to get you started. If
|
||||
you want more information, you can read the :ref:`Building Networks<networks-main>`
|
||||
and :ref:`Interfaces<interfaces-main>` chapters of this manual.
|
||||
|
||||
Connecting Reticulum Instances Over the Internet
|
||||
================================================
|
||||
Reticulum currently offers two interfaces suitable for connecting instances over the Internet: :ref:`TCP<interfaces-tcps>`
|
||||
and :ref:`I2P<interfaces-i2p>`. Each interface offers a different set of features, and Reticulum
|
||||
users should carefully choose the interface which best suites their needs.
|
||||
|
||||
The ``TCPServerInterface`` allows users to host an instance accessible over TCP/IP. This
|
||||
method is generally faster, lower latency, and more energy efficient than using ``I2PInterface``,
|
||||
however it also leaks more data about the server host.
|
||||
|
||||
TCP connections reveal the IP address of both your instance and the server to anyone who can
|
||||
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
|
||||
inspecting your packets may be able to record packet metadata like time of transmission and packet size.
|
||||
Even though Reticulum encrypts traffic, TCP does not, so an adversary may be able to use
|
||||
packet inspection to learn that a system is running Reticulum, and what other IP 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
|
||||
(I2P) <https://geti2p.net/en/>`_. To use this interface, users must also run an I2P daemon in
|
||||
parallel to ``rnsd``. For always-on I2P nodes it is recommended to use `i2pd <https://i2pd.website/>`_.
|
||||
|
||||
By default, I2P will encrypt and mix all traffic sent over the Internet, and
|
||||
hide both the sender and receiver Reticulum instance IP addresses. Running an I2P node
|
||||
will also relay other I2P user's encrypted packets, which will use extra
|
||||
bandwidth and compute power, but also makes timing attacks and other forms of
|
||||
deep-packet-inspection much more difficult.
|
||||
|
||||
I2P also allows users to host globally available Reticulum instances from non-public IP's and behind firewalls and NAT.
|
||||
|
||||
In general it is recommended to use an I2P node if you want to host a publicly accessible
|
||||
instance, while preserving anonymity. If you care more about performance, and a slightly
|
||||
easier setup, use TCP.
|
||||
|
||||
|
||||
Connect to the Public Testnet
|
||||
===========================================
|
||||
|
||||
An experimental public testnet has been made accessible over both I2P and TCP. You can join it
|
||||
by adding one of the following interfaces to your ``.reticulum/config`` file:
|
||||
|
||||
.. code::
|
||||
|
||||
# TCP/IP interface to the Dublin hub
|
||||
[[RNS Testnet Dublin]]
|
||||
type = TCPClientInterface
|
||||
enabled = yes
|
||||
target_host = dublin.connect.reticulum.network
|
||||
target_port = 4965
|
||||
|
||||
# TCP/IP interface to the Frankfurt hub
|
||||
[[RNS Testnet Dublin]]
|
||||
type = TCPClientInterface
|
||||
enabled = yes
|
||||
target_host = frankfurt.connect.reticulum.network
|
||||
target_port = 5377
|
||||
|
||||
# Interface to I2P hub A
|
||||
[[RNS Testnet I2P Hub A]]
|
||||
type = I2PInterface
|
||||
enabled = yes
|
||||
peers = uxg5kubabakh3jtnvsipingbr5574dle7bubvip7llfvwx2tgrua.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.
|
||||
|
||||
|
||||
Adding Radio Interfaces
|
||||
==============================================
|
||||
Once you have Reticulum installed and working, you can add radio interfaces with
|
||||
any compatible hardware you have available. Reticulum supports a wide range of radio
|
||||
hardware, and if you already have any available, it is very likely that it will
|
||||
work with Reticulum. For information on how to configure this, see the
|
||||
:ref:`Interfaces<interfaces-main>` section of this manual.
|
||||
|
||||
If you do not already have transceiver hardware available, you can easily and
|
||||
cheaply build an :ref:`RNode<rnode-main>`, which is a general-purpose long-range
|
||||
digital radio transceiver, that integrates easily with Reticulum.
|
||||
|
||||
To build one yourself requires installing a custom firmware on a supported LoRa
|
||||
development board with an auto-install script. Please see the :ref:`Communications Hardware<hardware-main>`
|
||||
chapter for a guide. If you prefer purchasing a ready-made unit, you can refer to the
|
||||
:ref:`list of suppliers<rnode-suppliers>`. For more information on RNode, you can also
|
||||
refer to these additional external resources:
|
||||
|
||||
* `How To Make Your Own RNodes <https://unsigned.io/how-to-make-your-own-rnodes/>`_
|
||||
* `Installing RNode Firmware on Compatible LoRa Devices <https://unsigned.io/installing-rnode-firmware-on-t-beam-and-lora32-devices/>`_
|
||||
* `Private, Secure and Uncensorable Messaging Over a LoRa Mesh <https://unsigned.io/private-messaging-over-lora/>`_
|
||||
* `RNode Firmware <https://github.com/markqvist/RNode_Firmware/>`_
|
||||
|
||||
If you have communications hardware that is not already supported by any of the
|
||||
:ref:`existing interface types<interfaces-main>`, but you think would be suitable for use with Reticulum,
|
||||
you are welcome to head over to the `GitHub discussion pages <https://github.com/markqvist/Reticulum/discussions>`_
|
||||
and propose adding an interface for the hardware.
|
||||
|
||||
|
||||
Develop a Program with Reticulum
|
||||
===========================================
|
||||
@@ -77,6 +228,13 @@ The above command will install Reticulum and dependencies, and you will be
|
||||
ready to import and use RNS in your own programs. The next step will most
|
||||
likely be to look at some :ref:`Example Programs<examples-main>`.
|
||||
|
||||
For extended functionality, you can install optional dependencies:
|
||||
|
||||
.. code::
|
||||
|
||||
pip3 install pyserial netifaces
|
||||
|
||||
|
||||
Further information can be found in the :ref:`API Reference<api-main>`.
|
||||
|
||||
|
||||
@@ -120,4 +278,96 @@ don't use pip, but try this recipe:
|
||||
python3 Examples/Filetransfer.py -h
|
||||
|
||||
When you have experimented with the basic examples, it's time to go read the
|
||||
:ref:`Understanding Reticulum<understanding-main>` chapter.
|
||||
:ref:`Understanding Reticulum<understanding-main>` chapter. Before submitting
|
||||
your first pull request, it is probably a good idea to introduce yourself on
|
||||
the `disucssion forum on GitHub <https://github.com/markqvist/Reticulum/discussions>`_,
|
||||
or ask one of the developers or maintainers for a good place to start.
|
||||
|
||||
|
||||
Reticulum on ARM64
|
||||
==============================================
|
||||
On some architectures, including ARM64, not all dependencies have precompiled
|
||||
binaries. On such systems, you will need to install ``python3-dev`` before
|
||||
installing Reticulum or programs that depend on Reticulum.
|
||||
|
||||
.. code::
|
||||
|
||||
# Install Python and development packages
|
||||
sudo apt update
|
||||
sudo apt install python3 python3-pip python3-dev
|
||||
|
||||
# Install Reticulum
|
||||
python3 -m pip install rns
|
||||
|
||||
|
||||
Reticulum on Android
|
||||
==============================================
|
||||
Reticulum can be used on Android in different ways. The easiest way to get
|
||||
started is using an app like `Sideband <https://unsigned.io/sideband>`_.
|
||||
|
||||
For more control and features, you can use Reticulum and related programs via
|
||||
the `Termux app <https://termux.com/>`_, at the time of writing available on
|
||||
`F-droid <https://f-droid.org>`_.
|
||||
|
||||
Termux is a terminal emulator and Linux environment for Android based devices,
|
||||
which includes the ability to use many different programs and libraries,
|
||||
including Reticulum.
|
||||
|
||||
Since the Python cryptography.io module does not offer pre-built wheels for
|
||||
Android, the standard one-line install of Reticulum does not work on Android,
|
||||
and a few extra commands are required.
|
||||
|
||||
From within Termux, execute the following:
|
||||
|
||||
.. code::
|
||||
|
||||
# First, make sure indexes and packages are up to date.
|
||||
pkg update
|
||||
pkg upgrade
|
||||
|
||||
# Then install dependencies for the cryptography library.
|
||||
pkg install python build-essential openssl libffi rust
|
||||
|
||||
# Make sure pip is up to date, and install the wheel module.
|
||||
pip3 install wheel pip --upgrade
|
||||
|
||||
# To allow the installer to build the cryptography module,
|
||||
# we need to let it know what platform we are compiling for:
|
||||
export CARGO_BUILD_TARGET="aarch64-linux-android"
|
||||
|
||||
# Start the install process for the cryptography module.
|
||||
# Depending on your device, this can take several minutes,
|
||||
# since the module must be compiled locally on your device.
|
||||
pip3 install cryptography
|
||||
|
||||
# If the above installation succeeds, you can now install
|
||||
# Reticulum and any related software
|
||||
pip3 install rns
|
||||
|
||||
It is also possible to include Reticulum in apps compiled and distributed as
|
||||
Android APKs. A detailed tutorial and example source code will be included
|
||||
here at a later point. Until then you can use the `Sideband source code <https://github.com/markqvist/sideband>`_ as an example and startig point.
|
||||
|
||||
Pure-Python Reticulum
|
||||
==============================================
|
||||
In some rare cases, and on more obscure system types, it is not possible to
|
||||
install one or more dependencies
|
||||
|
||||
On more unusual systems, and in some rare cases, it might not be possible to
|
||||
install or even compile one or more of the above modules. In such situations,
|
||||
you can use the ``rnspure`` package instead of the ``rns`` package. The ``rnspure``
|
||||
package requires no external dependencies for installation. Please note that the
|
||||
actual contents of the ``rns`` and ``rnspure`` packages are *completely identical*.
|
||||
The only difference is that the ``rnspure`` package lists no dependencies required
|
||||
for installation.
|
||||
|
||||
No matter how Reticulum is installed and started, it will load external dependencies
|
||||
only if they are *needed* and *available*. If for example you want to use Reticulum
|
||||
on a system that cannot support ``pyserial``, it is perfectly possible to do so using
|
||||
the `rnspure` package, but Reticulum will not be able to use serial-based interfaces.
|
||||
All other available modules will still be loaded when needed.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
.. _hardware-main:
|
||||
|
||||
***********************
|
||||
Communications Hardware
|
||||
***********************
|
||||
|
||||
One of the truly valuable aspects of Reticulum is the ability to use it over
|
||||
almost any conceivable kind of communications medium. The :ref:`interface types<interfaces-main>`
|
||||
available for configuration in Reticulum are flexible enough to cover the use
|
||||
of most wired and wireless communications hardware available, from decades-old
|
||||
packet radio modems to modern millimeter-wave backhaul systems.
|
||||
|
||||
If you already have or operate some kind of communications hardware, there is a
|
||||
very good chance that it will work with Reticulum out of the box. In case it does
|
||||
not, it is possible to provide the necessary glue with very little effort using
|
||||
for example the :ref:`PipeInterface<interfaces-pipe>` or the :ref:`TCPClientInterface<interfaces-tcpc>`
|
||||
in combination with code like `TCP KISS Server <https://github.com/simplyequipped/tcpkissserver>`_
|
||||
by `simplyequipped <https://github.com/simplyequipped>`_.
|
||||
|
||||
While this broad support and flexibility is very useful, an abundance of options
|
||||
can sometimes make it difficult to know where to begin, especially when you are
|
||||
starting from scratch.
|
||||
|
||||
This chapter will outline a few different sensible starting paths to get
|
||||
real-world functional wireless communications up and running with minimal cost
|
||||
and effort. Two fundamental devices categories will be covered, *RNodes* and
|
||||
*WiFi-based radios*.
|
||||
|
||||
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:
|
||||
|
||||
RNode
|
||||
=====
|
||||
|
||||
Reliable and general-purpose long-range digital radio transceiver systems are
|
||||
commonly either very expensive, difficult to set up and operate, hard to source,
|
||||
power-hungry, or all of the above at the same time. In an attempt to alleviate
|
||||
this situation, the transceiver system *RNode* was designed. It is important to
|
||||
note that RNode is not one specific device, from one particular vendor, but
|
||||
*an open plaform* that anyone can use to build interoperable digital transceivers
|
||||
suited to their needs and particular situations.
|
||||
|
||||
An RNode is a general purpose, interoperable, low-power and long-range, reliable,
|
||||
open and flexible radio communications device. Depending on its components, it can
|
||||
operate on many different frequency bands, and use many different modulation
|
||||
schemes, but most commonly, and for the purposes of this chapter, we will limit
|
||||
the discussion to RNodes using *LoRa* modulation in common ISM bands.
|
||||
|
||||
**Avoid Confusion!** RNodes can use LoRa as a *physical-layer modulation*, but it
|
||||
does not use, and has nothing to do with the *LoRaWAN* protocol and standard, commonly
|
||||
used for centrally controlled IoT devices. RNodes use *raw LoRa modulation*, without
|
||||
any additional protocol overhead. All high-level protocol functionality is handled
|
||||
directly by Reticulum.
|
||||
|
||||
.. _rnode-creating:
|
||||
|
||||
Creating RNodes
|
||||
^^^^^^^^^^^^^^^
|
||||
RNode has been designed as a system that is easy to replicate across time and
|
||||
space. You can put together a functioning transceiver using commonly available
|
||||
components, and a few open source software tools. While you can design and build RNodes
|
||||
completely from scratch, to your exact desired specifications, this chapter
|
||||
will explain the easiest possible approach to creating RNodes: Using common
|
||||
LoRa development boards. This approach can be boiled down to two simple steps:
|
||||
|
||||
1. Obtain one or more supported development boards
|
||||
2. Install the RNode firmware with the automated installer
|
||||
|
||||
Once the firmware has been installed and provisioned by the install script, it
|
||||
is ready to use with any software that supports RNodes, including Reticulum.
|
||||
The device can be used with Reticulum by adding an :ref:`RNodeInterface<interfaces-rnode>`
|
||||
to the configuration.
|
||||
|
||||
.. _rnode-supported:
|
||||
|
||||
Supported Boards
|
||||
^^^^^^^^^^^^^^^^
|
||||
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%
|
||||
: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
|
||||
"""""""""""""
|
||||
.. image:: graphics/board_tbeam.png
|
||||
:width: 75%
|
||||
:align: center
|
||||
|
||||
- **Supported Firmware Lines** v1.x & v2.x
|
||||
- **Transceiver IC** Semtech SX1276
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
|
||||
Heltec LoRa32 v2.0
|
||||
""""""""""""""""""
|
||||
.. image:: graphics/board_heltec32.png
|
||||
:width: 58%
|
||||
:align: center
|
||||
|
||||
- **Supported Firmware Lines** v1.x & v2.x
|
||||
- **Transceiver IC** Semtech SX1276
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
|
||||
|
||||
|
||||
Unsigned RNode v2.x
|
||||
"""""""""""""""""""
|
||||
.. image:: graphics/board_rnodev2.png
|
||||
:width: 58%
|
||||
:align: center
|
||||
|
||||
- **Supported Firmware Lines** v1.x & v2.x
|
||||
- **Transceiver IC** Semtech SX1276
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `unsigned.io <https://unsigned.io>`_
|
||||
|
||||
|
||||
Unsigned RNode v1.x
|
||||
"""""""""""""""""""
|
||||
.. image:: graphics/board_rnode.png
|
||||
:width: 50%
|
||||
:align: center
|
||||
|
||||
- **Supported Firmware Lines** v1.x
|
||||
- **Transceiver IC** Semtech SX1276
|
||||
- **Device Platform** AVR ATmega1284p
|
||||
- **Manufacturer** `unsigned.io <https://unsigned.io>`_
|
||||
|
||||
|
||||
.. _rnode-installation:
|
||||
|
||||
Installation
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Once you have obtained compatible boards, you can install the `RNode Firmware <https://github.com/markqvist/RNode_Firmware>`_
|
||||
using the `RNode Configuration Utility <https://github.com/markqvist/rnodeconfigutil>`_.
|
||||
Make sure that ``Python3`` and ``pip`` is installed on your system, and then install
|
||||
the config utility with ``pip``:
|
||||
|
||||
.. code::
|
||||
|
||||
pip3 install rnodeconf
|
||||
|
||||
Once installation has completed, it is time to start installing the firmware on your
|
||||
devices. Run ``rnodeconf`` in auto-install mode like so:
|
||||
|
||||
.. code::
|
||||
|
||||
rnodeconf --autoinstall
|
||||
|
||||
The utility will guide you through the installation process by asking a series of
|
||||
questions about your hardware. Simply follow the guide, and the utility will
|
||||
auto-install and configure your devices
|
||||
|
||||
**Important Note!** It is currently recommended to use the v1.x line of the RNode firmware,
|
||||
even though the v2.x line is available for early testing. The v2.x line should still be
|
||||
considered an experimental pre-release. Only use the v2.x firmware line if you want to test
|
||||
out the absolutely newest version, and don't care about stability.
|
||||
|
||||
.. _rnode-usage:
|
||||
|
||||
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.
|
||||
|
||||
|
||||
WiFi-based Hardware
|
||||
===================
|
||||
|
||||
It is possible to use all kinds of both short- and long-range WiFi-based hardware
|
||||
with Reticulum. Any kind of hardware that fully supports bridged Ethernet over the
|
||||
WiFi interface will work with the :ref:`AutoInterface<interfaces-auto>` in Reticulum.
|
||||
Most devices will behave like this by default, or allow it via configuration options.
|
||||
|
||||
This means that you can simply configure the physical links of the WiFi based devices,
|
||||
and start communicating over them using Reticulum. It is not necessary to enable any IP
|
||||
infrastructure such as DHCP servers, DNS or similar, as long as at least Ethernet is
|
||||
available, and packets are passed transparently over the physical WiFi-based devices.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: graphics/radio_rblhg5.png
|
||||
:width: 49%
|
||||
|
||||
.. image:: graphics/radio_is5ac.png
|
||||
:width: 49%
|
||||
|
||||
Below is a list of example WiFi (and similar) radios that work well for high capacity
|
||||
Reticulum links over long distances:
|
||||
|
||||
- `Ubiquiti airMAX radios <https://store.ui.com/collections/operator-airmax-devices>`_
|
||||
- `Ubiquiti LTU radios <https://store.ui.com/collections/operator-ltu>`_
|
||||
- `MikroTik radios <https://mikrotik.com/products/group/wireless-systems>`_
|
||||
|
||||
This list is by no means exhaustive, and only serves as a few examples of radio hardware
|
||||
that is relatively cheap while providing long range and high capacity for Reticulum
|
||||
networks. As in all other cases, it is also possible for Reticulum to co-exist with IP
|
||||
networks running concurrently on such devices.
|
||||
|
||||
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.
|
||||
@@ -5,21 +5,34 @@ This manual aims to provide you with all the information you need to
|
||||
understand Reticulum, build networks or develop programs using it, or
|
||||
to participate in the development of Reticulum itself.
|
||||
|
||||
.. only:: html
|
||||
|
||||
Table Of Contents
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
whatis
|
||||
gettingstartedfast
|
||||
using
|
||||
networks
|
||||
interfaces
|
||||
understanding
|
||||
reference
|
||||
hardware
|
||||
interfaces
|
||||
networks
|
||||
examples
|
||||
support
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
reference
|
||||
|
||||
|
||||
Indices and Tables
|
||||
==================
|
||||
.. only:: html
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
Indices and Tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
|
||||
@@ -18,6 +18,246 @@ For a high-level overview of how networks can be formed over different interface
|
||||
types, have a look at the :ref:`Building Networks<networks-main>` chapter of this
|
||||
manual.
|
||||
|
||||
|
||||
.. _interfaces-auto:
|
||||
|
||||
Auto Interface
|
||||
==============
|
||||
|
||||
The Auto Interface enables communication with other discoverable Reticulum
|
||||
nodes over autoconfigured IPv6 and UDP. It does not need any functional IP
|
||||
infrastructure like routers or DHCP servers, but will require at least some
|
||||
sort of switching medium between peers (a wired switch, a hub, a WiFi access
|
||||
point or similar), and that link-local IPv6 is enabled in your operating
|
||||
system, which should be enabled by default in almost all OSes.
|
||||
|
||||
.. code::
|
||||
|
||||
# This example demonstrates a TCP server interface.
|
||||
# It will listen for incoming connections on the
|
||||
# specified IP address and port number.
|
||||
|
||||
[[Default Interface]]
|
||||
type = AutoInterface
|
||||
interface_enabled = True
|
||||
|
||||
# You can create multiple isolated Reticulum
|
||||
# networks on the same physical LAN by
|
||||
# specifying different Group IDs.
|
||||
|
||||
group_id = reticulum
|
||||
|
||||
# You can also select specifically which
|
||||
# kernel networking devices to use.
|
||||
|
||||
devices = wlan0,eth1
|
||||
|
||||
# Or let AutoInterface use all suitable
|
||||
# devices except for a list of ignored ones.
|
||||
|
||||
ignored_devices = tun0,eth0
|
||||
|
||||
|
||||
If you are connected to the Internet with IPv6, and your provider will route
|
||||
IPv6 multicast, you can potentially configure the Auto Interface to globally
|
||||
autodiscover other Reticulum nodes within your selected Group ID. You can specify
|
||||
the discovery scope by setting it to one of ``link``, ``admin``, ``site``,
|
||||
``organisation`` or ``global``.
|
||||
|
||||
.. code::
|
||||
|
||||
[[Default Interface]]
|
||||
type = AutoInterface
|
||||
interface_enabled = True
|
||||
|
||||
# Configure global discovery
|
||||
|
||||
group_id = custom_network_name
|
||||
discovery_scope = global
|
||||
|
||||
# Other configuration options
|
||||
|
||||
discovery_port = 48555
|
||||
data_port = 49555
|
||||
|
||||
|
||||
.. _interfaces-i2p:
|
||||
|
||||
I2P Interface
|
||||
=============
|
||||
|
||||
The I2P interface lets you connect Reticulum instances over the
|
||||
`Invisible Internet Protocol <https://i2pd.website>`_. This can be
|
||||
especially useful in cases where you want to host a globally reachable
|
||||
Reticulum instance, but do not have access to any public IP addresses,
|
||||
have a frequently changing IP address, or have firewalls blocking
|
||||
inbound traffic.
|
||||
|
||||
Using the I2P interface, you will get a globally reachable, portable
|
||||
and persistent I2P address that your Reticulum instance can be reached
|
||||
at.
|
||||
|
||||
To use the I2P interface, you must have an I2P router running
|
||||
on your system. The easiest way to achieve this is to download and
|
||||
install the `latest release <https://github.com/PurpleI2P/i2pd/releases/latest>`_
|
||||
of the ``i2pd`` package. For more details about I2P, see the
|
||||
`geti2p.net website <https://geti2p.net/en/about/intro>`_.
|
||||
|
||||
When an I2P router is running on your system, you can simply add
|
||||
an I2P interface to Reticulum:
|
||||
|
||||
.. code::
|
||||
|
||||
[[I2P]]
|
||||
type = I2PInterface
|
||||
interface_enabled = yes
|
||||
connectable = yes
|
||||
|
||||
On the first start, Reticulum will generate a new I2P address for the
|
||||
interface and start listening for inbound traffic on it. This can take
|
||||
a while the first time, especially if your I2P router was also just
|
||||
started, and is not yet well-connected to the I2P network. When ready,
|
||||
you should see I2P base32 address printed to your log file. You can
|
||||
also inspect the status of the interface using the ``rnstatus`` utility.
|
||||
|
||||
To connect to other Reticulum instances over I2P, just add a comma-separated
|
||||
list of I2P base32 addresses to the ``peers`` option of the interface:
|
||||
|
||||
.. code::
|
||||
|
||||
[[I2P]]
|
||||
type = I2PInterface
|
||||
interface_enabled = yes
|
||||
connectable = yes
|
||||
peers = 5urvjicpzi7q3ybztsef4i5ow2aq4soktfj7zedz53s47r54jnqq.b32.i2p
|
||||
|
||||
It can take anywhere from a few seconds to a few minutes to establish
|
||||
I2P connections to the desired peers, so Reticulum handles the process
|
||||
in the background, and will output relevant events to the log.
|
||||
|
||||
**Please Note!** While the I2P interface is the simplest way to use
|
||||
Reticulum over I2P, it is also possible to tunnel the TCP server and
|
||||
client interfaces over I2P manually. This can be useful in situations
|
||||
where more control is needed, but requires manual tunnel setup through
|
||||
the I2P daemon configuration.
|
||||
|
||||
It is important to note that the two methods are *interchangably compatible*.
|
||||
You can use the I2PInterface to connect to a TCPServerInterface that
|
||||
was manually tunneled over I2P, for example. This offers a high degree
|
||||
of flexibility in network setup, while retaining ease of use in simpler
|
||||
use-cases.
|
||||
|
||||
|
||||
.. _interfaces-tcps:
|
||||
|
||||
TCP Server Interface
|
||||
====================
|
||||
|
||||
The TCP Server interface is suitable for allowing other peers to connect over
|
||||
the Internet or private IP networks. When a TCP server interface has been
|
||||
configured, other Reticulum peers can connect to it with a TCP Client interface.
|
||||
|
||||
.. code::
|
||||
|
||||
# This example demonstrates a TCP server interface.
|
||||
# It will listen for incoming connections on the
|
||||
# specified IP address and port number.
|
||||
|
||||
[[TCP Server Interface]]
|
||||
type = TCPServerInterface
|
||||
interface_enabled = True
|
||||
|
||||
# This configuration will listen on all IP
|
||||
# interfaces on port 4242
|
||||
|
||||
listen_ip = 0.0.0.0
|
||||
listen_port = 4242
|
||||
|
||||
# Alternatively you can bind to a specific IP
|
||||
|
||||
# listen_ip = 10.0.0.88
|
||||
# listen_port = 4242
|
||||
|
||||
# Or a specific network device
|
||||
|
||||
# device = eth0
|
||||
# port = 4242
|
||||
|
||||
**Please Note!** The TCP interfaces support tunneling over I2P, but to do so reliably,
|
||||
you must use the i2p_tunneled option:
|
||||
|
||||
.. code::
|
||||
|
||||
[[TCP Server on I2P]]
|
||||
type = TCPServerInterface
|
||||
interface_enabled = yes
|
||||
listen_ip = 127.0.0.1
|
||||
listen_port = 5001
|
||||
i2p_tunneled = yes
|
||||
|
||||
In almost all cases, it is easier to use the dedicated ``I2PInterface``, but for complete
|
||||
control, and using I2P routers running on external systems, this option also exists.
|
||||
|
||||
.. _interfaces-tcpc:
|
||||
|
||||
TCP Client Interface
|
||||
====================
|
||||
|
||||
To connect to a TCP server interface, you would naturally use the TCP client
|
||||
interface. Many TCP Client interfaces from different peers can connect to the
|
||||
same TCP Server interface at the same time.
|
||||
|
||||
The TCP interface types can also tolerate intermittency in the IP link layer.
|
||||
This means that Reticulum will gracefully handle IP links that go up and down,
|
||||
and restore connectivity after a failure, once the other end of a TCP interface reappears.
|
||||
|
||||
.. code::
|
||||
|
||||
# Here's an example of a TCP Client interface. The
|
||||
# target_host can either be an IP address or a hostname.
|
||||
|
||||
[[TCP Client Interface]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = True
|
||||
target_host = 127.0.0.1
|
||||
target_port = 4242
|
||||
|
||||
It is also possible to use this interface type to connect via other programs
|
||||
or hardware devices that expose a KISS interface on a TCP port, for example
|
||||
software-based soundmodems. To do this, use the ``kiss_framing`` option:
|
||||
|
||||
.. code::
|
||||
|
||||
# Here's an example of a TCP Client interface that connects
|
||||
# to a software TNC soundmodem on a KISS over TCP port.
|
||||
|
||||
[[TCP KISS Interface]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = True
|
||||
kiss_framing = True
|
||||
target_host = 127.0.0.1
|
||||
target_port = 8001
|
||||
|
||||
**Caution!** Only use the KISS framing option when connecting to external devices
|
||||
and programs like soundmodems and similar over TCP. When using the
|
||||
``TCPClientInterface`` in conjunction with the ``TCPServerInterface`` you should
|
||||
never enable ``kiss_framing``, since this will disable internal reliability and
|
||||
recovery mechanisms that greatly improves performance over unreliable and
|
||||
intermittent TCP links.
|
||||
|
||||
**Please Note!** The TCP interfaces support tunneling over I2P, but to do so reliably,
|
||||
you must use the i2p_tunneled option:
|
||||
|
||||
.. code::
|
||||
|
||||
[[TCP Client over I2P]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = yes
|
||||
target_host = 127.0.0.1
|
||||
target_port = 5001
|
||||
i2p_tunneled = yes
|
||||
|
||||
|
||||
.. _interfaces-udp:
|
||||
|
||||
UDP Interface
|
||||
@@ -28,19 +268,21 @@ 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.
|
||||
|
||||
The below example is enabled by default on new Reticulum installations,
|
||||
as it provides an easy way to get started and to test Reticulum on a
|
||||
pre-existing LAN.
|
||||
*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.
|
||||
|
||||
.. code::
|
||||
|
||||
# This example enables communication with other
|
||||
# local Reticulum peers over UDP.
|
||||
|
||||
[[Default UDP Interface]]
|
||||
[[UDP Interface]]
|
||||
type = UDPInterface
|
||||
interface_enabled = True
|
||||
outgoing = True
|
||||
|
||||
listen_ip = 0.0.0.0
|
||||
listen_port = 4242
|
||||
forward_ip = 255.255.255.255
|
||||
@@ -48,9 +290,7 @@ pre-existing LAN.
|
||||
|
||||
# The above configuration will allow communication
|
||||
# within the local broadcast domains of all local
|
||||
# IP interfaces. This is enabled by default as an
|
||||
# easy way to get started, but you might want to
|
||||
# consider altering it to something more specific.
|
||||
# IP interfaces.
|
||||
|
||||
# Instead of specifying listen_ip, listen_port,
|
||||
# forward_ip and forward_port, you can also bind
|
||||
@@ -78,64 +318,6 @@ pre-existing LAN.
|
||||
# forward_ip = 10.55.0.16
|
||||
# forward_port = 4242
|
||||
|
||||
.. _interfaces-tcps:
|
||||
|
||||
TCP Server Interface
|
||||
====================
|
||||
|
||||
The TCP Server interface is suitable for allowing other peers to connect over
|
||||
the Internet or private IP networks. When a TCP server interface has been
|
||||
configured, other Reticulum peers can connect to it with a TCP Client interface.
|
||||
|
||||
.. code::
|
||||
|
||||
# This example demonstrates a TCP server interface.
|
||||
# It will listen for incoming connections on the
|
||||
# specified IP address and port number.
|
||||
|
||||
[[TCP Server Interface]]
|
||||
type = TCPServerInterface
|
||||
interface_enabled = True
|
||||
outgoing = True
|
||||
|
||||
# This configuration will listen on all IP
|
||||
# interfaces on port 4242
|
||||
|
||||
listen_ip = 0.0.0.0
|
||||
listen_port = 4242
|
||||
|
||||
# Alternatively you can bind to a specific IP
|
||||
|
||||
# listen_ip = 10.0.0.88
|
||||
# listen_port = 4242
|
||||
|
||||
# Or a specific network device
|
||||
|
||||
# device = eth0
|
||||
# port = 4242
|
||||
|
||||
|
||||
.. _interfaces-tcpc:
|
||||
|
||||
TCP Client Interface
|
||||
====================
|
||||
|
||||
To connect to a TCP server interface, you would naturally use the TCP client
|
||||
interface. Many TCP Client interfaces from different peers can connect to the
|
||||
same TCP Server interface at the same time.
|
||||
|
||||
.. code::
|
||||
|
||||
# Here's an example of a TCP Client interface. The
|
||||
# target_host can either be an IP address or a hostname.
|
||||
|
||||
[[TCP Client Interface]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = True
|
||||
outgoing = True
|
||||
target_host = 127.0.0.1
|
||||
target_port = 4242
|
||||
|
||||
|
||||
.. _interfaces-rnode:
|
||||
|
||||
@@ -156,11 +338,6 @@ can be used, and offers full control over LoRa parameters.
|
||||
# Enable interface if you want use it!
|
||||
interface_enabled = True
|
||||
|
||||
# Allow transmit on interface. Setting
|
||||
# this to false will create a listen-
|
||||
# only interface.
|
||||
outgoing = true
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB0
|
||||
|
||||
@@ -211,7 +388,6 @@ directly over a wire-pair, or for using devices such as data radios and lasers.
|
||||
[[Serial Interface]]
|
||||
type = SerialInterface
|
||||
interface_enabled = True
|
||||
outgoing = True
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB0
|
||||
@@ -223,6 +399,31 @@ directly over a wire-pair, or for using devices such as data radios and lasers.
|
||||
parity = none
|
||||
stopbits = 1
|
||||
|
||||
.. _interfaces-pipe:
|
||||
|
||||
Pipe Interface
|
||||
==============
|
||||
|
||||
Using this interface, Reticulum can use any program as an interface via `stdin` and
|
||||
`stdout`. This can be used to easily create virtual interfaces, or to interface with
|
||||
custom hardware or other systems.
|
||||
|
||||
.. code::
|
||||
|
||||
[[Pipe Interface]]
|
||||
type = PipeInterface
|
||||
interface_enabled = True
|
||||
|
||||
# External command to execute
|
||||
command = netcat -l 5757
|
||||
|
||||
# Optional respawn delay, in seconds
|
||||
respawn_delay = 5
|
||||
|
||||
Reticulum will write all packets to `stdin` of the ``command`` option, and will
|
||||
continuously read and scan its `stdout` for Reticulum packets. If ``EOF`` is reached,
|
||||
Reticulum will try to respawn the program after waiting for ``respawn_interval`` seconds.
|
||||
|
||||
.. _interfaces-kiss:
|
||||
|
||||
KISS Interface
|
||||
@@ -238,7 +439,6 @@ for station identification purposes.
|
||||
[[Packet Radio KISS Interface]]
|
||||
type = KISSInterface
|
||||
interface_enabled = True
|
||||
outgoing = true
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB1
|
||||
@@ -309,9 +509,6 @@ beaconing functionality described above.
|
||||
# Enable interface if you want use it!
|
||||
interface_enabled = True
|
||||
|
||||
# Allow transmit on interface.
|
||||
outgoing = True
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB2
|
||||
|
||||
@@ -343,4 +540,209 @@ beaconing functionality described above.
|
||||
# Whether to use KISS flow-control.
|
||||
# This is useful for modems with a
|
||||
# small internal packet buffer.
|
||||
flow_control = false
|
||||
flow_control = false
|
||||
|
||||
.. _interfaces-options:
|
||||
|
||||
Common Interface Options
|
||||
========================
|
||||
|
||||
A number of general configuration options are available on most interfaces.
|
||||
These can be used to control various aspects of interface behaviour.
|
||||
|
||||
|
||||
* | The ``enabled`` option tells Reticulum whether or not
|
||||
to bring up the interface. Defaults to ``False``. For any
|
||||
interface to be brought up, the ``enabled`` option
|
||||
must be set to ``True`` or ``Yes``.
|
||||
|
||||
* | The ``mode`` option allows selecting the high-level behaviour
|
||||
of the interface from a number of options.
|
||||
|
||||
- The default value is ``full``. In this mode, all discovery,
|
||||
meshing and transport functionality is available.
|
||||
|
||||
- In the ``access_point`` (or shorthand ``ap``) mode, the
|
||||
interface will operate as a network access point. In this
|
||||
mode, announces will not be automatically broadcasted on
|
||||
the interface, and paths to destinations on the interface
|
||||
will have a much shorter expiry time. This mode is useful
|
||||
for creating interfaces that are mostly quiet, unless when
|
||||
someone is actually using them. An example of this could
|
||||
be a radio interface serving a wide area, where users are
|
||||
expected to connect momentarily, use the network, and then
|
||||
disappear again.
|
||||
|
||||
* | The ``outgoing`` option sets whether an interface is allowed
|
||||
to transmit. Defaults to ``True``. If set to ``False`` or ``No``
|
||||
the interface will only receive data, and never transmit.
|
||||
|
||||
* | The ``network_name`` option sets the virtual network name for
|
||||
the interface. This allows multiple separate network segments
|
||||
to exist on the same physical channel or medium.
|
||||
|
||||
* | The ``passphrase`` option sets an authentication passphrase on
|
||||
the interface. This option can be used in conjunction with the
|
||||
``network_name`` option, or be used alone.
|
||||
|
||||
* | The ``ifac_size`` option allows customising the length of the
|
||||
Interface Authentication Codes carried by each packet on named
|
||||
and/or authenticated network segments. It is set by default to
|
||||
a size suitable for the interface in question, but can be set
|
||||
to a custom size between 8 and 512 bits by using this option.
|
||||
In normal usage, this option should not be changed from the
|
||||
default.
|
||||
|
||||
* | The ``announce_cap`` option lets you configure the maximum
|
||||
bandwidth to allocate, at any given time, to propagating
|
||||
announces and other network upkeep traffic. It is configured at
|
||||
2% by default, and should normally not need to be changed. Can
|
||||
be set to any value between ``1`` and ``100``.
|
||||
|
||||
*If an interface exceeds its announce cap, it will queue announces
|
||||
for later transmission. Reticulum will always prioritise propagating
|
||||
announces from nearby nodes first. This ensures that the local
|
||||
topology is prioritised, and that slow networks are not overwhelmed
|
||||
by interconnected fast networks.*
|
||||
|
||||
*Destinations that are rapidly re-announcing will be down-prioritised
|
||||
further. Trying to get "first-in-line" by announce spamming will have
|
||||
the exact opposite effect: Getting moved to the back of the queue every
|
||||
time a new announce from the excessively announcing destination is received.*
|
||||
|
||||
*This means that it is always beneficial to select a balanced
|
||||
announce rate, and not announce more often than is actually necesarry
|
||||
for your application to function.*
|
||||
|
||||
* | The ``bitrate`` option configures the interface bitrate.
|
||||
Reticulum will use interface speeds reported by hardware, or
|
||||
try to guess a suitable rate when the hardware doesn't report
|
||||
any. In most cases, the automatically found rate should be
|
||||
sufficient, but it can be configured by using the ``bitrate``
|
||||
option, to set the interface speed in *bits per second*.
|
||||
|
||||
|
||||
.. _interfaces-modes:
|
||||
|
||||
Interface Modes
|
||||
===============
|
||||
|
||||
The optional ``mode`` setting is available on all interfaces, and allows
|
||||
selecting the high-level behaviour of the interface from a number of modes.
|
||||
These modes affect how Reticulum selects paths in the network, how announces
|
||||
are propagated, how long paths are valid and how paths are discovered.
|
||||
|
||||
Configuring modes on interfaces is **not** strictly necessary, but can be useful
|
||||
when building or connecting to more complex networks. If your Reticulum
|
||||
instance is not running a Transport Node, it is rarely useful to configure
|
||||
interface modes, and in such cases interfaces should generally be left in
|
||||
the default mode.
|
||||
|
||||
* | The default mode is ``full``. In this mode, all discovery,
|
||||
meshing and transport functionality is activated.
|
||||
|
||||
* | The ``gateway`` mode (or shorthand ``gw``) also has all
|
||||
discovery, meshing and transport functionality available,
|
||||
but will additionally try to discover unknown paths on
|
||||
behalf of other nodes residing on the ``gateway`` interface.
|
||||
If Reticulum receives a path request for an unknown
|
||||
destination, from a node on a ``gateway`` interface, it
|
||||
will try to discover this path via all other active interfaces,
|
||||
and forward the discovered path to the requestor if one is
|
||||
found.
|
||||
|
||||
| If you want to allow other nodes to widely resolve paths or connect
|
||||
to a network via an interface, it might be useful to put it in this
|
||||
mode. By creating a chain of ``gateway`` interfaces, other
|
||||
nodes will be able to immediately discover paths to any
|
||||
destination along the chain.
|
||||
|
||||
| *Please note!* It is the interface *facing the clients* that
|
||||
must be put into ``gateway`` mode for this to work, not
|
||||
the interface facing the wider network (for this, the ``boundary``
|
||||
mode can be useful, though).
|
||||
|
||||
* | In the ``access_point`` (or shorthand ``ap``) mode, the
|
||||
interface will operate as a network access point. In this
|
||||
mode, announces will not be automatically broadcasted on
|
||||
the interface, and paths to destinations on the interface
|
||||
will have a much shorter expiry time. In addition, path
|
||||
requests from clients on the access point interface will
|
||||
be handled in the same way as the ``gateway`` interface.
|
||||
|
||||
| This mode is useful for creating interfaces that remain
|
||||
quiet, until someone actually starts using them. An example
|
||||
of this could be a radio interface serving a wide area,
|
||||
where users are expected to connect momentarily, use the
|
||||
network, and then disappear again.
|
||||
|
||||
* | The ``roaming`` mode should be used on interfaces that are
|
||||
roaming (physically mobile), seen from the perspective of
|
||||
other nodes in the network. As an example, if a vehicle is
|
||||
equipped with an external LoRa interface, and an internal,
|
||||
WiFi-based interface, that serves devices that are moving
|
||||
*with* the vehicle, the external LoRa interface should be
|
||||
configured as ``roaming``, and the internal interface can
|
||||
be left in the default mode. With transport enabled, such
|
||||
a setup will allow all internal devices to reach each other,
|
||||
and all other devices that are available on the LoRa side
|
||||
of the network, when they are in range. Devices on the LoRa
|
||||
side of the network will also be able to reach devices
|
||||
internal to the vehicle, when it is in range. Paths via
|
||||
``roaming`` interfaces also expire faster.
|
||||
|
||||
* | The purpose of the ``boundary`` mode is to specify interfaces
|
||||
that establish connectivity with network segments that are
|
||||
significantly different than the one this node exists on.
|
||||
As an example, if a Reticulum instance is part of a LoRa-based
|
||||
network, but also has a high-speed connection to a
|
||||
public Transport Node available on the Internet, the interface
|
||||
connecting over the Internet should be set to ``boundary`` mode.
|
||||
|
||||
For a table describing the impact of all modes on announce propagation,
|
||||
please see the :ref:`Announce Propagation Rules<understanding-announcepropagation>` section.
|
||||
|
||||
.. _interfaces-announcerates:
|
||||
|
||||
Announce Rate Control
|
||||
=====================
|
||||
|
||||
The built-in announce control mechanisms and the default ``announce_cap``
|
||||
option described above are sufficient most of the time, but in some cases, especially on fast
|
||||
interfaces, it may be useful to control the target announce rate. Using the
|
||||
``announce_rate_target``, ``announce_rate_grace`` and ``announce_rate_penalty``
|
||||
options, this can be done on a per-interface basis, and moderates the *rate at
|
||||
which received announces are re-broadcasted to other interfaces*.
|
||||
|
||||
* | The ``announce_rate_target`` option sets the minimum amount of time,
|
||||
in seconds, that should pass between received announces, for any one
|
||||
destination. As an example, setting this value to ``3600`` means that
|
||||
announces *received* on this interface will only be re-transmitted and
|
||||
propagated to other interfaces once every hour, no matter how often they
|
||||
are received.
|
||||
|
||||
* | The optional ``announce_rate_grace`` defines the number of times a destination
|
||||
can violate the announce rate before the target rate is enforced.
|
||||
|
||||
* | The optional ``announce_rate_penalty`` configures an extra amount of
|
||||
time that is added to the normal rate target. As an example, if a penalty
|
||||
of ``7200`` seconds is defined, once the rate target is enforced, the
|
||||
destination in question will only have its announces propagated every
|
||||
3 hours, until it lowers its actual announce rate to within the target.
|
||||
|
||||
These mechanisms, in conjunction with the ``annouce_cap`` mechanisms mentioned
|
||||
above means that it is essential to select a balanced announce strategy for
|
||||
your destinations. The more balanced you can make this decision, the easier
|
||||
it will be for your destinations to make it into slower networks that many hops
|
||||
away. Or you can prioritise only reaching high-capacity networks with more frequent
|
||||
announces.
|
||||
|
||||
Current statistics and information about announce rates can be viewed using the
|
||||
``rnpath -r`` command.
|
||||
|
||||
It is important to note that there is no one right or wrong way to set up announce
|
||||
rates. Slower networks will naturally tend towards using less frequent announces to
|
||||
conserve bandwidth, while very fast networks can support applications that
|
||||
need very frequent announces. Reticulum implements these mechanisms to ensure
|
||||
that a large span of network types can seamlessly *co-exist* and interconnect.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Reticulum, which can often be easier than using traditional stacks, since you
|
||||
don't have to worry about coordinating addresses, subnets and routing for an
|
||||
entire network that you might not know how will evolve in the future. With
|
||||
Reticulum, you can simply add more segments to your network when it becomes
|
||||
necesarry, and Reticulum will handle the convergence of the entire network
|
||||
necessary, and Reticulum will handle the convergence of the entire network
|
||||
automatically.
|
||||
|
||||
Concepts & Overview
|
||||
@@ -18,15 +18,20 @@ Concepts & Overview
|
||||
There are important points that need to be kept in mind when building networks
|
||||
with Reticulum:
|
||||
|
||||
* | In a Reticulum network, any node can autonomously generate as many adresses
|
||||
* | In a Reticulum network, any node can autonomously generate as many addresses
|
||||
(called *destinations* in Reticulum terminology) as it needs, which become
|
||||
globally reachable to the rest of the network. There is no central point of
|
||||
control over the adress space.
|
||||
control over the address space.
|
||||
|
||||
* | Reticulum was designed to handle both very small, and very large networks.
|
||||
While the adress space can support billions of endpoints, Reticulum is
|
||||
While the address space can support billions of endpoints, Reticulum is
|
||||
also very useful when just a few devices needs to communicate.
|
||||
|
||||
* | Low-bandwidth networks, like LoRa and packet radio, can interoperate and
|
||||
interconnect with much larger and higher bandwidth networks without issue.
|
||||
Reticulum automatically manages the flow of information to and from various
|
||||
network segments, and when bandwidth is limited, local traffic is prioritised.
|
||||
|
||||
* | Reticulum provides sender/initiator anonymity by default. There is no way
|
||||
to filter traffic or discriminate it based on the source of the traffic.
|
||||
|
||||
@@ -47,18 +52,32 @@ with Reticulum:
|
||||
transport node. Letting every node be a transport node will in most cases
|
||||
degrade the performance and reliability of the network.
|
||||
|
||||
In general terms, if a node is stationary, well-connected and kept running
|
||||
*In general terms, if a node is stationary, well-connected and kept running
|
||||
most of the time, it is a good candidate to be a transport node. For optimal
|
||||
performance, a network should contain the amount of transport nodes that
|
||||
provides connectivity to the intended area / topography, and not many more
|
||||
than that.
|
||||
than that.*
|
||||
|
||||
* | Reticulum is designed to work reliably in open, trustless environments. This
|
||||
means you can use it to create open-access networks, where participants can
|
||||
join and leave in an free and unorganised manner. This property allows an
|
||||
entirely new, and so far, mostly unexplored class of networked applications,
|
||||
where networks, and the information flow within them can form and dissolve
|
||||
organically.
|
||||
|
||||
* | You can just as easily create closed networks, since Reticulum allows you to
|
||||
add authentication to any interface. This means you can restrict access on
|
||||
any interface type, even when using legacy devices, such as modems. You can
|
||||
also mix authenticated and open interfaces on the same system. See the
|
||||
:ref:`Common Interface Options<interfaces-options>` section of the :ref:`Interfaces<interfaces-main>`
|
||||
chapter of this manual for information on how to set up interface authentication.
|
||||
|
||||
|
||||
Reticulum allows you to mix very different kinds of networking mediums into a
|
||||
unified mesh, or to keep everything within one medium. You could build a "virtual
|
||||
network" running entirely over the Internet, where all nodes communicate over TCP
|
||||
and UDP "channels". You could also build such a network using MQTT or ZeroMQ as
|
||||
the underlying carrier for Reticulum.
|
||||
and UDP "channels". You could also build such a network using other already-established
|
||||
communications channels as the underlying carrier for Reticulum.
|
||||
|
||||
However, most real-world networks will probably involve either some form of
|
||||
wireless or direct hardline communications. To allow Reticulum to communicate
|
||||
@@ -94,8 +113,8 @@ WiFi based radios for interconnecting the sites.
|
||||
|
||||
At each site, a Raspberry Pi is installed to function as a gateway. A LoRa radio
|
||||
is connected to the Pi with a USB cable, and the WiFi radio is connected to the
|
||||
ethernet port of the Pi. At site B, two WiFi radios are needed to be able to reach
|
||||
both site A and site C, so an extra ethernet adapter is connected to the Pi in
|
||||
Ethernet port of the Pi. At site B, two WiFi radios are needed to be able to reach
|
||||
both site A and site C, so an extra Ethernet adapter is connected to the Pi in
|
||||
this location.
|
||||
|
||||
Once the hardware has been installed, Reticulum is installed on all the Pis, and at
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
:tocdepth: 4
|
||||
|
||||
.. _api-main:
|
||||
|
||||
*************
|
||||
API Reference
|
||||
*************
|
||||
This reference guide lists and explains all classes exposed by the RNS API.
|
||||
|
||||
Classes
|
||||
=========================
|
||||
Communication over a Reticulum network is achieved using a set of classes exposed by RNS.
|
||||
Communication over Reticulum networks is achieved by using a simple set of classes exposed by the RNS API.
|
||||
This chapter lists and explains all classes exposed by the Reticulum Network Stack API, along with their method signatures and usage. It can be used as a reference while writing applications that utilise Reticulum, or it can be read in entirity to gain an understanding of the complete functionality of RNS from a developers perspective.
|
||||
|
||||
.. _api-reticulum:
|
||||
|
||||
Reticulum
|
||||
---------
|
||||
.. only:: html
|
||||
|
||||
|start-h3| Reticulum |end-h3|
|
||||
|
||||
.. only:: latex
|
||||
|
||||
Reticulum
|
||||
---------
|
||||
|
||||
.. autoclass:: RNS.Reticulum
|
||||
:members:
|
||||
@@ -20,64 +25,120 @@ Reticulum
|
||||
|
||||
.. _api-identity:
|
||||
|
||||
Identity
|
||||
--------
|
||||
.. only:: html
|
||||
|
||||
|start-h3| Identity |end-h3|
|
||||
|
||||
.. only:: latex
|
||||
|
||||
Identity
|
||||
--------
|
||||
|
||||
.. autoclass:: RNS.Identity
|
||||
:members:
|
||||
|
||||
.. _api-destination:
|
||||
|
||||
Destination
|
||||
-----------
|
||||
.. only:: html
|
||||
|
||||
|start-h3| Destination |end-h3|
|
||||
|
||||
.. only:: latex
|
||||
|
||||
Destination
|
||||
-----------
|
||||
|
||||
.. autoclass:: RNS.Destination
|
||||
:members:
|
||||
|
||||
.. _api-packet:
|
||||
|
||||
Packet
|
||||
------
|
||||
.. only:: html
|
||||
|
||||
|start-h3| Packet |end-h3|
|
||||
|
||||
.. only:: latex
|
||||
|
||||
Packet
|
||||
------
|
||||
|
||||
.. autoclass:: RNS.Packet(destination, data, create_receipt = True)
|
||||
:members:
|
||||
|
||||
.. _api-packetreceipt:
|
||||
|
||||
Packet Receipt
|
||||
--------------
|
||||
.. only:: html
|
||||
|
||||
|start-h3| Packet Receipt |end-h3|
|
||||
|
||||
.. only:: latex
|
||||
|
||||
Packet Receipt
|
||||
--------------
|
||||
|
||||
.. autoclass:: RNS.PacketReceipt()
|
||||
:members:
|
||||
|
||||
.. _api-link:
|
||||
|
||||
Link
|
||||
----
|
||||
.. only:: html
|
||||
|
||||
|start-h3| Link |end-h3|
|
||||
|
||||
.. only:: latex
|
||||
|
||||
Link
|
||||
----
|
||||
|
||||
.. autoclass:: RNS.Link(destination, established_callback=None, closed_callback = None)
|
||||
:members:
|
||||
|
||||
.. _api-requestreceipt:
|
||||
|
||||
Request Receipt
|
||||
---------------
|
||||
.. only:: html
|
||||
|
||||
|start-h3| Request Receipt |end-h3|
|
||||
|
||||
.. only:: latex
|
||||
|
||||
Request Receipt
|
||||
---------------
|
||||
|
||||
.. autoclass:: RNS.RequestReceipt()
|
||||
:members:
|
||||
|
||||
.. _api-resource:
|
||||
|
||||
Resource
|
||||
--------
|
||||
.. only:: html
|
||||
|
||||
|start-h3| Resource |end-h3|
|
||||
|
||||
.. only:: latex
|
||||
|
||||
Resource
|
||||
--------
|
||||
|
||||
.. autoclass:: RNS.Resource(data, link, advertise=True, auto_compress=True, callback=None, progress_callback=None, timeout=None)
|
||||
:members:
|
||||
|
||||
.. _api-transport:
|
||||
|
||||
Transport
|
||||
---------
|
||||
.. only:: html
|
||||
|
||||
|start-h3| Transport |end-h3|
|
||||
|
||||
.. only:: latex
|
||||
|
||||
Transport
|
||||
---------
|
||||
|
||||
.. autoclass:: RNS.Transport
|
||||
:members:
|
||||
:members:
|
||||
|
||||
.. |start-h3| raw:: html
|
||||
|
||||
<h3>
|
||||
|
||||
.. |end-h3| raw:: html
|
||||
|
||||
</h3>
|
||||
@@ -0,0 +1,42 @@
|
||||
.. _support-main:
|
||||
|
||||
*****************
|
||||
Support Reticulum
|
||||
*****************
|
||||
You can help support the continued development of open, free and private communications
|
||||
systems by donating, providing feedback and contributing code and learning resources.
|
||||
|
||||
Donations
|
||||
=========
|
||||
Donations are gratefully accepted via the following channels:
|
||||
|
||||
|
||||
.. code:: text
|
||||
|
||||
Monero:
|
||||
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
|
||||
|
||||
Ethereum:
|
||||
0x81F7B979fEa6134bA9FD5c701b3501A2e61E897a
|
||||
|
||||
Bitcoin:
|
||||
3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq
|
||||
|
||||
Ko-Fi:
|
||||
https://ko-fi.com/markqvist
|
||||
|
||||
Are certain features in the development roadmap are important to you or your
|
||||
organisation? Make them a reality quickly by sponsoring their implementation.
|
||||
|
||||
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
|
||||
reporting or statistics is collected and reported by Reticulum under any
|
||||
circumstances, so we rely on old-fashioned human feedback.
|
||||
|
||||
Contribute Code
|
||||
===============
|
||||
Join us on `the GitHub repository <https://github.com/markqvist/reticulum>`_ to
|
||||
report issues, suggest functionality and contribute code to Reticulum.
|
||||
@@ -3,20 +3,21 @@
|
||||
***********************
|
||||
Understanding Reticulum
|
||||
***********************
|
||||
This chapter will briefly describe the overall purpose and operating principles of Reticulum, a
|
||||
networking stack designed for reliable and secure communication over high-latency, low-bandwidth
|
||||
links. It should give you an overview of how the stack works, and an understanding of how to
|
||||
This chapter will briefly describe the overall purpose and operating principles of Reticulum.
|
||||
It should give you an overview of how the stack works, and an understanding of how to
|
||||
develop networked applications using Reticulum.
|
||||
|
||||
This document is not an exhaustive source of information on Reticulum, at least not yet. Currently,
|
||||
the best place to go for such information is the Python reference implementation of Reticulum, along
|
||||
with the code examples and API reference. It is however an essential resource to understanding the
|
||||
general principles of Reticulum, how to apply them when creating your own networks or software.
|
||||
This chapter is not an exhaustive source of information on Reticulum, at least not yet. Currently,
|
||||
the only complete repository, and final authority on how Reticulum actually functions, is the Python
|
||||
reference implementation and API reference. That being said, this chapter is an essential resource in
|
||||
understanding how Reticulum works from a high-level perspective, along with the general principles of
|
||||
Reticulum, and how to apply them when creating your own networks or software.
|
||||
|
||||
After reading this document, you should be well-equipped to understand how a Reticulum network
|
||||
operates, what it can achieve, and how you can use it yourself. If you want to help out with the
|
||||
development, this is also the place to start, since it will provide a pretty clear overview of the
|
||||
sentiments and the philosophy behind Reticulum.
|
||||
sentiments and the philosophy behind Reticulum, what problems it seeks to solve, and how it
|
||||
approaches those solutions.
|
||||
|
||||
.. _understanding-motivation:
|
||||
|
||||
@@ -25,34 +26,41 @@ Motivation
|
||||
|
||||
The primary motivation for designing and implementing Reticulum has been the current lack of
|
||||
reliable, functional and secure minimal-infrastructure modes of digital communication. It is my
|
||||
belief that it is highly desirable to create a cheap and reliable way to set up a wide-range digital
|
||||
communication network that can securely allow exchange of information between people and
|
||||
belief that it is highly desirable to create a reliable and efficient way to set up long-range digital
|
||||
communication networks that can securely allow exchange of information between people and
|
||||
machines, with no central point of authority, control, censorship or barrier to entry.
|
||||
|
||||
Almost all of the various networking systems in use today share a common limitation, namely that they
|
||||
require large amounts of coordination and trust to work, and to join the networks you need approval
|
||||
Almost all of the various networking systems in use today share a common limitation: They
|
||||
require large amounts of coordination and centralised trust and power to function. To join such networks, you need approval
|
||||
of gatekeepers in control. This need for coordination and trust inevitably leads to an environment of
|
||||
central control, where it's very easy for infrastructure operators or governments to control or alter
|
||||
traffic, and censor or persecute unwanted actors.
|
||||
traffic, and censor or persecute unwanted actors. It also makes it completely impossible to freely deploy
|
||||
and use networks at will, like one would use other common tools that enhance individual agency and freedom.
|
||||
|
||||
Reticulum aims to require as little coordination and trust as possible. In fact, the only
|
||||
“coordination” required is to know the characteristics of physical medium carrying Reticulum traffic.
|
||||
Reticulum aims to require as little coordination and trust as possible. It aims to make secure,
|
||||
anonymous and permissionless networking and information exchange a tool that anyone can just pick up and use.
|
||||
|
||||
Since Reticulum is completely medium agnostic, this could be whatever is best suited to the situation.
|
||||
In some cases, this might be 1200 baud packet radio links over VHF frequencies, in other cases it might
|
||||
be a microwave network using off-the-shelf radios. At the time of release of this document, the
|
||||
recommended setup for development and testing is using LoRa radio modules with an open source firmware
|
||||
(see the section :ref:`Reference System Setup<understanding-referencesystem>`), connected to a small
|
||||
computer like a Raspberry Pi. As an example, the default reference setup provides a channel capacity
|
||||
of 5.4 Kbps, and a usable direct node-to-node range of around 15 kilometers (indefinitely extendable
|
||||
by using multiple hops).
|
||||
Since Reticulum is completely medium agnostic, it can be used to build networks on whatever is best
|
||||
suited to the situation, or whatever you have available. In some cases, this might be packet radio
|
||||
links over VHF frequencies, in other cases it might be a 2.4 GHz
|
||||
network using off-the-shelf radios, or it might be using common LoRa development boards.
|
||||
|
||||
At the time of release of this document, the fastest and easiest setup for development and testing is using
|
||||
LoRa radio modules with an open source firmware (see the section :ref:`Reference Setup<understanding-referencesystem>`),
|
||||
connected to any kind of computer or mobile device that Reticulum can run on.
|
||||
|
||||
The ultimate aim of Reticulum is to allow anyone to be their own network operator, and to make it
|
||||
cheap and easy to cover vast areas with a myriad of independent, interconnectable and autonomous networks.
|
||||
Reticulum **is not** *one network*, it **is a tool** to build *thousands of networks*. Networks without
|
||||
kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate
|
||||
with each other, and require no central oversight. Networks for human beings. *Networks for the people*.
|
||||
|
||||
.. _understanding-goals:
|
||||
|
||||
Goals
|
||||
=====
|
||||
|
||||
To be as widely usable and easy to use as possible, the following goals have been used to
|
||||
To be as widely usable and efficient to deploy as possible, the following goals have been used to
|
||||
guide the design of Reticulum:
|
||||
|
||||
|
||||
@@ -60,31 +68,34 @@ guide the design of Reticulum:
|
||||
Reticulum must be implemented with, and be able to run using only open source software. This is
|
||||
critical to ensuring the availability, security and transparency of the system.
|
||||
* **Hardware layer agnosticism**
|
||||
Reticulum shall be fully hardware agnostic, and shall be useable over a wide range
|
||||
Reticulum must be fully hardware agnostic, and shall be useable over a wide range of
|
||||
physical networking layers, such as data radios, serial lines, modems, handheld transceivers,
|
||||
wired ethernet, wifi, or anything else that can carry a digital data stream. Hardware made for
|
||||
wired Ethernet, WiFi, or anything else that can carry a digital data stream. Hardware made for
|
||||
dedicated Reticulum use shall be as cheap as possible and use off-the-shelf components, so
|
||||
it can be easily replicated.
|
||||
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 *1,000 bps*.
|
||||
as *500 bits per second*.
|
||||
* **Encryption by default**
|
||||
Reticulum must use encryption by default where possible and applicable.
|
||||
Reticulum must use strong encryption by default for all communication.
|
||||
* **Initiator Anonymity**
|
||||
It must be possible to communicate over a Reticulum network without revealing any identifying
|
||||
information about oneself.
|
||||
* **Unlicensed use**
|
||||
Reticulum shall be functional over physical communication mediums that do not require any
|
||||
form of license to use. Reticulum must be designed in a way, so it is usable over ISM radio
|
||||
frequency bands, and can provide functional long distance links in such conditions, for example
|
||||
by connecting a modem to a PMR or CB radio, or by using LoRa or WiFi modules.
|
||||
* **Supplied software**
|
||||
Apart from the core networking stack and API, that allows a developer to build
|
||||
applications with Reticulum, a basic communication suite using Reticulum must be
|
||||
implemented and released at the same time as Reticulum itself. This shall serve both as a
|
||||
functional communication suite, and as an example and learning resource to others wishing
|
||||
In addition to the core networking stack and API, that allows a developer to build
|
||||
applications with Reticulum, a basic set of Reticulum-based communication tools must be
|
||||
implemented and released along with Reticulum itself. These shall serve both as a
|
||||
functional, basic communication suite, and as an example and learning resource to others wishing
|
||||
to build applications with Reticulum.
|
||||
* **Ease of use**
|
||||
The reference implementation of Reticulum is written in Python, to make it easy to use
|
||||
and understand. A programmer with only basic experience should be able to use
|
||||
Reticulum in their own applications.
|
||||
Reticulum to write networked applications.
|
||||
* **Low cost**
|
||||
It shall be as cheap as possible to deploy a communication system based on Reticulum. This
|
||||
should be achieved by using cheap off-the-shelf hardware that potential users might already
|
||||
@@ -98,28 +109,38 @@ Introduction & Basic Functionality
|
||||
|
||||
Reticulum is a networking stack suited for high-latency, low-bandwidth links. Reticulum is at it’s
|
||||
core a *message oriented* system. It is suited for both local point-to-point or point-to-multipoint
|
||||
scenarios where alle nodes are within range of each other, as well as scenarios where packets need
|
||||
to be transported over multiple hops to reach the recipient.
|
||||
scenarios where all nodes are within range of each other, as well as scenarios where packets need
|
||||
to be transported over multiple hops in a complex network to reach the recipient.
|
||||
|
||||
Reticulum does away with the idea of addresses and ports known from IP, TCP and UDP. Instead
|
||||
Reticulum uses the singular concept of *destinations*. Any application using Reticulum as it’s
|
||||
networking stack will need to create one or more destinations to receive data, and know the
|
||||
destinations it needs to send data to.
|
||||
|
||||
All destinations in Reticulum are represented internally as 10 bytes, derived from truncating a full
|
||||
All destinations in Reticulum are _represented_ as a 16 byte hash. This hash is derived from truncating a full
|
||||
SHA-256 hash of identifying characteristics of the destination. To users, the destination addresses
|
||||
will be displayed as 10 bytes in hexadecimal representation, as in the following example: ``<80e29bf7cccaf31431b3>``.
|
||||
will be displayed as 16 hexadecimal bytes, like this example: ``<13425ec15b621c1d928589718000d814>``.
|
||||
|
||||
By default Reticulum encrypts all data using public-key cryptography. Any message sent to a
|
||||
destination is encrypted with that destinations public key. Reticulum can also set up an encrypted
|
||||
channel to a destination with *Perfect Forward Secrecy* and *Initiator Anonymity* using a elliptic
|
||||
curve cryptography and ephemeral keys derived from a Diffie Hellman exchange on Curve25519. In
|
||||
Reticulum terminology, this is called a *Link*.
|
||||
The truncation size of 16 bytes (128 bits) for destinations has been chosen as a reasonable trade-off
|
||||
between address space
|
||||
and packet overhead. The address space accommodated by this size can support many billions of
|
||||
simultaneously active devices on the same network, while keeping packet overhead low, which is
|
||||
essential on low-bandwidth networks. In the very unlikely case that this address space nears
|
||||
congestion, a one-line code change can upgrade the Reticulum address space all the way up to 256
|
||||
bits, ensuring the Reticulum address space could potentially support galactic-scale networks.
|
||||
This is obviously complete and ridiculous over-allocation, and as such, the current 128 bits should
|
||||
be sufficient, even far into the future.
|
||||
|
||||
By default Reticulum encrypts all data using elliptic curve cryptography and AES. Any packet sent to a
|
||||
destination is encrypted with a per-packet derived key. Reticulum can also set up an encrypted
|
||||
channel to a destination, called a *Link*. Both data sent over Links and single packets offer
|
||||
*Initiator Anonymity*, and links additionally offer *Forward Secrecy* by using an Elliptic Curve
|
||||
Diffie Hellman key exchange on Curve25519 to derive per-link ephemeral keys. The multi-hop transport,
|
||||
coordination, verification and reliability layers are fully autonomous and also based on elliptic
|
||||
curve cryptography.
|
||||
|
||||
Reticulum also offers symmetric key encryption for group-oriented communications, as well as
|
||||
unencrypted packets for broadcast purposes, or situations where you need the communication to be in
|
||||
plain text. The multi-hop transport, coordination, verification and reliability layers are fully
|
||||
autonomous and based on public key cryptography.
|
||||
unencrypted packets for local broadcast purposes.
|
||||
|
||||
Reticulum can connect to a variety of interfaces such as radio modems, data radios and serial ports,
|
||||
and offers the possibility to easily tunnel Reticulum traffic over IP links such as the Internet or
|
||||
@@ -135,22 +156,30 @@ destinations. Reticulum uses three different basic destination types, and one sp
|
||||
|
||||
|
||||
* **Single**
|
||||
The *single* destination type defines a public-key encrypted destination. Any data sent to this
|
||||
destination will be encrypted with the destination’s public key, and will only be readable by
|
||||
the creator of the destination.
|
||||
* **Group**
|
||||
The *group* destination type defines a symmetrically encrypted destination. Data sent to this
|
||||
destination will be encrypted with a symmetric key, and will be readable by anyone in
|
||||
possession of the key. The *group* destination can be used just as well by only two peers, as it
|
||||
can by many.
|
||||
The *single* destination type is the most common type in Reticulum, and should be used for
|
||||
most purposes. It is always identified by a unique public key. Any data sent to this
|
||||
destination will be encrypted using ephemeral keys derived from an ECDH key exchange, and will
|
||||
only be readable by the creator of the destination, who holds the corresponding private key.
|
||||
* **Plain**
|
||||
A *plain* destination type is unencrypted, and suited for traffic that should be broadcast to a
|
||||
number of users, or should be readable by anyone. Traffic to a *plain* destination is not encrypted.
|
||||
Generally, *plain* destinations can be used for broadcast information intended to be public.
|
||||
Plain destinations are only reachable directly, and packets addressed to plain destinations are
|
||||
never transported over multiple hops in the network. To be transportable over multiple hops in Reticulum, information
|
||||
*must* be encrypted, since Reticulum uses the per-packet encryption to verify routing paths and
|
||||
keep them alive.
|
||||
* **Group**
|
||||
The *group* special destination type, that defines a symmetrically encrypted virtual destination.
|
||||
Data sent to this destination will be encrypted with a symmetric key, and will be readable by
|
||||
anyone in possession of the key, but as with the *plain* destination type, packets to this type
|
||||
of destination are not currently transported over multiple hops, although a planned upgrade
|
||||
to Reticulum will allow globally reachable *group* destinations.
|
||||
* **Link**
|
||||
A *link* is a special destination type, that serves as an abstract channel to a *single*
|
||||
destination, directly connected or over multiple hops. The *link* also offers reliability and
|
||||
more efficient encryption, forward secrecy, initiator anonymity, and as such can be useful even
|
||||
when a node is directly reachable.
|
||||
when a node is directly reachable. It also offers a more capable API and allows easily carrying
|
||||
out requests and responses, large data transfers and more.
|
||||
|
||||
.. _understanding-destinationnaming:
|
||||
|
||||
@@ -158,7 +187,7 @@ Destination Naming
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Destinations are created and named in an easy to understand dotted notation of *aspects*, and
|
||||
represented on the network as a hash of this value. The hash is a SHA-256 truncated to 80 bits. The
|
||||
represented on the network as a hash of this value. The hash is a SHA-256 truncated to 128 bits. The
|
||||
top level aspect should always be a unique identifier for the application using the destination.
|
||||
The next levels of aspects can be defined in any way by the creator of the application.
|
||||
|
||||
@@ -174,7 +203,7 @@ application name, a device type and measurement type, like this:
|
||||
aspects : remotesensor, temperature
|
||||
|
||||
full name : environmentlogger.remotesensor.temperature
|
||||
hash : fa7ddfab5213f916dea
|
||||
hash : 4faf1b2e0a077e6a9d92fa051f256038
|
||||
|
||||
For the *single* destination, Reticulum will automatically append the associated public key as a
|
||||
destination aspect before hashing. This is done to ensure only the correct destination is reached,
|
||||
@@ -191,10 +220,10 @@ packet.
|
||||
|
||||
In actual use of *single* destination naming, it is advisable not to use any uniquely identifying
|
||||
features in aspect naming. Aspect names should be general terms describing what kind of destination
|
||||
is represented. The uniquely identifying aspect is always acheived by the appending the public key,
|
||||
which expands the destination into a uniquely identifyable one.
|
||||
is represented. The uniquely identifying aspect is always achieved by the appending the public key,
|
||||
which expands the destination into a uniquely identifiable one. Reticulum does this automatically.
|
||||
|
||||
Any destination on a Reticulum network can be addressed and reached simply by knowning its
|
||||
Any destination on a Reticulum network can be addressed and reached simply by knowing its
|
||||
destination hash (and public key, but if the public key is not known, it can be requested from the
|
||||
network simply by knowing the destination hash). The use of app names and aspects makes it easy to
|
||||
structure Reticulum programs and makes it possible to filter what information and data your program
|
||||
@@ -208,30 +237,32 @@ To recap, the different destination types should be used in the following situat
|
||||
When private communication between two or more endpoints is needed. Supports multiple hops
|
||||
indirectly, but must first be established through a *single* destination.
|
||||
* **Plain**
|
||||
When plain-text communication is desirable, for example when broadcasting information.
|
||||
When plain-text communication is desirable, for example when broadcasting information, or for local discovery purposes.
|
||||
|
||||
To communicate with a *single* destination, you need to know it’s public key. Any method for
|
||||
obtaining the public key is valid, but Reticulum includes a simple mechanism for making other
|
||||
nodes aware of your destinations public key, called the *announce*. It is also possible to request
|
||||
an unknown public key from the network, as all participating nodes serve as a distributed ledger
|
||||
an unknown public key from the network, as all transport instances serve as a distributed ledger
|
||||
of public keys.
|
||||
|
||||
Note that public key information can be shared and verified in many other ways than using the
|
||||
built-in *announce* functionality, and that it is therefore not required to use the announce/request
|
||||
Note that public key information can be shared and verified in other ways than using the
|
||||
built-in *announce* functionality, and that it is therefore not required to use the *announce* and *path request*
|
||||
functionality to obtain public keys. It is by far the easiest though, and should definitely be used
|
||||
if there is not a good reason for doing it differently.
|
||||
if there is not a very good reason for doing it differently.
|
||||
|
||||
.. _understanding-keyannouncements:
|
||||
|
||||
Public Key Announcements
|
||||
------------------------
|
||||
|
||||
An *announce* will send a special packet over any configured interfaces, containing all needed
|
||||
An *announce* will send a special packet over any relevant interfaces, containing all needed
|
||||
information about the destination hash and public key, and can also contain some additional,
|
||||
application specific data. The entire packet is signed by the sender to ensure authenticity. It is not
|
||||
required to use the announce functionality, but in many cases it will be the simplest way to share
|
||||
public keys on the network. As an example, an announce in a simple messenger application might
|
||||
contain the following information:
|
||||
public keys on the network. The announce mechanism also serves to establish end-to-end connectivity
|
||||
to the announced destination, as the announce propagates through the network.
|
||||
|
||||
As an example, an announce in a simple messenger application might contain the following information:
|
||||
|
||||
|
||||
* The announcers destination hash
|
||||
@@ -244,13 +275,21 @@ With this information, any Reticulum node that receives it will be able to recon
|
||||
destination to securely communicate with that destination. You might have noticed that there is one
|
||||
piece of information lacking to reconstruct full knowledge of the announced destination, and that is
|
||||
the aspect names of the destination. These are intentionally left out to save bandwidth, since they
|
||||
will be implicit in almost all cases. If a destination name is not entirely implicit, information can be
|
||||
included in the application specific data part that will allow the receiver to infer the naming.
|
||||
will be implicit in almost all cases. The receiving application will already know them. If a destination
|
||||
name is not entirely implicit, information can be included in the application specific data part that
|
||||
will allow the receiver to infer the naming.
|
||||
|
||||
It is important to note that announces will be forwarded throughout the network according to a
|
||||
certain pattern. This will be detailed in the section
|
||||
:ref:`The Announce Mechanism in Detail<understanding-announce>`.
|
||||
|
||||
In Reticulum, destinations are allowed to move around the network at will. This is very different from
|
||||
protocols such as IP, where an address is always expected to stay within the network segment it was assigned in.
|
||||
This limitation does not exist in Reticulum, and any destination is *completely portable* over the entire topography
|
||||
of the network, and *can even be moved to other Reticulum networks* than the one it was created in, and
|
||||
still become reachable. To update it's reachability, a destination simply needs to send an announce on any
|
||||
networks it is part of. After a short while, it will be globally reachable in the network.
|
||||
|
||||
Seeing how *single* destinations are always tied to a private/public key pair leads us to the next topic.
|
||||
|
||||
.. _understanding-identities:
|
||||
@@ -259,21 +298,22 @@ Identities
|
||||
----------
|
||||
|
||||
In Reticulum, an *identity* does not necessarily represent a personal identity, but is an abstraction that
|
||||
can represent any kind of *verified entity*. This could very well be a person, but it could also be the
|
||||
can represent any kind of *verifiable entity*. This could very well be a person, but it could also be the
|
||||
control interface of a machine, a program, robot, computer, sensor or something else entirely. In
|
||||
general, any kind of agent that can act, or be acted upon, or store or manipulate information, can be
|
||||
represented as an identity.
|
||||
represented as an identity. An *identity* can be used to create any number of destinations.
|
||||
|
||||
As we have seen, a *single* destination will always have an *identity* tied to it, but not *plain* or *group*
|
||||
A *single* destination will always have an *identity* tied to it, but not *plain* or *group*
|
||||
destinations. Destinations and identities share a multilateral connection. You can create a
|
||||
destination, and if it is not connected to an identity upon creation, it will just create a new one to use
|
||||
automatically. This may be desirable in some situations, but often you will probably want to create
|
||||
the identity first, and then link it to created destinations.
|
||||
the identity first, and then use it to create new destinations.
|
||||
|
||||
Building upon the simple messenger example, we could use an identity to represent the user of the
|
||||
application. Destinations created will then be linked to this identity to allow communication to
|
||||
reach the user. In all cases it is of great importance to store the private keys associated with any
|
||||
Reticulum Identity securely and privately.
|
||||
As an example, we could use an identity to represent the user of a messaging application.
|
||||
Destinations can then be created by this identity to allow communication to reach the user.
|
||||
In all cases it is of great importance to store the private keys associated with any
|
||||
Reticulum Identity securely and privately, since obtaining access to the identity keys equals
|
||||
obtaining access and controlling reachability to any destinations created by that identity.
|
||||
|
||||
.. _understanding-gettingfurther:
|
||||
|
||||
@@ -292,57 +332,73 @@ In the following sections, two concepts that allow this will be introduced, *pat
|
||||
Reticulum Transport
|
||||
===================
|
||||
|
||||
The term routing has been purposefully avoided until now. The current methods of routing used in IP-based
|
||||
networks are fundamentally incompatible with the physical link types that Reticulum was designed to handle.
|
||||
These routing methodologies assume trust at the physical layer, and often needs a lot more bandwidth than
|
||||
Reticulum can assume is available.
|
||||
The methods of routing used in traditional networks are fundamentally incompatible with the physical medium
|
||||
types and circumstances that Reticulum was designed to handle. These mechanisms mostly assume trust at the physical layer,
|
||||
and often needs a lot more bandwidth than Reticulum can assume is available. Since Reticulum is designed to
|
||||
survive running over open radio spectrum, no such trust can be assumed, and bandwidth is often very limited.
|
||||
|
||||
Since Reticulum is designed to run over open radio spectrum, no such trust exists, and bandwidth is often
|
||||
very limited. Existing routing protocols like BGP or OSPF carry too much overhead to be practically
|
||||
useable over bandwidth-limited, high-latency links.
|
||||
|
||||
To overcome such challenges, Reticulum’s *Transport* system uses public-key cryptography to
|
||||
implement the concept of *paths* that allow discovery of how to get information to a certain
|
||||
To overcome such challenges, Reticulum’s *Transport* system uses asymmetric elliptic curve cryptography to
|
||||
implement the concept of *paths* that allow discovery of how to get information closer to a certain
|
||||
destination. It is important to note that no single node in a Reticulum network knows the complete
|
||||
path to a destination. Every Transport node participating in a Reticulum network will only
|
||||
know what the most direct way to get a packet one hop closer to it's destination is.
|
||||
know the most direct way to get a packet one hop closer to it's destination.
|
||||
|
||||
|
||||
.. _understanding-nodetypes:
|
||||
|
||||
Node Types
|
||||
----------
|
||||
|
||||
Currently, Reticulum distinguishes between two types of network nodes. All nodes on a Reticulum network
|
||||
are *Reticulum Instances*, and some are also *Transport Nodes*. If a system running Reticulum is fixed in
|
||||
one place, and is intended to be kept available most of the time, it is a good contender to be a *Transport Node*.
|
||||
|
||||
Any Reticulum Instance can become a Transport Node by enabling it in the configuration.
|
||||
This distinction is made by the user configuring the node, and is used to determine what nodes on the
|
||||
network will help forward traffic, and what nodes rely on other nodes for wider connectivity.
|
||||
|
||||
If a node is an *Instance* it should be given the configuration directive ``enable_transport = No``, which
|
||||
is the default setting.
|
||||
|
||||
If it is a *Transport Node*, it should be given the configuration directive ``enable_transport = Yes``.
|
||||
|
||||
|
||||
.. _understanding-announce:
|
||||
|
||||
The Announce Mechanism in Detail
|
||||
--------------------------------
|
||||
|
||||
When an *announce* is transmitted by a node, it will be forwarded by any node receiving it, but
|
||||
according to some specific rules:
|
||||
When an *announce* for a destination is transmitted by from a Reticulum instance, it will be forwarded by
|
||||
any transport node receiving it, but according to some specific rules:
|
||||
|
||||
|
||||
* | If this exact announce has already been received before, ignore it.
|
||||
|
||||
* | If not, record into a table which node the announce was received from, and how many times in
|
||||
* | If not, record into a table which Transport Node the announce was received from, and how many times in
|
||||
total it has been retransmitted to get here.
|
||||
|
||||
* | If the announce has been retransmitted *m+1* times, it will not be forwarded. By default, *m* is
|
||||
set to 18.
|
||||
* | If the announce has been retransmitted *m+1* times, it will not be forwarded any more. By default, *m* is
|
||||
set to 128.
|
||||
|
||||
* | The announce will be assigned a delay *d* = c\ :sup:`h` seconds, where *c* is a decay constant, and *h* is the amount of times this packet has already been forwarded.
|
||||
* | After a randomised delay, the announce will be retransmitted on all interfaces that have bandwidth
|
||||
available for processing announces. By default, the maximum bandwidth allocation for processing
|
||||
announces is set at 2%, but can be configured on a per-interface basis.
|
||||
|
||||
* | The packet will be given a priority *p = 1/d*.
|
||||
* | If any given interface does not have enough bandwidth available for retransmitting the announce,
|
||||
the announce will be assigned a priority inversely proportional to it's hop count, and be inserted
|
||||
into a queue managed by the interface.
|
||||
|
||||
* | If at least *d* seconds has passed since the announce was received, and no other packets with a
|
||||
priority higher than *p* are waiting in the queue (see Packet Prioritisation), and the channel is
|
||||
not utilized by other traffic, the announce will be forwarded.
|
||||
* | When the interface has bandwidth available for processing an announce, it will prioritise announces
|
||||
for destinations that are closest in terms of hops, thus prioritising reachability and connectivity
|
||||
of local nodes, even on slow networks that connect to wider and faster networks.
|
||||
|
||||
* | If no other nodes are heard retransmitting the announce with a greater hop count than when
|
||||
it left this node, transmitting it will be retried *r* times. By default, *r* is set to 1. Retries
|
||||
follow same rules as above, with the exception that it must wait for at least *d* = c\ :sup:`h+1` +
|
||||
t + rand(0, rw) seconds. This amount of time is equal to the amount of time it would take the next
|
||||
node to retransmit the packet, plus a random window. By default, *t* is set to 10 seconds, and the
|
||||
random window *rw* is set to 10 seconds.
|
||||
* | After the announce has been re-transmitted, and if no other nodes are heard retransmitting the announce
|
||||
with a greater hop count than when it left this node, transmitting it will be retried *r* times. By default,
|
||||
*r* is set to 1.
|
||||
|
||||
* | If a newer announce from the same destination arrives, while an identical one is already in
|
||||
the queue, the newest announce is discarded. If the newest announce contains different
|
||||
application specific data, it will replace the old announce, but will use *d* and *p* of the old
|
||||
announce.
|
||||
* | If a newer announce from the same destination arrives, while an identical one is already waiting
|
||||
to be transmitted, the newest announce is discarded. If the newest announce contains different
|
||||
application specific data, it will replace the old announce.
|
||||
|
||||
Once an announce has reached a node in the network, any other node in direct contact with that
|
||||
node will be able to reach the destination the announce originated from, simply by sending a packet
|
||||
@@ -350,11 +406,16 @@ addressed to that destination. Any node with knowledge of the announce will be a
|
||||
packet towards the destination by looking up the next node with the shortest amount of hops to the
|
||||
destination.
|
||||
|
||||
According to these rules and default constants, an announce will propagate throughout the network
|
||||
in a predictable way. In an example network utilising the default constants, and with an average link
|
||||
distance of *Lavg =* 15 kilometers, an announce will be able to propagate outwards to a radius of 180
|
||||
kilometers in 34 minutes, and a *maximum announce radius* of 270 kilometers in approximately 3
|
||||
days.
|
||||
According to these rules, an announce will propagate throughout the network in a predictable way,
|
||||
and make the announced destination reachable in a short amount of time. Fast networks that have the
|
||||
capacity to process many announces can reach full convergence very quickly, even when constantly adding
|
||||
new destinations. Slower segments of such networks might take a bit longer to gain full knowledge about
|
||||
the wide and fast networks they are connected to, but can still do so over time, while prioritising full
|
||||
and quickly converging end-to-end connectivity for their local, slower segments.
|
||||
|
||||
In general, even extremely complex networks, that utilize the maximum 128 hops will converge to full
|
||||
end-to-end connectivity in about one minute, given there is enough bandwidth available to process
|
||||
the required amount of announces.
|
||||
|
||||
.. _understanding-paths:
|
||||
|
||||
@@ -382,8 +443,7 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
|
||||
* | When the destination receives the packet, it can itself perform an ECDH key exchange and decrypt the
|
||||
packet.
|
||||
|
||||
* | A new ephemeral key is used for every packet sent in this way, and forward secrecy is guaranteed on a
|
||||
per packet level.
|
||||
* | A new ephemeral key is used for every packet sent in this way.
|
||||
|
||||
* | Once the packet has been received and decrypted by the addressed destination, that destination can opt
|
||||
to *prove* its receipt of the packet. It does this by calculating the SHA-256 hash of the received packet,
|
||||
@@ -393,7 +453,7 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
|
||||
|
||||
* | In case the packet is addressed to a *group* destination type, the packet will be encrypted with the
|
||||
pre-shared AES-128 key associated with the destination. In case the packet is addressed to a *plain*
|
||||
destination type, the payload data will not be encrypted. Neither of these two destination types offer
|
||||
destination type, the payload data will not be encrypted. Neither of these two destination types can offer
|
||||
forward secrecy. In general, it is recommended to always use the *single* destination type, unless it is
|
||||
strictly necessary to use one of the others.
|
||||
|
||||
@@ -401,7 +461,7 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
|
||||
For exchanges of larger amounts of data, or when longer sessions of bidirectional communication is desired, Reticulum offers the *Link* API. To establish a *link*, the following process is employed:
|
||||
|
||||
* | First, the node that wishes to establish a link will send out a special packet, that
|
||||
traverses the network and locates the desired destination. Along the way, the nodes that
|
||||
traverses the network and locates the desired destination. Along the way, the Transport Nodes that
|
||||
forward the packet will take note of this *link request*.
|
||||
|
||||
* | Second, if the destination accepts the *link request* , it will send back a packet that proves the
|
||||
@@ -412,27 +472,36 @@ For exchanges of larger amounts of data, or when longer sessions of bidirectiona
|
||||
* | When the validity of the *link* has been accepted by forwarding nodes, these nodes will
|
||||
remember the *link* , and it can subsequently be used by referring to a hash representing it.
|
||||
|
||||
* | As a part of the *link request* , a Diffie-Hellman key exchange takes place, that sets up an
|
||||
efficiently encrypted tunnel between the two nodes, using elliptic curve cryptography. As such,
|
||||
this mode of communication is preferred, even for situations when nodes can directly communicate,
|
||||
when the amount of data to be exchanged numbers in the tens of packets.
|
||||
* | As a part of the *link request*, an Elliptic Curve Diffie-Hellman key exchange takes place, that sets up an
|
||||
efficiently encrypted tunnel between the two nodes. As such, this mode of communication is preferred,
|
||||
even for situations when nodes can directly communicate, when the amount of data to be exchanged numbers
|
||||
in the tens of packets, or whenever the use of the more advanced API functions is desired.
|
||||
|
||||
* | When a *link* has been set up, it automatically provides message receipt functionality, through
|
||||
the same *proof* mechanism discussed before, so the sending node can obtain verified confirmation
|
||||
that the information reached the intended recipient.
|
||||
|
||||
In a moment, we will discuss the details of how this methodology is implemented, but let’s first
|
||||
recap what purposes this methodology serves. We first ensure that the node answering our request
|
||||
is actually the one we want to communicate with, and not a malicious actor pretending to be so.
|
||||
At the same time we establish an efficient encrypted channel. The setup of this is relatively cheap in
|
||||
terms of bandwidth, so it can be used just for a short exchange, and then recreated as needed, which will
|
||||
also rotate encryption keys. The link can also be kept alive for longer periods of time, if this is
|
||||
more suitable to the application. The procedure also inserts the *link id* , a hash calculated from the link request packet, into the memory of forwarding nodes, which means that the communicating nodes can thereafter reach each other simply by referring to this *link id*.
|
||||
* | Once the *link* has been set up, the initiator can remain anonymous, or choose to authenticate towards
|
||||
the destination using a Reticulum Identity. This authentication is happening inside the encrypted
|
||||
link, and is only revealed to the verified destination, and no intermediaries.
|
||||
|
||||
The combined bandwidth cost of setting up a link is 3 packets totalling 237 bytes (more info in the
|
||||
In a moment, we will discuss the details of how this methodology is
|
||||
implemented, but let’s first recap what purposes this methodology serves. We
|
||||
first ensure that the node answering our request is actually the one we want to
|
||||
communicate with, and not a malicious actor pretending to be so. At the same
|
||||
time we establish an efficient encrypted channel. The setup of this is
|
||||
relatively cheap in terms of bandwidth, so it can be used just for a short
|
||||
exchange, and then recreated as needed, which will also rotate encryption keys.
|
||||
The link can also be kept alive for longer periods of time, if this is more
|
||||
suitable to the application. The procedure also inserts the *link id* , a hash
|
||||
calculated from the link request packet, into the memory of forwarding nodes,
|
||||
which means that the communicating nodes can thereafter reach each other simply
|
||||
by referring to this *link id*.
|
||||
|
||||
The combined bandwidth cost of setting up a link is 3 packets totalling 297 bytes (more info in the
|
||||
:ref:`Binary Packet Format<understanding-packetformat>` section). The amount of bandwidth used on keeping
|
||||
a link open is practically negligible, at 0.62 bits per second. Even on a slow 1200 bits per second packet
|
||||
radio channel, 100 concurrent links will still leave 95% channel capacity for actual data.
|
||||
a link open is practically negligible, at 0.45 bits per second. Even on a slow 1200 bits per second packet
|
||||
radio channel, 100 concurrent links will still leave 96% channel capacity for actual data.
|
||||
|
||||
|
||||
Link Establishment in Detail
|
||||
@@ -474,14 +543,16 @@ an arbitrary number of hops, where information will be exchanged between two nod
|
||||
|
||||
* | A *link proof* packet is now constructed and transmitted over the network. This packet is
|
||||
addressed to the *link id* of the *link*. It contains the following data: The newly generated X25519
|
||||
public key *LKr* and an Ed25519 signature of the *link id* and *LKr* made by the signing key of
|
||||
public key *LKr* and an Ed25519 signature of the *link id* and *LKr* made by the *original signing key* of
|
||||
the addressed destination.
|
||||
|
||||
* | By verifying this *link proof* packet, all nodes that originally transported the *link request*
|
||||
packet to the destination from the originator can now verify that the intended destination received
|
||||
the request and accepted it, and that the path they chose for forwarding the request was valid.
|
||||
In sucessfully carrying out this verification, the transporting nodes marks the link as active.
|
||||
In successfully carrying out this verification, the transporting nodes marks the link as active.
|
||||
An abstract bi-directional communication channel has now been established along a path in the network.
|
||||
Packets can now be exchanged bi-directionally from either end of the link simply by adressing the
|
||||
packets to the *link id* of the link.
|
||||
|
||||
* | When the source receives the *proof* , it will know unequivocally that a verified path has been
|
||||
established to the destination. It can now also use the X25519 public key contained in the
|
||||
@@ -507,7 +578,7 @@ the transfer is needed.
|
||||
This is the purpose of the Reticulum :ref:`Resource<api-resource>`. A *Resource* can automatically
|
||||
handle the reliable transfer of an arbitrary amount of data over an established :ref:`Link<api-link>`.
|
||||
Resources can auto-compress data, will handle breaking the data into individual packets, sequencing
|
||||
the transfer and reassembling the data on the other end.
|
||||
the transfer, integrity verification and reassembling the data on the other end.
|
||||
|
||||
:ref:`Resources<api-resource>` are programmatically very simple to use, and only requires a few lines
|
||||
of codes to reliably transfer any amount of data. They can be used to transfer data stored in memory,
|
||||
@@ -515,57 +586,62 @@ or stream data directly from files.
|
||||
|
||||
.. _understanding-referencesystem:
|
||||
|
||||
Reference System Setup
|
||||
Reference Setup
|
||||
======================
|
||||
|
||||
This section will detail the recommended *Reference System Setup* for Reticulum. It is important to
|
||||
note that Reticulum is designed to be usable over more or less any medium that allows you to send
|
||||
and receive data in a digital form, and satisfies some very low minimum requirements. The
|
||||
communication channel must support at least half-duplex operation, and provide an average
|
||||
throughput of around 1000 bits per second, and supports a physical layer MTU of 500 bytes. The
|
||||
Reticulum software should be able to run on more or less any hardware that can provide a Python 3.x
|
||||
This section will detail a recommended *Reference Setup* for Reticulum. It is important to
|
||||
note that Reticulum is designed to be usable on more or less any computing device, and over more
|
||||
or less any medium that allows you to send and receive data, which satisfies some very low
|
||||
minimum requirements.
|
||||
|
||||
The communication channel must support at least half-duplex operation, and provide an average
|
||||
throughput of around 500 bits per second, and supports a physical layer MTU of 500 bytes. The
|
||||
Reticulum stack should be able to run on more or less any hardware that can provide a Python 3.x
|
||||
runtime environment.
|
||||
|
||||
That being said, the reference setup has been outlined to provide a common platform for anyone
|
||||
That being said, this reference setup has been outlined to provide a common platform for anyone
|
||||
who wants to help in the development of Reticulum, and for everyone who wants to know a
|
||||
recommended setup to get started. A reference system consists of three parts:
|
||||
recommended setup to get started experimenting. A reference system consists of three parts:
|
||||
|
||||
* **A channel access device**
|
||||
Or *CAD* , in short, provides access to the physical medium whereupon the communication
|
||||
* **An Interface Device**
|
||||
Which provides access to the physical medium whereupon the communication
|
||||
takes place, for example a radio with an integrated modem. A setup with a separate modem
|
||||
connected to a radio would also be termed a “channel access device”.
|
||||
* **A host device**
|
||||
Some sort of computing device that can run the necessary software, communicates with the
|
||||
channel access device, and provides user interaction.
|
||||
* **A software stack**
|
||||
connected to a radio would also be an interface device.
|
||||
* **A Host Device**
|
||||
Some sort of computing device that can run the necessary software, communicate with the
|
||||
interface device, and provide user interaction.
|
||||
* **A Software Stack**
|
||||
The software implementing the Reticulum protocol and applications using it.
|
||||
|
||||
The reference setup can be considered a relatively stable platform to develop on, and also to start
|
||||
building networks on. While details of the implementation might change at the current stage of
|
||||
building networks or applications on. While details of the implementation might change at the current stage of
|
||||
development, it is the goal to maintain hardware compatibility for as long as entirely possible, and
|
||||
the current reference setup has been determined to provide a functional platform for many years
|
||||
into the future. The current Reference System Setup is as follows:
|
||||
|
||||
|
||||
* **Channel Access Device**
|
||||
* **Interface Device**
|
||||
A data radio consisting of a LoRa radio module, and a microcontroller with open source
|
||||
firmware, that can connect to host devices via USB. It operates in either the 430, 868 or 900
|
||||
MHz frequency bands. More details can be found on the `RNode Page <https://unsigned.io/rnode>`_.
|
||||
* **Host device**
|
||||
* **Host Device**
|
||||
Any computer device running Linux and Python. A Raspberry Pi with a Debian based OS is
|
||||
recommended.
|
||||
* **Software stack**
|
||||
The current Reference Implementation Release of Reticulum, running on a Debian based
|
||||
* **Software Stack**
|
||||
The most recently released Python Implementation of Reticulum, running on a Debian based
|
||||
operating system.
|
||||
|
||||
It is very important to note, that the reference channel access device **does not** use the LoRaWAN
|
||||
standard, but uses a custom MAC layer on top of the plain LoRa modulation! As such, you will
|
||||
need a plain LoRa radio module connected to an MCU with the correct firmware. Full details on how to
|
||||
To avoid confusion, it is very important to note, that the reference interface device **does not**
|
||||
use the LoRaWAN standard, but uses a custom MAC layer on top of the plain LoRa modulation! As such, you will
|
||||
need a plain LoRa radio module connected to an controller with the correct firmware. Full details on how to
|
||||
get or make such a device is available on the `RNode Page <https://unsigned.io/rnode>`_.
|
||||
|
||||
With the current reference setup, it should be possible to get on a Reticulum network for around 100$
|
||||
even if you have none of the hardware already, and need to purchase everything.
|
||||
|
||||
This reference setup is of course just a recommendation for getting started easily, and you should
|
||||
tailor it to your own specific needs, or whatever hardware you have available.
|
||||
|
||||
.. _understanding-protocolspecifics:
|
||||
|
||||
Protocol Specifics
|
||||
@@ -576,19 +652,6 @@ Reticulum, but non critical in understanding how the protocol works on a general
|
||||
treated more as a reference than as essential reading.
|
||||
|
||||
|
||||
Node Types
|
||||
----------
|
||||
|
||||
Currently Reticulum defines two node types, the *Station* and the *Peer*. A node is a *station* if it fixed
|
||||
in one place, and if it is intended to be kept online most of the time. Otherwise the node is a *peer*.
|
||||
This distinction is made by the user configuring the node, and is used to determine what nodes on the
|
||||
network will help forward traffic, and what nodes rely on other nodes for connectivity.
|
||||
|
||||
If a node is a *Peer* it should be given the configuration directive ``enable_transport = No``.
|
||||
|
||||
If it is a *Station*, it should be given the configuration directive ``enable_transport = Yes``.
|
||||
|
||||
|
||||
Packet Prioritisation
|
||||
---------------------
|
||||
|
||||
@@ -596,15 +659,30 @@ Currently, Reticulum is completely priority-agnostic regarding general traffic.
|
||||
on a first-come, first-serve basis. Announce re-transmission are handled according to the re-transmission
|
||||
times and priorities described earlier in this chapter.
|
||||
|
||||
It is possible that a prioritisation engine could be added to Reticulum in the future, but in
|
||||
the light of Reticulums goal of equal access, doing so would need to be the subject of careful
|
||||
investigation of the consequences first.
|
||||
|
||||
Interface Access Codes
|
||||
----------------------
|
||||
|
||||
Reticulum can create named virtual networks, and networks that are only accessible by knowing a preshared
|
||||
passphrase. The configuration of this is detailed in the :ref:`Common Interface Options<interfaces-options>`
|
||||
section. To implement these feature, Reticulum uses the concept of Interface Access Codes, that are calculated
|
||||
and verified per packet.
|
||||
|
||||
An interface with a named virtual network or passphrase authentication enabled will derive a shared Ed25519
|
||||
signing identity, and for every outbound packet generate a signature of the entire packet. This signature is
|
||||
then inserted into the packet as an Interface Access Code before transmission. Depending on the speed and
|
||||
capabilities of the interface, the IFAC can be the full 512-bit Ed25519 signature, or a truncated version.
|
||||
Configured IFAC length can be inspected for all interfaces with the ``rnstatus`` utility.
|
||||
|
||||
Upon receipt, the interface will check that the signature matches the expected value, and drop the packet if it
|
||||
does not. This ensures that only packets sent with the correct naming and/or passphrase parameters are allowed to
|
||||
pass onto the network.
|
||||
|
||||
|
||||
.. _understanding-packetformat:
|
||||
|
||||
Binary Packet Format
|
||||
--------------------
|
||||
Wire Format
|
||||
-----------
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
@@ -612,30 +690,39 @@ Binary Packet Format
|
||||
|
||||
A Reticulum packet is composed of the following fields:
|
||||
|
||||
[HEADER 2 bytes] [ADDRESSES 10/20 bytes] [CONTEXT 1 byte] [DATA 0-477 bytes]
|
||||
[HEADER 2 bytes] [ADDRESSES 16/32 bytes] [CONTEXT 1 byte] [DATA 0-465 bytes]
|
||||
|
||||
* The HEADER field is 2 bytes long.
|
||||
* Byte 1: [Header Type], [Propagation Type], [Destination Type] and [Packet Type]
|
||||
* Byte 1: [IFAC Flag], [Header Type], [Propagation Type], [Destination Type] and [Packet Type]
|
||||
* Byte 2: Number of hops
|
||||
|
||||
* Interface Access Code field if the IFAC flag was set.
|
||||
* The length of the Interface Access Code can vary from
|
||||
1 to 64 bytes according to physical interface
|
||||
capabilities and configuration.
|
||||
|
||||
* The ADDRESSES field contains either 1 or 2 addresses.
|
||||
* Each address is 10 bytes long.
|
||||
* Each address is 16 bytes long.
|
||||
* The Header Type flag in the HEADER field determines
|
||||
whether the ADDRESSES field contains 1 or 2 addresses.
|
||||
* Addresses are Reticulum hashes truncated to 10 bytes.
|
||||
* Addresses are SHA-256 hashes truncated to 16 bytes.
|
||||
|
||||
* The CONTEXT field is 1 byte.
|
||||
* It is used by Reticulum to determine packet context.
|
||||
|
||||
* The DATA field is between 0 and 477 bytes.
|
||||
* The DATA field is between 0 and 465 bytes.
|
||||
* It contains the packets data payload.
|
||||
|
||||
IFAC Flag
|
||||
-----------------
|
||||
open 0 Packet for publically accessible interface
|
||||
authenticated 1 Interface authentication is included in packet
|
||||
|
||||
|
||||
Header Types
|
||||
-----------------
|
||||
type 1 00 Two byte header, one 10 byte address field
|
||||
type 2 01 Two byte header, two 10 byte address fields
|
||||
type 3 10 Reserved
|
||||
type 4 11 Reserved
|
||||
type 1 0 Two byte header, one 16 byte address field
|
||||
type 2 1 Two byte header, two 16 byte address fields
|
||||
|
||||
|
||||
Propagation Types
|
||||
@@ -664,42 +751,147 @@ Binary Packet Format
|
||||
|
||||
+- Packet Example -+
|
||||
|
||||
HEADER FIELD ADDRESSES FIELD CONTEXT FIELD DATA FIELD
|
||||
HEADER FIELD DESTINATION FIELDS CONTEXT FIELD DATA FIELD
|
||||
_______|_______ ________________|________________ ________|______ __|_
|
||||
| | | | | | | |
|
||||
01010000 00000100 [ADDR1, 10 bytes] [ADDR2, 10 bytes] [CONTEXT, 1 byte] [DATA]
|
||||
| | | | |
|
||||
| | | | +-- Hops = 4
|
||||
| | | +------- Packet Type = DATA
|
||||
| | +--------- Destination Type = SINGLE
|
||||
| +----------- Propagation Type = TRANSPORT
|
||||
+------------- Header Type = HEADER_2 (two byte header, two address fields)
|
||||
01010000 00000100 [HASH1, 16 bytes] [HASH2, 16 bytes] [CONTEXT, 1 byte] [DATA]
|
||||
|| | | | |
|
||||
|| | | | +-- Hops = 4
|
||||
|| | | +------- Packet Type = DATA
|
||||
|| | +--------- Destination Type = SINGLE
|
||||
|| +----------- Propagation Type = TRANSPORT
|
||||
|+------------- Header Type = HEADER_2 (two byte header, two address fields)
|
||||
+-------------- Access Codes = DISABLED
|
||||
|
||||
|
||||
+- Packet Example -+
|
||||
+- Packet Example -+
|
||||
|
||||
HEADER FIELD ADDRESSES FIELD CONTEXT FIELD DATA FIELD
|
||||
HEADER FIELD DESTINATION FIELD CONTEXT FIELD DATA FIELD
|
||||
_______|_______ _______|_______ ________|______ __|_
|
||||
| | | | | | | |
|
||||
00000000 00000111 [ADDR1, 10 bytes] [CONTEXT, 1 byte] [DATA]
|
||||
| | | | |
|
||||
| | | | +-- Hops = 7
|
||||
| | | +------- Packet Type = DATA
|
||||
| | +--------- Destination Type = SINGLE
|
||||
| +----------- Propagation Type = BROADCAST
|
||||
+------------- Header Type = HEADER_1 (two byte header, one address field)
|
||||
00000000 00000111 [HASH1, 16 bytes] [CONTEXT, 1 byte] [DATA]
|
||||
|| | | | |
|
||||
|| | | | +-- Hops = 0
|
||||
|| | | +------- Packet Type = DATA
|
||||
|| | +--------- Destination Type = SINGLE
|
||||
|| +----------- Propagation Type = BROADCAST
|
||||
|+------------- Header Type = HEADER_1 (two byte header, one address field)
|
||||
+-------------- Access Codes = DISABLED
|
||||
|
||||
|
||||
Size examples of different packet types
|
||||
---------------------------------------
|
||||
+- Packet Example -+
|
||||
|
||||
The following table lists example sizes of various
|
||||
packet types. The size listed are the complete on-
|
||||
wire size including all fields.
|
||||
HEADER FIELD IFAC FIELD DESTINATION FIELD CONTEXT FIELD DATA FIELD
|
||||
_______|_______ ______|______ _______|_______ ________|______ __|_
|
||||
| | | | | | | | | |
|
||||
10000000 00000111 [IFAC, N bytes] [HASH1, 16 bytes] [CONTEXT, 1 byte] [DATA]
|
||||
|| | | | |
|
||||
|| | | | +-- Hops = 0
|
||||
|| | | +------- Packet Type = DATA
|
||||
|| | +--------- Destination Type = SINGLE
|
||||
|| +----------- Propagation Type = BROADCAST
|
||||
|+------------- Header Type = HEADER_1 (two byte header, one address field)
|
||||
+-------------- Access Codes = ENABLED
|
||||
|
||||
- Path Request : 33 bytes
|
||||
- Announce : 151 bytes
|
||||
- Link Request : 77 bytes
|
||||
- Link Proof : 77 bytes
|
||||
- Link RTT packet : 83 bytes
|
||||
- Link keepalive : 14 bytes
|
||||
|
||||
Size examples of different packet types
|
||||
---------------------------------------
|
||||
|
||||
The following table lists example sizes of various
|
||||
packet types. The size listed are the complete on-
|
||||
wire size counting all fields including headers,
|
||||
but excluding any interface access codes.
|
||||
|
||||
- Path Request : 51 bytes
|
||||
- Announce : 167 bytes
|
||||
- Link Request : 83 bytes
|
||||
- Link Proof : 115 bytes
|
||||
- Link RTT packet : 99 bytes
|
||||
- Link keepalive : 20 bytes
|
||||
|
||||
|
||||
.. _understanding-announcepropagation:
|
||||
|
||||
Announce Propagation Rules
|
||||
--------------------------
|
||||
|
||||
The following table illustrates the rules for automatically propagating announces
|
||||
from one interface type to another, for all possible combinations. For the purpose
|
||||
of announce propagation, the *Full* and *Gateway* modes are identical.
|
||||
|
||||
.. image:: graphics/if_mode_graph_b.png
|
||||
|
||||
See the :ref:`Interface Modes<interfaces-modes>` section for a conceptual overview
|
||||
of the different interface modes, and how they are configured.
|
||||
|
||||
..
|
||||
(.. code-block:: text)
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼───> Full >───┼── ✕ ── AP
|
||||
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
|
||||
Roaming ─── ✓ ──┘ └── ✓ ── Roaming
|
||||
|
||||
Full ────── ✕ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✕ ──┼────> AP >────┼── ✕ ── AP
|
||||
Boundary ── ✕ ──┤ ├── ✓ ── Boundary
|
||||
Roaming ─── ✕ ──┘ └── ✓ ── Roaming
|
||||
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼─> Roaming >──┼── ✕ ── AP
|
||||
Boundary ── ✕ ──┤ ├── ✕ ── Boundary
|
||||
Roaming ─── ✕ ──┘ └── ✕ ── Roaming
|
||||
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼─> Boundary >─┼── ✕ ── AP
|
||||
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
|
||||
Roaming ─── ✕ ──┘ └── ✕ ── Roaming
|
||||
|
||||
|
||||
.. _understanding-primitives:
|
||||
|
||||
Cryptographic Primitives
|
||||
------------------------
|
||||
|
||||
Reticulum has been designed to use a simple suite of efficient, strong and modern
|
||||
cryptographic primitives, with widely available implementations that can be used
|
||||
both on general-purpose CPUs and on microcontrollers. The necessary primitives are:
|
||||
|
||||
* Ed25519 for signatures
|
||||
|
||||
* X22519 for ECDH key exchanges
|
||||
|
||||
* HKDF for key derivation
|
||||
|
||||
* Fernet for encrypted tokens
|
||||
|
||||
* AES-128 in CBC mode
|
||||
|
||||
* HMAC for message authentication
|
||||
|
||||
* SHA-256
|
||||
|
||||
* SHA-512
|
||||
|
||||
In the default installation configuration, the ``X25519``, ``Ed25519`` and ``AES-128-CBC``
|
||||
primitives are provided by `OpenSSL <https://www.openssl.org/>`_ (via the `PyCA/cryptography <https://github.com/pyca/cryptography>`_
|
||||
package). The hashing functions ``SHA-256`` and ``SHA-512`` are provided by the standard
|
||||
Python `hashlib <https://docs.python.org/3/library/hashlib.html>`_. The ``HKDF``, ``HMAC``,
|
||||
``Fernet`` primitives, and the ``PKCS7`` padding function are always provided by the
|
||||
following internal implementations:
|
||||
|
||||
- ``RNS/Cryptography/HKDF.py``
|
||||
- ``RNS/Cryptography/HMAC.py``
|
||||
- ``RNS/Cryptography/Fernet.py``
|
||||
- ``RNS/Cryptography/PKCS7.py``
|
||||
|
||||
|
||||
Reticulum also includes a complete implementation of all necessary primitives in pure Python.
|
||||
If OpenSSL & PyCA are not available on the system when Reticulum is started, Reticulum will
|
||||
instead use the internal pure-python primitives. A trivial consequence of this is performance,
|
||||
with the OpenSSL backend being *much* faster. The most important consequence however, is the
|
||||
potential loss of security by using primitives that has not seen the same amount of scrutiny,
|
||||
testing and review as those from OpenSSL.
|
||||
|
||||
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.
|
||||
|
||||
@@ -6,20 +6,155 @@ Using Reticulum on Your System
|
||||
|
||||
Reticulum is not installed as a driver or kernel module, as one might expect
|
||||
of a networking stack. Instead, Reticulum is distributed as a Python module.
|
||||
This means that no special privileges are required to install or use it.
|
||||
Any program or application that uses Reticulum will automatically load and
|
||||
initialise Reticulum when it starts.
|
||||
This means that no special privileges are required to install or use it. It
|
||||
is also very light-weight, and easy to transfer to, and install on new systems.
|
||||
|
||||
When you have Reticulum installed, any program or application that uses Reticulum
|
||||
will automatically load and initialise Reticulum when it starts, if it is not
|
||||
already running.
|
||||
|
||||
In many cases, this approach is sufficient. When any program needs to use
|
||||
Reticulum, it is loaded, initialised, interfaces are brought up, and the
|
||||
program can now communicate over Reticulum. If another program starts up
|
||||
and also wants access to the same Reticulum network, the instance is simply
|
||||
shared. This works for any number of programs running concurrently, and is
|
||||
very easy to use, but depending on your use case, there are other options.
|
||||
program can now communicate over any Reticulum networks available. If another
|
||||
program starts up and also wants access to the same Reticulum network, the
|
||||
instance is simply shared. This works for any number of programs running
|
||||
concurrently, and is very easy to use, but depending on your use case, there
|
||||
are other options.
|
||||
|
||||
Configuration & Data
|
||||
--------------------
|
||||
|
||||
Reticulum stores all information that it needs to function in a single file-system
|
||||
directory. When Reticulum is started, it will look for a valid configuration
|
||||
directory in the following places:
|
||||
|
||||
- ``/etc/reticulum``
|
||||
- ``~/.config/reticulum``
|
||||
- ``~/.reticulum``
|
||||
|
||||
If no existing configuration directory is found, the directory ``~/.reticulum``
|
||||
is created, and the default configuration will be automatically created here.
|
||||
You can move it to one of the other locations if you wish.
|
||||
|
||||
It is also possible to use completely arbitrary configuration directories by
|
||||
specifying the relevant command-line parameters when running Reticulum-based
|
||||
programs. You can also run multiple separate Reticulum instances on the same
|
||||
physical system, either in isolation from each other, or connected together.
|
||||
|
||||
In most cases, a single physical system will only need to run one Reticulum
|
||||
instance. This can either be launched at boot, as a system service, or simply
|
||||
be brought up when a program needs it. In either case, any number of programs
|
||||
running on the same system will automatically share the same Reticulum instance,
|
||||
if the configuration allows for it, which it does by default.
|
||||
|
||||
The entire configuration of Reticulum is found in the ``~/.reticulum/config``
|
||||
file. When Reticulum is first started on a new system, a basic, but fully functional
|
||||
configuration file is created. The default configuration looks like this:
|
||||
|
||||
.. code::
|
||||
|
||||
# This is the default Reticulum config file.
|
||||
# You should probably edit it to include any additional,
|
||||
# interfaces and settings you might need.
|
||||
|
||||
# Only the most basic options are included in this default
|
||||
# configuration. To see a more verbose, and much longer,
|
||||
# configuration example, you can run the command:
|
||||
# rnsd --exampleconfig
|
||||
|
||||
|
||||
[reticulum]
|
||||
|
||||
# If you enable Transport, your system will route traffic
|
||||
# for other peers, pass announces and serve path requests.
|
||||
# This should only be done for systems that are suited to
|
||||
# act as transport nodes, ie. if they are stationary and
|
||||
# always-on. This directive is optional and can be removed
|
||||
# for brevity.
|
||||
|
||||
enable_transport = False
|
||||
|
||||
|
||||
# By default, the first program to launch the Reticulum
|
||||
# Network Stack will create a shared instance, that other
|
||||
# programs can communicate with. Only the shared instance
|
||||
# opens all the configured interfaces directly, and other
|
||||
# local programs communicate with the shared instance over
|
||||
# a local socket. This is completely transparent to the
|
||||
# user, and should generally be turned on. This directive
|
||||
# is optional and can be removed for brevity.
|
||||
|
||||
share_instance = Yes
|
||||
|
||||
|
||||
# If you want to run multiple *different* shared instances
|
||||
# on the same system, you will need to specify different
|
||||
# shared instance ports for each. The defaults are given
|
||||
# below, and again, these options can be left out if you
|
||||
# don't need them.
|
||||
|
||||
shared_instance_port = 37428
|
||||
instance_control_port = 37429
|
||||
|
||||
|
||||
# You can configure Reticulum to panic and forcibly close
|
||||
# if an unrecoverable interface error occurs, such as the
|
||||
# hardware device for an interface disappearing. This is
|
||||
# an optional directive, and can be left out for brevity.
|
||||
# This behaviour is disabled by default.
|
||||
|
||||
panic_on_interface_error = No
|
||||
|
||||
|
||||
[logging]
|
||||
# Valid log levels are 0 through 7:
|
||||
# 0: Log only critical information
|
||||
# 1: Log errors and lower log levels
|
||||
# 2: Log warnings and lower log levels
|
||||
# 3: Log notices and lower log levels
|
||||
# 4: Log info and lower (this is the default)
|
||||
# 5: Verbose logging
|
||||
# 6: Debug logging
|
||||
# 7: Extreme logging
|
||||
|
||||
loglevel = 4
|
||||
|
||||
|
||||
# The interfaces section defines the physical and virtual
|
||||
# interfaces Reticulum will use to communicate on. This
|
||||
# section will contain examples for a variety of interface
|
||||
# types. You can modify these or use them as a basis for
|
||||
# your own config, or simply remove the unused ones.
|
||||
|
||||
[interfaces]
|
||||
|
||||
# This interface enables communication with other
|
||||
# link-local Reticulum nodes over UDP. It does not
|
||||
# need any functional IP infrastructure like routers
|
||||
# or DHCP servers, but will require that at least link-
|
||||
# local IPv6 is enabled in your operating system, which
|
||||
# should be enabled by default in almost any OS. See
|
||||
# the Reticulum Manual for more configuration options.
|
||||
|
||||
[[Default Interface]]
|
||||
type = AutoInterface
|
||||
interface_enabled = True
|
||||
|
||||
If Reticulum infrastructure already exists locally, you probably don't need to
|
||||
change anything, and you may already be connected to a wider network. If not,
|
||||
you will probably need to add relevant *interfaces* to the configuration, in
|
||||
order to communicate with other systems. It is a good idea to read the comments
|
||||
and explanations in the above default config. It will teach you the basic
|
||||
concepts you need to understand to configure your network. Once you have done that,
|
||||
take a look at the :ref:`Interfaces<interfaces-main>` chapter of this manual.
|
||||
|
||||
Included Utility Programs
|
||||
-------------------------
|
||||
|
||||
Reticulum includes a range of useful utilities, both for managing your Reticulum
|
||||
networks, and for carrying out common tasks over Reticulum networks, such as
|
||||
transferring files to remote systems, and executing commands and programs remotely.
|
||||
|
||||
If you often use Reticulum from several different programs, or simply want
|
||||
Reticulum to stay available all the time, for example if you are hosting
|
||||
a transport node, you might want to run Reticulum as a separate service that
|
||||
@@ -28,8 +163,8 @@ other programs, applications and services can utilise.
|
||||
The rnsd Utility
|
||||
================
|
||||
|
||||
To do so is very easy. Simply run the included ``rnsd`` command. When ``rnsd``
|
||||
is running, it will keep all configured interfaces open, handle transport if
|
||||
It is very easy to run Reticulum as a service. Simply run the included ``rnsd`` command.
|
||||
When ``rnsd`` is running, it will keep all configured interfaces open, handle transport if
|
||||
it is enabled, and allow any other programs to immediately utilise the
|
||||
Reticulum network it is configured for.
|
||||
|
||||
@@ -57,6 +192,7 @@ the same system.
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
|
||||
You can easily add ``rnsd`` as an always-on service by :ref:`configuring a service<using-systemd>`.
|
||||
|
||||
The rnstatus Utility
|
||||
====================
|
||||
@@ -71,33 +207,49 @@ interfaces, similar to the ``ifconfig`` program.
|
||||
|
||||
# Example output
|
||||
Shared Instance[37428]
|
||||
Status: Up
|
||||
Connected applications: 1
|
||||
RX: 1.13 KB
|
||||
TX: 1.07 KB
|
||||
Status : Up
|
||||
Serving : 1 program
|
||||
Rate : 1.00 Gbps
|
||||
Traffic : 83.13 KB↑
|
||||
86.10 KB↓
|
||||
|
||||
UDPInterface[Default UDP Interface/0.0.0.0:4242]
|
||||
Status: Up
|
||||
RX: 1.01 KB
|
||||
TX: 1.01 KB
|
||||
AutoInterface[Local]
|
||||
Status : Up
|
||||
Mode : Full
|
||||
Rate : 10.00 Mbps
|
||||
Peers : 1 reachable
|
||||
Traffic : 63.23 KB↑
|
||||
80.17 KB↓
|
||||
|
||||
TCPInterface[RNS Testnet Frankfurt/frankfurt.rns.unsigned.io:4965]
|
||||
Status: Up
|
||||
RX: 1.37 KB
|
||||
TX: 9.02 KB
|
||||
Status : Up
|
||||
Mode : Full
|
||||
Rate : 10.00 Mbps
|
||||
Traffic : 187.27 KB↑
|
||||
74.17 KB↓
|
||||
|
||||
RNodeInterface[RNode UHF]
|
||||
Status : Up
|
||||
Mode : Access Point
|
||||
Rate : 1.30 kbps
|
||||
Access : 64-bit IFAC by <…e702c42ba8>
|
||||
Traffic : 8.49 KB↑
|
||||
9.23 KB↓
|
||||
|
||||
Reticulum Transport Instance <5245a8efe1788c6a1cd36144a270e13b> running
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnsd [-h] [--config CONFIG] [-v] [-q] [--version]
|
||||
usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-v]
|
||||
|
||||
Reticulum Network Stack Daemon
|
||||
Reticulum Network Stack Status
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
-a, --all show all interfaces
|
||||
-v, --verbose
|
||||
|
||||
|
||||
The rnpath Utility
|
||||
@@ -109,24 +261,29 @@ destinations on the Reticulum network.
|
||||
.. code:: text
|
||||
|
||||
# Run rnpath
|
||||
rnpath eca6f4e4dc26ae329e61
|
||||
rnpath c89b4da064bf66d280f0e4d8abfd9806
|
||||
|
||||
# Example output
|
||||
Path found, destination <eca6f4e4dc26ae329e61> is 4 hops away via <56b115c30cd386cad69c> on TCPInterface[Testnet/frankfurt.rns.unsigned.io:4965]
|
||||
Path found, destination <c89b4da064bf66d280f0e4d8abfd9806> is 4 hops away via <f53a1c4278e0726bb73fcc623d6ce763> on TCPInterface[Testnet/frankfurt.connect.reticulu.network:4965]
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnpath.py [-h] [--config CONFIG] [--version] [-v] [destination]
|
||||
|
||||
usage: rnpath [-h] [--config CONFIG] [--version] [-t] [-r] [-d] [-D] [-w seconds] [-v] [destination]
|
||||
|
||||
Reticulum Path Discovery Utility
|
||||
|
||||
|
||||
positional arguments:
|
||||
destination hexadecimal hash of the destination
|
||||
|
||||
destination hexadecimal hash of the destination
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
--version show program's version number and exit
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
--version show program's version number and exit
|
||||
-t, --table show all known paths
|
||||
-r, --rates show announce rate info
|
||||
-d, --drop remove the path to a destination
|
||||
-D, --drop-announces drop all queued announces
|
||||
-w seconds timeout before giving up
|
||||
-v, --verbose
|
||||
|
||||
|
||||
@@ -141,16 +298,16 @@ destinations will not have this option enabled, and will not be probable.
|
||||
.. code:: text
|
||||
|
||||
# Run rnprobe
|
||||
python3 -m RNS.Utilities.rnprobe example_utilities.echo.request 9382f334de63217a4278
|
||||
rnprobe example_utilities.echo.request 2d03725b327348980d570f739a3a5708
|
||||
|
||||
# Example output
|
||||
Sent 16 byte probe to <9382f334de63217a4278>
|
||||
Valid reply received from <9382f334de63217a4278>
|
||||
Sent 16 byte probe to <2d03725b327348980d570f739a3a5708>
|
||||
Valid reply received from <2d03725b327348980d570f739a3a5708>
|
||||
Round-trip time is 38.469 milliseconds over 2 hops
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnprobe.py [-h] [--config CONFIG] [--version] [-v] [full_name] [destination_hash]
|
||||
usage: rnprobe [-h] [--config CONFIG] [--version] [-v] [full_name] [destination_hash]
|
||||
|
||||
Reticulum Probe Utility
|
||||
|
||||
@@ -162,4 +319,204 @@ destinations will not have this option enabled, and will not be probable.
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
--version show program's version number and exit
|
||||
-v, --verbose
|
||||
-v, --verbose
|
||||
|
||||
|
||||
The rncp Utility
|
||||
================
|
||||
|
||||
The ``rncp`` utility is a simple file transfer tool. Using it, you can transfer
|
||||
files through Reticulum.
|
||||
|
||||
.. code:: text
|
||||
|
||||
# Run rncp on the receiving system, specifying which identities
|
||||
# are allowed to send files
|
||||
rncp --receive -a 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.
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rncp [-h] [--config path] [-v] [-q] [-p] [-r] [-b] [-a allowed_hash] [-n] [-w seconds] [--version] [file] [destination]
|
||||
|
||||
Reticulum File Transfer Utility
|
||||
|
||||
positional arguments:
|
||||
file file to be transferred
|
||||
destination hexadecimal hash of the receiver
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--config path path to alternative Reticulum config directory
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
-r, --receive wait for incoming files
|
||||
-b, --no-announce don't announce at program start
|
||||
-a allowed_hash accept from this identity
|
||||
-n, --no-auth accept files from anyone
|
||||
-w seconds sender timeout before giving up
|
||||
--version show program's version number and exit
|
||||
-v, --verbose
|
||||
|
||||
|
||||
The rnx Utility
|
||||
================
|
||||
|
||||
The ``rnx`` utility is a basic remote command execution program. It allows you to
|
||||
execute commands on remote systems over Reticulum, and to view returned command
|
||||
output.
|
||||
|
||||
.. code:: text
|
||||
|
||||
# Run rnx on the listening system, specifying which identities
|
||||
# are allowed to execute commands
|
||||
rncp --listen -a 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.
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnx [-h] [--config path] [-v] [-q] [-p] [-l] [-i identity] [-x] [-b] [-a allowed_hash] [-n] [-N] [-d] [-m] [-w seconds] [-W seconds] [--stdin STDIN] [--stdout STDOUT] [--stderr STDERR] [--version]
|
||||
[destination] [command]
|
||||
|
||||
Reticulum Remote Execution Utility
|
||||
|
||||
positional arguments:
|
||||
destination hexadecimal hash of the listener
|
||||
command command to be execute
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
--config path path to alternative Reticulum config directory
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
-l, --listen listen for incoming commands
|
||||
-i identity path to identity to use
|
||||
-x, --interactive enter interactive mode
|
||||
-b, --no-announce don't announce at program start
|
||||
-a allowed_hash accept from this identity
|
||||
-n, --noauth accept files from anyone
|
||||
-N, --noid don't identify to listener
|
||||
-d, --detailed show detailed result output
|
||||
-m mirror exit code of remote command
|
||||
-w seconds connect and request timeout before giving up
|
||||
-W seconds max result download time
|
||||
--stdin STDIN pass input to stdin
|
||||
--stdout STDOUT max size in bytes of returned stdout
|
||||
--stderr STDERR max size in bytes of returned stderr
|
||||
--version show program's version number and exit
|
||||
|
||||
|
||||
Improving System Configuration
|
||||
------------------------------
|
||||
|
||||
If you are setting up a system for permanent use with Reticulum, there is a
|
||||
few system configuration changes that can make this easier to administrate.
|
||||
These changes will be detailed here.
|
||||
|
||||
|
||||
Fixed Serial Port Names
|
||||
=======================
|
||||
|
||||
On a Reticulum instance with several serial port based interfaces, it can be
|
||||
beneficial to use the fixed device names for the serial ports, instead
|
||||
of the dynamically allocated shorthands such as ``/dev/ttyUSB0``. Under most
|
||||
Debian-based distributions, including Ubuntu and Raspberry Pi OS, these nodes
|
||||
can be found under ``/dev/serial/by-id``.
|
||||
|
||||
You can use such a device path directly in place of the numbered shorthands.
|
||||
Here is an example of a packet radio TNC configured as such:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[[Packet Radio KISS Interface]]
|
||||
type = KISSInterface
|
||||
interface_enabled = True
|
||||
outgoing = true
|
||||
port = /dev/serial/by-id/usb-FTDI_FT230X_Basic_UART_43891CKM-if00-port0
|
||||
speed = 115200
|
||||
databits = 8
|
||||
parity = none
|
||||
stopbits = 1
|
||||
preamble = 150
|
||||
txtail = 10
|
||||
persistence = 200
|
||||
slottime = 20
|
||||
|
||||
Using this methodology avoids potential naming mix-ups where physical devices
|
||||
might be plugged and unplugged in different orders, or when device name
|
||||
assignment varies from one boot to another.
|
||||
|
||||
.. _using-systemd:
|
||||
|
||||
Reticulum as a System Service
|
||||
=============================
|
||||
|
||||
Instead of starting Reticulum manually, you can install ``rnsd`` as a system
|
||||
service and have it start automatically at boot.
|
||||
|
||||
If you installed Reticulum with ``pip``, the ``rnsd`` program will most likely
|
||||
be located in a user-local installation path only, which means ``systemd`` will not
|
||||
be able to execute it. In this case, you can simply symlink the ``rnsd`` program
|
||||
into a directory that is in systemd's path:
|
||||
|
||||
.. code:: text
|
||||
|
||||
sudo ln -s $(which rnsd) /usr/local/bin/
|
||||
|
||||
You can then create the service file ``/etc/systemd/system/rnsd.service`` with the
|
||||
following content:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[Unit]
|
||||
Description=Reticulum Network Stack Daemon
|
||||
After=multi-user.target
|
||||
|
||||
[Service]
|
||||
# If you run Reticulum on WiFi devices,
|
||||
# or other devices that need some extra
|
||||
# time to initialise, you might want to
|
||||
# add a short delay before Reticulum is
|
||||
# started by systemd:
|
||||
# ExecStartPre=/bin/sleep 10
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
User=USERNAMEHERE
|
||||
ExecStart=rnsd --service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Be sure to replace ``USERNAMEHERE`` with the user you want to run ``rnsd`` as.
|
||||
|
||||
To manually start ``rnsd`` run:
|
||||
|
||||
.. code:: text
|
||||
|
||||
sudo systemctl start rnsd
|
||||
|
||||
If you want to automatically start ``rnsd`` at boot, run:
|
||||
|
||||
.. code:: text
|
||||
|
||||
sudo systemctl enable rnsd
|
||||
@@ -2,36 +2,56 @@
|
||||
What is Reticulum?
|
||||
******************
|
||||
|
||||
Reticulum is a cryptography-based networking stack for wide-area networks built on readily available hardware, and can operate even with very high latency and extremely low bandwidth.
|
||||
Reticulum is a cryptography-based networking stack for building both local and
|
||||
wide-area networks with readily available hardware, that can continue to operate
|
||||
under adverse conditions, such as extremely low bandwidth and very high latency.
|
||||
|
||||
Reticulum allows you to build very wide-area networks with off-the-shelf tools, and offers end-to-end encryption, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, unforgeable packet acknowledgements and more.
|
||||
Reticulum allows you to build wide-area networks with off-the-shelf tools, and
|
||||
offers end-to-end encryption, forward secrecy, autoconfiguring cryptographically
|
||||
backed multi-hop transport, efficient addressing, unforgeable packet
|
||||
acknowledgements and more.
|
||||
|
||||
Reticulum is a complete networking stack, and does not need IP or higher layers, although it is easy to utilise IP (with TCP or UDP) as the underlying carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the Internet or private IP networks. Reticulum is built directly on cryptographic principles, allowing resilience and stable functionality in open and trustless networks.
|
||||
From a users perspective, Reticulum allows the creation of applications that
|
||||
respect and empower the autonomy and sovereignty of communities and individuals.
|
||||
Reticulum enables secure digital communication that cannot be subjected to
|
||||
outside control, manipulation or censorship.
|
||||
|
||||
No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python 3. Reticulum runs well even on small single-board computers like the Pi Zero.
|
||||
Reticulum enables the construction of both small and potentially planetary-scale
|
||||
networks, without any need for hierarchical or beaureucratic structures to control
|
||||
or manage them, while ensuring individuals and communities full sovereignty
|
||||
over their own network segments.
|
||||
|
||||
Reticulum is a complete networking stack, and does not need IP or higher
|
||||
layers, although it is easy to utilise IP (with TCP or UDP) as the underlying
|
||||
carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the
|
||||
Internet or private IP networks. Reticulum is built directly on cryptographic
|
||||
principles, allowing resilience and stable functionality in open and trustless
|
||||
networks.
|
||||
|
||||
No kernel modules or drivers are required. Reticulum runs completely in
|
||||
userland, and can run on practically any system that runs Python 3. Reticulum
|
||||
runs well even on small single-board computers like the Pi Zero.
|
||||
|
||||
|
||||
Current Status
|
||||
==============
|
||||
Reticulum should currently be considered beta software. All core protocol features are implemented and functioning, but additions will probably occur as real-world use is explored. There will be bugs. The API and wire-format can be considered relatively stable at the moment, but could change if warranted.
|
||||
|
||||
|
||||
Caveat Emptor
|
||||
==============
|
||||
Reticulum is an experimental networking stack, and should be considered as such. While it has been built with cryptography best-practices very foremost in mind, it has not been externally security audited, and there could very well be privacy-breaking bugs. To be considered secure, Reticulum needs a thourough security review by independt cryptographers and security researchers. If you want to help out, or help sponsor an audit, please do get in touch.
|
||||
**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.
|
||||
|
||||
|
||||
What does Reticulum Offer?
|
||||
==========================
|
||||
* Coordination-less globally unique adressing and identification
|
||||
* Coordination-less globally unique addressing and identification
|
||||
|
||||
* Fully self-configuring multi-hop routing
|
||||
|
||||
* Complete initiator anonymity, communicate without revealing your identity
|
||||
|
||||
* Asymmetric X25519 encryption and Ed25519 signatures as a basis for all communication
|
||||
* Asymmetric encryption based on X25519, and Ed25519 signatures as a basis for all communication
|
||||
|
||||
* Forward Secrecy with ephemereal Elliptic Curve Diffie-Hellman keys on Curve25519
|
||||
* Forward Secrecy by using ephemeral Elliptic Curve Diffie-Hellman keys on Curve25519
|
||||
|
||||
* Reticulum uses the `Fernet <https://github.com/fernet/spec/blob/master/Spec.md>`_ specification for on-the-wire / over-the-air encryption
|
||||
|
||||
@@ -49,7 +69,13 @@ What does Reticulum Offer?
|
||||
|
||||
* An intuitive and developer-friendly API
|
||||
|
||||
* Reliable and efficient transfer of arbritrary amounts of data
|
||||
* Efficient link establishment
|
||||
|
||||
* Total bandwidth cost of setting up a link is only 3 packets, totalling 297 bytes
|
||||
|
||||
* Low cost of keeping links open at only 0.44 bits per second
|
||||
|
||||
* Reliable and efficient transfer of arbitrary amounts of data
|
||||
|
||||
* Reticulum can handle a few bytes of data or files of many gigabytes
|
||||
|
||||
@@ -57,52 +83,86 @@ What does Reticulum Offer?
|
||||
|
||||
* The API is very easy to use, and provides transfer progress
|
||||
|
||||
* Efficient link establishment
|
||||
* Authentication and virtual network segmentation on all supported interface types
|
||||
|
||||
* Total bandwidth cost of setting up a link is only 3 packets, totalling 237 bytes
|
||||
|
||||
* Low cost of keeping links open at only 0.62 bits per second
|
||||
* Flexible scalability allowing extremely low-bandwidth networks to co-exist and interoperate with large, high-bandwidth networks
|
||||
|
||||
|
||||
Where can Reticulum be Used?
|
||||
============================
|
||||
Over practically any medium that can support at least a half-duplex channel
|
||||
with 1.000 bits per second throughput, and an MTU of 500 bytes. Data radios,
|
||||
with 500 bits per second throughput, and an MTU of 500 bytes. Data radios,
|
||||
modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes,
|
||||
ad-hoc WiFi, free-space optical links and similar systems are all examples
|
||||
of the types of interfaces Reticulum was designed for.
|
||||
|
||||
An open-source LoRa-based interface called `RNode <https://unsigned.io/rnode>`_
|
||||
has been designed specifically for use with Reticulum. It is possible to build
|
||||
yourself, or it can be purchased as a complete transceiver that just needs a
|
||||
USB connection to the host.
|
||||
has been designed as an example transceiver that is very suitable for
|
||||
Reticulum. It is possible to build it yourself, to transform a common LoRa
|
||||
development board into one, or it can be purchased as a complete transceiver.
|
||||
|
||||
Reticulum can also be encapsulated over existing IP networks, so there's
|
||||
nothing stopping you from using it over wired ethernet or your local WiFi
|
||||
nothing stopping you from using it over wired Ethernet or your local WiFi
|
||||
network, where it'll work just as well. In fact, one of the strengths of
|
||||
Reticulum is how easily it allows you to connect different mediums into a
|
||||
self-configuring, resilient and encrypted mesh.
|
||||
|
||||
As an example, it's possible to set up a Raspberry Pi connected to both a
|
||||
LoRa radio, a packet radio TNC and a WiFi network. Once the interfaces are
|
||||
configured, Reticulum will take care of the rest, and any device on the WiFi
|
||||
added, Reticulum will take care of the rest, and any device on the WiFi
|
||||
network can communicate with nodes on the LoRa and packet radio sides of the
|
||||
network, and vice versa.
|
||||
|
||||
Interface Types and Devices
|
||||
===========================
|
||||
Reticulum implements a range of generalised interface types that covers most of the communications hardware that Reticulum can run over. If your hardware is not supported, it's relatively simple to implement an interface class. Currently, the following interfaces are supported:
|
||||
Reticulum implements a range of generalised interface types that covers the communications hardware that Reticulum can run over. If your hardware is not supported, it's relatively simple to implement an interface class. Currently, Reticulum can use the following devices and communication mediums:
|
||||
|
||||
* Any ethernet device
|
||||
* Any Ethernet device
|
||||
|
||||
* WiFi devices
|
||||
|
||||
* Wired Ethernet devices
|
||||
|
||||
* Fibre-optic transceivers
|
||||
|
||||
* Data radios with Ethernet ports
|
||||
|
||||
* LoRa using `RNode <https://unsigned.io/rnode>`_
|
||||
|
||||
* Can be installed on `many popular LoRa boards <https://github.com/markqvist/rnodeconfigutil#supported-devices>`_
|
||||
|
||||
* Can be purchased as a `ready to use transceiver <https://unsigned.io/rnode>`_
|
||||
|
||||
* Packet Radio TNCs, such as `OpenModem <https://unsigned.io/openmodem>`_
|
||||
|
||||
* Any packet radio TNC in KISS mode
|
||||
|
||||
* Ideal for VHF and UHF radio
|
||||
|
||||
* Any device with a serial port
|
||||
|
||||
* The I2P network
|
||||
|
||||
* TCP over IP networks
|
||||
|
||||
* UDP over IP networks
|
||||
|
||||
For a full list and more details, see the :ref:`Supported Interfaces<interfaces-main>` chapter.
|
||||
* Anything you can connect via stdio
|
||||
|
||||
* Reticulum can use external programs and pipes as interfaces
|
||||
|
||||
* This can be used to easily hack in virtual interfaces
|
||||
|
||||
* Or to quickly create interfaces with custom hardware
|
||||
|
||||
For a full list and more details, see the :ref:`Supported Interfaces<interfaces-main>` chapter.
|
||||
|
||||
|
||||
Caveat Emptor
|
||||
==============
|
||||
Reticulum is an experimental networking stack, and should be considered as
|
||||
such. While it has been built with cryptography best-practices very foremost in
|
||||
mind, it has not been externally security audited, and there could very well be
|
||||
privacy-breaking bugs. To be considered secure, Reticulum needs a thorough
|
||||
security review by independent cryptographers and security researchers. If you
|
||||
want to help out, or help sponsor an audit, please do get in touch.
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* _sphinx_javascript_frameworks_compat.js
|
||||
* ~~~~~~~~~~
|
||||
*
|
||||
* Compatability shim for jQuery and underscores.js.
|
||||
*
|
||||
* WILL BE REMOVED IN Sphinx 6.0
|
||||
* xref RemovedInSphinx60Warning
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* select a different prefix for underscore
|
||||
*/
|
||||
$u = _.noConflict();
|
||||
|
||||
|
||||
/**
|
||||
* small helper function to urldecode strings
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
|
||||
*/
|
||||
jQuery.urldecode = function(x) {
|
||||
if (!x) {
|
||||
return x
|
||||
}
|
||||
return decodeURIComponent(x.replace(/\+/g, ' '));
|
||||
};
|
||||
|
||||
/**
|
||||
* small helper function to urlencode strings
|
||||
*/
|
||||
jQuery.urlencode = encodeURIComponent;
|
||||
|
||||
/**
|
||||
* This function returns the parsed url parameters of the
|
||||
* current request. Multiple values per key are supported,
|
||||
* it will always return arrays of strings for the value parts.
|
||||
*/
|
||||
jQuery.getQueryParameters = function(s) {
|
||||
if (typeof s === 'undefined')
|
||||
s = document.location.search;
|
||||
var parts = s.substr(s.indexOf('?') + 1).split('&');
|
||||
var result = {};
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var tmp = parts[i].split('=', 2);
|
||||
var key = jQuery.urldecode(tmp[0]);
|
||||
var value = jQuery.urldecode(tmp[1]);
|
||||
if (key in result)
|
||||
result[key].push(value);
|
||||
else
|
||||
result[key] = [value];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* highlight a given string on a jquery object by wrapping it in
|
||||
* span elements with the given class name.
|
||||
*/
|
||||
jQuery.fn.highlightText = function(text, className) {
|
||||
function highlight(node, addItems) {
|
||||
if (node.nodeType === 3) {
|
||||
var val = node.nodeValue;
|
||||
var pos = val.toLowerCase().indexOf(text);
|
||||
if (pos >= 0 &&
|
||||
!jQuery(node.parentNode).hasClass(className) &&
|
||||
!jQuery(node.parentNode).hasClass("nohighlight")) {
|
||||
var span;
|
||||
var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
|
||||
if (isInSVG) {
|
||||
span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
|
||||
} else {
|
||||
span = document.createElement("span");
|
||||
span.className = className;
|
||||
}
|
||||
span.appendChild(document.createTextNode(val.substr(pos, text.length)));
|
||||
node.parentNode.insertBefore(span, node.parentNode.insertBefore(
|
||||
document.createTextNode(val.substr(pos + text.length)),
|
||||
node.nextSibling));
|
||||
node.nodeValue = val.substr(0, pos);
|
||||
if (isInSVG) {
|
||||
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
||||
var bbox = node.parentElement.getBBox();
|
||||
rect.x.baseVal.value = bbox.x;
|
||||
rect.y.baseVal.value = bbox.y;
|
||||
rect.width.baseVal.value = bbox.width;
|
||||
rect.height.baseVal.value = bbox.height;
|
||||
rect.setAttribute('class', className);
|
||||
addItems.push({
|
||||
"parent": node.parentNode,
|
||||
"target": rect});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!jQuery(node).is("button, select, textarea")) {
|
||||
jQuery.each(node.childNodes, function() {
|
||||
highlight(this, addItems);
|
||||
});
|
||||
}
|
||||
}
|
||||
var addItems = [];
|
||||
var result = this.each(function() {
|
||||
highlight(this, addItems);
|
||||
});
|
||||
for (var i = 0; i < addItems.length; ++i) {
|
||||
jQuery(addItems[i].parent).before(addItems[i].target);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/*
|
||||
* backward compatibility for jQuery.browser
|
||||
* This will be supported until firefox bug is fixed.
|
||||
*/
|
||||
if (!jQuery.browser) {
|
||||
jQuery.uaMatch = function(ua) {
|
||||
ua = ua.toLowerCase();
|
||||
|
||||
var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
|
||||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
|
||||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
|
||||
/(msie) ([\w.]+)/.exec(ua) ||
|
||||
ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
|
||||
[];
|
||||
|
||||
return {
|
||||
browser: match[ 1 ] || "",
|
||||
version: match[ 2 ] || "0"
|
||||
};
|
||||
};
|
||||
jQuery.browser = {};
|
||||
jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
|
||||
}
|
||||