Reticulum Network Stack α
-Reticulum is a cryptography-based networking stack for high-latency, wide-area networks built on readily available hardware. Reticulum allows you to build very wide-area networks with off-the-shelf tools, and offers end-to-end encryption, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, resource caching, unforgeable packet acknowledgements and much more.
-Reticulum is a complete networking stack, and does not use IP or higher layers, although it can be easily tunnelled through conventional IP networks. This frees up overhead, that has been utilised to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality in open and trustless networks.
+Reticulum is a cryptography-based networking stack for high-latency, wide-area networks built on readily available hardware. Reticulum allows you to build very wide-area networks with off-the-shelf tools, and offers end-to-end encryption, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, unforgeable packet acknowledgements and more.
+Reticulum is a complete networking stack, and does not use IP or higher layers, although it is easy to utilise IP (with TCP or UDP) as the underlying carrier for Reticulum.
+Having no dependencies on traditional networking stacks free up overhead that has been utilised to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality in open and trustless networks.
No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python 3.
For more info, see unsigned.io/projects/reticulum
Notable Features
@@ -1066,7 +1067,7 @@ body .markdown-body-
+
- Reticulum uses the Fernet specification for encryption on links and to group destinations
- AES-128 in CBC mode with PKCS7 padding
- HMAC using SHA256 for authentication
- IVs are generated through os.urandom() @@ -1075,7 +1076,7 @@ body .markdown-body
- Unforgeable packet delivery confirmations
- A variety of supported interface types
- Efficient and easy resource transfers -
- A simple and easy-to-use API +
- An intuitive and easy-to-use API
Where can Reticulum be used?
On practically any hardware that can support at least a half-duplex channel with 1.000 bits per second throughput, and an MTU of 500 bytes. Data radios, modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, free-space optical links and similar systems are all examples of the types of interfaces Reticulum was designed for.
diff --git a/README.md b/README.md index fc67897..c62658c 100755 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ Reticulum Network Stack α ========== -Reticulum is a cryptography-based networking stack for high-latency, wide-area networks built on readily available hardware. Reticulum allows you to build very wide-area networks with off-the-shelf tools, and offers end-to-end encryption, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, resource caching, unforgeable packet acknowledgements and much more. +Reticulum is a cryptography-based networking stack for high-latency, wide-area networks built on readily available hardware. Reticulum allows you to build very wide-area networks with off-the-shelf tools, and offers end-to-end encryption, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, unforgeable packet acknowledgements and more. -Reticulum is a complete networking stack, and does not use IP or higher layers, although it can be easily tunnelled through conventional IP networks. This frees up overhead, that has been utilised to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality in open and trustless networks. +Reticulum is a complete networking stack, and does not use IP or higher layers, although it is easy to utilise IP (with TCP or UDP) as the underlying carrier for Reticulum. + +Having no dependencies on traditional networking stacks free up overhead that has been utilised to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality in open and trustless networks. No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python 3. @@ -14,14 +16,14 @@ For more info, see [unsigned.io/projects/reticulum](https://unsigned.io/projects - Fully self-configuring multi-hop routing - Asymmetric RSA encryption and signatures as basis for all communication - Perfect Forward Secrecy on links with ephemereal Elliptic Curve Diffie-Hellman keys (on the SECP256R1 curve) - - Reticulum uses the [Fernet](https://github.com/fernet/spec/blob/master/Spec.md) specification for encryption to group destinations and on links + - Reticulum uses the [Fernet](https://github.com/fernet/spec/blob/master/Spec.md) specification for encryption on links and to group destinations - 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 - Efficient and easy resource transfers - - A simple and easy-to-use API + - An intuitive and easy-to-use API ## Where can Reticulum be used? On practically any hardware that can support at least a half-duplex channel with 1.000 bits per second throughput, and an MTU of 500 bytes. Data radios, modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, free-space optical links and similar systems are all examples of the types of interfaces Reticulum was designed for. diff --git a/RNS/Bundle.py b/RNS/Bundle.py new file mode 100644 index 0000000..e69de29 diff --git a/RNS/Packet.py b/RNS/Packet.py index 2834105..42f56a1 100755 --- a/RNS/Packet.py +++ b/RNS/Packet.py @@ -89,6 +89,7 @@ class Packet: self.packet_hash = None self.attached_interface = attached_interface + self.receiving_interface = None def getPackedFlags(self): if self.context == Packet.LRPROOF: @@ -127,9 +128,12 @@ class Packet: # Keepalive packets contain no actual # data self.ciphertext = self.data + elif self.context == Packet.CACHE_REQUEST: + # Cache-requests are not encrypted + self.ciphertext = self.data else: # In all other cases, we encrypt the packet - # with the destination's public key + # with the destination's encryption method self.ciphertext = self.destination.encrypt(self.data) if self.header_type == Packet.HEADER_2: diff --git a/RNS/Resource.py b/RNS/Resource.py index 306b30a..7e62ec2 100644 --- a/RNS/Resource.py +++ b/RNS/Resource.py @@ -22,7 +22,7 @@ class Resource: # # A small system in this regard is # defined as a Raspberry Pi, which should - # be able to compress, encrypt and hashmap + # be able to compress, encrypt and hash-map # the resource in about 10 seconds. MAX_EFFICIENT_SIZE = 16 * 1024 * 1024 @@ -308,7 +308,7 @@ class Resource: if sleep_time < 0: if self.retries_left > 0: - RNS.log("Timeout waiting for parts, requesting retry", RNS.LOG_DEBUG) + RNS.log("Timed out waiting for parts, requesting retry", RNS.LOG_DEBUG) if self.window > self.window_min: self.window -= 1 if self.window_max > self.window_min: @@ -344,7 +344,7 @@ class Resource: expected_data = self.hash + self.expected_proof expected_proof_packet = RNS.Packet(self.link, expected_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.RESOURCE_PRF) expected_proof_packet.pack() - RNS.Transport.cache_request(expected_proof_packet.packet_hash) + RNS.Transport.cache_request(expected_proof_packet.packet_hash, self.link) self.last_part_sent = time.time() sleep_time = 0.001 diff --git a/RNS/Transport.py b/RNS/Transport.py index ed4d9c7..fde1cd0 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -338,7 +338,7 @@ class Transport: new_raw += Transport.destination_table[packet.destination_hash][1] new_raw += packet.raw[2:] # TODO: Remove at some point - RNS.log("Packet was inserted into transport via "+RNS.prettyhexrep(Transport.destination_table[packet.destination_hash][1])+" on: "+str(outbound_interface), RNS.LOG_EXTREME) + # RNS.log("Packet was inserted into transport via "+RNS.prettyhexrep(Transport.destination_table[packet.destination_hash][1])+" on: "+str(outbound_interface), RNS.LOG_EXTREME) outbound_interface.processOutgoing(new_raw) Transport.destination_table[packet.destination_hash][0] = time.time() sent = True @@ -359,7 +359,7 @@ class Transport: new_raw += Transport.destination_table[packet.destination_hash][1] new_raw += packet.raw[2:] # TODO: Remove at some point - RNS.log("Packet was inserted into transport via "+RNS.prettyhexrep(Transport.destination_table[packet.destination_hash][1])+" on: "+str(outbound_interface), RNS.LOG_EXTREME) + # RNS.log("Packet was inserted into transport via "+RNS.prettyhexrep(Transport.destination_table[packet.destination_hash][1])+" on: "+str(outbound_interface), RNS.LOG_EXTREME) outbound_interface.processOutgoing(new_raw) Transport.destination_table[packet.destination_hash][0] = time.time() sent = True @@ -416,6 +416,7 @@ class Transport: Transport.jobs_locked = False return sent + @staticmethod def packet_filter(packet): # TODO: Think long and hard about this. @@ -429,8 +430,11 @@ class Transport: return True if packet.context == RNS.Packet.RESOURCE: return True + if packet.context == RNS.Packet.CACHE_REQUEST: + return True if packet.destination_type == RNS.Destination.PLAIN: return True + if not packet.packet_hash in Transport.packet_hashlist: return True else: @@ -455,9 +459,6 @@ class Transport: RNS.log(str(interface)+" received packet with hash "+RNS.prettyhexrep(packet.packet_hash), RNS.LOG_EXTREME) if len(Transport.local_client_interfaces) > 0: - new_raw = packet.raw[0:1] - new_raw += struct.pack("!B", packet.hops) - new_raw += packet.raw[2:] if Transport.is_local_client_interface(interface): packet.hops -= 1 @@ -506,10 +507,19 @@ class Transport: # shared instance, so they look directly reach- # able), and reinsert, so the normal transport # implementation can handle the packet. - if packet.transport_id == None and for_local_client: packet.transport_id = Transport.identity.hash + # If this is a cache request, and we can fullfill + # it, do so and stop processing. Otherwise resume + # normal processing. + if packet.context == RNS.Packet.CACHE_REQUEST: + if Transport.cache_request_packet(packet): + return + + # If the packet is in transport, check whether we + # are the designated next hop, and process it + # accordingly if we are. if packet.transport_id != None and packet.packet_type != RNS.Packet.ANNOUNCE: if packet.transport_id == Transport.identity.hash: RNS.log("Received packet in transport for "+RNS.prettyhexrep(packet.destination_hash)+" with matching transport ID, transporting it...", RNS.LOG_DEBUG) @@ -563,10 +573,8 @@ class Transport: else: # TODO: There should probably be some kind of REJECT # mechanism here, to signal to the source that their - # expected path failed + # expected path failed. RNS.log("Got packet in transport, but no known path to final destination. Dropping packet.", RNS.LOG_DEBUG) - else: - pass # Link transport handling. Directs packets according # to entries in the link tables @@ -872,10 +880,8 @@ class Transport: @staticmethod def shouldCache(packet): - # TODO: Implement sensible rules for which - # packets to cache - #if packet.context == RNS.Packet.RESOURCE_PRF: - # return True + if packet.context == RNS.Packet.RESOURCE_PRF: + return True return False @@ -889,10 +895,14 @@ class Transport: if RNS.Transport.shouldCache(packet) or force_cache: try: packet_hash = RNS.hexrep(packet.getHash(), delimit=False) - file = open(RNS.Reticulum.cachepath+"/"+packet_hash, "wb") - file.write(packet.raw) + interface_reference = None + if packet.receiving_interface != None: + interface_reference = str(packet.receiving_interface) + + file = open(RNS.Reticulum.cachepath+"/"+packet_hash, "wb") + file.write(umsgpack.packb([packet.raw, interface_reference])) file.close() - RNS.log("Wrote packet "+packet_hash+" to cache", RNS.LOG_EXTREME) + except Exception as e: RNS.log("Error writing packet to cache", RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e)) @@ -902,11 +912,19 @@ class Transport: try: packet_hash = RNS.hexrep(packet_hash, delimit=False) path = RNS.Reticulum.cachepath+"/"+packet_hash + if os.path.isfile(path): file = open(path, "rb") - raw = file.read() + cached_data = umsgpack.unpackb(file.read()) file.close() - packet = RNS.Packet(None, raw) + + packet = RNS.Packet(None, cached_data[0]) + interface_reference = cached_data[1] + + for interface in Transport.interfaces: + if str(interface) == interface_reference: + packet.receiving_interface = interface + return packet else: return None @@ -914,33 +932,34 @@ class Transport: RNS.log("Exception occurred while getting cached packet.", RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) - # TODO: Implement cache requests. Needs methodology - # rethinking. This is skeleton code. @staticmethod def cache_request_packet(packet): if len(packet.data) == RNS.Identity.HASHLENGTH/8: - packet_hash = RNS.hexrep(packet.data, delimit=False) - packet = Transport.get_cached_packet(packet_hash) + packet = Transport.get_cached_packet(packet.data) if packet != None: - # TODO: Implement outbound for this - pass + # If the packet was retrieved from the local + # cache, replay it to the Transport instance, + # so that it can be directed towards it original + # destination. + Transport.inbound(packet.raw, packet.receiving_interface) + return True else: - pass - - # TODO: Implement cache requests. Needs methodology - # rethinking. This is skeleton code. - @staticmethod - def cache_request(packet_hash): - RNS.log("Cache request for "+RNS.prettyhexrep(packet_hash), RNS.LOG_EXTREME) - path = RNS.Reticulum.cachepath+"/"+RNS.hexrep(packet_hash, delimit=False) - if os.path.isfile(path): - file = open(path, "rb") - raw = file.read() - Transport.inbound(raw) - file.close() + return False else: - cache_request_packet = RNS.Packet(Transport.transport_destination(), packet_hash, context = RNS.Packet.CACHE_REQUEST) + return False + + @staticmethod + def cache_request(packet_hash, destination): + cached_packet = Transport.get_cached_packet(packet_hash) + if cached_packet: + # The packet was found in the local cache, + # replay it to the Transport instance. + Transport.inbound(packet.raw, packet.receiving_interface) + else: + # The packet is not in the local cache, + # query the network. + RNS.Packet(destination, packet_hash, context = RNS.Packet.CACHE_REQUEST).send() @staticmethod def hasPath(destination_hash): @@ -1026,13 +1045,6 @@ class Transport: else: RNS.log("No known path to requested destination, ignoring request", RNS.LOG_DEBUG) - # TODO: Currently only used for cache requests. - # Needs rethink. - @staticmethod - def transport_destination(): - # TODO: implement this - pass - @staticmethod def from_local_client(packet): if hasattr(packet.receiving_interface, "parent_interface"):