/ 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: self.pack()
293  
294              if RNS.Transport.outbound(self): return self.receipt
295              else:
296                  RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
297                  self.sent = False
298                  self.receipt = None
299                  return False
300                  
301          else:
302              raise IOError("Packet was already sent")
303  
304      def resend(self):
305          """
306          Re-sends the packet.
307          
308          :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.
309          """
310          if self.sent:
311              # Re-pack the packet to obtain new ciphertext for
312              # encrypted destinations
313              self.pack()
314              
315              if RNS.Transport.outbound(self):
316                  return self.receipt
317              else:
318                  RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
319                  self.sent = False
320                  self.receipt = None
321                  return False
322          else:
323              raise IOError("Packet was not sent yet")
324  
325      def prove(self, destination=None):
326          if self.fromPacked and hasattr(self, "destination") and self.destination:
327              if self.destination.identity and self.destination.identity.prv:
328                  self.destination.identity.prove(self, destination)
329          elif self.fromPacked and hasattr(self, "link") and self.link:
330              self.link.prove_packet(self)
331          else:
332              RNS.log("Could not prove packet associated with neither a destination nor a link", RNS.LOG_ERROR)
333  
334      # Generates a special destination that allows Reticulum
335      # to direct the proof back to the proved packet's sender
336      def generate_proof_destination(self):
337          return ProofDestination(self)
338  
339      def validate_proof_packet(self, proof_packet):
340          return self.receipt.validate_proof_packet(proof_packet)
341  
342      def validate_proof(self, proof):
343          return self.receipt.validate_proof(proof)
344  
345      def update_hash(self):
346          self.packet_hash = self.get_hash()
347  
348      def get_hash(self):
349          return RNS.Identity.full_hash(self.get_hashable_part())
350  
351      def getTruncatedHash(self):
352          return RNS.Identity.truncated_hash(self.get_hashable_part())
353  
354      def get_hashable_part(self):
355          hashable_part = bytes([self.raw[0] & 0b00001111])
356          if self.header_type == Packet.HEADER_2:
357              hashable_part += self.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:]
358          else:
359              hashable_part += self.raw[2:]
360  
361          return hashable_part
362  
363      def get_rssi(self):
364          """
365          :returns: The physical layer *Received Signal Strength Indication* if available, otherwise ``None``.
366          """
367          if self.rssi != None:
368              return self.rssi
369          else:
370              return reticulum.get_packet_rssi(self.packet_hash)
371              
372      def get_snr(self):
373          """
374          :returns: The physical layer *Signal-to-Noise Ratio* if available, otherwise ``None``.
375          """
376          if self.snr != None:
377              return self.snr
378          else:
379              return reticulum.get_packet_snr(self.packet_hash)
380  
381      def get_q(self):
382          """
383          :returns: The physical layer *Link Quality* if available, otherwise ``None``.
384          """
385          if self.q != None:
386              return self.q
387          else:
388              return reticulum.get_packet_q(self.packet_hash)
389  
390  class ProofDestination:
391      def __init__(self, packet):
392          self.hash = packet.get_hash()[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8];
393          self.type = RNS.Destination.SINGLE
394  
395      def encrypt(self, plaintext):
396          return plaintext
397  
398  
399  class PacketReceipt:
400      """
401      The PacketReceipt class is used to receive notifications about
402      :ref:`RNS.Packet<api-packet>` instances sent over the network. Instances
403      of this class are never created manually, but always returned from
404      the *send()* method of a :ref:`RNS.Packet<api-packet>` instance.
405      """
406      # Receipt status constants
407      FAILED    = 0x00
408      SENT      = 0x01
409      DELIVERED = 0x02
410      CULLED    = 0xFF
411  
412  
413      EXPL_LENGTH = RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8
414      IMPL_LENGTH = RNS.Identity.SIGLENGTH//8
415  
416      # Creates a new packet receipt from a sent packet
417      def __init__(self, packet):
418          self.hash           = packet.get_hash()
419          self.truncated_hash = packet.getTruncatedHash()
420          self.sent           = True
421          self.sent_at        = time.time()
422          self.proved         = False
423          self.status         = PacketReceipt.SENT
424          self.destination    = packet.destination
425          self.callbacks      = PacketReceiptCallbacks()
426          self.concluded_at   = None
427          self.proof_packet   = None
428  
429          if packet.destination.type == RNS.Destination.LINK:
430              self.timeout    = max(packet.destination.rtt * packet.destination.traffic_timeout_factor, RNS.Link.TRAFFIC_TIMEOUT_MIN_MS/1000)
431          else:
432              self.timeout    = RNS.Reticulum.get_instance().get_first_hop_timeout(self.destination.hash)
433              self.timeout   += Packet.TIMEOUT_PER_HOP * RNS.Transport.hops_to(self.destination.hash)
434  
435      def get_status(self):
436          """
437          :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``. 
438          """
439          return self.status
440  
441      # Validate a proof packet
442      def validate_proof_packet(self, proof_packet):
443          if hasattr(proof_packet, "link") and proof_packet.link:
444              return self.validate_link_proof(proof_packet.data, proof_packet.link, proof_packet)
445          else:
446              return self.validate_proof(proof_packet.data, proof_packet)
447  
448      # Validate a raw proof for a link
449      def validate_link_proof(self, proof, link, proof_packet=None):
450          # TODO: Hardcoded as explicit proofs for now
451          if True or len(proof) == PacketReceipt.EXPL_LENGTH:
452              # This is an explicit proof
453              proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
454              signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
455              if proof_hash == self.hash:
456                  proof_valid = link.validate(signature, self.hash)
457                  if proof_valid:
458                      self.status = PacketReceipt.DELIVERED
459                      self.proved = True
460                      self.concluded_at = time.time()
461                      self.proof_packet = proof_packet
462                      link.last_proof = self.concluded_at
463  
464                      if self.callbacks.delivery != None:
465                          try:
466                              self.callbacks.delivery(self)
467                          except Exception as e:
468                              RNS.log("An error occurred while evaluating external delivery callback for "+str(link), RNS.LOG_ERROR)
469                              RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
470                              RNS.trace_exception(e)
471                              
472                      return True
473                  else:
474                      return False
475              else:
476                  return False
477          elif len(proof) == PacketReceipt.IMPL_LENGTH:
478              pass
479              # TODO: Why is this disabled?
480              # signature = proof[:RNS.Identity.SIGLENGTH//8]
481              # proof_valid = self.link.validate(signature, self.hash)
482              # if proof_valid:
483              #       self.status = PacketReceipt.DELIVERED
484              #       self.proved = True
485              #       self.concluded_at = time.time()
486              #       if self.callbacks.delivery != None:
487              #           self.callbacks.delivery(self)
488              #       RNS.log("valid")
489              #       return True
490              # else:
491              #   RNS.log("invalid")
492              #   return False
493          else:
494              return False
495  
496      # Validate a raw proof
497      def validate_proof(self, proof, proof_packet=None):
498          if len(proof) == PacketReceipt.EXPL_LENGTH:
499              # This is an explicit proof
500              proof_hash = proof[:RNS.Identity.HASHLENGTH//8]
501              signature = proof[RNS.Identity.HASHLENGTH//8:RNS.Identity.HASHLENGTH//8+RNS.Identity.SIGLENGTH//8]
502              if proof_hash == self.hash and hasattr(self.destination, "identity") and self.destination.identity != None:
503                  proof_valid = self.destination.identity.validate(signature, self.hash)
504                  if proof_valid:
505                      self.status = PacketReceipt.DELIVERED
506                      self.proved = True
507                      self.concluded_at = time.time()
508                      self.proof_packet = proof_packet
509  
510                      if self.callbacks.delivery != None:
511                          try:
512                              self.callbacks.delivery(self)
513                          except Exception as e:
514                              RNS.log("Error while executing proof validated callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
515  
516                      return True
517                  else:
518                      return False
519              else:
520                  return False
521          elif len(proof) == PacketReceipt.IMPL_LENGTH:
522              # This is an implicit proof
523  
524              if not hasattr(self.destination, "identity"):
525                  return False
526  
527              if self.destination.identity == None:
528                  return False
529  
530              signature = proof[:RNS.Identity.SIGLENGTH//8]
531              proof_valid = self.destination.identity.validate(signature, self.hash)
532              if proof_valid:
533                      self.status = PacketReceipt.DELIVERED
534                      self.proved = True
535                      self.concluded_at = time.time()
536                      self.proof_packet = proof_packet
537  
538                      if self.callbacks.delivery != None:
539                          try:
540                              self.callbacks.delivery(self)
541                          except Exception as e:
542                              RNS.log("Error while executing proof validated callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
543                              
544                      return True
545              else:
546                  return False
547          else:
548              return False
549  
550      def get_rtt(self):
551          """
552          :returns: The round-trip-time in seconds
553          """
554          return self.concluded_at - self.sent_at
555  
556      def is_timed_out(self):
557          return (self.sent_at+self.timeout < time.time())
558  
559      def check_timeout(self):
560          if self.status == PacketReceipt.SENT and self.is_timed_out():
561              if self.timeout == -1:
562                  self.status = PacketReceipt.CULLED
563              else:
564                  self.status = PacketReceipt.FAILED
565  
566              self.concluded_at = time.time()
567  
568              if self.callbacks.timeout:
569                  thread = threading.Thread(target=self.callbacks.timeout, args=(self,))
570                  thread.daemon = True
571                  thread.start()
572  
573  
574      def set_timeout(self, timeout):
575          """
576          Sets a timeout in seconds
577          
578          :param timeout: The timeout in seconds.
579          """
580          self.timeout = float(timeout)
581  
582      def set_delivery_callback(self, callback):
583          """
584          Sets a function that gets called if a successfull delivery has been proven.
585  
586          :param callback: A *callable* with the signature *callback(packet_receipt)*
587          """
588          self.callbacks.delivery = callback
589  
590      # Set a function that gets called if the
591      # delivery times out
592      def set_timeout_callback(self, callback):
593          """
594          Sets a function that gets called if the delivery times out.
595  
596          :param callback: A *callable* with the signature *callback(packet_receipt)*
597          """
598          self.callbacks.timeout = callback
599  
600  class PacketReceiptCallbacks:
601      def __init__(self):
602          self.delivery = None
603          self.timeout  = None