/ RNS / Packet.py
Packet.py
  1  # Reticulum License
  2  #
  3  # Copyright (c) 2016-2025 Mark Qvist
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # - The Software shall not be used in any kind of system which includes amongst
 13  #   its functions the ability to purposefully do harm to human beings.
 14  #
 15  # - The Software shall not be used, directly or indirectly, in the creation of
 16  #   an artificial intelligence, machine learning or language model training
 17  #   dataset, including but not limited to any use that contributes to the
 18  #   training or development of such a model or algorithm.
 19  #
 20  # - The above copyright notice and this permission notice shall be included in
 21  #   all copies or substantial portions of the Software.
 22  #
 23  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 24  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 25  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 26  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 27  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 28  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 29  # SOFTWARE.
 30  
 31  import threading
 32  import struct
 33  import math
 34  import time
 35  import RNS
 36  
 37  class Packet:
 38      """
 39      The Packet class is used to create packet instances that can be sent
 40      over a Reticulum network. Packets will automatically be encrypted if
 41      they are addressed to a ``RNS.Destination.SINGLE`` destination,
 42      ``RNS.Destination.GROUP`` destination or a :ref:`RNS.Link<api-link>`.
 43  
 44      For ``RNS.Destination.GROUP`` destinations, Reticulum will use the
 45      pre-shared key configured for the destination. All packets to group
 46      destinations are encrypted with the same AES-256 key.
 47  
 48      For ``RNS.Destination.SINGLE`` destinations, Reticulum will use a newly
 49      derived ephemeral AES-256 key for every packet.
 50  
 51      For :ref:`RNS.Link<api-link>` destinations, Reticulum will use per-link
 52      ephemeral keys, and offers **Forward Secrecy**.
 53  
 54      :param destination: A :ref:`RNS.Destination<api-destination>` instance to which the packet will be sent.
 55      :param data: The data payload to be included in the packet as *bytes*.
 56      :param create_receipt: Specifies whether a :ref:`RNS.PacketReceipt<api-packetreceipt>` should be created when instantiating the packet.
 57      """
 58  
 59      # Packet types
 60      DATA         = 0x00     # Data packets
 61      ANNOUNCE     = 0x01     # Announces
 62      LINKREQUEST  = 0x02     # Link requests
 63      PROOF        = 0x03     # Proofs
 64      types        = [DATA, ANNOUNCE, LINKREQUEST, PROOF]
 65  
 66      # Header types
 67      HEADER_1     = 0x00     # Normal header format
 68      HEADER_2     = 0x01     # Header format used for packets in transport
 69      header_types = [HEADER_1, HEADER_2]
 70  
 71      # Packet context types
 72      NONE           = 0x00   # Generic data packet
 73      RESOURCE       = 0x01   # Packet is part of a resource
 74      RESOURCE_ADV   = 0x02   # Packet is a resource advertisement
 75      RESOURCE_REQ   = 0x03   # Packet is a resource part request
 76      RESOURCE_HMU   = 0x04   # Packet is a resource hashmap update
 77      RESOURCE_PRF   = 0x05   # Packet is a resource proof
 78      RESOURCE_ICL   = 0x06   # Packet is a resource initiator cancel message
 79      RESOURCE_RCL   = 0x07   # Packet is a resource receiver cancel message
 80      CACHE_REQUEST  = 0x08   # Packet is a cache request
 81      REQUEST        = 0x09   # Packet is a request
 82      RESPONSE       = 0x0A   # Packet is a response to a request
 83      PATH_RESPONSE  = 0x0B   # Packet is a response to a path request
 84      COMMAND        = 0x0C   # Packet is a command
 85      COMMAND_STATUS = 0x0D   # Packet is a status of an executed command
 86      CHANNEL        = 0x0E   # Packet contains link channel data
 87      KEEPALIVE      = 0xFA   # Packet is a keepalive packet
 88      LINKIDENTIFY   = 0xFB   # Packet is a link peer identification proof
 89      LINKCLOSE      = 0xFC   # Packet is a link close message
 90      LINKPROOF      = 0xFD   # Packet is a link packet proof
 91      LRRTT          = 0xFE   # Packet is a link request round-trip time measurement
 92      LRPROOF        = 0xFF   # Packet is a link request proof
 93  
 94      # Context flag values
 95      FLAG_SET       = 0x01
 96      FLAG_UNSET     = 0x00
 97  
 98      # This is used to calculate allowable
 99      # payload sizes
100      HEADER_MAXSIZE = RNS.Reticulum.HEADER_MAXSIZE
101      MDU            = RNS.Reticulum.MDU
102  
103      # With an MTU of 500, the maximum of data we can
104      # send in a single encrypted packet is given by
105      # the below calculation; 383 bytes.
106      ENCRYPTED_MDU  = math.floor((RNS.Reticulum.MDU-RNS.Identity.TOKEN_OVERHEAD-RNS.Identity.KEYSIZE//16)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
107      """
108      The maximum size of the payload data in a single encrypted packet 
109      """
110      PLAIN_MDU      = MDU
111      """
112      The maximum size of the payload data in a single unencrypted packet
113      """
114  
115      TIMEOUT_PER_HOP = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT
116  
117      __slots__  = "hops", "header", "header_type", "packet_type", "transport_type", "context", "context_flag", "destination"
118      __slots__ += "transport_id", "data", "flags", "raw", "packed", "sent", "create_receipt", "receipt", "fromPacked", "MTU"
119      __slots__ += "sent_at", "packet_hash", "ratchet_id", "attached_interface", "receiving_interface", "rssi", "snr", "q"
120      __slots__ += "ciphertext", "plaintext", "destination_hash", "destination_type", "link", "map_hash"
121  
122      def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST,
123                   header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True, context_flag=FLAG_UNSET):
124  
125          if destination != None:
126              if transport_type == None:
127                  transport_type = RNS.Transport.BROADCAST
128  
129              self.header_type    = header_type
130              self.packet_type    = packet_type
131              self.transport_type = transport_type
132              self.context        = context
133              self.context_flag   = context_flag
134  
135              self.hops           = 0;
136              self.destination    = destination
137              self.transport_id   = transport_id
138              self.data           = data
139              self.flags          = self.get_packed_flags()
140  
141              self.raw            = None
142              self.packed         = False
143              self.sent           = False
144              self.create_receipt = create_receipt
145              self.receipt        = None
146              self.fromPacked     = False
147          else:
148              self.raw            = data
149              self.packed         = True
150              self.fromPacked     = True
151              self.create_receipt = False
152  
153          if destination and destination.type == RNS.Destination.LINK:
154              self.MTU     = destination.mtu
155          else:
156              self.MTU     = RNS.Reticulum.MTU
157  
158          self.sent_at     = None
159          self.packet_hash = None
160          self.ratchet_id  = None
161  
162          self.attached_interface = attached_interface
163          self.receiving_interface = None
164          self.rssi = None
165          self.snr = None
166          self.q = None
167  
168      def get_packed_flags(self):
169          if self.context == Packet.LRPROOF:
170              packed_flags = (self.header_type << 6) | (self.context_flag << 5) | (self.transport_type << 4) | (RNS.Destination.LINK << 2) | self.packet_type
171          else:
172              packed_flags = (self.header_type << 6) | (self.context_flag << 5) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
173  
174          return packed_flags
175  
176      def pack(self):
177          self.destination_hash = self.destination.hash
178          self.header = b""
179          self.header += struct.pack("!B", self.flags)
180          self.header += struct.pack("!B", self.hops)
181  
182          if self.context == Packet.LRPROOF:
183              self.header += self.destination.link_id
184              self.ciphertext = self.data
185          else:
186              if self.header_type == Packet.HEADER_1:
187                  self.header += self.destination.hash
188  
189                  if self.packet_type == Packet.ANNOUNCE:
190                      # Announce packets are not encrypted
191                      self.ciphertext = self.data
192                  elif self.packet_type == Packet.LINKREQUEST:
193                      # Link request packets are not encrypted
194                      self.ciphertext = self.data
195                  elif self.packet_type == Packet.PROOF and self.context == Packet.RESOURCE_PRF:
196                      # Resource proofs are not encrypted
197                      self.ciphertext = self.data
198                  elif self.packet_type == Packet.PROOF and self.destination.type == RNS.Destination.LINK:
199                      # Packet proofs over links are not encrypted
200                      self.ciphertext = self.data
201                  elif self.context == Packet.RESOURCE:
202                      # A resource takes care of encryption
203                      # by itself
204                      self.ciphertext = self.data
205                  elif self.context == Packet.KEEPALIVE:
206                      # Keepalive packets contain no actual
207                      # data
208                      self.ciphertext = self.data
209                  elif self.context == Packet.CACHE_REQUEST:
210                      # Cache-requests are not encrypted
211                      self.ciphertext = self.data
212                  else:
213                      # In all other cases, we encrypt the packet
214                      # with the destination's encryption method
215                      self.ciphertext = self.destination.encrypt(self.data)
216                      if hasattr(self.destination, "latest_ratchet_id"):
217                          self.ratchet_id = self.destination.latest_ratchet_id
218  
219              if self.header_type == Packet.HEADER_2:
220                  if self.transport_id != None:
221                      self.header += self.transport_id
222                      self.header += self.destination.hash
223  
224                      if self.packet_type == Packet.ANNOUNCE:
225                          # Announce packets are not encrypted
226                          self.ciphertext = self.data
227                  else:
228                      raise IOError("Packet with header type 2 must have a transport ID")
229  
230  
231          self.header += bytes([self.context])
232          self.raw = self.header + self.ciphertext
233  
234          if len(self.raw) > self.MTU:
235              raise IOError("Packet size of "+str(len(self.raw))+" exceeds MTU of "+str(self.MTU)+" bytes")
236  
237          self.packed = True
238          self.update_hash()
239  
240  
241      def unpack(self):
242          try:
243              self.flags = self.raw[0]
244              self.hops  = self.raw[1]
245  
246              self.header_type      = (self.flags & 0b01000000) >> 6
247              self.context_flag     = (self.flags & 0b00100000) >> 5
248              self.transport_type   = (self.flags & 0b00010000) >> 4
249              self.destination_type = (self.flags & 0b00001100) >> 2
250              self.packet_type      = (self.flags & 0b00000011)
251  
252              DST_LEN = RNS.Reticulum.TRUNCATED_HASHLENGTH//8
253  
254              if self.header_type == Packet.HEADER_2:
255                  self.transport_id = self.raw[2:DST_LEN+2]
256                  self.destination_hash = self.raw[DST_LEN+2:2*DST_LEN+2]
257                  self.context = ord(self.raw[2*DST_LEN+2:2*DST_LEN+3])
258                  self.data = self.raw[2*DST_LEN+3:]
259              else:
260                  self.transport_id = None
261                  self.destination_hash = self.raw[2:DST_LEN+2]
262                  self.context = ord(self.raw[DST_LEN+2:DST_LEN+3])
263                  self.data = self.raw[DST_LEN+3:]
264  
265              self.packed = False
266              self.update_hash()
267              return True
268  
269          except Exception as e:
270              RNS.log("Received malformed packet, dropping it. The contained exception was: "+str(e), RNS.LOG_EXTREME)
271              return False
272  
273      def send(self):
274          """
275          Sends the packet.
276          
277          :returns: A :ref:`RNS.PacketReceipt<api-packetreceipt>` instance if *create_receipt* was set to *True* when the packet was instantiated, if not returns *None*. If the packet could not be sent *False* is returned.
278          """
279          if not self.sent:
280              if self.destination.type == RNS.Destination.LINK:
281                  if self.destination.status == RNS.Link.CLOSED:
282                      RNS.log("Attempt to transmit over a closed link, dropping packet", RNS.LOG_DEBUG)
283                      self.sent = False
284                      self.receipt = None
285                      return False
286  
287                  else:
288                      self.destination.last_outbound = time.time()
289                      self.destination.tx += 1
290                      self.destination.txbytes += len(self.data)
291  
292              if not self.packed:
293                  self.pack()
294  
295              if RNS.Transport.outbound(self):
296                  return self.receipt
297              else:
298                  RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
299                  self.sent = False
300                  self.receipt = None
301                  return False
302                  
303          else:
304              raise IOError("Packet was already sent")
305  
306      def resend(self):
307          """
308          Re-sends the packet.
309          
310          :returns: A :ref:`RNS.PacketReceipt<api-packetreceipt>` instance if *create_receipt* was set to *True* when the packet was instantiated, if not returns *None*. If the packet could not be sent *False* is returned.
311          """
312          if self.sent:
313              # Re-pack the packet to obtain new ciphertext for
314              # encrypted destinations
315              self.pack()
316              
317              if RNS.Transport.outbound(self):
318                  return self.receipt
319              else:
320                  RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
321                  self.sent = False
322                  self.receipt = None
323                  return False
324          else:
325              raise IOError("Packet was not sent yet")
326  
327      def prove(self, destination=None):
328          if self.fromPacked and hasattr(self, "destination") and self.destination:
329              if self.destination.identity and self.destination.identity.prv:
330                  self.destination.identity.prove(self, destination)
331          elif self.fromPacked and hasattr(self, "link") and self.link:
332              self.link.prove_packet(self)
333          else:
334              RNS.log("Could not prove packet associated with neither a destination nor a link", RNS.LOG_ERROR)
335  
336      # Generates a special destination that allows Reticulum
337      # to direct the proof back to the proved packet's sender
338      def generate_proof_destination(self):
339          return ProofDestination(self)
340  
341      def validate_proof_packet(self, proof_packet):
342          return self.receipt.validate_proof_packet(proof_packet)
343  
344      def validate_proof(self, proof):
345          return self.receipt.validate_proof(proof)
346  
347      def update_hash(self):
348          self.packet_hash = self.get_hash()
349  
350      def get_hash(self):
351          return RNS.Identity.full_hash(self.get_hashable_part())
352  
353      def getTruncatedHash(self):
354          return RNS.Identity.truncated_hash(self.get_hashable_part())
355  
356      def get_hashable_part(self):
357          hashable_part = bytes([self.raw[0] & 0b00001111])
358          if self.header_type == Packet.HEADER_2:
359              hashable_part += self.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:]
360          else:
361              hashable_part += self.raw[2:]
362  
363          return hashable_part
364  
365      def get_rssi(self):
366          """
367          :returns: The physical layer *Received Signal Strength Indication* if available, otherwise ``None``.
368          """
369          if self.rssi != None:
370              return self.rssi
371          else:
372              return reticulum.get_packet_rssi(self.packet_hash)
373              
374      def get_snr(self):
375          """
376          :returns: The physical layer *Signal-to-Noise Ratio* if available, otherwise ``None``.
377          """
378          if self.snr != None:
379              return self.snr
380          else:
381              return reticulum.get_packet_snr(self.packet_hash)
382  
383      def get_q(self):
384          """
385          :returns: The physical layer *Link Quality* if available, otherwise ``None``.
386          """
387          if self.q != None:
388              return self.q
389          else:
390              return reticulum.get_packet_q(self.packet_hash)
391  
392  class ProofDestination:
393      def __init__(self, packet):
394          self.hash = packet.get_hash()[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8];
395          self.type = RNS.Destination.SINGLE
396  
397      def encrypt(self, plaintext):
398          return plaintext
399  
400  
401  class PacketReceipt:
402      """
403      The PacketReceipt class is used to receive notifications about
404      :ref:`RNS.Packet<api-packet>` instances sent over the network. Instances
405      of this class are never created manually, but always returned from
406      the *send()* method of a :ref:`RNS.Packet<api-packet>` instance.
407      """
408      # Receipt status constants
409      FAILED    = 0x00
410      SENT      = 0x01
411      DELIVERED = 0x02
412      CULLED    = 0xFF
413  
414  
415      EXPL_LENGTH = RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8
416      IMPL_LENGTH = RNS.Identity.SIGLENGTH//8
417  
418      # Creates a new packet receipt from a sent packet
419      def __init__(self, packet):
420          self.hash           = packet.get_hash()
421          self.truncated_hash = packet.getTruncatedHash()
422          self.sent           = True
423          self.sent_at        = time.time()
424          self.proved         = False
425          self.status         = PacketReceipt.SENT
426          self.destination    = packet.destination
427          self.callbacks      = PacketReceiptCallbacks()
428          self.concluded_at   = None
429          self.proof_packet   = None
430  
431          if packet.destination.type == RNS.Destination.LINK:
432              self.timeout    = max(packet.destination.rtt * packet.destination.traffic_timeout_factor, RNS.Link.TRAFFIC_TIMEOUT_MIN_MS/1000)
433          else:
434              self.timeout    = RNS.Reticulum.get_instance().get_first_hop_timeout(self.destination.hash)
435              self.timeout   += Packet.TIMEOUT_PER_HOP * RNS.Transport.hops_to(self.destination.hash)
436  
437      def get_status(self):
438          """
439          :returns: The status of the associated :ref:`RNS.Packet<api-packet>` instance. Can be one of ``RNS.PacketReceipt.SENT``, ``RNS.PacketReceipt.DELIVERED``, ``RNS.PacketReceipt.FAILED`` or ``RNS.PacketReceipt.CULLED``. 
440          """
441          return self.status
442  
443      # Validate a proof packet
444      def validate_proof_packet(self, proof_packet):
445          if hasattr(proof_packet, "link") and proof_packet.link:
446              return self.validate_link_proof(proof_packet.data, proof_packet.link, proof_packet)
447          else:
448              return self.validate_proof(proof_packet.data, proof_packet)
449  
450      # Validate a raw proof for a link
451      def validate_link_proof(self, proof, link, proof_packet=None):
452          # TODO: Hardcoded as explicit proofs for now
453          if True or len(proof) == PacketReceipt.EXPL_LENGTH:
454              # This is an explicit proof
455              proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
456              signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
457              if proof_hash == self.hash:
458                  proof_valid = link.validate(signature, self.hash)
459                  if proof_valid:
460                      self.status = PacketReceipt.DELIVERED
461                      self.proved = True
462                      self.concluded_at = time.time()
463                      self.proof_packet = proof_packet
464                      link.last_proof = self.concluded_at
465  
466                      if self.callbacks.delivery != None:
467                          try:
468                              self.callbacks.delivery(self)
469                          except Exception as e:
470                              RNS.log("An error occurred while evaluating external delivery callback for "+str(link), RNS.LOG_ERROR)
471                              RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
472                              RNS.trace_exception(e)
473                              
474                      return True
475                  else:
476                      return False
477              else:
478                  return False
479          elif len(proof) == PacketReceipt.IMPL_LENGTH:
480              pass
481              # TODO: Why is this disabled?
482              # signature = proof[:RNS.Identity.SIGLENGTH//8]
483              # proof_valid = self.link.validate(signature, self.hash)
484              # if proof_valid:
485              #       self.status = PacketReceipt.DELIVERED
486              #       self.proved = True
487              #       self.concluded_at = time.time()
488              #       if self.callbacks.delivery != None:
489              #           self.callbacks.delivery(self)
490              #       RNS.log("valid")
491              #       return True
492              # else:
493              #   RNS.log("invalid")
494              #   return False
495          else:
496              return False
497  
498      # Validate a raw proof
499      def validate_proof(self, proof, proof_packet=None):
500          if len(proof) == PacketReceipt.EXPL_LENGTH:
501              # This is an explicit proof
502              proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
503              signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
504              if proof_hash == self.hash and hasattr(self.destination, "identity") and self.destination.identity != None:
505                  proof_valid = self.destination.identity.validate(signature, self.hash)
506                  if proof_valid:
507                      self.status = PacketReceipt.DELIVERED
508                      self.proved = True
509                      self.concluded_at = time.time()
510                      self.proof_packet = proof_packet
511  
512                      if self.callbacks.delivery != None:
513                          try:
514                              self.callbacks.delivery(self)
515                          except Exception as e:
516                              RNS.log("Error while executing proof validated callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
517  
518                      return True
519                  else:
520                      return False
521              else:
522                  return False
523          elif len(proof) == PacketReceipt.IMPL_LENGTH:
524              # This is an implicit proof
525  
526              if not hasattr(self.destination, "identity"):
527                  return False
528  
529              if self.destination.identity == None:
530                  return False
531  
532              signature = proof[:RNS.Identity.SIGLENGTH//8]
533              proof_valid = self.destination.identity.validate(signature, self.hash)
534              if proof_valid:
535                      self.status = PacketReceipt.DELIVERED
536                      self.proved = True
537                      self.concluded_at = time.time()
538                      self.proof_packet = proof_packet
539  
540                      if self.callbacks.delivery != None:
541                          try:
542                              self.callbacks.delivery(self)
543                          except Exception as e:
544                              RNS.log("Error while executing proof validated callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
545                              
546                      return True
547              else:
548                  return False
549          else:
550              return False
551  
552      def get_rtt(self):
553          """
554          :returns: The round-trip-time in seconds
555          """
556          return self.concluded_at - self.sent_at
557  
558      def is_timed_out(self):
559          return (self.sent_at+self.timeout < time.time())
560  
561      def check_timeout(self):
562          if self.status == PacketReceipt.SENT and self.is_timed_out():
563              if self.timeout == -1:
564                  self.status = PacketReceipt.CULLED
565              else:
566                  self.status = PacketReceipt.FAILED
567  
568              self.concluded_at = time.time()
569  
570              if self.callbacks.timeout:
571                  thread = threading.Thread(target=self.callbacks.timeout, args=(self,))
572                  thread.daemon = True
573                  thread.start()
574  
575  
576      def set_timeout(self, timeout):
577          """
578          Sets a timeout in seconds
579          
580          :param timeout: The timeout in seconds.
581          """
582          self.timeout = float(timeout)
583  
584      def set_delivery_callback(self, callback):
585          """
586          Sets a function that gets called if a successfull delivery has been proven.
587  
588          :param callback: A *callable* with the signature *callback(packet_receipt)*
589          """
590          self.callbacks.delivery = callback
591  
592      # Set a function that gets called if the
593      # delivery times out
594      def set_timeout_callback(self, callback):
595          """
596          Sets a function that gets called if the delivery times out.
597  
598          :param callback: A *callable* with the signature *callback(packet_receipt)*
599          """
600          self.callbacks.timeout = callback
601  
602  class PacketReceiptCallbacks:
603      def __init__(self):
604          self.delivery = None
605          self.timeout  = None