Reticulum/RNS/Packet.py

415 lines
13 KiB
Python
Raw Normal View History

import threading
2016-06-03 19:02:02 +02:00
import struct
import math
2018-03-20 12:32:41 +01:00
import time
import RNS
2016-06-03 19:02:02 +02:00
class Packet:
2018-04-25 12:23:56 +02:00
# Packet types
DATA = 0x00 # Data packets
ANNOUNCE = 0x01 # Announces
LINKREQUEST = 0x02 # Link requests
PROOF = 0x03 # Proofs
2018-04-16 22:09:23 +02:00
types = [DATA, ANNOUNCE, LINKREQUEST, PROOF]
2016-06-03 19:02:02 +02:00
2018-04-25 12:23:56 +02:00
# Header types
2018-04-18 23:31:17 +02:00
HEADER_1 = 0x00 # Normal header format
2020-03-01 16:56:49 +01:00
HEADER_2 = 0x01 # Header format used for packets in transport
2018-04-18 23:31:17 +02:00
HEADER_3 = 0x02 # Reserved
HEADER_4 = 0x03 # Reserved
2018-03-19 16:39:08 +01:00
header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4]
2016-06-03 19:02:02 +02:00
# Data packet context types
2018-04-25 12:23:56 +02:00
NONE = 0x00 # Generic data packet
RESOURCE = 0x01 # Packet is part of a resource
RESOURCE_ADV = 0x02 # Packet is a resource advertisement
RESOURCE_REQ = 0x03 # Packet is a resource part request
RESOURCE_HMU = 0x04 # Packet is a resource hashmap update
RESOURCE_PRF = 0x05 # Packet is a resource proof
RESOURCE_ICL = 0x06 # Packet is a resource initiator cancel message
RESOURCE_RCL = 0x07 # Packet is a resource receiver cancel message
CACHE_REQUEST = 0x08 # Packet is a cache request
REQUEST = 0x09 # Packet is a request
RESPONSE = 0x0A # Packet is a response to a request
2020-03-06 12:55:05 +01:00
PATH_RESPONSE = 0x0B # Packet is a response to a path request
COMMAND = 0x0C # Packet is a command
COMMAND_STATUS = 0x0D # Packet is a status of an executed command
KEEPALIVE = 0xFB # Packet is a keepalive packet
LINKCLOSE = 0xFC # Packet is a link close message
LINKPROOF = 0xFD # Packet is a link packet proof
2018-04-25 12:23:56 +02:00
LRRTT = 0xFE # Packet is a link request round-trip time measurement
LRPROOF = 0xFF # Packet is a link request proof
2018-04-18 23:31:17 +02:00
2018-04-25 12:23:56 +02:00
# This is used to calculate allowable
# payload sizes
HEADER_MAXSIZE = 23
MDU = RNS.Reticulum.MDU
# With an MTU of 500, the maximum RSA-encrypted
# amount of data we can send in a single packet
# is given by the below calculation; 258 bytes.
RSA_MDU = math.floor(MDU/RNS.Identity.DECRYPT_CHUNKSIZE)*RNS.Identity.ENCRYPT_CHUNKSIZE
PLAIN_MDU = MDU
2018-04-18 23:31:17 +02:00
2018-04-25 12:23:56 +02:00
# TODO: This should be calculated
# more intelligently
# Default packet timeout
2018-04-18 23:31:17 +02:00
TIMEOUT = 60
2018-04-17 17:46:48 +02:00
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):
2018-03-19 16:39:08 +01:00
if destination != None:
if transport_type == None:
transport_type = RNS.Transport.BROADCAST
2016-06-03 19:02:02 +02:00
2018-03-19 16:39:08 +01:00
self.header_type = header_type
self.packet_type = packet_type
self.transport_type = transport_type
2018-04-18 23:31:17 +02:00
self.context = context
2016-06-03 19:02:02 +02:00
self.hops = 0;
2018-03-19 16:39:08 +01:00
self.destination = destination
self.transport_id = transport_id
self.data = data
self.flags = self.getPackedFlags()
self.raw = None
self.packed = False
self.sent = False
self.create_receipt = create_receipt
self.receipt = None
self.fromPacked = False
2018-03-19 16:39:08 +01:00
else:
2018-03-20 12:32:41 +01:00
self.raw = data
self.packed = True
self.fromPacked = True
self.create_receipt = False
2018-03-20 12:32:41 +01:00
2019-11-09 23:47:42 +01:00
self.MTU = RNS.Reticulum.MTU
self.sent_at = None
2018-03-20 12:32:41 +01:00
self.packet_hash = None
2018-03-19 16:39:08 +01:00
self.attached_interface = attached_interface
2020-06-14 18:33:01 +02:00
self.receiving_interface = None
2018-03-19 16:39:08 +01:00
def getPackedFlags(self):
2018-04-18 23:31:17 +02:00
if self.context == Packet.LRPROOF:
2018-04-16 17:13:39 +02:00
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | RNS.Destination.LINK | self.packet_type
else:
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
2018-03-19 16:39:08 +01:00
return packed_flags
def pack(self):
2020-03-01 16:56:49 +01:00
self.destination_hash = self.destination.hash
2020-04-22 12:07:13 +02:00
self.header = b""
2018-03-19 16:39:08 +01:00
self.header += struct.pack("!B", self.flags)
self.header += struct.pack("!B", self.hops)
2018-04-18 23:31:17 +02:00
if self.context == Packet.LRPROOF:
2018-04-16 17:13:39 +02:00
self.header += self.destination.link_id
2018-03-19 18:11:50 +01:00
self.ciphertext = self.data
2018-04-18 23:31:17 +02:00
else:
if self.header_type == Packet.HEADER_1:
self.header += self.destination.hash
if self.packet_type == Packet.ANNOUNCE:
# Announce packets are not encrypted
self.ciphertext = self.data
elif self.packet_type == Packet.PROOF and self.context == Packet.RESOURCE_PRF:
# Resource proofs are not encrypted
self.ciphertext = self.data
elif self.packet_type == Packet.PROOF and self.destination.type == RNS.Destination.LINK:
# Packet proofs over links are not encrypted
self.ciphertext = self.data
2018-04-18 23:31:17 +02:00
elif self.context == Packet.RESOURCE:
# A resource takes care of symmetric
# encryption by itself
self.ciphertext = self.data
elif self.context == Packet.KEEPALIVE:
# Keepalive packets contain no actual
# data
self.ciphertext = self.data
2020-06-14 18:33:01 +02:00
elif self.context == Packet.CACHE_REQUEST:
# Cache-requests are not encrypted
self.ciphertext = self.data
2018-04-18 23:31:17 +02:00
else:
# In all other cases, we encrypt the packet
2020-06-14 18:33:01 +02:00
# with the destination's encryption method
2018-04-18 23:31:17 +02:00
self.ciphertext = self.destination.encrypt(self.data)
if self.header_type == Packet.HEADER_2:
2019-11-09 23:47:42 +01:00
if self.transport_id != None:
self.header += self.transport_id
self.header += self.destination.hash
if self.packet_type == Packet.ANNOUNCE:
# Announce packets are not encrypted
self.ciphertext = self.data
2018-04-18 23:31:17 +02:00
else:
raise IOError("Packet with header type 2 must have a transport ID")
2020-04-22 12:07:13 +02:00
self.header += bytes([self.context])
2018-03-19 16:39:08 +01:00
self.raw = self.header + self.ciphertext
if len(self.raw) > self.MTU:
raise IOError("Packet size of "+str(len(self.raw))+" exceeds MTU of "+str(self.MTU)+" bytes")
self.packed = True
2020-03-01 16:56:49 +01:00
self.updateHash()
2018-03-19 16:39:08 +01:00
def unpack(self):
2020-04-22 12:07:13 +02:00
self.flags = self.raw[0]
self.hops = self.raw[1]
2018-03-19 16:39:08 +01:00
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]
2018-04-18 23:31:17 +02:00
self.context = ord(self.raw[22:23])
self.data = self.raw[23:]
2018-03-19 16:39:08 +01:00
else:
self.transport_id = None
self.destination_hash = self.raw[2:12]
2018-04-18 23:31:17 +02:00
self.context = ord(self.raw[12:13])
self.data = self.raw[13:]
2018-03-19 16:39:08 +01:00
self.packed = False
2020-03-01 16:56:49 +01:00
self.updateHash()
2018-03-19 16:39:08 +01:00
2020-05-13 16:02:37 +02:00
# Sends the packet. Returns a receipt if one is generated,
# or None if no receipt is available. Returns False if the
# packet could not be sent.
2018-03-19 16:39:08 +01:00
def send(self):
if not self.sent:
if self.destination.type == RNS.Destination.LINK:
if self.destination.status == RNS.Link.CLOSED:
raise IOError("Attempt to transmit over a closed link")
else:
self.destination.last_outbound = time.time()
self.destination.tx += 1
self.destination.txbytes += len(self.data)
2018-04-16 17:13:39 +02:00
if not self.packed:
self.pack()
2018-04-17 17:46:48 +02:00
if RNS.Transport.outbound(self):
return self.receipt
else:
2020-05-13 16:02:37 +02:00
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
self.sent = False
self.receipt = None
return False
2016-06-03 19:02:02 +02:00
else:
raise IOError("Packet was already sent")
def resend(self):
if self.sent:
2018-04-23 23:42:16 +02:00
if RNS.Transport.outbound(self):
return self.receipt
else:
2020-05-13 16:02:37 +02:00
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
self.sent = False
self.receipt = None
return False
2016-06-03 19:02:02 +02:00
else:
2018-03-20 12:32:41 +01:00
raise IOError("Packet was not sent yet")
2018-04-17 17:46:48 +02:00
def prove(self, destination=None):
if self.fromPacked and hasattr(self, "destination") and self.destination:
2018-03-20 12:32:41 +01:00
if self.destination.identity and self.destination.identity.prv:
self.destination.identity.prove(self, destination)
elif self.fromPacked and hasattr(self, "link") and self.link:
self.link.prove_packet(self)
else:
RNS.log("Could not prove packet associated with neither a destination nor a link", RNS.LOG_ERROR)
2018-03-20 12:32:41 +01:00
2018-04-17 17:46:48 +02:00
# Generates a special destination that allows Reticulum
# to direct the proof back to the proved packet's sender
def generateProofDestination(self):
return ProofDestination(self)
2018-03-20 12:32:41 +01:00
def validateProofPacket(self, proof_packet):
2018-04-17 17:46:48 +02:00
return self.receipt.validateProofPacket(proof_packet)
2018-03-20 12:32:41 +01:00
def validateProof(self, proof):
2018-04-17 17:46:48 +02:00
return self.receipt.validateProof(proof)
def updateHash(self):
self.packet_hash = self.getHash()
def getHash(self):
return RNS.Identity.fullHash(self.getHashablePart())
2020-03-06 22:20:50 +01:00
def getTruncatedHash(self):
return RNS.Identity.truncatedHash(self.getHashablePart())
2018-04-17 17:46:48 +02:00
def getHashablePart(self):
2020-04-22 12:07:13 +02:00
hashable_part = bytes([self.raw[0] & 0b00001111])
2020-03-01 16:56:49 +01:00
if self.header_type == Packet.HEADER_2:
hashable_part += self.raw[12:]
else:
hashable_part += self.raw[2:]
return hashable_part
2018-03-20 12:32:41 +01:00
2018-04-17 17:46:48 +02:00
class ProofDestination:
def __init__(self, packet):
self.hash = packet.getHash()[:10];
self.type = RNS.Destination.SINGLE
2018-03-20 12:32:41 +01:00
2018-04-17 17:46:48 +02:00
def encrypt(self, plaintext):
return plaintext
2018-03-20 12:32:41 +01:00
2018-04-18 23:31:17 +02:00
class PacketReceipt:
# Receipt status constants
FAILED = 0x00
SENT = 0x01
DELIVERED = 0x02
CULLED = 0xFF
2018-04-18 23:31:17 +02:00
2020-04-22 12:07:13 +02:00
EXPL_LENGTH = RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8
IMPL_LENGTH = RNS.Identity.SIGLENGTH//8
2018-04-18 23:31:17 +02:00
# Creates a new packet receipt from a sent packet
def __init__(self, packet):
self.hash = packet.getHash()
self.sent = True
self.sent_at = time.time()
self.timeout = Packet.TIMEOUT
self.proved = False
self.status = PacketReceipt.SENT
self.destination = packet.destination
self.callbacks = PacketReceiptCallbacks()
self.concluded_at = None
# Validate a proof packet
def validateProofPacket(self, proof_packet):
if hasattr(proof_packet, "link") and proof_packet.link:
return self.validate_link_proof(proof_packet.data, proof_packet.link)
else:
return self.validateProof(proof_packet.data)
# Validate a raw proof for a link
def validate_link_proof(self, proof, link):
# TODO: Hardcoded as explicit proofs for now
if True or len(proof) == PacketReceipt.EXPL_LENGTH:
# This is an explicit proof
2020-04-22 12:07:13 +02:00
proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
if proof_hash == self.hash:
proof_valid = link.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
self.proved = True
self.concluded_at = time.time()
if self.callbacks.delivery != None:
self.callbacks.delivery(self)
return True
else:
return False
else:
return False
elif len(proof) == PacketReceipt.IMPL_LENGTH:
pass
2020-04-22 12:07:13 +02:00
# TODO: Why is this disabled?
# signature = proof[:RNS.Identity.SIGLENGTH//8]
# proof_valid = self.link.validate(signature, self.hash)
# if proof_valid:
# self.status = PacketReceipt.DELIVERED
# self.proved = True
# self.concluded_at = time.time()
# if self.callbacks.delivery != None:
# self.callbacks.delivery(self)
# RNS.log("valid")
# return True
# else:
# RNS.log("invalid")
# return False
else:
return False
2018-04-18 23:31:17 +02:00
# Validate a raw proof
def validateProof(self, proof):
if len(proof) == PacketReceipt.EXPL_LENGTH:
# This is an explicit proof
2020-04-22 12:07:13 +02:00
proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
2018-04-18 23:31:17 +02:00
if proof_hash == self.hash:
proof_valid = self.destination.identity.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
self.proved = True
2018-04-25 00:20:57 +02:00
self.concluded_at = time.time()
2018-04-18 23:31:17 +02:00
if self.callbacks.delivery != None:
self.callbacks.delivery(self)
return True
else:
return False
else:
return False
elif len(proof) == PacketReceipt.IMPL_LENGTH:
# This is an implicit proof
2020-03-06 12:55:05 +01:00
if self.destination.identity == None:
return False
2020-04-22 12:07:13 +02:00
signature = proof[:RNS.Identity.SIGLENGTH//8]
2018-04-18 23:31:17 +02:00
proof_valid = self.destination.identity.validate(signature, self.hash)
if proof_valid:
self.status = PacketReceipt.DELIVERED
self.proved = True
2018-04-25 00:20:57 +02:00
self.concluded_at = time.time()
2018-04-18 23:31:17 +02:00
if self.callbacks.delivery != None:
self.callbacks.delivery(self)
return True
else:
return False
else:
return False
2018-04-25 00:20:57 +02:00
def rtt(self):
return self.concluded_at - self.sent_at
2018-04-18 23:31:17 +02:00
def is_timed_out(self):
2018-04-18 23:31:17 +02:00
return (self.sent_at+self.timeout < time.time())
def check_timeout(self):
if self.is_timed_out():
if self.timeout == -1:
self.status = PacketReceipt.CULLED
else:
self.status = PacketReceipt.FAILED
2018-04-18 23:31:17 +02:00
self.concluded_at = time.time()
2018-04-18 23:31:17 +02:00
if self.callbacks.timeout:
thread = threading.Thread(target=self.callbacks.timeout, args=(self,))
thread.setDaemon(True)
thread.start()
#self.callbacks.timeout(self)
2018-04-18 23:31:17 +02:00
# Set the timeout in seconds
def set_timeout(self, timeout):
2018-04-18 23:31:17 +02:00
self.timeout = float(timeout)
# Set a function that gets called when
# a successfull delivery has been proved
def delivery_callback(self, callback):
self.callbacks.delivery = callback
# Set a function that gets called if the
# delivery times out
def timeout_callback(self, callback):
self.callbacks.timeout = callback
class PacketReceiptCallbacks:
def __init__(self):
self.delivery = None
self.timeout = None