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