From 7be9728ce6c9f42352fcec342db0aa967778a69e Mon Sep 17 00:00:00 2001 From: trecker Date: Thu, 13 Mar 2025 12:03:35 -0400 Subject: [PATCH 01/16] initial import --- pyproject.toml | 6 + scapy/contrib/dtn/bpv7.py | 780 ++++++++++++++++++++++++++++++++++++ scapy/contrib/dtn/cbor.py | 269 +++++++++++++ scapy/contrib/dtn/common.py | 34 ++ scapy/contrib/dtn/tcpcl.py | 245 +++++++++++ 5 files changed, 1334 insertions(+) create mode 100644 scapy/contrib/dtn/bpv7.py create mode 100644 scapy/contrib/dtn/cbor.py create mode 100644 scapy/contrib/dtn/common.py create mode 100644 scapy/contrib/dtn/tcpcl.py diff --git a/pyproject.toml b/pyproject.toml index a1b8d2423bd..b86a7482f1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,12 +58,18 @@ all = [ "pyx", "cryptography>=2.0", "matplotlib", + "flynn>=1.0.0.b2", + "crcmod>=1.7" ] doc = [ "sphinx>=7.0.0", "sphinx_rtd_theme>=1.3.0", "tox>=3.0.0", ] +dtn = [ + "flynn>=1.0.0.b2", + "crcmod>=1.7" +] # setuptools specific diff --git a/scapy/contrib/dtn/bpv7.py b/scapy/contrib/dtn/bpv7.py new file mode 100644 index 00000000000..2ae2e48cd6c --- /dev/null +++ b/scapy/contrib/dtn/bpv7.py @@ -0,0 +1,780 @@ +# scapy.contrib.description = Bundle Protocol version 7 (BPv7) +# scappy.contrib.status = loads + +from scapy.packet import Packet +from scapy.fields import ( + PacketField, + MultipleTypeField, + BitEnumField, + PacketListField, + ConditionalField, + FieldListField, + BitField, + BitFieldLenField +) +from scapy.all import raw +from scapy.contrib.dtn.cbor import ( + CBORInteger, + CBORByteString, + CBORIntOrText, + CBORArray, + CBORNull, + CBORStopCode, + CBORAny, + CBORPacketField, + CBORPacketFieldWithRemain +) +import scapy.contrib.dtn.common as Common + +import time +import crcmod.predefined +from enum import IntFlag +from typing import Tuple, List +import re + + +class InvalidCRCType(Exception): + """ + Exception raised when an invalid CRC type code is + encountered. + + Attributes: + type_code: the invalid type code + """ + def __init__(self, type_code): + super().__init__(f'Tried to compute a CRC using an invalid type code: {type_code}') + + +def compute_crc(crc_type:int, pkt:bytes, ignore_existing:bool=True): + # prepare parameters + if (crc_type == CrcTypes.CRC32C): + size = 4 + crcfun = crcmod.predefined.mkCrcFun('crc-32c') + elif (crc_type == CrcTypes.CRC16): + size = 2 + crcfun = crcmod.predefined.mkCrcFun('x-25') + else: + raise InvalidCRCType(crc_type) + + crc_index = len(pkt) - size + + # Wipe anything in existing crc field + if ignore_existing: + pkt = pkt[:crc_index] + b'\x00' * size + + return crcfun(pkt).to_bytes(size, 'big'), crc_index + + +class PacketFieldWithRemain(PacketField): + """ + The regular Packet.getfield() never returns the remaining bytes, so the CRC or + other following fields get lost. This getfield does return the remaining bytes. + """ + def getfield(self, pkt:Packet, s:bytes)->Tuple[bytes, Packet]: + i = self.m2i(pkt, s) + remain_size = len(s) - len(raw(i)) + remain=s[-remain_size:] + return remain, i + + +class IPN(CBORArray): + fields_desc= CBORArray.fields_desc + [ + CBORInteger("node_id", 1), + CBORInteger("service_number", 1), + ] + + ipn_re = re.compile(r"ipn:(.*)\.(.*)") + + # pylint: disable=W0201 + # field (instance variable) initialization is handled via "fields_desc" + def from_string(self, ipn_str:str): + result = IPN.ipn_re.search(ipn_str) + self.node_id = int(result.group(1)) + self.service_number = int(result.group(2)) + + return self + + def __str__(self): + return f"ipn:{self.node_id}.{self.service_number}" + + def __eq__(self, other): + if isinstance(other, IPN): + return (self.node_id == other.node_id) and (self.service_number == other.service_number) + return False + +class DTN(Packet): + fields_desc=[ + CBORIntOrText("uri", 0) # can be 0 (type Int) or a string (type String) + ] + + def extract_padding(self, s): + return "", s + + def __eq__(self, other): + if isinstance(other, DTN): + return self.uri == other.uri + return False + + +class EndpointID(CBORArray): + fields_desc = CBORArray.fields_desc + [ + CBORInteger("scheme_code", 2), + MultipleTypeField( + [ + (PacketFieldWithRemain("ssp", DTN(), DTN), lambda pkt: pkt.scheme_code == 1), + (PacketFieldWithRemain("ssp", IPN(), IPN), lambda pkt: pkt.scheme_code == 2) + ], + PacketFieldWithRemain("ssp", IPN(), IPN) + ) + ] + + def dissect(self, s:bytes): + """ This dissect doesn't process the payload, because there is none. """ + s = self.pre_dissect(s) + s = self.do_dissect(s) + s = self.post_dissect(s) + + def __eq__(self, other): + if isinstance(other, EndpointID): + return (self.scheme_code == other.scheme_code) and (self.ssp == other.ssp) + return False + +# pylint: disable=R0903 +# Packet types are not intended to have many(any) public functions +class Timestamp(CBORArray): + fields_desc = CBORArray.fields_desc + [ + CBORInteger("t", 0), + CBORInteger("seq", 0) + ] + + def __ge__(self, other): + if isinstance(other, Timestamp): + return (self.t >= other.t) and (self.seq >= other.seq) + return False + + def __gt__(self, other): + if isinstance(other, Timestamp): + return (self.t > other.t) or (self.t==other.t and (self.seq > other.seq)) + return False + +class BlockTypes: + """ + Bundle block type codes + """ + PRIMARY=0 + PAYLOAD=1 + AUTHENTICATION=2 + INTEGRITY=3 + CONFIDENTIALITY=4 + PREV_HOP=5 + PREV_NODE=6 + AGE=7 + HOP_COUNT=10 + BLOCK_INTEGRITY=11 + BLOCK_CONFIDENTIALITY=12 + +class CrcTypes: + """ + Bundle CRC type codes + """ + NONE = 0 + CRC16 = 1 + CRC32C = 2 + + +# pylint: disable=R0903 +# Packet types are not intended to have many(any) public functions +class SecurityTargets(CBORArray): + fields_desc = [ + CBORArray._major_type, + BitFieldLenField("add", 0, 5, count_of="targets"), + FieldListField("targets", [0], CBORInteger("tgt", 0), count_from=lambda pkt: pkt.add) + ] + + def count_additional_fields(self): + return len(self.getfieldval("targets")) + + +class CBORTuple(CBORArray): + """ + A pair of CBOR integers consisting of [id, value]. + """ + fields_desc = CBORArray.fields_desc + [ + CBORInteger("id", 0), + CBORAny("value", b'\x00') + ] + + +class CBORTupleArray(CBORArray): + fields_desc = [ + CBORArray._major_type, + BitFieldLenField("add", None, 5, count_of="tuples"), + PacketListField("tuples", [CBORTuple()], CBORTuple, count_from=lambda pkt: pkt.add) + ] + + def find_value_with_id(self, target_id:int): + """ Find the tuple with the specified id and return the value. """ + try: + tup = next(x for x in self.tuples if x.id == target_id) + except StopIteration: + return None + + return tup.value + + def count_additional_fields(self)->int: + return len(self.getfieldval("tuples")) + + +class SecurityResults(CBORArray): + """ A CBOR array of CBORTupleArrays. """ + fields_desc = [ + CBORArray._major_type, + BitFieldLenField("add", None, 5, count_of="results"), + PacketListField("results", [CBORTupleArray()], CBORTupleArray, count_from=lambda pkt: pkt.add) + ] + + def count_additional_fields(self): + return len(self.getfieldval("results")) + + +class AbstractSecurityBlock(Packet): + """ + The structure of the security-specific parts of the BIB and BCB are identical + and are defined here. This structure will reside in the block-specific data + field of a BPv7 canonical block. + """ + fields_desc = [ + PacketField("security_targets", SecurityTargets(), SecurityTargets), + CBORInteger("security_context_id", 0), + CBORInteger("security_context_flags", 1), + PacketField("security_source", EndpointID(), EndpointID), + ConditionalField(PacketField("security_context_parameters", CBORTupleArray(), CBORTupleArray), + lambda p: (p.security_context_flags & 1)), + PacketField("security_results", SecurityResults(), SecurityResults) + ] + + def dissect(self, s:bytes): + """ This dissect doesn't process the payload, because there is none. """ + s = self.pre_dissect(s) + s = self.do_dissect(s) + s = self.post_dissect(s) + + +class CanonicalBlock(CBORArray): + class CtrlFlags(IntFlag): + """ + Block Processing Control Flags + """ + BLOCK_MUST_BE_REPLICATED = 0x01 + REPORT_IF_UNPROCESSABLE = 0x02 + DELETE_BUNDLE_IF_UNPROCESSED = 0x04 + DISCARD_IF_NOT_PROCESSED = 0x010 + + TypeCodes = { + BlockTypes.PAYLOAD: "payload", + BlockTypes.AUTHENTICATION: "authentication", + BlockTypes.INTEGRITY: "integrity", + BlockTypes.CONFIDENTIALITY: "confidentiality", + BlockTypes.PREV_HOP: "prev_hop", + BlockTypes.PREV_NODE: "prev_node", + BlockTypes.AGE: "age", + BlockTypes.HOP_COUNT: "hop_count", + BlockTypes.BLOCK_INTEGRITY: "block_integrity", + BlockTypes.BLOCK_CONFIDENTIALITY: "block_confidentiality" + } + + fields_template: Common.FieldsTemplate = { + 'type_code': BitEnumField("type_code", BlockTypes.PAYLOAD, 8, TypeCodes), + 'block_number': CBORInteger("block_number", 1), + 'flags': CBORInteger("flags", 0), + 'crc_type': CBORInteger("crc_type", CrcTypes.CRC32C), + 'data': CBORByteString("data", b'\xDE\xAD\xBE\xEF'), + 'crc': ConditionalField( + MultipleTypeField( + [ + (CBORByteString("crc", b"\x00\x00"), lambda pkt: pkt.crc_type == CrcTypes.CRC16), + (CBORByteString("crc", b"\x00\x00\x00\x00"), lambda pkt: pkt.crc_type == CrcTypes.CRC32C) + ], + CBORNull("crc", None) + ), lambda pkt: pkt.crc_type != CrcTypes.NONE + ) + } + + fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + encrypted = False + encrypted_by = [] + + def get_header(self)->bytes: + return raw(self)[1:4] + + def post_dissect(self, s): + """ + Because some block elements--such as CRCs and CBOR array headers--are added to the + raw representation via overriding the post_build method (and correspondingly removed + during pre_dissect), the raw packet cache must be cleared. Otherwise, some important + methods will be broken for Blocks built from sniffed packets; for example, + `raw(Bundle(raw_bytes_received_from_socket))` will not produce valid bundle bytes. + See the comment linked below and the subsequent comment with a solution copied here. + + https://github.com/secdev/scapy/issues/1021#issuecomment-704472941 + """ + self.raw_packet_cache = None # Reset packet to allow post_build + return s + + def post_build(self, pkt, pay): + pkt = self.set_additional_fields(pkt) + + if self.crc_type != CrcTypes.NONE: + crc, index = compute_crc(self.crc_type, pkt) + pkt = pkt[:index] + crc + + return pkt + pay + + def get_block_bytes(self) -> bytes: + return raw(self) + + def count_additional_fields(self): + return 5 if self.crc_type == CrcTypes.NONE else 6 + + +class PayloadBlock(CanonicalBlock): + """ + Contains the bundle payload. + """ + fields_template = Common.template_replace(CanonicalBlock.fields_template, { + 'type_code': BitEnumField("type_code", BlockTypes.PAYLOAD, 8, CanonicalBlock.TypeCodes) + }) + + fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + + +# pylint: disable=R0901 +class EncryptedPayloadBlock(PayloadBlock): + """ + Contains the bundle payload. The data field is encrypted. + """ + encrypted = True + + +class PreviousNodeBlock(CanonicalBlock): + """ + Contains the ID of the node that forwarded this bundle. + """ + fields_template = Common.template_replace(CanonicalBlock.fields_template, { + 'type_code': BitEnumField("type_code", BlockTypes.PREV_NODE, 8, CanonicalBlock.TypeCodes), + 'data': CBORPacketField("data", EndpointID(), EndpointID) + }) + + fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + + +# pylint: disable=R0901 +class EncryptedPreviousNodeBlock(PreviousNodeBlock): + """ + Contains the ID of the node that forwarded this bundle. The data field is encrypted. + """ + fields_template = Common.template_replace(PreviousNodeBlock.fields_template, { + # The data field defintion from the parent class cannot be used here. That data + # is now encrypted and cannot be decrypted to its original bytes (within the + # scope of this module), so the Packet that it represents cannot be dissected. + # Instead, the data field defintion from CanonicalBlock is used. + 'data': CBORByteString("data", b'\xDE\xAD\xBE\xEF') + }) + + fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + encrypted = True + + +class BundleAge(Packet): + fields_desc = [ CBORInteger("age", 0) ] + + def __eq__(self, other): + if isinstance(other, BundleAge): + return self.age == other.age + return False + + def dissect(self, s:bytes): + """ This dissect doesn't process the payload, because there is none. """ + s = self.pre_dissect(s) + s = self.do_dissect(s) + s = self.post_dissect(s) + + +class BundleAgeBlock(CanonicalBlock): + """ + Contains the number of milliseconds that have elapsed between the time the + bundle was created and the time at which it was most recently forwarded. + """ + fields_template = Common.template_replace(CanonicalBlock.fields_template, { + 'type_code': BitEnumField("type_code", BlockTypes.AGE, 8, CanonicalBlock.TypeCodes), + 'data': CBORPacketFieldWithRemain("data", BundleAge(), BundleAge) + }) + + fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + + +# pylint: disable=R0901 +class EncryptedBundleAgeBlock(BundleAgeBlock): + """ + Contains the number of milliseconds that have elapsed between the time the + bundle was created and the time at which it was most recently forwarded. + The data field is encrypted. + """ + fields_template = Common.template_replace(BundleAgeBlock.fields_template, { + 'data': CBORByteString("data", b'\xDE\xAD\xBE\xEF') + }) + + fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + encrypted = True + + +class HopCount(CBORArray): + fields_desc = CBORArray.fields_desc + [ + CBORInteger("limit", 0), + CBORInteger("count", 0) + ] + + def __eq__(self, other): + if isinstance(other, HopCount): + return (self.limit == other.limit) and (self.count == other.count) + return False + + +class HopCountBlock(CanonicalBlock): + """ + Contains information on the Bundle's allowed number of hops and the hops that + have already happened. + """ + fields_template = Common.template_replace(CanonicalBlock.fields_template, { + 'type_code': BitEnumField("type_code", BlockTypes.HOP_COUNT, 8, CanonicalBlock.TypeCodes), + 'data': CBORPacketField("data", HopCount(), HopCount) + }) + + fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + + +# pylint: disable=R0901 +class EncryptedHopCountBlock(HopCountBlock): + """ + Contains information on the Bundle's allowed number of hops and the hops that + have already happened. The data field is encrypted. + """ + fields_template = Common.template_replace(HopCountBlock.fields_template, { + 'data': CBORByteString("data", b'\xDE\xAD\xBE\xEF') + }) + + fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + encrypted = True + + +class BlockIntegrityBlock(CanonicalBlock): + """ + This defines a CanonicalBlock with its type code as 11 and an + AbstractSecurityBlock as its data field. + """ + fields_template = Common.template_replace(CanonicalBlock.fields_template, { + 'type_code': BitEnumField("type_code", BlockTypes.BLOCK_INTEGRITY, 8, CanonicalBlock.TypeCodes), + 'data': CBORPacketFieldWithRemain("data", AbstractSecurityBlock(), AbstractSecurityBlock) + }) + + fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + + +# pylint: disable=R0901 +class EncryptedBlockIntegrityBlock(BlockIntegrityBlock): + """ + This defines a CanonicalBlock with its type code as 11 and an encrypted + AbstractSecurityBlock as its data field. + """ + fields_template = Common.template_replace(BlockIntegrityBlock.fields_template, { + 'data': CBORByteString("data", b'\xDE\xAD\xBE\xEF') + }) + + fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + encrypted = True + +class BlockConfidentialityBlock(CanonicalBlock): + """ + This defines a CanonicalBlock with its type code as 12 and an + AbstractSecurityBlock as its data field. + """ + fields_template = Common.template_replace(CanonicalBlock.fields_template, { + 'type_code': BitEnumField("type_code", BlockTypes.BLOCK_CONFIDENTIALITY, 8, CanonicalBlock.TypeCodes), + 'data': CBORPacketFieldWithRemain("data", AbstractSecurityBlock(), AbstractSecurityBlock) + }) + + fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + +class UnassignedExtensionBlock(CanonicalBlock): + """ An extension block with an unassigned type code < 192. """ + +class EncryptedUnassignedExtensionBlock(CanonicalBlock): + """ An extension block with an unassigned type code < 192. The data field is encrypted. """ + encrypted = True + +class ReservedExtensionBlock(CanonicalBlock): + """ An extension block with a type code 192-255. """ + +class EncryptedReservedExtensionBlock(CanonicalBlock): + """ An extension block with a type code 192-255. The data field is encrypted. """ + encrypted = True + +class PrimaryBlock(CBORArray): + class CtrlFlags(IntFlag): + """ + Bundle Processing Control Flags + """ + BUNDLE_IS_FRAGMENT = 0x01 + ADMIN_RECORD = 0x02 + MUST_NOT_BE_FRAGMENTED = 0x04 + ACKNOWLEDGEMENT_REQUESTED = 0x20 + STATUS_TIME_REQUESTED = 0x40 + + REQUEST_REPORTING_OF_BUNDLE_RECEPTION = 0x4000 + REQUEST_REPORTING_OF_BUNDLE_FORWARDING = 0x10000 + REQUEST_REPORTING_OF_BUNDLE_DELIVERY = 0x20000 + REQUEST_REPORTING_OF_BUNDLE_DELETION = 0x40000 + + fields_desc = CBORArray.fields_desc + [ + CBORInteger("version", 7), + CBORInteger("flags", 0), + CBORInteger("crc_type", CrcTypes.CRC32C), + PacketField("dest", EndpointID(), EndpointID), + PacketField("src", EndpointID(), EndpointID), + PacketField("report", EndpointID(scheme_code=1), EndpointID), + PacketField("creation_timestamp", Timestamp(t=int(time.time())), Timestamp), + CBORInteger("lifetime", 0), + ConditionalField(CBORInteger("fragment_offset", 0), + lambda pkt: pkt.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT), + ConditionalField(CBORInteger("total_adu_length", 0), + lambda pkt: pkt.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT), + ConditionalField( + MultipleTypeField( + [ + (CBORByteString("crc", b"\x00\x00"), lambda pkt: pkt.crc_type == CrcTypes.CRC16), + (CBORByteString("crc", b"\x00\x00\x00\x00"), lambda pkt: pkt.crc_type == CrcTypes.CRC32C) + ], + CBORNull("crc", None) + ), lambda pkt: pkt.crc_type != CrcTypes.NONE + ) + ] + + def dissect(self, s:bytes): + """ This dissect doesn't process the payload, because there is none. """ + s = self.pre_dissect(s) + s = self.do_dissect(s) + s = self.post_dissect(s) + + def post_dissect(self, s): + # see docstring for equivalent Canonical Block method + self.raw_packet_cache = None # Reset packet to allow post_build + return s + + def post_build(self, pkt, pay): + pkt = self.set_additional_fields(pkt) + + if self.crc_type != CrcTypes.NONE: + # insert crc + crc, index = compute_crc(self.crc_type, pkt) + pkt = pkt[:index] + crc + + return pkt + pay + + def count_additional_fields(self): + count = 8 + if self.crc_type != CrcTypes.NONE: count += 1 + if self.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT: count += 2 + return count + +TYPE_CODE_TO_BLOCK_TYPE_MAP = { + # (type_code, is_encrypted): block_type + (BlockTypes.PRIMARY, False): PrimaryBlock, + (BlockTypes.PRIMARY, True): None, # should not happen + (BlockTypes.PAYLOAD, False): PayloadBlock, + (BlockTypes.PAYLOAD, True): EncryptedPayloadBlock, + (BlockTypes.PREV_NODE, False): PreviousNodeBlock, + (BlockTypes.PREV_NODE, True): EncryptedPreviousNodeBlock, + (BlockTypes.AGE, False): BundleAgeBlock, + (BlockTypes.AGE, True): EncryptedBundleAgeBlock, + (BlockTypes.HOP_COUNT, False): HopCountBlock, + (BlockTypes.HOP_COUNT, True): EncryptedHopCountBlock, + (BlockTypes.BLOCK_INTEGRITY, False): BlockIntegrityBlock, + (BlockTypes.BLOCK_INTEGRITY, True): EncryptedBlockIntegrityBlock, + (BlockTypes.BLOCK_CONFIDENTIALITY, False): BlockConfidentialityBlock, + (BlockTypes.BLOCK_CONFIDENTIALITY, True): None # should not happen +} + +UNENCRYPTED_TO_ENCRYPTED_TYPE_MAP = { + PayloadBlock: EncryptedPayloadBlock, + PreviousNodeBlock: EncryptedPreviousNodeBlock, + BundleAgeBlock: EncryptedBundleAgeBlock, + HopCountBlock: EncryptedHopCountBlock, + BlockIntegrityBlock: EncryptedBlockIntegrityBlock, + UnassignedExtensionBlock: EncryptedUnassignedExtensionBlock, + ReservedExtensionBlock: EncryptedReservedExtensionBlock +} + +ENCRYPTED_TO_UNENCRYPTED_TYPE_MAP = { + EncryptedPayloadBlock: PayloadBlock, + EncryptedPreviousNodeBlock: PreviousNodeBlock, + EncryptedBundleAgeBlock: BundleAgeBlock, + EncryptedHopCountBlock: HopCountBlock, + EncryptedBlockIntegrityBlock: BlockIntegrityBlock, + EncryptedUnassignedExtensionBlock: UnassignedExtensionBlock, + EncryptedReservedExtensionBlock: ReservedExtensionBlock +} + + +def next_block_type(pkt, lst, cur, remain): + del pkt, lst, cur # Not used + if remain is None or remain == b'\xff': return None + return Bundle.identify_block(remain) + +def guess_block_class(block_bytes, pkt): + del pkt # Not used + if block_bytes is None or block_bytes == b'\xff': return None + return Bundle.identify_block(block_bytes) + +class Bundle(CBORArray): + def count_additional_fields(self)->int: + return 31 + + fields_desc = [ + CBORArray._major_type, + BitField("add", 31, 5), + PacketFieldWithRemain("primary_block", PrimaryBlock(), PrimaryBlock), + PacketListField("canonical_blocks", [CanonicalBlock()], next_cls_cb=next_block_type), + CBORStopCode("stop_code", 31) + ] + + @staticmethod + def type_code_to_block_type(type_code:int, encrypted:bool=False): + map_key = (type_code, encrypted) + if map_key not in TYPE_CODE_TO_BLOCK_TYPE_MAP: + if type_code < 192: + if encrypted: return EncryptedUnassignedExtensionBlock + return UnassignedExtensionBlock + if type_code >= 192: + if encrypted: return EncryptedReservedExtensionBlock + return ReservedExtensionBlock + return CanonicalBlock + + return TYPE_CODE_TO_BLOCK_TYPE_MAP[map_key] + + def find_block_by_type(self, block_type, excluded_block_nums:List[int]=None)->CanonicalBlock: + """ + Find the first canonical block matching the specified type, with a block number not + in the excluded list. """ + if excluded_block_nums is None: + excluded_block_nums = [] + try: + block = next(x for x in self.canonical_blocks if isinstance(x, block_type) and \ + x.block_number not in excluded_block_nums) + except StopIteration: + block = None + + return block + + def find_block_by_type_code(self, type_code:int, + excluded_block_nums:List[int]=None)->CanonicalBlock: + """ + Find the first block matching the specified type code, with a block number not + in the excluded list. + """ + if excluded_block_nums is None: + excluded_block_nums = [] + try: + block = next(x for x in self.canonical_blocks if (x.type_code == type_code) and \ + (x.block_number not in excluded_block_nums)) + except StopIteration: + block = None + + return block + + def find_block_by_number(self, block_num:int)->CanonicalBlock: + """ Find the canonical block with the specified block number. """ + try: + block = next(x for x in self.canonical_blocks if x.block_number == block_num) + except StopIteration: + block = None + + return block + + def get_new_block_number(self)->int: + """ Return a new canonical block number one higher than the highest in use. """ + new_num = 2 + + for block in self.canonical_blocks: + if block.block_number >= new_num: + new_num = block.block_number + 1 + + return new_num + + def add_block(self, block:CanonicalBlock, block_num_to_insert_above=1, + select_block_number=False)->CanonicalBlock: + """ Insert an extension block just before the block with the specified block number. """ + if select_block_number: + block.block_number = self.get_new_block_number() + + insert_pos = -1 + for idx, test_block in enumerate(self.canonical_blocks): + if test_block.block_number == block_num_to_insert_above: + insert_pos = idx + + if insert_pos == -1: + raise ValueError("Could not find block number to insert above", block_num_to_insert_above) + + self.canonical_blocks.insert(insert_pos, block) + + return block + + def replace_block_by_block_num(self, block_num:int, new_block:CanonicalBlock): + for idx, block in enumerate(self.canonical_blocks): + if block.block_number == block_num: + self.canonical_blocks[idx] = new_block + return + + @staticmethod + def identify_block(block_bytes:bytes): + """ Determine the type of the canonical block. """ + type_code = block_bytes[1] + + block_type = Bundle.type_code_to_block_type(type_code) + encrypted_block_type = Bundle.type_code_to_block_type(type_code, True) + + if (encrypted_block_type is not block_type): + # Try to construct the block as the unencrypted type. If it + # doesn't work, specify the encrypted version. If it works due to + # chance arrangement of bytes but is actually encrypted, it will + # be corrected by post_dissect() + try: + _ = block_type(block_bytes) + # pylint: disable=W0702, W0718 + # Scapy just raises a generic Exception if it fails + except Exception: + block_type = encrypted_block_type + + return block_type + + + def post_dissect(self, s): + """ + Find the BCBs and check their security targets to definitively determine + which blocks are encrypted. + """ + for bcb_block in self.canonical_blocks: + if isinstance(bcb_block, BlockConfidentialityBlock): + for idx, block in enumerate(self.canonical_blocks): + if block.block_number in bcb_block.data.security_targets.targets: + block_type = type(block) + new_block = block + + # Found a block originally detected as unencrypted, but the + # BCB specifies is encrypted. Replace with an encrypted type. + if not block_type.encrypted: + encrypted_type = UNENCRYPTED_TO_ENCRYPTED_TYPE_MAP[block_type] + new_block = encrypted_type(raw(block)) + self.canonical_blocks[idx] = new_block + + new_block.encrypted_by.append(bcb_block.block_number) + return s diff --git a/scapy/contrib/dtn/cbor.py b/scapy/contrib/dtn/cbor.py new file mode 100644 index 00000000000..6bdb24b9e69 --- /dev/null +++ b/scapy/contrib/dtn/cbor.py @@ -0,0 +1,269 @@ +# scapy.contrib.description = Concise Binary Object Representation (CBOR) +# scapy.contrib.status = library + +from scapy.fields import ( + Field, + BitField, + BitEnumField, + PacketField +) +from scapy.packet import Packet +from scapy.all import raw +import flynn +from typing import Tuple, List, Union + +MajorTypes = { + 0: "unsigned", + 1: "negative", + 2: "byte string", + 3: "text string", + 4: "array", + 5: "map", + 6: "tag", + 7: "simple/float" +} + +class MajorTypeException(Exception): + """This exception indicates that a CBOR object has an unexpected value for its Major Type. + + Attributes: + actual -- the integer value of the actual Major Type + expected -- an integer or list of integers indicating the acceptable Major Type values""" + def __init__(self, actual: int, expected: Union[int, List[int]]): + + message = f'[Error] Major type {actual} does not refer to a(n)' + if isinstance(expected, int): + typ = MajorTypes[expected] + message += f' {typ}.' + else: + typ = MajorTypes[expected[0]] + message += f' {typ}' + for val in expected[1:]: + typ = MajorTypes[val] + message += f' or {typ}' + message += '.' + super().__init__(message) + +class StopCodeException(Exception): + def __init__(self, value): + super().__init__(f'[Error] Major type {value} does not refer to a stop code.') + +class AdditionalInfoException(Exception): + """This exception indicates that a CBOR object has an unexpected value for its Additional Info.""" + def __init__(self): + super().__init__('[Error] Invalid additional info.') + +class UnhandledTypeException(Exception): + def __init__(self, value, cls): + super().__init__(f"[Error] Major type {value} is not handled by {cls}.") + + +# CBOR definitions +class CBORNull(Field): + """This class exists so that it can be used in a MultipleTypeField containing CBOR values. + Every option given to a MultipleTypeField must be a field with at least a name. + Thus, if one of the MultipleType options should be that no field whatsoever is present, + you need a field that produces no bytes when added to the packet. + CBORNull can serve this purpose.""" + + +class CBORBase(Field): + @staticmethod + def static_get_head_info(b): + if len(b)==0: return None, None + + head = b[0] + major_type = head >> 5 + add_info = head & 0b00011111 + + return major_type, add_info + + def get_head_info(self, b): + return CBORBase.static_get_head_info(b) + + def addfield(self, pkt, s, val): + return s + flynn.dumps(val) + + +class CBORInteger(CBORBase): + @staticmethod + def get_value(add_info, b): + if add_info < 24: + val_length = 1 # 1 byte head, argument=add_info + val = add_info + elif add_info == 24: + val_length = 2 # 1 byte head + 1 byte argument + val = b[1] + elif add_info == 25: + val_length = 3 # 1 byte head + 2 byte argument + val = int.from_bytes(b[1:3], byteorder='big') + elif add_info == 26: + val_length = 5 # 4 byte argument + val = int.from_bytes(b[1:5], byteorder='big') + elif add_info == 27: + val_length = 9 # 8 byte argument + val = int.from_bytes(b[1:9], byteorder='big') + else: + raise AdditionalInfoException() + + return b[val_length:], val + + def getfield(self, pkt, s): + major_type, add_info = self.get_head_info(s) + + if major_type != 0: + raise MajorTypeException(major_type, 0) + + return CBORInteger.get_value(add_info, s) + + +class CBORStringBase(CBORBase): + @staticmethod + def get_value(add_info, b): + if add_info < 24: + arg_size = 0 + data_size = add_info # argument = data size = additional info + else: + if add_info == 24: + arg_size = 1 + elif add_info == 25: + arg_size = 2 + elif add_info == 26: + arg_size = 4 + elif add_info == 27: + arg_size = 8 + else: + raise AdditionalInfoException() + + # size of argument is known now, so + # get value of the argument, which contains the size of the data + data_size = int.from_bytes(b[1:1+arg_size], byteorder='big') + val_length = 1 + arg_size + data_size + val = b[1+arg_size:val_length] + + return b[val_length:], val + + +class CBORByteString(CBORStringBase): + def getfield(self, pkt, s): + major_type, add_info = self.get_head_info(s) + + if major_type != 2: + raise MajorTypeException(major_type, 2) + + return CBORStringBase.get_value(add_info, s) + + +class CBORTextString(CBORStringBase): + def getfield(self, pkt, s): + major_type, add_info = self.get_head_info(s) + + if major_type != 3: + raise MajorTypeException(major_type, 3) + + return CBORStringBase.get_value(add_info, s) + + +class CBORIntOrText(CBORBase): + def getfield(self, pkt, s): + major_type, add_info = self.get_head_info(s) + + if major_type == 0: return CBORInteger.get_value(add_info, s) + if major_type == 3: return CBORStringBase.get_value(add_info, s) + + raise MajorTypeException(major_type, [0, 3]) + + +class CBORStopCode(CBORBase): + def addfield(self, pkt, s, val): + return s + b'\xff' + + @staticmethod + def get_value(add_info, b): + return b[1:], add_info + + def getfield(self, pkt, s): + major_type, add_info = self.get_head_info(s) + if major_type != 7: + raise StopCodeException(major_type) + + return CBORStopCode.get_value(add_info, s) + + +class CBORAny(CBORBase): + def getfield(self, pkt, s): + major_type, add_info = self.get_head_info(s) + + if major_type == 0: return CBORInteger.get_value(add_info, s) + if major_type in [2, 3]: return CBORStringBase.get_value(add_info, s) + if major_type == 7: return CBORStopCode.get_value(add_info, s) + + raise UnhandledTypeException(major_type, CBORAny) + + +class CBORArray(Packet): + _major_type = BitEnumField("major_type", 4, 3, MajorTypes) + _add = BitField("add", 0, 5) # additional information = length + + # head fields + fields_desc=[_major_type, _add] + + def count_additional_fields(self)->int: + """ + Return the number of fields other than the two head fields. This method does + not work correctly with ConditionalFields and should be overridden when that + field type is in use. + """ + head_field_count = 2 + return len(self.default_fields) - head_field_count + + def set_additional_fields(self, pkt:Packet)->bytes: + """ For an, the add field is set to the number of elements minus the two head fields. """ + # pylint: disable=W0201 + # field (instance variable) initialization is handled via "fields_desc" + self.add = self.count_additional_fields() + head = (self.major_type << 5) | self.add + + return head.to_bytes(1, 'big') + pkt[1:] + + def post_build(self, pkt:bytes, pay:bytes)->bytes: + return self.set_additional_fields(pkt) + pay + + def extract_padding(self, s): + return "", s + + +class CBORPacketField(PacketField): + def i2m(self, pkt:Packet, i)->bytes: + if i is None: + return b"" + + return flynn.dumps(raw(i)) + + def m2i(self, pkt:Packet, m): + _, add_info = CBORBase.static_get_head_info(m) + remain, decoded_m =CBORStringBase.get_value(add_info, m) + try: + # we want to set parent wherever possible + return self.cls(decoded_m + remain, _parent=pkt) # type: ignore + except TypeError: + return self.cls(decoded_m + remain) + + +class CBORPacketFieldWithRemain(CBORPacketField): + """ + The regular Packet.getfield() never returns the remaining bytes, so the CRC or + other following fields get lost. This getfield does return the remaining bytes. + """ + def m2i(self, pkt:Packet, m): + _, add_info = CBORBase.static_get_head_info(m) + remain, decoded_m =CBORStringBase.get_value(add_info, m) + try: + # we want to set parent wherever possible + return remain, self.cls(decoded_m + remain, _parent=pkt) # type: ignore + except TypeError: + return remain, self.cls(decoded_m + remain) + + def getfield(self, pkt:Packet, s:bytes)->Tuple[bytes, Packet]: + remain, i = self.m2i(pkt, s) + return remain, i diff --git a/scapy/contrib/dtn/common.py b/scapy/contrib/dtn/common.py new file mode 100644 index 00000000000..25501375eed --- /dev/null +++ b/scapy/contrib/dtn/common.py @@ -0,0 +1,34 @@ +from scapy.packet import Packet, Raw +from scapy.fields import Field +from typing import Dict, List + +class NoPayloadPacket(Packet): + """A packet with no payload layer to bind.""" + + def extract_padding(self, s): + return "", s + + def post_dissect(self, s): + try: + if self[Raw].load is not None: + raise ValueError(f"found payload in {Packet} when none was expected") + except IndexError: # No Raw layer found, i.e. no unparsed payload is present + pass + return s + +class ControlPacket(NoPayloadPacket): + """A packet containing control data, rather than user data.""" + +class FieldPacket(NoPayloadPacket): + """A packet intended for use as a field (i.e. PacketField or PacketListField) + in another Packet, rather than one sent or received on the wire. Useful when you need + heterogeneous, compound data similar to a record/struct within another Packet.""" + +FieldsTemplate = Dict[str, Field] + +def template_replace(template: FieldsTemplate, new_values: FieldsTemplate) -> FieldsTemplate: + return {**template, **new_values} + +def make_fields_desc(template: FieldsTemplate) -> List[Field]: + return list(template.values()) + diff --git a/scapy/contrib/dtn/tcpcl.py b/scapy/contrib/dtn/tcpcl.py new file mode 100644 index 00000000000..74c15bfefaf --- /dev/null +++ b/scapy/contrib/dtn/tcpcl.py @@ -0,0 +1,245 @@ +# scapy.contrib.description = TCP Convergence Layer version 4 (TCPCLv4) +# scappy.contrib.status = loads + +from scapy.packet import Packet, Raw, bind_layers +from scapy.fields import ( + XByteEnumField, + PacketField, + BitField, + BitFieldLenField, + ConditionalField, + PacketListField, + StrLenField, + FieldLenField +) + +import struct +from enum import IntEnum + +from scapy.contrib.dtn.common import ControlPacket, FieldPacket +import scapy.contrib.dtn.bpv7 as BPv7 + +class MagicValueError(Exception): + """ + Exception raised when a ContactHeader is dissected and the magic value is incorrect. + """ + def __init__(self, value): + super().__init__(f"Tried to decode ContactHeader with invalid magic value of {value}") + +class ContactHeader(ControlPacket): + MAGIC_VALUE = 0x64746e21 + + class Flag(IntEnum): + CAN_TLS = 0x01 + + fields_desc = [ + BitField("magic", MAGIC_VALUE, 32), + BitField("version", 4, 8), + XByteEnumField("flags", 0, { Flag.CAN_TLS: "can tls" }) + ] + + def post_dissect(self, s): + if self.magic != self.MAGIC_VALUE: + raise MagicValueError(self.magic) + return super().post_dissect(s) + +class MsgHeader(Packet): + class MsgType(IntEnum): + SESS_INIT=0x07 + SESS_TERM=0x05 + XFER_SEGMENT=0x01 + XFER_ACK=0x02 + XFER_REFUSE=0x03 + KEEPALIVE=0x04 + MSG_REJECT=0x06 + + fields_desc = [ + XByteEnumField("type", MsgType.XFER_SEGMENT, { + MsgType.SESS_INIT: "sess_init", + MsgType.SESS_TERM: "sess_term", + MsgType.XFER_SEGMENT: "xfer_segment", + MsgType.XFER_ACK: "xfer_ack", + MsgType.XFER_REFUSE: "xfer_refuse", + MsgType.KEEPALIVE: "keepalive", + MsgType.MSG_REJECT: "msg_reject" + }) + ] + +class Ext(FieldPacket): + """ + Class definition for an Extension Item in the format of a Type-Length-Value container. + """ + class Flag(IntEnum): + CRITICAL = 0x01 + + class Type(IntEnum): + LENGTH = 0x0001 + + fields_desc = [ + XByteEnumField("flags", 0, { Flag.CRITICAL: "critical" }), + BitField("type", 0, 16), + BitFieldLenField("length", default=0, size=16, length_of="data"), + StrLenField("data", 0, length_from=lambda pkt: pkt.length) + ] + +class SessInit(ControlPacket): + fields_desc = [ + BitField("keepalive", 0, 16), + BitField("segment_mru", 0, 64), + BitField("transfer_mru", 0, 64), + FieldLenField("id_length", None, length_of="id", fmt="H"), # Node ID Length (U16) + StrLenField("id", b"", length_from=lambda pkt: pkt.id_length), # Node ID Data (variable) + BitFieldLenField("ext_length", 0, 32, length_of="ext_items"), + ConditionalField( + PacketListField("ext_items", + [], + Ext, + length_from=lambda pkt: pkt.ext_length), + lambda pkt: pkt.ext_length > 0) + ] + +class Keepalive(ControlPacket): + """ + A keepalive message consists only of a MsgHeader with the type code KEEPALIVE. + """ + +class MsgReject(ControlPacket): + class ReasonCode(IntEnum): + UNKNOWN = 0x01 + UNSUPPORTED = 0x02 + UNEXPECTED = 0x03 + + fields_desc = [ + XByteEnumField("reason", ReasonCode.UNSUPPORTED, { + ReasonCode.UNKNOWN: "message type unknown", + ReasonCode.UNSUPPORTED: "message unsupported", + ReasonCode.UNEXPECTED: "message unexpected" + }), + PacketField("header", MsgHeader(), MsgHeader) + ] + +class Xfer(Packet): + """ + Abstract class containing fields and flags common to Xfer messages + """ + class Flag(IntEnum): + END = 0x01 + START = 0x02 + + fields_desc = [ + XByteEnumField("flags", 0, { + Flag.END: "END", + Flag.START: "START", + Flag.START | Flag.END: "START|END" + }), + BitField("id", 0, 64) + ] + +class InvalidPayloadError(Exception): + """ + This error indicates that an XferSegment contains raw bytes instead of + a properly formatted Bundle as its payload. + """ + def __init__(self, payload_bytes): + super().__init__(f"Failed to fully parse Bundle from Xfer payload: bundle={payload_bytes}") + +class XferSegment(Xfer): + """ + Packet for transferring a data segment + """ + fields_desc = Xfer.fields_desc + [ + ConditionalField( + BitFieldLenField("ext_length", default=0, size=32, length_of="ext_items"), + lambda pkt: pkt.flags & Xfer.Flag.START), + ConditionalField( + PacketListField("ext_items", + [Ext(type=Ext.Type.LENGTH)], + Ext, + length_from=lambda pkt: pkt.ext_length), + lambda pkt: (pkt.flags & Xfer.Flag.START) and (pkt.ext_length > 0)), + BitField("length", default=0, size=64) + ] + + def post_build(self, pkt, pay): + # calculate the length field + if not self.length: + index = len(pkt)-8 # size of length is 8 bytes, thus position=len(pkt)-8 + length = len(pay) + pkt = pkt[:index] + struct.pack('!Q', length) + return pkt + pay + + def post_dissect(self, s): + "An XferSegment message should have a Bundle as payload. If it has raw bytes instead, raise an error." + try: + if self[Raw].load is not None: + raise InvalidPayloadError(self[Raw].load) + except IndexError: # Raw layer or load field does not exist + pass # no action required + + return s + +class XferAck(ControlPacket): + fields_desc = Xfer.fields_desc + [ + BitField("length", default=0, size=64) + ] + +class XferRefuse(ControlPacket): + class ReasonCode(IntEnum): + UNKNOWN = 0x00 + COMPLETED = 0x01 + NO_RESOURCES = 0x02 + RETRANSMIT = 0x03 + NOT_ACCEPTABLE = 0x04 + EXT_FAIL = 0x05 + SESS_TERM = 0x06 + + fields_desc = [ + XByteEnumField("reason", ReasonCode.UNKNOWN, { + ReasonCode.UNKNOWN: "unknown", + ReasonCode.COMPLETED: "complete bundle received", + ReasonCode.NO_RESOURCES: "resources exhausted", + ReasonCode.RETRANSMIT: "retransmit bundle", + ReasonCode.NOT_ACCEPTABLE: "bundle not acceptable", + ReasonCode.EXT_FAIL: "failed to process extensions", + ReasonCode.SESS_TERM: "session is terminating" + }), + BitField("id", 0, 64) + ] + +class SessTerm(ControlPacket): + class Flag(IntEnum): + REPLY = 0x01 + + class ReasonCode(IntEnum): + UNKNOWN = 0x00 + TIMEOUT = 0x01 + MISMATCH = 0x02 + BUSY = 0x03 + CONTACT_FAIL = 0x04 + NO_RESOURCES = 0x05 + + fields_desc = [ + XByteEnumField("flags", 0, { Flag.REPLY: "reply" }), + XByteEnumField("reason", ReasonCode.UNKNOWN, { + ReasonCode.UNKNOWN: "unknown", + ReasonCode.TIMEOUT: "idle timeout", + ReasonCode.MISMATCH: "version mismatch", + ReasonCode.BUSY: "entity busy", + ReasonCode.CONTACT_FAIL: "failed to process contact header or sess init", + ReasonCode.NO_RESOURCES: "entity resource exhaustion" + }), + + ] + +# Bind all TCPCL message headers to TCPCL messages. +# This way, if `some_bytes` consists of the raw representation of a TCPCL message, +# you can evaluate e.g. `x=MsgHeader(some_bytes)` and `x` will be a Packet consisting of a TCPCL +# MsgHeader with the correct type code plus a payload of the correct TCPCL message type. +bind_layers(MsgHeader, SessInit, type=MsgHeader.MsgType.SESS_INIT) +bind_layers(MsgHeader, Keepalive, type=MsgHeader.MsgType.KEEPALIVE) +bind_layers(MsgHeader, MsgReject, type=MsgHeader.MsgType.MSG_REJECT) +bind_layers(MsgHeader, XferSegment, type=MsgHeader.MsgType.XFER_SEGMENT) +bind_layers(MsgHeader, XferAck, type=MsgHeader.MsgType.XFER_ACK) +bind_layers(MsgHeader, XferRefuse, type=MsgHeader.MsgType.XFER_REFUSE) +bind_layers(MsgHeader, SessTerm, type=MsgHeader.MsgType.SESS_TERM) +bind_layers(XferSegment, BPv7.Bundle) From 91dd6753332e93e24fd3b3879b087f0f9e918621 Mon Sep 17 00:00:00 2001 From: trecker Date: Thu, 3 Jul 2025 16:12:10 -0400 Subject: [PATCH 02/16] fix typos and add author info --- scapy/contrib/dtn/bpv7.py | 10 +++++++++- scapy/contrib/dtn/cbor.py | 8 ++++++++ scapy/contrib/dtn/tcpcl.py | 9 ++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/scapy/contrib/dtn/bpv7.py b/scapy/contrib/dtn/bpv7.py index 2ae2e48cd6c..98288de6d9e 100644 --- a/scapy/contrib/dtn/bpv7.py +++ b/scapy/contrib/dtn/bpv7.py @@ -1,5 +1,13 @@ # scapy.contrib.description = Bundle Protocol version 7 (BPv7) -# scappy.contrib.status = loads +# scapy.contrib.status = loads + +""" + Bundle Protocol version 7 (BPv7) layer + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :authors: Timothy Recker, timothy.recker@nasa.gov + Tad Kollar, tad.kollar@nasa.gov +""" from scapy.packet import Packet from scapy.fields import ( diff --git a/scapy/contrib/dtn/cbor.py b/scapy/contrib/dtn/cbor.py index 6bdb24b9e69..3c7c909c888 100644 --- a/scapy/contrib/dtn/cbor.py +++ b/scapy/contrib/dtn/cbor.py @@ -1,6 +1,14 @@ # scapy.contrib.description = Concise Binary Object Representation (CBOR) # scapy.contrib.status = library +""" + Concise Binary Object Representation (CBOR) utility + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :authors: Timothy Recker, timothy.recker@nasa.gov + Tad Kollar, tad.kollar@nasa.gov +""" + from scapy.fields import ( Field, BitField, diff --git a/scapy/contrib/dtn/tcpcl.py b/scapy/contrib/dtn/tcpcl.py index 74c15bfefaf..4bcbca002cf 100644 --- a/scapy/contrib/dtn/tcpcl.py +++ b/scapy/contrib/dtn/tcpcl.py @@ -1,5 +1,12 @@ # scapy.contrib.description = TCP Convergence Layer version 4 (TCPCLv4) -# scappy.contrib.status = loads +# scapy.contrib.status = loads + +""" + TCP Convergence Layer version 4 (TCPCLv4) layer + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :author: Timothy Recker, timothy.recker@nasa.gov +""" from scapy.packet import Packet, Raw, bind_layers from scapy.fields import ( From 56fb1757a47c371492bae62aa21454709edd095a Mon Sep 17 00:00:00 2001 From: T-recks Date: Tue, 19 Aug 2025 22:09:00 -0700 Subject: [PATCH 03/16] add module info to dtn.common --- scapy/contrib/dtn/common.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scapy/contrib/dtn/common.py b/scapy/contrib/dtn/common.py index 25501375eed..56ca1245138 100644 --- a/scapy/contrib/dtn/common.py +++ b/scapy/contrib/dtn/common.py @@ -1,3 +1,6 @@ +# scapy.contrib.description = utility functions and classes for DTN module +# scapy.contrib.status = library + from scapy.packet import Packet, Raw from scapy.fields import Field from typing import Dict, List From 79f3d4c3cf4eb275d2f1d239671ce71eacb14e21 Mon Sep 17 00:00:00 2001 From: T-recks Date: Wed, 20 Aug 2025 21:47:37 -0700 Subject: [PATCH 04/16] fix whitespace --- scapy/contrib/dtn/bpv7.py | 174 ++++++++++++++++++++---------------- scapy/contrib/dtn/cbor.py | 94 ++++++++++--------- scapy/contrib/dtn/common.py | 10 ++- scapy/contrib/dtn/tcpcl.py | 56 +++++++----- 4 files changed, 191 insertions(+), 143 deletions(-) diff --git a/scapy/contrib/dtn/bpv7.py b/scapy/contrib/dtn/bpv7.py index 98288de6d9e..d6aceb4f5cd 100644 --- a/scapy/contrib/dtn/bpv7.py +++ b/scapy/contrib/dtn/bpv7.py @@ -53,7 +53,7 @@ def __init__(self, type_code): super().__init__(f'Tried to compute a CRC using an invalid type code: {type_code}') -def compute_crc(crc_type:int, pkt:bytes, ignore_existing:bool=True): +def compute_crc(crc_type: int, pkt: bytes, ignore_existing: bool = True): # prepare parameters if (crc_type == CrcTypes.CRC32C): size = 4 @@ -63,7 +63,7 @@ def compute_crc(crc_type:int, pkt:bytes, ignore_existing:bool=True): crcfun = crcmod.predefined.mkCrcFun('x-25') else: raise InvalidCRCType(crc_type) - + crc_index = len(pkt) - size # Wipe anything in existing crc field @@ -78,15 +78,15 @@ class PacketFieldWithRemain(PacketField): The regular Packet.getfield() never returns the remaining bytes, so the CRC or other following fields get lost. This getfield does return the remaining bytes. """ - def getfield(self, pkt:Packet, s:bytes)->Tuple[bytes, Packet]: + def getfield(self, pkt: Packet, s: bytes) -> Tuple[bytes, Packet]: i = self.m2i(pkt, s) remain_size = len(s) - len(raw(i)) - remain=s[-remain_size:] + remain = s[-remain_size:] return remain, i - + class IPN(CBORArray): - fields_desc= CBORArray.fields_desc + [ + fields_desc = CBORArray.fields_desc + [ CBORInteger("node_id", 1), CBORInteger("service_number", 1), ] @@ -95,7 +95,7 @@ class IPN(CBORArray): # pylint: disable=W0201 # field (instance variable) initialization is handled via "fields_desc" - def from_string(self, ipn_str:str): + def from_string(self, ipn_str: str): result = IPN.ipn_re.search(ipn_str) self.node_id = int(result.group(1)) self.service_number = int(result.group(2)) @@ -108,11 +108,12 @@ def __str__(self): def __eq__(self, other): if isinstance(other, IPN): return (self.node_id == other.node_id) and (self.service_number == other.service_number) - return False + return False + class DTN(Packet): - fields_desc=[ - CBORIntOrText("uri", 0) # can be 0 (type Int) or a string (type String) + fields_desc = [ + CBORIntOrText("uri", 0) # can be 0 (type Int) or a string (type String) ] def extract_padding(self, s): @@ -136,17 +137,18 @@ class EndpointID(CBORArray): ) ] - def dissect(self, s:bytes): + def dissect(self, s: bytes): """ This dissect doesn't process the payload, because there is none. """ s = self.pre_dissect(s) s = self.do_dissect(s) - s = self.post_dissect(s) + s = self.post_dissect(s) def __eq__(self, other): if isinstance(other, EndpointID): return (self.scheme_code == other.scheme_code) and (self.ssp == other.ssp) return False + # pylint: disable=R0903 # Packet types are not intended to have many(any) public functions class Timestamp(CBORArray): @@ -162,24 +164,26 @@ def __ge__(self, other): def __gt__(self, other): if isinstance(other, Timestamp): - return (self.t > other.t) or (self.t==other.t and (self.seq > other.seq)) + return (self.t > other.t) or (self.t == other.t and (self.seq > other.seq)) return False + class BlockTypes: """ Bundle block type codes """ - PRIMARY=0 - PAYLOAD=1 - AUTHENTICATION=2 - INTEGRITY=3 - CONFIDENTIALITY=4 - PREV_HOP=5 - PREV_NODE=6 - AGE=7 - HOP_COUNT=10 - BLOCK_INTEGRITY=11 - BLOCK_CONFIDENTIALITY=12 + PRIMARY = 0 + PAYLOAD = 1 + AUTHENTICATION = 2 + INTEGRITY = 3 + CONFIDENTIALITY = 4 + PREV_HOP = 5 + PREV_NODE = 6 + AGE = 7 + HOP_COUNT = 10 + BLOCK_INTEGRITY = 11 + BLOCK_CONFIDENTIALITY = 12 + class CrcTypes: """ @@ -201,7 +205,7 @@ class SecurityTargets(CBORArray): def count_additional_fields(self): return len(self.getfieldval("targets")) - + class CBORTuple(CBORArray): """ @@ -220,7 +224,7 @@ class CBORTupleArray(CBORArray): PacketListField("tuples", [CBORTuple()], CBORTuple, count_from=lambda pkt: pkt.add) ] - def find_value_with_id(self, target_id:int): + def find_value_with_id(self, target_id: int): """ Find the tuple with the specified id and return the value. """ try: tup = next(x for x in self.tuples if x.id == target_id) @@ -229,20 +233,20 @@ def find_value_with_id(self, target_id:int): return tup.value - def count_additional_fields(self)->int: + def count_additional_fields(self) -> int: return len(self.getfieldval("tuples")) - + class SecurityResults(CBORArray): """ A CBOR array of CBORTupleArrays. """ fields_desc = [ CBORArray._major_type, BitFieldLenField("add", None, 5, count_of="results"), PacketListField("results", [CBORTupleArray()], CBORTupleArray, count_from=lambda pkt: pkt.add) - ] + ] def count_additional_fields(self): - return len(self.getfieldval("results")) + return len(self.getfieldval("results")) class AbstractSecurityBlock(Packet): @@ -260,8 +264,8 @@ class AbstractSecurityBlock(Packet): lambda p: (p.security_context_flags & 1)), PacketField("security_results", SecurityResults(), SecurityResults) ] - - def dissect(self, s:bytes): + + def dissect(self, s: bytes): """ This dissect doesn't process the payload, because there is none. """ s = self.pre_dissect(s) s = self.do_dissect(s) @@ -312,7 +316,7 @@ class CtrlFlags(IntFlag): encrypted = False encrypted_by = [] - def get_header(self)->bytes: + def get_header(self) -> bytes: return raw(self)[1:4] def post_dissect(self, s): @@ -340,11 +344,11 @@ def post_build(self, pkt, pay): def get_block_bytes(self) -> bytes: return raw(self) - + def count_additional_fields(self): return 5 if self.crc_type == CrcTypes.NONE else 6 - + class PayloadBlock(CanonicalBlock): """ Contains the bundle payload. @@ -394,18 +398,18 @@ class EncryptedPreviousNodeBlock(PreviousNodeBlock): class BundleAge(Packet): - fields_desc = [ CBORInteger("age", 0) ] + fields_desc = [CBORInteger("age", 0)] def __eq__(self, other): if isinstance(other, BundleAge): return self.age == other.age return False - def dissect(self, s:bytes): + def dissect(self, s: bytes): """ This dissect doesn't process the payload, because there is none. """ s = self.pre_dissect(s) s = self.do_dissect(s) - s = self.post_dissect(s) + s = self.post_dissect(s) class BundleAgeBlock(CanonicalBlock): @@ -493,7 +497,7 @@ class EncryptedBlockIntegrityBlock(BlockIntegrityBlock): """ This defines a CanonicalBlock with its type code as 11 and an encrypted AbstractSecurityBlock as its data field. - """ + """ fields_template = Common.template_replace(BlockIntegrityBlock.fields_template, { 'data': CBORByteString("data", b'\xDE\xAD\xBE\xEF') }) @@ -501,6 +505,7 @@ class EncryptedBlockIntegrityBlock(BlockIntegrityBlock): fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) encrypted = True + class BlockConfidentialityBlock(CanonicalBlock): """ This defines a CanonicalBlock with its type code as 12 and an @@ -513,20 +518,25 @@ class BlockConfidentialityBlock(CanonicalBlock): fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) + class UnassignedExtensionBlock(CanonicalBlock): """ An extension block with an unassigned type code < 192. """ + class EncryptedUnassignedExtensionBlock(CanonicalBlock): - """ An extension block with an unassigned type code < 192. The data field is encrypted. """ + """ An extension block with an unassigned type code < 192. The data field is encrypted. """ encrypted = True + class ReservedExtensionBlock(CanonicalBlock): """ An extension block with a type code 192-255. """ + class EncryptedReservedExtensionBlock(CanonicalBlock): - """ An extension block with a type code 192-255. The data field is encrypted. """ + """ An extension block with a type code 192-255. The data field is encrypted. """ encrypted = True + class PrimaryBlock(CBORArray): class CtrlFlags(IntFlag): """ @@ -542,7 +552,7 @@ class CtrlFlags(IntFlag): REQUEST_REPORTING_OF_BUNDLE_FORWARDING = 0x10000 REQUEST_REPORTING_OF_BUNDLE_DELIVERY = 0x20000 REQUEST_REPORTING_OF_BUNDLE_DELETION = 0x40000 - + fields_desc = CBORArray.fields_desc + [ CBORInteger("version", 7), CBORInteger("flags", 0), @@ -567,7 +577,7 @@ class CtrlFlags(IntFlag): ) ] - def dissect(self, s:bytes): + def dissect(self, s: bytes): """ This dissect doesn't process the payload, because there is none. """ s = self.pre_dissect(s) s = self.do_dissect(s) @@ -587,17 +597,20 @@ def post_build(self, pkt, pay): pkt = pkt[:index] + crc return pkt + pay - + def count_additional_fields(self): count = 8 - if self.crc_type != CrcTypes.NONE: count += 1 - if self.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT: count += 2 + if self.crc_type != CrcTypes.NONE: + count += 1 + if self.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT: + count += 2 return count + TYPE_CODE_TO_BLOCK_TYPE_MAP = { # (type_code, is_encrypted): block_type (BlockTypes.PRIMARY, False): PrimaryBlock, - (BlockTypes.PRIMARY, True): None, # should not happen + (BlockTypes.PRIMARY, True): None, # should not happen (BlockTypes.PAYLOAD, False): PayloadBlock, (BlockTypes.PAYLOAD, True): EncryptedPayloadBlock, (BlockTypes.PREV_NODE, False): PreviousNodeBlock, @@ -609,7 +622,7 @@ def count_additional_fields(self): (BlockTypes.BLOCK_INTEGRITY, False): BlockIntegrityBlock, (BlockTypes.BLOCK_INTEGRITY, True): EncryptedBlockIntegrityBlock, (BlockTypes.BLOCK_CONFIDENTIALITY, False): BlockConfidentialityBlock, - (BlockTypes.BLOCK_CONFIDENTIALITY, True): None # should not happen + (BlockTypes.BLOCK_CONFIDENTIALITY, True): None # should not happen } UNENCRYPTED_TO_ENCRYPTED_TYPE_MAP = { @@ -634,17 +647,21 @@ def count_additional_fields(self): def next_block_type(pkt, lst, cur, remain): - del pkt, lst, cur # Not used - if remain is None or remain == b'\xff': return None + del pkt, lst, cur # Not used + if remain is None or remain == b'\xff': + return None return Bundle.identify_block(remain) + def guess_block_class(block_bytes, pkt): - del pkt # Not used - if block_bytes is None or block_bytes == b'\xff': return None + del pkt # Not used + if block_bytes is None or block_bytes == b'\xff': + return None return Bundle.identify_block(block_bytes) + class Bundle(CBORArray): - def count_additional_fields(self)->int: + def count_additional_fields(self) -> int: return 31 fields_desc = [ @@ -656,35 +673,37 @@ def count_additional_fields(self)->int: ] @staticmethod - def type_code_to_block_type(type_code:int, encrypted:bool=False): + def type_code_to_block_type(type_code: int, encrypted: bool = False): map_key = (type_code, encrypted) if map_key not in TYPE_CODE_TO_BLOCK_TYPE_MAP: if type_code < 192: - if encrypted: return EncryptedUnassignedExtensionBlock + if encrypted: + return EncryptedUnassignedExtensionBlock return UnassignedExtensionBlock if type_code >= 192: - if encrypted: return EncryptedReservedExtensionBlock + if encrypted: + return EncryptedReservedExtensionBlock return ReservedExtensionBlock return CanonicalBlock - + return TYPE_CODE_TO_BLOCK_TYPE_MAP[map_key] - def find_block_by_type(self, block_type, excluded_block_nums:List[int]=None)->CanonicalBlock: + def find_block_by_type(self, block_type, excluded_block_nums: List[int] = None) -> CanonicalBlock: """ Find the first canonical block matching the specified type, with a block number not in the excluded list. """ if excluded_block_nums is None: excluded_block_nums = [] try: - block = next(x for x in self.canonical_blocks if isinstance(x, block_type) and \ + block = next(x for x in self.canonical_blocks if isinstance(x, block_type) and x.block_number not in excluded_block_nums) except StopIteration: block = None return block - - def find_block_by_type_code(self, type_code:int, - excluded_block_nums:List[int]=None)->CanonicalBlock: + + def find_block_by_type_code(self, type_code: int, + excluded_block_nums: List[int] = None) -> CanonicalBlock: """ Find the first block matching the specified type code, with a block number not in the excluded list. @@ -692,14 +711,14 @@ def find_block_by_type_code(self, type_code:int, if excluded_block_nums is None: excluded_block_nums = [] try: - block = next(x for x in self.canonical_blocks if (x.type_code == type_code) and \ + block = next(x for x in self.canonical_blocks if (x.type_code == type_code) and (x.block_number not in excluded_block_nums)) except StopIteration: block = None return block - - def find_block_by_number(self, block_num:int)->CanonicalBlock: + + def find_block_by_number(self, block_num: int) -> CanonicalBlock: """ Find the canonical block with the specified block number. """ try: block = next(x for x in self.canonical_blocks if x.block_number == block_num) @@ -707,8 +726,8 @@ def find_block_by_number(self, block_num:int)->CanonicalBlock: block = None return block - - def get_new_block_number(self)->int: + + def get_new_block_number(self) -> int: """ Return a new canonical block number one higher than the highest in use. """ new_num = 2 @@ -717,9 +736,9 @@ def get_new_block_number(self)->int: new_num = block.block_number + 1 return new_num - - def add_block(self, block:CanonicalBlock, block_num_to_insert_above=1, - select_block_number=False)->CanonicalBlock: + + def add_block(self, block: CanonicalBlock, block_num_to_insert_above=1, + select_block_number=False) -> CanonicalBlock: """ Insert an extension block just before the block with the specified block number. """ if select_block_number: block.block_number = self.get_new_block_number() @@ -735,15 +754,15 @@ def add_block(self, block:CanonicalBlock, block_num_to_insert_above=1, self.canonical_blocks.insert(insert_pos, block) return block - - def replace_block_by_block_num(self, block_num:int, new_block:CanonicalBlock): + + def replace_block_by_block_num(self, block_num: int, new_block: CanonicalBlock): for idx, block in enumerate(self.canonical_blocks): if block.block_number == block_num: self.canonical_blocks[idx] = new_block return - + @staticmethod - def identify_block(block_bytes:bytes): + def identify_block(block_bytes: bytes): """ Determine the type of the canonical block. """ type_code = block_bytes[1] @@ -764,14 +783,13 @@ def identify_block(block_bytes:bytes): return block_type - def post_dissect(self, s): """ Find the BCBs and check their security targets to definitively determine which blocks are encrypted. """ for bcb_block in self.canonical_blocks: - if isinstance(bcb_block, BlockConfidentialityBlock): + if isinstance(bcb_block, BlockConfidentialityBlock): for idx, block in enumerate(self.canonical_blocks): if block.block_number in bcb_block.data.security_targets.targets: block_type = type(block) @@ -783,6 +801,6 @@ def post_dissect(self, s): encrypted_type = UNENCRYPTED_TO_ENCRYPTED_TYPE_MAP[block_type] new_block = encrypted_type(raw(block)) self.canonical_blocks[idx] = new_block - + new_block.encrypted_by.append(bcb_block.block_number) - return s + return s \ No newline at end of file diff --git a/scapy/contrib/dtn/cbor.py b/scapy/contrib/dtn/cbor.py index 3c7c909c888..b9ee15e1c76 100644 --- a/scapy/contrib/dtn/cbor.py +++ b/scapy/contrib/dtn/cbor.py @@ -20,7 +20,7 @@ import flynn from typing import Tuple, List, Union -MajorTypes = { +MajorTypes = { 0: "unsigned", 1: "negative", 2: "byte string", @@ -31,6 +31,7 @@ 7: "simple/float" } + class MajorTypeException(Exception): """This exception indicates that a CBOR object has an unexpected value for its Major Type. @@ -38,7 +39,7 @@ class MajorTypeException(Exception): actual -- the integer value of the actual Major Type expected -- an integer or list of integers indicating the acceptable Major Type values""" def __init__(self, actual: int, expected: Union[int, List[int]]): - + message = f'[Error] Major type {actual} does not refer to a(n)' if isinstance(expected, int): typ = MajorTypes[expected] @@ -52,19 +53,22 @@ def __init__(self, actual: int, expected: Union[int, List[int]]): message += '.' super().__init__(message) + class StopCodeException(Exception): def __init__(self, value): super().__init__(f'[Error] Major type {value} does not refer to a stop code.') + class AdditionalInfoException(Exception): """This exception indicates that a CBOR object has an unexpected value for its Additional Info.""" def __init__(self): super().__init__('[Error] Invalid additional info.') + class UnhandledTypeException(Exception): def __init__(self, value, cls): super().__init__(f"[Error] Major type {value} is not handled by {cls}.") - + # CBOR definitions class CBORNull(Field): @@ -78,13 +82,14 @@ class CBORNull(Field): class CBORBase(Field): @staticmethod def static_get_head_info(b): - if len(b)==0: return None, None + if len(b) == 0: + return None, None head = b[0] major_type = head >> 5 - add_info = head & 0b00011111 + add_info = head & 0b00011111 - return major_type, add_info + return major_type, add_info def get_head_info(self, b): return CBORBase.static_get_head_info(b) @@ -97,19 +102,19 @@ class CBORInteger(CBORBase): @staticmethod def get_value(add_info, b): if add_info < 24: - val_length = 1 # 1 byte head, argument=add_info + val_length = 1 # 1 byte head, argument=add_info val = add_info elif add_info == 24: - val_length = 2 # 1 byte head + 1 byte argument + val_length = 2 # 1 byte head + 1 byte argument val = b[1] elif add_info == 25: - val_length = 3 # 1 byte head + 2 byte argument + val_length = 3 # 1 byte head + 2 byte argument val = int.from_bytes(b[1:3], byteorder='big') elif add_info == 26: - val_length = 5 # 4 byte argument + val_length = 5 # 4 byte argument val = int.from_bytes(b[1:5], byteorder='big') elif add_info == 27: - val_length = 9 # 8 byte argument + val_length = 9 # 8 byte argument val = int.from_bytes(b[1:9], byteorder='big') else: raise AdditionalInfoException() @@ -121,16 +126,16 @@ def getfield(self, pkt, s): if major_type != 0: raise MajorTypeException(major_type, 0) - + return CBORInteger.get_value(add_info, s) - + class CBORStringBase(CBORBase): @staticmethod def get_value(add_info, b): if add_info < 24: arg_size = 0 - data_size = add_info # argument = data size = additional info + data_size = add_info # argument = data size = additional info else: if add_info == 24: arg_size = 1 @@ -145,13 +150,13 @@ def get_value(add_info, b): # size of argument is known now, so # get value of the argument, which contains the size of the data - data_size = int.from_bytes(b[1:1+arg_size], byteorder='big') - val_length = 1 + arg_size + data_size - val = b[1+arg_size:val_length] + data_size = int.from_bytes(b[1:1 + arg_size], byteorder='big') + val_length = 1 + arg_size + data_size + val = b[1 + arg_size:val_length] + + return b[val_length:], val - return b[val_length:], val - class CBORByteString(CBORStringBase): def getfield(self, pkt, s): major_type, add_info = self.get_head_info(s) @@ -176,8 +181,10 @@ class CBORIntOrText(CBORBase): def getfield(self, pkt, s): major_type, add_info = self.get_head_info(s) - if major_type == 0: return CBORInteger.get_value(add_info, s) - if major_type == 3: return CBORStringBase.get_value(add_info, s) + if major_type == 0: + return CBORInteger.get_value(add_info, s) + if major_type == 3: + return CBORStringBase.get_value(add_info, s) raise MajorTypeException(major_type, [0, 3]) @@ -197,26 +204,29 @@ def getfield(self, pkt, s): return CBORStopCode.get_value(add_info, s) - + class CBORAny(CBORBase): def getfield(self, pkt, s): major_type, add_info = self.get_head_info(s) - if major_type == 0: return CBORInteger.get_value(add_info, s) - if major_type in [2, 3]: return CBORStringBase.get_value(add_info, s) - if major_type == 7: return CBORStopCode.get_value(add_info, s) + if major_type == 0: + return CBORInteger.get_value(add_info, s) + if major_type in [2, 3]: + return CBORStringBase.get_value(add_info, s) + if major_type == 7: + return CBORStopCode.get_value(add_info, s) raise UnhandledTypeException(major_type, CBORAny) - + class CBORArray(Packet): _major_type = BitEnumField("major_type", 4, 3, MajorTypes) - _add = BitField("add", 0, 5) # additional information = length - + _add = BitField("add", 0, 5) # additional information = length + # head fields - fields_desc=[_major_type, _add] + fields_desc = [_major_type, _add] - def count_additional_fields(self)->int: + def count_additional_fields(self) -> int: """ Return the number of fields other than the two head fields. This method does not work correctly with ConditionalFields and should be overridden when that @@ -225,53 +235,53 @@ def count_additional_fields(self)->int: head_field_count = 2 return len(self.default_fields) - head_field_count - def set_additional_fields(self, pkt:Packet)->bytes: + def set_additional_fields(self, pkt: Packet) -> bytes: """ For an, the add field is set to the number of elements minus the two head fields. """ # pylint: disable=W0201 # field (instance variable) initialization is handled via "fields_desc" self.add = self.count_additional_fields() head = (self.major_type << 5) | self.add - + return head.to_bytes(1, 'big') + pkt[1:] - - def post_build(self, pkt:bytes, pay:bytes)->bytes: + + def post_build(self, pkt: bytes, pay: bytes) -> bytes: return self.set_additional_fields(pkt) + pay - + def extract_padding(self, s): return "", s class CBORPacketField(PacketField): - def i2m(self, pkt:Packet, i)->bytes: + def i2m(self, pkt: Packet, i) -> bytes: if i is None: return b"" return flynn.dumps(raw(i)) - def m2i(self, pkt:Packet, m): + def m2i(self, pkt: Packet, m): _, add_info = CBORBase.static_get_head_info(m) - remain, decoded_m =CBORStringBase.get_value(add_info, m) + remain, decoded_m = CBORStringBase.get_value(add_info, m) try: # we want to set parent wherever possible return self.cls(decoded_m + remain, _parent=pkt) # type: ignore except TypeError: return self.cls(decoded_m + remain) - + class CBORPacketFieldWithRemain(CBORPacketField): """ The regular Packet.getfield() never returns the remaining bytes, so the CRC or other following fields get lost. This getfield does return the remaining bytes. """ - def m2i(self, pkt:Packet, m): + def m2i(self, pkt: Packet, m): _, add_info = CBORBase.static_get_head_info(m) - remain, decoded_m =CBORStringBase.get_value(add_info, m) + remain, decoded_m = CBORStringBase.get_value(add_info, m) try: # we want to set parent wherever possible return remain, self.cls(decoded_m + remain, _parent=pkt) # type: ignore except TypeError: return remain, self.cls(decoded_m + remain) - def getfield(self, pkt:Packet, s:bytes)->Tuple[bytes, Packet]: + def getfield(self, pkt: Packet, s: bytes) -> Tuple[bytes, Packet]: remain, i = self.m2i(pkt, s) return remain, i diff --git a/scapy/contrib/dtn/common.py b/scapy/contrib/dtn/common.py index 56ca1245138..911f1144f3b 100644 --- a/scapy/contrib/dtn/common.py +++ b/scapy/contrib/dtn/common.py @@ -5,33 +5,39 @@ from scapy.fields import Field from typing import Dict, List + class NoPayloadPacket(Packet): """A packet with no payload layer to bind.""" def extract_padding(self, s): return "", s + def post_dissect(self, s): try: if self[Raw].load is not None: raise ValueError(f"found payload in {Packet} when none was expected") - except IndexError: # No Raw layer found, i.e. no unparsed payload is present + except IndexError: # No Raw layer found, i.e. no unparsed payload is present pass return s + class ControlPacket(NoPayloadPacket): """A packet containing control data, rather than user data.""" + class FieldPacket(NoPayloadPacket): """A packet intended for use as a field (i.e. PacketField or PacketListField) in another Packet, rather than one sent or received on the wire. Useful when you need heterogeneous, compound data similar to a record/struct within another Packet.""" + FieldsTemplate = Dict[str, Field] + def template_replace(template: FieldsTemplate, new_values: FieldsTemplate) -> FieldsTemplate: return {**template, **new_values} + def make_fields_desc(template: FieldsTemplate) -> List[Field]: return list(template.values()) - diff --git a/scapy/contrib/dtn/tcpcl.py b/scapy/contrib/dtn/tcpcl.py index 4bcbca002cf..8301e3ac3d4 100644 --- a/scapy/contrib/dtn/tcpcl.py +++ b/scapy/contrib/dtn/tcpcl.py @@ -26,6 +26,7 @@ from scapy.contrib.dtn.common import ControlPacket, FieldPacket import scapy.contrib.dtn.bpv7 as BPv7 + class MagicValueError(Exception): """ Exception raised when a ContactHeader is dissected and the magic value is incorrect. @@ -33,16 +34,17 @@ class MagicValueError(Exception): def __init__(self, value): super().__init__(f"Tried to decode ContactHeader with invalid magic value of {value}") + class ContactHeader(ControlPacket): MAGIC_VALUE = 0x64746e21 - + class Flag(IntEnum): CAN_TLS = 0x01 - + fields_desc = [ BitField("magic", MAGIC_VALUE, 32), BitField("version", 4, 8), - XByteEnumField("flags", 0, { Flag.CAN_TLS: "can tls" }) + XByteEnumField("flags", 0, {Flag.CAN_TLS: "can tls"}) ] def post_dissect(self, s): @@ -50,15 +52,16 @@ def post_dissect(self, s): raise MagicValueError(self.magic) return super().post_dissect(s) + class MsgHeader(Packet): class MsgType(IntEnum): - SESS_INIT=0x07 - SESS_TERM=0x05 - XFER_SEGMENT=0x01 - XFER_ACK=0x02 - XFER_REFUSE=0x03 - KEEPALIVE=0x04 - MSG_REJECT=0x06 + SESS_INIT = 0x07 + SESS_TERM = 0x05 + XFER_SEGMENT = 0x01 + XFER_ACK = 0x02 + XFER_REFUSE = 0x03 + KEEPALIVE = 0x04 + MSG_REJECT = 0x06 fields_desc = [ XByteEnumField("type", MsgType.XFER_SEGMENT, { @@ -72,6 +75,7 @@ class MsgType(IntEnum): }) ] + class Ext(FieldPacket): """ Class definition for an Extension Item in the format of a Type-Length-Value container. @@ -83,19 +87,20 @@ class Type(IntEnum): LENGTH = 0x0001 fields_desc = [ - XByteEnumField("flags", 0, { Flag.CRITICAL: "critical" }), + XByteEnumField("flags", 0, {Flag.CRITICAL: "critical"}), BitField("type", 0, 16), BitFieldLenField("length", default=0, size=16, length_of="data"), StrLenField("data", 0, length_from=lambda pkt: pkt.length) ] + class SessInit(ControlPacket): fields_desc = [ BitField("keepalive", 0, 16), BitField("segment_mru", 0, 64), BitField("transfer_mru", 0, 64), FieldLenField("id_length", None, length_of="id", fmt="H"), # Node ID Length (U16) - StrLenField("id", b"", length_from=lambda pkt: pkt.id_length), # Node ID Data (variable) + StrLenField("id", b"", length_from=lambda pkt: pkt.id_length), # Node ID Data (variable) BitFieldLenField("ext_length", 0, 32, length_of="ext_items"), ConditionalField( PacketListField("ext_items", @@ -105,11 +110,13 @@ class SessInit(ControlPacket): lambda pkt: pkt.ext_length > 0) ] + class Keepalive(ControlPacket): """ A keepalive message consists only of a MsgHeader with the type code KEEPALIVE. """ - + + class MsgReject(ControlPacket): class ReasonCode(IntEnum): UNKNOWN = 0x01 @@ -125,6 +132,7 @@ class ReasonCode(IntEnum): PacketField("header", MsgHeader(), MsgHeader) ] + class Xfer(Packet): """ Abstract class containing fields and flags common to Xfer messages @@ -142,14 +150,16 @@ class Flag(IntEnum): BitField("id", 0, 64) ] + class InvalidPayloadError(Exception): """ - This error indicates that an XferSegment contains raw bytes instead of + This error indicates that an XferSegment contains raw bytes instead of a properly formatted Bundle as its payload. """ def __init__(self, payload_bytes): super().__init__(f"Failed to fully parse Bundle from Xfer payload: bundle={payload_bytes}") + class XferSegment(Xfer): """ Packet for transferring a data segment @@ -170,26 +180,28 @@ class XferSegment(Xfer): def post_build(self, pkt, pay): # calculate the length field if not self.length: - index = len(pkt)-8 # size of length is 8 bytes, thus position=len(pkt)-8 + index = len(pkt) - 8 # size of length is 8 bytes, thus position=len(pkt)-8 length = len(pay) pkt = pkt[:index] + struct.pack('!Q', length) return pkt + pay - + def post_dissect(self, s): "An XferSegment message should have a Bundle as payload. If it has raw bytes instead, raise an error." try: if self[Raw].load is not None: raise InvalidPayloadError(self[Raw].load) - except IndexError: # Raw layer or load field does not exist - pass # no action required + except IndexError: # Raw layer or load field does not exist + pass # no action required return s - + + class XferAck(ControlPacket): fields_desc = Xfer.fields_desc + [ BitField("length", default=0, size=64) ] + class XferRefuse(ControlPacket): class ReasonCode(IntEnum): UNKNOWN = 0x00 @@ -213,6 +225,7 @@ class ReasonCode(IntEnum): BitField("id", 0, 64) ] + class SessTerm(ControlPacket): class Flag(IntEnum): REPLY = 0x01 @@ -226,7 +239,7 @@ class ReasonCode(IntEnum): NO_RESOURCES = 0x05 fields_desc = [ - XByteEnumField("flags", 0, { Flag.REPLY: "reply" }), + XByteEnumField("flags", 0, {Flag.REPLY: "reply"}), XByteEnumField("reason", ReasonCode.UNKNOWN, { ReasonCode.UNKNOWN: "unknown", ReasonCode.TIMEOUT: "idle timeout", @@ -235,9 +248,10 @@ class ReasonCode(IntEnum): ReasonCode.CONTACT_FAIL: "failed to process contact header or sess init", ReasonCode.NO_RESOURCES: "entity resource exhaustion" }), - + ] + # Bind all TCPCL message headers to TCPCL messages. # This way, if `some_bytes` consists of the raw representation of a TCPCL message, # you can evaluate e.g. `x=MsgHeader(some_bytes)` and `x` will be a Packet consisting of a TCPCL From d14c746b84698dfd9d0af42e4141a529d3c16967 Mon Sep 17 00:00:00 2001 From: T-recks Date: Thu, 21 Aug 2025 09:20:50 -0700 Subject: [PATCH 05/16] more pep8 fixes --- scapy/contrib/dtn/bpv7.py | 8 ++++---- scapy/contrib/dtn/cbor.py | 16 ++++++++-------- scapy/contrib/dtn/common.py | 1 - 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/scapy/contrib/dtn/bpv7.py b/scapy/contrib/dtn/bpv7.py index d6aceb4f5cd..e6e2fa9354c 100644 --- a/scapy/contrib/dtn/bpv7.py +++ b/scapy/contrib/dtn/bpv7.py @@ -563,9 +563,9 @@ class CtrlFlags(IntFlag): PacketField("creation_timestamp", Timestamp(t=int(time.time())), Timestamp), CBORInteger("lifetime", 0), ConditionalField(CBORInteger("fragment_offset", 0), - lambda pkt: pkt.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT), + lambda pkt: pkt.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT), ConditionalField(CBORInteger("total_adu_length", 0), - lambda pkt: pkt.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT), + lambda pkt: pkt.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT), ConditionalField( MultipleTypeField( [ @@ -574,7 +574,7 @@ class CtrlFlags(IntFlag): ], CBORNull("crc", None) ), lambda pkt: pkt.crc_type != CrcTypes.NONE - ) + ) ] def dissect(self, s: bytes): @@ -803,4 +803,4 @@ def post_dissect(self, s): self.canonical_blocks[idx] = new_block new_block.encrypted_by.append(bcb_block.block_number) - return s \ No newline at end of file + return s diff --git a/scapy/contrib/dtn/cbor.py b/scapy/contrib/dtn/cbor.py index b9ee15e1c76..ade89f457f4 100644 --- a/scapy/contrib/dtn/cbor.py +++ b/scapy/contrib/dtn/cbor.py @@ -21,14 +21,14 @@ from typing import Tuple, List, Union MajorTypes = { - 0: "unsigned", - 1: "negative", - 2: "byte string", - 3: "text string", - 4: "array", - 5: "map", - 6: "tag", - 7: "simple/float" + 0: "unsigned", + 1: "negative", + 2: "byte string", + 3: "text string", + 4: "array", + 5: "map", + 6: "tag", + 7: "simple/float" } diff --git a/scapy/contrib/dtn/common.py b/scapy/contrib/dtn/common.py index 911f1144f3b..7efb4085db0 100644 --- a/scapy/contrib/dtn/common.py +++ b/scapy/contrib/dtn/common.py @@ -12,7 +12,6 @@ class NoPayloadPacket(Packet): def extract_padding(self, s): return "", s - def post_dissect(self, s): try: if self[Raw].load is not None: From 1121357b7f6088f8f24d158c815ab28199e14f0e Mon Sep 17 00:00:00 2001 From: T-recks Date: Fri, 22 Aug 2025 05:35:43 -0700 Subject: [PATCH 06/16] revert pyproject changes --- pyproject.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 08dcc51c955..501096e4ac4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,8 +58,6 @@ all = [ "pyx", "cryptography>=2.0", "matplotlib", - "flynn>=1.0.0.b2", - "crcmod>=1.7" ] doc = [ "cryptography>=2.0", @@ -67,10 +65,6 @@ doc = [ "sphinx_rtd_theme>=1.3.0", "tox>=3.0.0", ] -dtn = [ - "flynn>=1.0.0.b2", - "crcmod>=1.7" -] # setuptools specific From 5e26d68480cd9ff03d5dfcbcb26826003ced0047 Mon Sep 17 00:00:00 2001 From: T-recks Date: Sat, 23 Aug 2025 10:40:30 -0700 Subject: [PATCH 07/16] translate bpv7 tests to uts --- test/contrib/bpv7.uts | 223 +++++++++++++++++++++++++++ test/pcaps/bpv7_bundle_with_con.pcap | Bin 0 -> 264 bytes 2 files changed, 223 insertions(+) create mode 100644 test/contrib/bpv7.uts create mode 100644 test/pcaps/bpv7_bundle_with_con.pcap diff --git a/test/contrib/bpv7.uts b/test/contrib/bpv7.uts new file mode 100644 index 00000000000..ffcabc55ec1 --- /dev/null +++ b/test/contrib/bpv7.uts @@ -0,0 +1,223 @@ +% Bundle Protocol version 7 tests + ++ Simple BPv7 Tests + += Test decode +~ bpv7 + +* dissecting valid bundle from string and checking all fields for accuracy +import scapy.contrib.dtn.bpv7 as BPv7 + +bs = '9f8907040282028202018202820101820100821b000000b4e6fc6dae001a000f4240440512dd21860a021002448218640044db675d49860101000258640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044dd3243fcff' + +bundle = BPv7.Bundle(bytearray.fromhex(bs)) +bundle + +assert bundle.primary_block.version == 7, "Wrong Bundle Protocol version" +assert bundle.primary_block.flags == BPv7.PrimaryBlock.CtrlFlags.MUST_NOT_BE_FRAGMENTED, "Wrong flags in primary block" +assert bundle.primary_block.crc_type == 2, "Wrong crc type in primary block" +assert bundle.primary_block.dest.scheme_code == 2, "Wrong destination EID type" +assert bundle.primary_block.dest.ssp.node_id == 2, "Wrong destination id" +assert bundle.primary_block.dest.ssp.service_number == 1, "Wrong destination service number" +assert bundle.primary_block.src.scheme_code == 2, "Wrong source EID type" +assert bundle.primary_block.src.ssp.node_id == 1, "Wrong source id" +assert bundle.primary_block.src.ssp.service_number == 1, "Wrong source service number" +assert bundle.primary_block.report.scheme_code == 1, "Wrong report-to EID type" +assert bundle.primary_block.report.ssp.uri == 0, "Wrong report-to id" +assert bundle.primary_block.creation_timestamp.t == 776969416110, "Wrong timestamp" +assert bundle.primary_block.creation_timestamp.seq == 0, "Wrong seq" +assert bundle.primary_block.lifetime == 1000000, "Wrong lifetime" +assert bundle.primary_block.crc == b'\x05\x12\xdd\x21', "Wrong crc in primary block" + +assert len(bundle.canonical_blocks) == 2, "Wrong number of canonical blocks" +block1 = bundle.canonical_blocks[0] +assert block1.type_code == 10, "Expected type hop_count in first block" +assert block1.block_number == 2, "Wrong block number in first canonical block" +assert block1.flags == BPv7.CanonicalBlock.CtrlFlags.DISCARD_IF_NOT_PROCESSED, "Wrong flags in first canonical block" +assert block1.crc_type == 2, "Wrong crc type in first canonical block" +assert block1.data == BPv7.HopCount(limit=100, count=0), "Expected cbor [0x18, 0x00] as hop count data" +assert block1.crc == b'\xdb\x67\x5d\x49', "Wrong crc in first canonical block" + +block2 = bundle.canonical_blocks[1] +assert block2.type_code == 1, "Expected type payload in second block" +assert block2.block_number == 1, "Wrong block number in second canonical block" +assert block2.flags == 0, "Wrong flags in second canonical block" +assert block2.crc_type == 2, "Wrong crc type in second canonical block" +assert block2.data == b'\x00' * 100, "Expected 100 bytes of zero as bundle payload" +assert block2.crc == b'\xdd\x32\x43\xfc', "Wrong crc in second canonical block" + += Test decode invalid bundle +~ bpv7 + +* attempting to decode a bundle with two primary block elements (should fail to dissect) +bs = '9f8907040282028202018202820403820105821b000000b700dfc451001a000f424044b3cf0f1d8907040282028202018202820403820105821b000000b700dfc451001a000f424044b3cf0f1dff' + +try: + BPv7.Bundle(bytearray.fromhex(bs)) + assert False +except: + assert True + += Test encode +~ bpv7 + +* building bundle +block1 = BPv7.HopCountBlock( + block_number=2, + flags=BPv7.CanonicalBlock.CtrlFlags.DISCARD_IF_NOT_PROCESSED, + crc_type=2, + data=BPv7.HopCount(limit=100, count=0) + # crc=b'\xdb\x67\x5d\x49' +) +block2 = BPv7.PayloadBlock( + block_number=1, + flags=0, + crc_type=2, + data=b'\x00'*100 + # crc=b'\xdd\x32\x43\xfc' +) +canonical_blocks=[block1, block2] +destination=BPv7.IPN(node_id=2, service_number=1) +source=BPv7.IPN(node_id=1, service_number=1) +report_to=BPv7.DTN(uri=0) +creation_time=BPv7.Timestamp(t=776969416110, seq=0) +primary_block = BPv7.PrimaryBlock( + version=7, + flags=BPv7.PrimaryBlock.CtrlFlags.MUST_NOT_BE_FRAGMENTED, + crc_type=2, + dest=BPv7.EndpointID(scheme_code=2, ssp=destination), + src=BPv7.EndpointID(scheme_code=2, ssp=source), + report=BPv7.EndpointID(scheme_code=1, ssp=report_to), + creation_timestamp=creation_time, + lifetime=1000000, + # crc=b'\x05\x12\xdd\x21', +) +bundle = BPv7.Bundle( + primary_block = primary_block, + canonical_blocks=canonical_blocks +) + +bundle + +bs = '9f8907040282028202018202820101820100821b000000b4e6fc6dae001a000f4240440512dd21860a021002448218640044db675d49860101000258640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044dd3243fcff' + +captured_bytes = bytearray.fromhex(bs) +bundle_bytes = bytearray(raw(bundle)) +captured_bytes == bundle_bytes + += Test blocks +~ bpv7 + +* testing a bundle with default block instances can be built without error +bundle=BPv7.Bundle(canonical_blocks=[ + BPv7.PreviousNodeBlock(), + BPv7.HopCountBlock(), + BPv7.BundleAgeBlock(), + BPv7.BlockIntegrityBlock(), + BPv7.BlockConfidentialityBlock(), + BPv7.EncryptedHopCountBlock(), + BPv7.EncryptedPreviousNodeBlock(), + BPv7.EncryptedBundleAgeBlock(), + BPv7.EncryptedBlockIntegrityBlock(), + BPv7.PayloadBlock(), +]) +b = bundle.build() +b +b is not None + ++ BPv7 with BPSec tests + += Test bpsec bundle integrity block +~ bpv7 bpsec debug + +* test that a non-default BIB can be created +b = BPv7.BlockIntegrityBlock( + block_number=3, + flags=BPv7.CanonicalBlock.CtrlFlags.BLOCK_MUST_BE_REPLICATED, + crc_type=BPv7.CrcTypes.NONE, + data=BPv7.AbstractSecurityBlock( + security_targets=BPv7.SecurityTargets(targets=[1,2,3]), + security_context_id=2, + security_context_flags=1, + security_source=BPv7.EndpointID(scheme_code=2, ssp=BPv7.IPN(node_id=1, service_number=1)), + security_context_parameters=BPv7.CBORTupleArray(tuples=[ + BPv7.CBORTuple(id=1,value=bytes.fromhex('136B229B84CA0200243B0000')), + BPv7.CBORTuple(id=2,value=3), + BPv7.CBORTuple(id=4,value=7) + ]), + security_results=BPv7.SecurityResults(results=BPv7.CBORTupleArray(tuples=[ + BPv7.CBORTuple(id=1,value=bytes.fromhex('CA492BCE6F1B4C7AF3995A985432409F')) + ])) + ) +).build() +b +b is not None + += Test bpsec bundle confidentiality +~ bpv7 bpsec debug + +* testing confidentiality block +f = open(scapy_path("test/pcaps/bpv7_bundle_with_con.pcap"), "rb") +content = f.read() + +raw_bundle = content[22:] + +my_bundle = BPv7.Bundle( + primary_block=BPv7.PrimaryBlock( + version=7, + flags=BPv7.PrimaryBlock.CtrlFlags.MUST_NOT_BE_FRAGMENTED, + crc_type=BPv7.CrcTypes.CRC32C, + dest=BPv7.EndpointID(scheme_code=2, ssp=BPv7.IPN(node_id=2, service_number=1)), + src=BPv7.EndpointID(scheme_code=2, ssp=BPv7.IPN(node_id=1, service_number=1)), + report=BPv7.EndpointID(scheme_code=1, ssp=BPv7.DTN(uri=0)), + creation_timestamp=BPv7.Timestamp(t=785620852727, seq=0), + lifetime=1000000, + # crc=b'\xEFA\xC0e' + ), + canonical_blocks=[ + BPv7.PreviousNodeBlock( + block_number=4, + flags=16, + crc_type=BPv7.CrcTypes.CRC32C, + data=BPv7.EndpointID(scheme_code=2, ssp=BPv7.IPN(node_id=10, service_number=0)), + # crc=b'\xED6\x9A\xB2' + ), + BPv7.BlockConfidentialityBlock( + block_number=3, + flags=BPv7.CanonicalBlock.CtrlFlags.BLOCK_MUST_BE_REPLICATED, + crc_type=BPv7.CrcTypes.NONE, + data=BPv7.AbstractSecurityBlock( + security_targets=BPv7.SecurityTargets(targets=[1]), + security_context_id=2, + security_context_flags=1, + security_source=BPv7.EndpointID(scheme_code=2, ssp=BPv7.IPN(node_id=1, service_number=1)), + security_context_parameters=BPv7.CBORTupleArray(tuples=[ + BPv7.CBORTuple(id=1,value=bytes.fromhex('136B229B84CA0200243B0000')), + BPv7.CBORTuple(id=2,value=3), + BPv7.CBORTuple(id=4,value=7) + ]), + security_results=BPv7.SecurityResults(results=BPv7.CBORTupleArray(tuples=[ + BPv7.CBORTuple(id=1,value=bytes.fromhex('CA492BCE6F1B4C7AF3995A985432409F')) + ])) + ) + ), + BPv7.HopCountBlock( + block_number=2, + flags=BPv7.CanonicalBlock.CtrlFlags.DISCARD_IF_NOT_PROCESSED, + crc_type=BPv7.CrcTypes.CRC32C, + data=BPv7.HopCount(limit=100, count=1), + # crc=b'\x34\x57\x36\x50' + ), + BPv7.PayloadBlock( + block_number=1, + flags=0, + crc_type=BPv7.CrcTypes.CRC32C, + data=bytes.fromhex("7E4B954DCCEA632B68C0732AE92B067895CAA6676D9556D0F1B28BBDA03DB2B9FB3F4C85EECBB3C00B8104968511F80EEC12FB993ADA63D79AFE368D0780A53713AC50E889303D7B8739CA306DD62AD8533DDCDCFD73BBE1D49CDA182CAB3CB17058DDD0"), + # crc=b'\x5\x84\xB5\xF4B' + ) + + ] +) + +my_bundle +raw(my_bundle) == raw_bundle \ No newline at end of file diff --git a/test/pcaps/bpv7_bundle_with_con.pcap b/test/pcaps/bpv7_bundle_with_con.pcap new file mode 100644 index 0000000000000000000000000000000000000000..f29eba08415cab75893d6d0f82e02f5285ad240c GIT binary patch literal 264 zcmZQ%W&i_g6$lL`Kh5uCXJKjrLPj9L$k@cl&?F6%+4gGL{qGD?4E#A2DCMM=47WT%*CdPnM zp4#W~rG2VC&y1Q8V&pKtjf+Wu$)!mmh0(<%+$^Atk&%HZBBjoIs_&Us$=VqQinU&9 zvsFwzwJbe%YS@L3o4WTdu-&xtx4lp6yVIKwa5u6{YZd&#_eSXVOsiYT*Ju4R>t%0P yYA(Dc;6 Date: Sat, 23 Aug 2025 11:15:06 -0700 Subject: [PATCH 08/16] update comments --- test/contrib/bpv7.uts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/contrib/bpv7.uts b/test/contrib/bpv7.uts index ffcabc55ec1..29d96917e25 100644 --- a/test/contrib/bpv7.uts +++ b/test/contrib/bpv7.uts @@ -3,7 +3,7 @@ + Simple BPv7 Tests = Test decode -~ bpv7 +~ dtn bpv7 * dissecting valid bundle from string and checking all fields for accuracy import scapy.contrib.dtn.bpv7 as BPv7 @@ -47,7 +47,7 @@ assert block2.data == b'\x00' * 100, "Expected 100 bytes of zero as bundle paylo assert block2.crc == b'\xdd\x32\x43\xfc', "Wrong crc in second canonical block" = Test decode invalid bundle -~ bpv7 +~ dtn bpv7 * attempting to decode a bundle with two primary block elements (should fail to dissect) bs = '9f8907040282028202018202820403820105821b000000b700dfc451001a000f424044b3cf0f1d8907040282028202018202820403820105821b000000b700dfc451001a000f424044b3cf0f1dff' @@ -59,7 +59,7 @@ except: assert True = Test encode -~ bpv7 +~ dtn bpv7 * building bundle block1 = BPv7.HopCountBlock( @@ -106,7 +106,7 @@ bundle_bytes = bytearray(raw(bundle)) captured_bytes == bundle_bytes = Test blocks -~ bpv7 +~ dtn bpv7 * testing a bundle with default block instances can be built without error bundle=BPv7.Bundle(canonical_blocks=[ @@ -128,7 +128,7 @@ b is not None + BPv7 with BPSec tests = Test bpsec bundle integrity block -~ bpv7 bpsec debug +~ dtn bpv7 bpsec * test that a non-default BIB can be created b = BPv7.BlockIntegrityBlock( @@ -153,8 +153,8 @@ b = BPv7.BlockIntegrityBlock( b b is not None -= Test bpsec bundle confidentiality -~ bpv7 bpsec debug += Test bpsec bundle confidentiality blocks +~ dtn bpv7 bpsec * testing confidentiality block f = open(scapy_path("test/pcaps/bpv7_bundle_with_con.pcap"), "rb") From 3371d84df14177f7ed2c0a3ffd17302894a9797d Mon Sep 17 00:00:00 2001 From: T-recks Date: Sat, 23 Aug 2025 12:00:54 -0700 Subject: [PATCH 09/16] translate tcpcl tests to uts --- scapy/contrib/dtn/tcpcl_session.py | 112 +++++++++++++++++++++++++++++ test/contrib/bpv7.uts | 12 ++-- test/contrib/tcpcl.uts | 63 ++++++++++++++++ test/pcaps/tcpcl.pcap | Bin 0 -> 3083 bytes 4 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 scapy/contrib/dtn/tcpcl_session.py create mode 100644 test/contrib/tcpcl.uts create mode 100644 test/pcaps/tcpcl.pcap diff --git a/scapy/contrib/dtn/tcpcl_session.py b/scapy/contrib/dtn/tcpcl_session.py new file mode 100644 index 00000000000..48f69b0ce82 --- /dev/null +++ b/scapy/contrib/dtn/tcpcl_session.py @@ -0,0 +1,112 @@ +# scapy.contrib.description = TCP Convergence Layer version 4 (TCPCLv4) +# scapy.contrib.status = loads + +# These classes support unit testing of the TCPCL scapy layer (scapy.contrib.dtn.tcpcl) and illustrate how the protocol messages may be used to emulate a TCPCL session. + +from scapy.all import Raw, raw, TCP, Packet, bind_layers, split_layers +import scapy.contrib.dtn.tcpcl as TCPCL +from typing import List + + +class Session: + """ + TCPCL messages are conventionally, but not necessarily, sent on port 4556. Since this cannot be relied upon, especially on a localhost session, the best way to bind TCP packets to TCPCL message is to track the state of a TCPCL session. Once Contact Headers are successfuly exchanged, TCP packets can be assumed to carry payloads of TCPCL messages until the session ends. + """ + def __init__(self): + self.contact_init = False + self.contact_ack = False + self.is_active = False + self.term_begun = False + self.sport = 0 + self.dport = 0 + + @staticmethod + def bind_messages(sport, dport): + bind_layers(TCP, TCPCL.MsgHeader, sport=sport, dport=dport) + bind_layers(TCP, TCPCL.MsgHeader, sport=dport, dport=sport) + + @staticmethod + def split_messages(sport, dport): + split_layers(TCP, TCPCL.MsgHeader, sport=sport, dport=dport) + split_layers(TCP, TCPCL.MsgHeader, sport=dport, dport=sport) + + def activate(self): + if not (self.contact_init and self.contact_ack): + raise Exception("tried to activate a session before initialization and acknowledgement") + + self.is_active = self.contact_init and self.contact_ack + + Session.bind_messages(self.sport, self.dport) + + def terminate(self): + if not (self.contact_init and self.contact_ack): + raise Exception("tried to terminate a session while none was active") + + self.is_active = self.contact_ack = self.contact_init = False + + Session.split_messages(self.sport, self.dport) + + def init_contact(self, sport, dport): + self.contact_init=True + self.sport = sport + self.dport = dport + + def init_timeout(self): + self.contact_init=False + + def proc_ack(self): + self.contact_ack=True + self.activate() + + def proc_term(self): + self.term_begun = True + + def proc_term_ack(self): + self.terminate() + +class TestTcpcl(): + + @staticmethod + def check_pkt(pkt: Packet, options: List[Packet]): + """Asserts that pkt is equal to one of the packets in options (according to the raw representation)""" + for opt in options: + assert raw(pkt) in list(map(raw, options)), "Failed to build a properly formatted TCPCL message" + + @staticmethod + def make_prn(): + """Define a function for processing packets that closes over a new Session. + Return it for use in Scapy.sniff.""" + + sess = Session() + + def process(pkt): + # Manage session initialization + if not sess.is_active: + try: # try to find a Contact Header + pay = pkt[Raw].load + contact = TCPCL.ContactHeader(pay) # should raise unhandled error if + # the TCP payload does not fit ContactHeader + # replace pkt's raw payload with a ContactHeader formatted payload + pkt[TCP].remove_payload() + pkt = pkt / contact + + # process ContactHeader + if sess.contact_init: # session aready initialized, Header is an ack + sess.proc_ack() + print("BEGIN TCPCL SESSION") + else: + sess.init_contact(pkt[TCP].sport, pkt[TCP].dport) + except IndexError: # no TCP payload to process + pass + else: # currently in an active session + if TCPCL.SessTerm in pkt: + # process SessTerm + if sess.term_begun: + sess.proc_term_ack() + print("END TCPCL SESSION") + else: + sess.proc_term() + + return pkt # end of process + + return process # end of make_prn diff --git a/test/contrib/bpv7.uts b/test/contrib/bpv7.uts index 29d96917e25..4e517c992a1 100644 --- a/test/contrib/bpv7.uts +++ b/test/contrib/bpv7.uts @@ -4,8 +4,8 @@ = Test decode ~ dtn bpv7 - * dissecting valid bundle from string and checking all fields for accuracy + import scapy.contrib.dtn.bpv7 as BPv7 bs = '9f8907040282028202018202820101820100821b000000b4e6fc6dae001a000f4240440512dd21860a021002448218640044db675d49860101000258640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044dd3243fcff' @@ -48,8 +48,8 @@ assert block2.crc == b'\xdd\x32\x43\xfc', "Wrong crc in second canonical block" = Test decode invalid bundle ~ dtn bpv7 - * attempting to decode a bundle with two primary block elements (should fail to dissect) + bs = '9f8907040282028202018202820403820105821b000000b700dfc451001a000f424044b3cf0f1d8907040282028202018202820403820105821b000000b700dfc451001a000f424044b3cf0f1dff' try: @@ -60,8 +60,8 @@ except: = Test encode ~ dtn bpv7 +* building a bundle -* building bundle block1 = BPv7.HopCountBlock( block_number=2, flags=BPv7.CanonicalBlock.CtrlFlags.DISCARD_IF_NOT_PROCESSED, @@ -107,8 +107,8 @@ captured_bytes == bundle_bytes = Test blocks ~ dtn bpv7 - * testing a bundle with default block instances can be built without error + bundle=BPv7.Bundle(canonical_blocks=[ BPv7.PreviousNodeBlock(), BPv7.HopCountBlock(), @@ -129,8 +129,8 @@ b is not None = Test bpsec bundle integrity block ~ dtn bpv7 bpsec +* testing that a non-default BIB can be created -* test that a non-default BIB can be created b = BPv7.BlockIntegrityBlock( block_number=3, flags=BPv7.CanonicalBlock.CtrlFlags.BLOCK_MUST_BE_REPLICATED, @@ -155,8 +155,8 @@ b is not None = Test bpsec bundle confidentiality blocks ~ dtn bpv7 bpsec - * testing confidentiality block + f = open(scapy_path("test/pcaps/bpv7_bundle_with_con.pcap"), "rb") content = f.read() diff --git a/test/contrib/tcpcl.uts b/test/contrib/tcpcl.uts new file mode 100644 index 00000000000..8dcd291d517 --- /dev/null +++ b/test/contrib/tcpcl.uts @@ -0,0 +1,63 @@ +% TCP Convergence Layer tests + ++ Test full TCPCL session + += Test dissect and build +~ dtn tcpcl +* testing packet dissection and build for full TCPCL session from pcap + +import scapy.contrib.dtn.tcpcl as TCPCL +from scapy.contrib.dtn.tcpcl_session import TestTcpcl + +# Test dissection from pcap +pkts=sniff(offline=scapy_path("test/pcaps/tcpcl.pcap"), + prn=TestTcpcl.make_prn()) +assert len(pkts) == 26, "Failed to dissect some packets" + +# Define expected messages +init1 = TCPCL.SessInit( + keepalive=17, + segment_mru=200000, + transfer_mru=10000000, + id=b"ipn:1.0" +) +init2 = TCPCL.SessInit( + keepalive=15, + segment_mru=200000, + transfer_mru=10000000, + id=b"ipn:10.0" +) +bundle0 = bytearray.fromhex('9f8907040282028202018202820101820100821b000000b4e6fc6dae001a000f4240440512dd21860a021002448218640044db675d49860101000258640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044dd3243fcff') +bundle1 = bytearray.fromhex('9f8907040282028202018202820101820100821b000000b4e6fc6db8001a000f424044bb1ffd92860a021002448218640044db675d4986010100025864010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004419e13bfbff') +bundle2 = bytearray.fromhex('9f8907040282028202018202820101820100821b000000b4e6fc6dc2001a000f42404418438afa860a021002448218640044db675d498601010002586402000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000445178c503ff') +bundle3 = bytearray.fromhex('9f8907040282028202018202820101820100821b000000b4e6fcfb66001a000f4240449fdacc81860a021002448218640044db675d49860101000258642c0e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044b1439f4cff') +flags = TCPCL.Xfer.Flag.START | TCPCL.Xfer.Flag.END +xfer0 = TCPCL.XferSegment(flags=flags, id=0) / bundle0 +xfer1 = TCPCL.XferSegment(flags=flags, id=1) / bundle1 +xfer2 = TCPCL.XferSegment(flags=flags, id=2) / bundle2 +xfer3 = TCPCL.XferSegment(flags=flags, id=3628) / bundle3 +ack0 = TCPCL.XferAck(flags=flags, id=0, length=167) +ack1 = TCPCL.XferAck(flags=flags, id=1, length=167) +ack2 = TCPCL.XferAck(flags=flags, id=2, length=167) +ack3 = TCPCL.XferAck(flags=flags, id=3628, length=167) +term0 = TCPCL.SessTerm() +term1 = TCPCL.SessTerm(flags=TCPCL.SessTerm.Flag.REPLY) + +# Test that built TCPCL messages have the expected value +# (including auto-computed fields, such as length fields). +# They should be identical to the packets from the pcap. +for pkt in pkts: + try: + msg = pkt[TCPCL.MsgHeader].payload + mtype = type(msg) + if mtype == TCPCL.SessInit: + TestTcpcl.check_pkt(msg, [init1, init2]) + elif mtype == TCPCL.XferSegment: + TestTcpcl.check_pkt(msg, [xfer0, xfer1, xfer2, xfer3]) + elif mtype == TCPCL.XferAck: + TestTcpcl.check_pkt(msg, [ack0, ack1, ack2, ack3]) + elif mtype == TCPCL.SessTerm: + TestTcpcl.check_pkt(msg, [term0, term1]) + except IndexError: # pkt contains no TCPCL msg + continue + diff --git a/test/pcaps/tcpcl.pcap b/test/pcaps/tcpcl.pcap new file mode 100644 index 0000000000000000000000000000000000000000..bac0bec11cdb5122f3e48001668e79c6fbdf6f8d GIT binary patch literal 3083 zcmcJRYivtl7{}lDwAHCgTOSxAR)X3@Y$TFlS=M3|UnZ82#g;I)xP*l81;^%|&_v?$ ztt5+navPQfUsy!MVuBcE#ayC3NMs4McB|)kdfxV&le3o(^DggpcFFJgzrTO?ZZF!J zFDlH=a?8#*e6r;I-3@JxoGpWA-1?Qr7BjXm{<55v4_oo^0An0?^q99j()6qVfAWHo z?i`rIm|{d zwvkxBYrD@*cBcS$@T6)2xpy9kq?!X4IG8b5(mf|-N!8y(yl+V~CS8PQ&n4Xl+`*H^ zUz35af~z^50Zu)pq{v-_PWw)AwiaaK)-Q6pGX92$4=st96spi#Dnp`^c?n`0RZbtz zpH-Q2^N#JM#k1ixn0YPl4s*5+o^k6JO{`Y?$jv1&QM2s<+e=lP7&1&Ob22wU^b_-F z;+)*bmJ&s#t&0x%MU2$6Ed_1GCT*MGkY$yiZT&UQ(l8jee$gR*#NQIpZ%M>Md=`hO z8jD13(=)W^Xymxlt~9_Khj*MeaMmypzIXFWYTFeyfw?QngN`xXz9TNhqXZlB>shdC zfg$}LL;z-kTJ9S!`)iKW9 zTQycO>xkkE6=q0Y``x7clbDey@wpvJ3>ZtS!WrX`SkoyjF-FAcmPBKT1xU=6h?lll zvJzXh67NjzKW$y2o+OAv{V_QH!-6E0IQg}-#1xfyL+H>-3_ywZOeOk2M?tnkaR@4L z^?+3QJ5oMP%+N}_2PIb1jNI={AP0EWvM`2dEI7D-5Yx#l#iaJQzB8G+@L9sylFpZt$fyK9qK?k*gt9&7b;fAbVBCIzY!0p**XDl-K=s;6G-x^#`KsaDKvtGlJRcFrz%Ag7O+t zUL>wVn$h8441-rBWzg_U3Vy~Eyas|dQt%w~DFp9uaDslnG)!`-CPq-w76H|gHbT-` zEP!CQERN04(^4e1!l2 literal 0 HcmV?d00001 From f736de6600250a41bede0abc82541aa10fd6f2ee Mon Sep 17 00:00:00 2001 From: T-recks Date: Sat, 23 Aug 2025 12:05:54 -0700 Subject: [PATCH 10/16] fix pep8 whitespace errors --- scapy/contrib/dtn/tcpcl_session.py | 37 +++++++++++++++--------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/scapy/contrib/dtn/tcpcl_session.py b/scapy/contrib/dtn/tcpcl_session.py index 48f69b0ce82..75274c1afef 100644 --- a/scapy/contrib/dtn/tcpcl_session.py +++ b/scapy/contrib/dtn/tcpcl_session.py @@ -33,7 +33,7 @@ def split_messages(sport, dport): def activate(self): if not (self.contact_init and self.contact_ack): raise Exception("tried to activate a session before initialization and acknowledgement") - + self.is_active = self.contact_init and self.contact_ack Session.bind_messages(self.sport, self.dport) @@ -41,21 +41,21 @@ def activate(self): def terminate(self): if not (self.contact_init and self.contact_ack): raise Exception("tried to terminate a session while none was active") - + self.is_active = self.contact_ack = self.contact_init = False Session.split_messages(self.sport, self.dport) - + def init_contact(self, sport, dport): - self.contact_init=True + self.contact_init = True self.sport = sport self.dport = dport def init_timeout(self): - self.contact_init=False + self.contact_init = False def proc_ack(self): - self.contact_ack=True + self.contact_ack = True self.activate() def proc_term(self): @@ -64,6 +64,7 @@ def proc_term(self): def proc_term_ack(self): self.terminate() + class TestTcpcl(): @staticmethod @@ -71,34 +72,34 @@ def check_pkt(pkt: Packet, options: List[Packet]): """Asserts that pkt is equal to one of the packets in options (according to the raw representation)""" for opt in options: assert raw(pkt) in list(map(raw, options)), "Failed to build a properly formatted TCPCL message" - + @staticmethod def make_prn(): """Define a function for processing packets that closes over a new Session. Return it for use in Scapy.sniff.""" - + sess = Session() def process(pkt): # Manage session initialization if not sess.is_active: - try: # try to find a Contact Header + try: # try to find a Contact Header pay = pkt[Raw].load - contact = TCPCL.ContactHeader(pay) # should raise unhandled error if - # the TCP payload does not fit ContactHeader + contact = TCPCL.ContactHeader(pay) # should raise unhandled error if + # the TCP payload does not fit ContactHeader # replace pkt's raw payload with a ContactHeader formatted payload pkt[TCP].remove_payload() pkt = pkt / contact # process ContactHeader - if sess.contact_init: # session aready initialized, Header is an ack + if sess.contact_init: # session aready initialized, Header is an ack sess.proc_ack() print("BEGIN TCPCL SESSION") else: sess.init_contact(pkt[TCP].sport, pkt[TCP].dport) - except IndexError: # no TCP payload to process + except IndexError: # no TCP payload to process pass - else: # currently in an active session + else: # currently in an active session if TCPCL.SessTerm in pkt: # process SessTerm if sess.term_begun: @@ -106,7 +107,7 @@ def process(pkt): print("END TCPCL SESSION") else: sess.proc_term() - - return pkt # end of process - - return process # end of make_prn + + return pkt # end of process + + return process # end of make_prn From 9f525b6652e972f644baf98dc182b154a8167238 Mon Sep 17 00:00:00 2001 From: T-recks Date: Sun, 24 Aug 2025 11:01:48 -0700 Subject: [PATCH 11/16] add spdx --- scapy/contrib/dtn/bpv7.py | 4 ++++ scapy/contrib/dtn/cbor.py | 4 ++++ scapy/contrib/dtn/common.py | 4 ++++ scapy/contrib/dtn/tcpcl.py | 4 ++++ scapy/contrib/dtn/tcpcl_session.py | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/scapy/contrib/dtn/bpv7.py b/scapy/contrib/dtn/bpv7.py index e6e2fa9354c..7d63b156bf2 100644 --- a/scapy/contrib/dtn/bpv7.py +++ b/scapy/contrib/dtn/bpv7.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + # scapy.contrib.description = Bundle Protocol version 7 (BPv7) # scapy.contrib.status = loads diff --git a/scapy/contrib/dtn/cbor.py b/scapy/contrib/dtn/cbor.py index ade89f457f4..0afffe10b43 100644 --- a/scapy/contrib/dtn/cbor.py +++ b/scapy/contrib/dtn/cbor.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + # scapy.contrib.description = Concise Binary Object Representation (CBOR) # scapy.contrib.status = library diff --git a/scapy/contrib/dtn/common.py b/scapy/contrib/dtn/common.py index 7efb4085db0..12f0c33f492 100644 --- a/scapy/contrib/dtn/common.py +++ b/scapy/contrib/dtn/common.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + # scapy.contrib.description = utility functions and classes for DTN module # scapy.contrib.status = library diff --git a/scapy/contrib/dtn/tcpcl.py b/scapy/contrib/dtn/tcpcl.py index 8301e3ac3d4..c01fb96f7d9 100644 --- a/scapy/contrib/dtn/tcpcl.py +++ b/scapy/contrib/dtn/tcpcl.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + # scapy.contrib.description = TCP Convergence Layer version 4 (TCPCLv4) # scapy.contrib.status = loads diff --git a/scapy/contrib/dtn/tcpcl_session.py b/scapy/contrib/dtn/tcpcl_session.py index 75274c1afef..876ec2cec86 100644 --- a/scapy/contrib/dtn/tcpcl_session.py +++ b/scapy/contrib/dtn/tcpcl_session.py @@ -1,3 +1,7 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + # scapy.contrib.description = TCP Convergence Layer version 4 (TCPCLv4) # scapy.contrib.status = loads From 700caaed88621f0f70d3fcbd3cf978c659bb0984 Mon Sep 17 00:00:00 2001 From: T-recks Date: Sun, 24 Aug 2025 11:27:57 -0700 Subject: [PATCH 12/16] reformat with black --- scapy/contrib/dtn/bpv7.py | 376 +++++++++++++++++++---------- scapy/contrib/dtn/cbor.py | 53 ++-- scapy/contrib/dtn/common.py | 4 +- scapy/contrib/dtn/tcpcl.py | 164 ++++++++----- scapy/contrib/dtn/tcpcl_session.py | 19 +- 5 files changed, 391 insertions(+), 225 deletions(-) diff --git a/scapy/contrib/dtn/bpv7.py b/scapy/contrib/dtn/bpv7.py index 7d63b156bf2..74493a2efa0 100644 --- a/scapy/contrib/dtn/bpv7.py +++ b/scapy/contrib/dtn/bpv7.py @@ -6,11 +6,11 @@ # scapy.contrib.status = loads """ - Bundle Protocol version 7 (BPv7) layer - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Bundle Protocol version 7 (BPv7) layer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :authors: Timothy Recker, timothy.recker@nasa.gov - Tad Kollar, tad.kollar@nasa.gov +:authors: Timothy Recker, timothy.recker@nasa.gov + Tad Kollar, tad.kollar@nasa.gov """ from scapy.packet import Packet @@ -22,7 +22,7 @@ ConditionalField, FieldListField, BitField, - BitFieldLenField + BitFieldLenField, ) from scapy.all import raw from scapy.contrib.dtn.cbor import ( @@ -34,7 +34,7 @@ CBORStopCode, CBORAny, CBORPacketField, - CBORPacketFieldWithRemain + CBORPacketFieldWithRemain, ) import scapy.contrib.dtn.common as Common @@ -53,18 +53,21 @@ class InvalidCRCType(Exception): Attributes: type_code: the invalid type code """ + def __init__(self, type_code): - super().__init__(f'Tried to compute a CRC using an invalid type code: {type_code}') + super().__init__( + f"Tried to compute a CRC using an invalid type code: {type_code}" + ) def compute_crc(crc_type: int, pkt: bytes, ignore_existing: bool = True): # prepare parameters - if (crc_type == CrcTypes.CRC32C): + if crc_type == CrcTypes.CRC32C: size = 4 - crcfun = crcmod.predefined.mkCrcFun('crc-32c') - elif (crc_type == CrcTypes.CRC16): + crcfun = crcmod.predefined.mkCrcFun("crc-32c") + elif crc_type == CrcTypes.CRC16: size = 2 - crcfun = crcmod.predefined.mkCrcFun('x-25') + crcfun = crcmod.predefined.mkCrcFun("x-25") else: raise InvalidCRCType(crc_type) @@ -72,9 +75,9 @@ def compute_crc(crc_type: int, pkt: bytes, ignore_existing: bool = True): # Wipe anything in existing crc field if ignore_existing: - pkt = pkt[:crc_index] + b'\x00' * size + pkt = pkt[:crc_index] + b"\x00" * size - return crcfun(pkt).to_bytes(size, 'big'), crc_index + return crcfun(pkt).to_bytes(size, "big"), crc_index class PacketFieldWithRemain(PacketField): @@ -82,6 +85,7 @@ class PacketFieldWithRemain(PacketField): The regular Packet.getfield() never returns the remaining bytes, so the CRC or other following fields get lost. This getfield does return the remaining bytes. """ + def getfield(self, pkt: Packet, s: bytes) -> Tuple[bytes, Packet]: i = self.m2i(pkt, s) remain_size = len(s) - len(raw(i)) @@ -111,7 +115,9 @@ def __str__(self): def __eq__(self, other): if isinstance(other, IPN): - return (self.node_id == other.node_id) and (self.service_number == other.service_number) + return (self.node_id == other.node_id) and ( + self.service_number == other.service_number + ) return False @@ -134,15 +140,21 @@ class EndpointID(CBORArray): CBORInteger("scheme_code", 2), MultipleTypeField( [ - (PacketFieldWithRemain("ssp", DTN(), DTN), lambda pkt: pkt.scheme_code == 1), - (PacketFieldWithRemain("ssp", IPN(), IPN), lambda pkt: pkt.scheme_code == 2) + ( + PacketFieldWithRemain("ssp", DTN(), DTN), + lambda pkt: pkt.scheme_code == 1, + ), + ( + PacketFieldWithRemain("ssp", IPN(), IPN), + lambda pkt: pkt.scheme_code == 2, + ), ], - PacketFieldWithRemain("ssp", IPN(), IPN) - ) + PacketFieldWithRemain("ssp", IPN(), IPN), + ), ] def dissect(self, s: bytes): - """ This dissect doesn't process the payload, because there is none. """ + """This dissect doesn't process the payload, because there is none.""" s = self.pre_dissect(s) s = self.do_dissect(s) s = self.post_dissect(s) @@ -156,10 +168,7 @@ def __eq__(self, other): # pylint: disable=R0903 # Packet types are not intended to have many(any) public functions class Timestamp(CBORArray): - fields_desc = CBORArray.fields_desc + [ - CBORInteger("t", 0), - CBORInteger("seq", 0) - ] + fields_desc = CBORArray.fields_desc + [CBORInteger("t", 0), CBORInteger("seq", 0)] def __ge__(self, other): if isinstance(other, Timestamp): @@ -176,6 +185,7 @@ class BlockTypes: """ Bundle block type codes """ + PRIMARY = 0 PAYLOAD = 1 AUTHENTICATION = 2 @@ -193,6 +203,7 @@ class CrcTypes: """ Bundle CRC type codes """ + NONE = 0 CRC16 = 1 CRC32C = 2 @@ -204,7 +215,9 @@ class SecurityTargets(CBORArray): fields_desc = [ CBORArray._major_type, BitFieldLenField("add", 0, 5, count_of="targets"), - FieldListField("targets", [0], CBORInteger("tgt", 0), count_from=lambda pkt: pkt.add) + FieldListField( + "targets", [0], CBORInteger("tgt", 0), count_from=lambda pkt: pkt.add + ), ] def count_additional_fields(self): @@ -215,9 +228,10 @@ class CBORTuple(CBORArray): """ A pair of CBOR integers consisting of [id, value]. """ + fields_desc = CBORArray.fields_desc + [ CBORInteger("id", 0), - CBORAny("value", b'\x00') + CBORAny("value", b"\x00"), ] @@ -225,11 +239,13 @@ class CBORTupleArray(CBORArray): fields_desc = [ CBORArray._major_type, BitFieldLenField("add", None, 5, count_of="tuples"), - PacketListField("tuples", [CBORTuple()], CBORTuple, count_from=lambda pkt: pkt.add) + PacketListField( + "tuples", [CBORTuple()], CBORTuple, count_from=lambda pkt: pkt.add + ), ] def find_value_with_id(self, target_id: int): - """ Find the tuple with the specified id and return the value. """ + """Find the tuple with the specified id and return the value.""" try: tup = next(x for x in self.tuples if x.id == target_id) except StopIteration: @@ -242,11 +258,17 @@ def count_additional_fields(self) -> int: class SecurityResults(CBORArray): - """ A CBOR array of CBORTupleArrays. """ + """A CBOR array of CBORTupleArrays.""" + fields_desc = [ CBORArray._major_type, BitFieldLenField("add", None, 5, count_of="results"), - PacketListField("results", [CBORTupleArray()], CBORTupleArray, count_from=lambda pkt: pkt.add) + PacketListField( + "results", + [CBORTupleArray()], + CBORTupleArray, + count_from=lambda pkt: pkt.add, + ), ] def count_additional_fields(self): @@ -259,18 +281,23 @@ class AbstractSecurityBlock(Packet): and are defined here. This structure will reside in the block-specific data field of a BPv7 canonical block. """ + fields_desc = [ PacketField("security_targets", SecurityTargets(), SecurityTargets), CBORInteger("security_context_id", 0), CBORInteger("security_context_flags", 1), PacketField("security_source", EndpointID(), EndpointID), - ConditionalField(PacketField("security_context_parameters", CBORTupleArray(), CBORTupleArray), - lambda p: (p.security_context_flags & 1)), - PacketField("security_results", SecurityResults(), SecurityResults) + ConditionalField( + PacketField( + "security_context_parameters", CBORTupleArray(), CBORTupleArray + ), + lambda p: (p.security_context_flags & 1), + ), + PacketField("security_results", SecurityResults(), SecurityResults), ] def dissect(self, s: bytes): - """ This dissect doesn't process the payload, because there is none. """ + """This dissect doesn't process the payload, because there is none.""" s = self.pre_dissect(s) s = self.do_dissect(s) s = self.post_dissect(s) @@ -281,6 +308,7 @@ class CtrlFlags(IntFlag): """ Block Processing Control Flags """ + BLOCK_MUST_BE_REPLICATED = 0x01 REPORT_IF_UNPROCESSABLE = 0x02 DELETE_BUNDLE_IF_UNPROCESSED = 0x04 @@ -296,24 +324,31 @@ class CtrlFlags(IntFlag): BlockTypes.AGE: "age", BlockTypes.HOP_COUNT: "hop_count", BlockTypes.BLOCK_INTEGRITY: "block_integrity", - BlockTypes.BLOCK_CONFIDENTIALITY: "block_confidentiality" + BlockTypes.BLOCK_CONFIDENTIALITY: "block_confidentiality", } fields_template: Common.FieldsTemplate = { - 'type_code': BitEnumField("type_code", BlockTypes.PAYLOAD, 8, TypeCodes), - 'block_number': CBORInteger("block_number", 1), - 'flags': CBORInteger("flags", 0), - 'crc_type': CBORInteger("crc_type", CrcTypes.CRC32C), - 'data': CBORByteString("data", b'\xDE\xAD\xBE\xEF'), - 'crc': ConditionalField( + "type_code": BitEnumField("type_code", BlockTypes.PAYLOAD, 8, TypeCodes), + "block_number": CBORInteger("block_number", 1), + "flags": CBORInteger("flags", 0), + "crc_type": CBORInteger("crc_type", CrcTypes.CRC32C), + "data": CBORByteString("data", b"\xde\xad\xbe\xef"), + "crc": ConditionalField( MultipleTypeField( [ - (CBORByteString("crc", b"\x00\x00"), lambda pkt: pkt.crc_type == CrcTypes.CRC16), - (CBORByteString("crc", b"\x00\x00\x00\x00"), lambda pkt: pkt.crc_type == CrcTypes.CRC32C) + ( + CBORByteString("crc", b"\x00\x00"), + lambda pkt: pkt.crc_type == CrcTypes.CRC16, + ), + ( + CBORByteString("crc", b"\x00\x00\x00\x00"), + lambda pkt: pkt.crc_type == CrcTypes.CRC32C, + ), ], - CBORNull("crc", None) - ), lambda pkt: pkt.crc_type != CrcTypes.NONE - ) + CBORNull("crc", None), + ), + lambda pkt: pkt.crc_type != CrcTypes.NONE, + ), } fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) @@ -357,9 +392,15 @@ class PayloadBlock(CanonicalBlock): """ Contains the bundle payload. """ - fields_template = Common.template_replace(CanonicalBlock.fields_template, { - 'type_code': BitEnumField("type_code", BlockTypes.PAYLOAD, 8, CanonicalBlock.TypeCodes) - }) + + fields_template = Common.template_replace( + CanonicalBlock.fields_template, + { + "type_code": BitEnumField( + "type_code", BlockTypes.PAYLOAD, 8, CanonicalBlock.TypeCodes + ) + }, + ) fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) @@ -369,6 +410,7 @@ class EncryptedPayloadBlock(PayloadBlock): """ Contains the bundle payload. The data field is encrypted. """ + encrypted = True @@ -376,10 +418,16 @@ class PreviousNodeBlock(CanonicalBlock): """ Contains the ID of the node that forwarded this bundle. """ - fields_template = Common.template_replace(CanonicalBlock.fields_template, { - 'type_code': BitEnumField("type_code", BlockTypes.PREV_NODE, 8, CanonicalBlock.TypeCodes), - 'data': CBORPacketField("data", EndpointID(), EndpointID) - }) + + fields_template = Common.template_replace( + CanonicalBlock.fields_template, + { + "type_code": BitEnumField( + "type_code", BlockTypes.PREV_NODE, 8, CanonicalBlock.TypeCodes + ), + "data": CBORPacketField("data", EndpointID(), EndpointID), + }, + ) fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) @@ -389,13 +437,17 @@ class EncryptedPreviousNodeBlock(PreviousNodeBlock): """ Contains the ID of the node that forwarded this bundle. The data field is encrypted. """ - fields_template = Common.template_replace(PreviousNodeBlock.fields_template, { - # The data field defintion from the parent class cannot be used here. That data - # is now encrypted and cannot be decrypted to its original bytes (within the - # scope of this module), so the Packet that it represents cannot be dissected. - # Instead, the data field defintion from CanonicalBlock is used. - 'data': CBORByteString("data", b'\xDE\xAD\xBE\xEF') - }) + + fields_template = Common.template_replace( + PreviousNodeBlock.fields_template, + { + # The data field defintion from the parent class cannot be used here. That data + # is now encrypted and cannot be decrypted to its original bytes (within the + # scope of this module), so the Packet that it represents cannot be dissected. + # Instead, the data field defintion from CanonicalBlock is used. + "data": CBORByteString("data", b"\xde\xad\xbe\xef") + }, + ) fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) encrypted = True @@ -410,7 +462,7 @@ def __eq__(self, other): return False def dissect(self, s: bytes): - """ This dissect doesn't process the payload, because there is none. """ + """This dissect doesn't process the payload, because there is none.""" s = self.pre_dissect(s) s = self.do_dissect(s) s = self.post_dissect(s) @@ -421,10 +473,16 @@ class BundleAgeBlock(CanonicalBlock): Contains the number of milliseconds that have elapsed between the time the bundle was created and the time at which it was most recently forwarded. """ - fields_template = Common.template_replace(CanonicalBlock.fields_template, { - 'type_code': BitEnumField("type_code", BlockTypes.AGE, 8, CanonicalBlock.TypeCodes), - 'data': CBORPacketFieldWithRemain("data", BundleAge(), BundleAge) - }) + + fields_template = Common.template_replace( + CanonicalBlock.fields_template, + { + "type_code": BitEnumField( + "type_code", BlockTypes.AGE, 8, CanonicalBlock.TypeCodes + ), + "data": CBORPacketFieldWithRemain("data", BundleAge(), BundleAge), + }, + ) fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) @@ -436,9 +494,11 @@ class EncryptedBundleAgeBlock(BundleAgeBlock): bundle was created and the time at which it was most recently forwarded. The data field is encrypted. """ - fields_template = Common.template_replace(BundleAgeBlock.fields_template, { - 'data': CBORByteString("data", b'\xDE\xAD\xBE\xEF') - }) + + fields_template = Common.template_replace( + BundleAgeBlock.fields_template, + {"data": CBORByteString("data", b"\xde\xad\xbe\xef")}, + ) fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) encrypted = True @@ -447,7 +507,7 @@ class EncryptedBundleAgeBlock(BundleAgeBlock): class HopCount(CBORArray): fields_desc = CBORArray.fields_desc + [ CBORInteger("limit", 0), - CBORInteger("count", 0) + CBORInteger("count", 0), ] def __eq__(self, other): @@ -461,10 +521,16 @@ class HopCountBlock(CanonicalBlock): Contains information on the Bundle's allowed number of hops and the hops that have already happened. """ - fields_template = Common.template_replace(CanonicalBlock.fields_template, { - 'type_code': BitEnumField("type_code", BlockTypes.HOP_COUNT, 8, CanonicalBlock.TypeCodes), - 'data': CBORPacketField("data", HopCount(), HopCount) - }) + + fields_template = Common.template_replace( + CanonicalBlock.fields_template, + { + "type_code": BitEnumField( + "type_code", BlockTypes.HOP_COUNT, 8, CanonicalBlock.TypeCodes + ), + "data": CBORPacketField("data", HopCount(), HopCount), + }, + ) fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) @@ -475,9 +541,11 @@ class EncryptedHopCountBlock(HopCountBlock): Contains information on the Bundle's allowed number of hops and the hops that have already happened. The data field is encrypted. """ - fields_template = Common.template_replace(HopCountBlock.fields_template, { - 'data': CBORByteString("data", b'\xDE\xAD\xBE\xEF') - }) + + fields_template = Common.template_replace( + HopCountBlock.fields_template, + {"data": CBORByteString("data", b"\xde\xad\xbe\xef")}, + ) fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) encrypted = True @@ -488,10 +556,18 @@ class BlockIntegrityBlock(CanonicalBlock): This defines a CanonicalBlock with its type code as 11 and an AbstractSecurityBlock as its data field. """ - fields_template = Common.template_replace(CanonicalBlock.fields_template, { - 'type_code': BitEnumField("type_code", BlockTypes.BLOCK_INTEGRITY, 8, CanonicalBlock.TypeCodes), - 'data': CBORPacketFieldWithRemain("data", AbstractSecurityBlock(), AbstractSecurityBlock) - }) + + fields_template = Common.template_replace( + CanonicalBlock.fields_template, + { + "type_code": BitEnumField( + "type_code", BlockTypes.BLOCK_INTEGRITY, 8, CanonicalBlock.TypeCodes + ), + "data": CBORPacketFieldWithRemain( + "data", AbstractSecurityBlock(), AbstractSecurityBlock + ), + }, + ) fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) @@ -502,9 +578,11 @@ class EncryptedBlockIntegrityBlock(BlockIntegrityBlock): This defines a CanonicalBlock with its type code as 11 and an encrypted AbstractSecurityBlock as its data field. """ - fields_template = Common.template_replace(BlockIntegrityBlock.fields_template, { - 'data': CBORByteString("data", b'\xDE\xAD\xBE\xEF') - }) + + fields_template = Common.template_replace( + BlockIntegrityBlock.fields_template, + {"data": CBORByteString("data", b"\xde\xad\xbe\xef")}, + ) fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) encrypted = True @@ -515,29 +593,42 @@ class BlockConfidentialityBlock(CanonicalBlock): This defines a CanonicalBlock with its type code as 12 and an AbstractSecurityBlock as its data field. """ - fields_template = Common.template_replace(CanonicalBlock.fields_template, { - 'type_code': BitEnumField("type_code", BlockTypes.BLOCK_CONFIDENTIALITY, 8, CanonicalBlock.TypeCodes), - 'data': CBORPacketFieldWithRemain("data", AbstractSecurityBlock(), AbstractSecurityBlock) - }) + + fields_template = Common.template_replace( + CanonicalBlock.fields_template, + { + "type_code": BitEnumField( + "type_code", + BlockTypes.BLOCK_CONFIDENTIALITY, + 8, + CanonicalBlock.TypeCodes, + ), + "data": CBORPacketFieldWithRemain( + "data", AbstractSecurityBlock(), AbstractSecurityBlock + ), + }, + ) fields_desc = CBORArray.fields_desc + Common.make_fields_desc(fields_template) class UnassignedExtensionBlock(CanonicalBlock): - """ An extension block with an unassigned type code < 192. """ + """An extension block with an unassigned type code < 192.""" class EncryptedUnassignedExtensionBlock(CanonicalBlock): - """ An extension block with an unassigned type code < 192. The data field is encrypted. """ + """An extension block with an unassigned type code < 192. The data field is encrypted.""" + encrypted = True class ReservedExtensionBlock(CanonicalBlock): - """ An extension block with a type code 192-255. """ + """An extension block with a type code 192-255.""" class EncryptedReservedExtensionBlock(CanonicalBlock): - """ An extension block with a type code 192-255. The data field is encrypted. """ + """An extension block with a type code 192-255. The data field is encrypted.""" + encrypted = True @@ -546,6 +637,7 @@ class CtrlFlags(IntFlag): """ Bundle Processing Control Flags """ + BUNDLE_IS_FRAGMENT = 0x01 ADMIN_RECORD = 0x02 MUST_NOT_BE_FRAGMENTED = 0x04 @@ -566,23 +658,34 @@ class CtrlFlags(IntFlag): PacketField("report", EndpointID(scheme_code=1), EndpointID), PacketField("creation_timestamp", Timestamp(t=int(time.time())), Timestamp), CBORInteger("lifetime", 0), - ConditionalField(CBORInteger("fragment_offset", 0), - lambda pkt: pkt.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT), - ConditionalField(CBORInteger("total_adu_length", 0), - lambda pkt: pkt.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT), + ConditionalField( + CBORInteger("fragment_offset", 0), + lambda pkt: pkt.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT, + ), + ConditionalField( + CBORInteger("total_adu_length", 0), + lambda pkt: pkt.flags & PrimaryBlock.CtrlFlags.BUNDLE_IS_FRAGMENT, + ), ConditionalField( MultipleTypeField( [ - (CBORByteString("crc", b"\x00\x00"), lambda pkt: pkt.crc_type == CrcTypes.CRC16), - (CBORByteString("crc", b"\x00\x00\x00\x00"), lambda pkt: pkt.crc_type == CrcTypes.CRC32C) + ( + CBORByteString("crc", b"\x00\x00"), + lambda pkt: pkt.crc_type == CrcTypes.CRC16, + ), + ( + CBORByteString("crc", b"\x00\x00\x00\x00"), + lambda pkt: pkt.crc_type == CrcTypes.CRC32C, + ), ], - CBORNull("crc", None) - ), lambda pkt: pkt.crc_type != CrcTypes.NONE - ) + CBORNull("crc", None), + ), + lambda pkt: pkt.crc_type != CrcTypes.NONE, + ), ] def dissect(self, s: bytes): - """ This dissect doesn't process the payload, because there is none. """ + """This dissect doesn't process the payload, because there is none.""" s = self.pre_dissect(s) s = self.do_dissect(s) s = self.post_dissect(s) @@ -626,7 +729,7 @@ def count_additional_fields(self): (BlockTypes.BLOCK_INTEGRITY, False): BlockIntegrityBlock, (BlockTypes.BLOCK_INTEGRITY, True): EncryptedBlockIntegrityBlock, (BlockTypes.BLOCK_CONFIDENTIALITY, False): BlockConfidentialityBlock, - (BlockTypes.BLOCK_CONFIDENTIALITY, True): None # should not happen + (BlockTypes.BLOCK_CONFIDENTIALITY, True): None, # should not happen } UNENCRYPTED_TO_ENCRYPTED_TYPE_MAP = { @@ -636,7 +739,7 @@ def count_additional_fields(self): HopCountBlock: EncryptedHopCountBlock, BlockIntegrityBlock: EncryptedBlockIntegrityBlock, UnassignedExtensionBlock: EncryptedUnassignedExtensionBlock, - ReservedExtensionBlock: EncryptedReservedExtensionBlock + ReservedExtensionBlock: EncryptedReservedExtensionBlock, } ENCRYPTED_TO_UNENCRYPTED_TYPE_MAP = { @@ -646,20 +749,20 @@ def count_additional_fields(self): EncryptedHopCountBlock: HopCountBlock, EncryptedBlockIntegrityBlock: BlockIntegrityBlock, EncryptedUnassignedExtensionBlock: UnassignedExtensionBlock, - EncryptedReservedExtensionBlock: ReservedExtensionBlock + EncryptedReservedExtensionBlock: ReservedExtensionBlock, } def next_block_type(pkt, lst, cur, remain): del pkt, lst, cur # Not used - if remain is None or remain == b'\xff': + if remain is None or remain == b"\xff": return None return Bundle.identify_block(remain) def guess_block_class(block_bytes, pkt): del pkt # Not used - if block_bytes is None or block_bytes == b'\xff': + if block_bytes is None or block_bytes == b"\xff": return None return Bundle.identify_block(block_bytes) @@ -672,8 +775,10 @@ def count_additional_fields(self) -> int: CBORArray._major_type, BitField("add", 31, 5), PacketFieldWithRemain("primary_block", PrimaryBlock(), PrimaryBlock), - PacketListField("canonical_blocks", [CanonicalBlock()], next_cls_cb=next_block_type), - CBORStopCode("stop_code", 31) + PacketListField( + "canonical_blocks", [CanonicalBlock()], next_cls_cb=next_block_type + ), + CBORStopCode("stop_code", 31), ] @staticmethod @@ -692,22 +797,29 @@ def type_code_to_block_type(type_code: int, encrypted: bool = False): return TYPE_CODE_TO_BLOCK_TYPE_MAP[map_key] - def find_block_by_type(self, block_type, excluded_block_nums: List[int] = None) -> CanonicalBlock: + def find_block_by_type( + self, block_type, excluded_block_nums: List[int] = None + ) -> CanonicalBlock: """ Find the first canonical block matching the specified type, with a block number not - in the excluded list. """ + in the excluded list.""" if excluded_block_nums is None: excluded_block_nums = [] try: - block = next(x for x in self.canonical_blocks if isinstance(x, block_type) and - x.block_number not in excluded_block_nums) + block = next( + x + for x in self.canonical_blocks + if isinstance(x, block_type) + and x.block_number not in excluded_block_nums + ) except StopIteration: block = None return block - def find_block_by_type_code(self, type_code: int, - excluded_block_nums: List[int] = None) -> CanonicalBlock: + def find_block_by_type_code( + self, type_code: int, excluded_block_nums: List[int] = None + ) -> CanonicalBlock: """ Find the first block matching the specified type code, with a block number not in the excluded list. @@ -715,24 +827,30 @@ def find_block_by_type_code(self, type_code: int, if excluded_block_nums is None: excluded_block_nums = [] try: - block = next(x for x in self.canonical_blocks if (x.type_code == type_code) and - (x.block_number not in excluded_block_nums)) + block = next( + x + for x in self.canonical_blocks + if (x.type_code == type_code) + and (x.block_number not in excluded_block_nums) + ) except StopIteration: block = None return block def find_block_by_number(self, block_num: int) -> CanonicalBlock: - """ Find the canonical block with the specified block number. """ + """Find the canonical block with the specified block number.""" try: - block = next(x for x in self.canonical_blocks if x.block_number == block_num) + block = next( + x for x in self.canonical_blocks if x.block_number == block_num + ) except StopIteration: block = None return block def get_new_block_number(self) -> int: - """ Return a new canonical block number one higher than the highest in use. """ + """Return a new canonical block number one higher than the highest in use.""" new_num = 2 for block in self.canonical_blocks: @@ -741,9 +859,13 @@ def get_new_block_number(self) -> int: return new_num - def add_block(self, block: CanonicalBlock, block_num_to_insert_above=1, - select_block_number=False) -> CanonicalBlock: - """ Insert an extension block just before the block with the specified block number. """ + def add_block( + self, + block: CanonicalBlock, + block_num_to_insert_above=1, + select_block_number=False, + ) -> CanonicalBlock: + """Insert an extension block just before the block with the specified block number.""" if select_block_number: block.block_number = self.get_new_block_number() @@ -753,7 +875,9 @@ def add_block(self, block: CanonicalBlock, block_num_to_insert_above=1, insert_pos = idx if insert_pos == -1: - raise ValueError("Could not find block number to insert above", block_num_to_insert_above) + raise ValueError( + "Could not find block number to insert above", block_num_to_insert_above + ) self.canonical_blocks.insert(insert_pos, block) @@ -767,13 +891,13 @@ def replace_block_by_block_num(self, block_num: int, new_block: CanonicalBlock): @staticmethod def identify_block(block_bytes: bytes): - """ Determine the type of the canonical block. """ + """Determine the type of the canonical block.""" type_code = block_bytes[1] block_type = Bundle.type_code_to_block_type(type_code) encrypted_block_type = Bundle.type_code_to_block_type(type_code, True) - if (encrypted_block_type is not block_type): + if encrypted_block_type is not block_type: # Try to construct the block as the unencrypted type. If it # doesn't work, specify the encrypted version. If it works due to # chance arrangement of bytes but is actually encrypted, it will @@ -802,7 +926,9 @@ def post_dissect(self, s): # Found a block originally detected as unencrypted, but the # BCB specifies is encrypted. Replace with an encrypted type. if not block_type.encrypted: - encrypted_type = UNENCRYPTED_TO_ENCRYPTED_TYPE_MAP[block_type] + encrypted_type = UNENCRYPTED_TO_ENCRYPTED_TYPE_MAP[ + block_type + ] new_block = encrypted_type(raw(block)) self.canonical_blocks[idx] = new_block diff --git a/scapy/contrib/dtn/cbor.py b/scapy/contrib/dtn/cbor.py index 0afffe10b43..4d49210d220 100644 --- a/scapy/contrib/dtn/cbor.py +++ b/scapy/contrib/dtn/cbor.py @@ -6,19 +6,14 @@ # scapy.contrib.status = library """ - Concise Binary Object Representation (CBOR) utility - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Concise Binary Object Representation (CBOR) utility +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :authors: Timothy Recker, timothy.recker@nasa.gov - Tad Kollar, tad.kollar@nasa.gov +:authors: Timothy Recker, timothy.recker@nasa.gov + Tad Kollar, tad.kollar@nasa.gov """ -from scapy.fields import ( - Field, - BitField, - BitEnumField, - PacketField -) +from scapy.fields import Field, BitField, BitEnumField, PacketField from scapy.packet import Packet from scapy.all import raw import flynn @@ -32,7 +27,7 @@ 4: "array", 5: "map", 6: "tag", - 7: "simple/float" + 7: "simple/float", } @@ -41,32 +36,35 @@ class MajorTypeException(Exception): Attributes: actual -- the integer value of the actual Major Type - expected -- an integer or list of integers indicating the acceptable Major Type values""" + expected -- an integer or list of integers indicating the acceptable Major Type values + """ + def __init__(self, actual: int, expected: Union[int, List[int]]): - message = f'[Error] Major type {actual} does not refer to a(n)' + message = f"[Error] Major type {actual} does not refer to a(n)" if isinstance(expected, int): typ = MajorTypes[expected] - message += f' {typ}.' + message += f" {typ}." else: typ = MajorTypes[expected[0]] - message += f' {typ}' + message += f" {typ}" for val in expected[1:]: typ = MajorTypes[val] - message += f' or {typ}' - message += '.' + message += f" or {typ}" + message += "." super().__init__(message) class StopCodeException(Exception): def __init__(self, value): - super().__init__(f'[Error] Major type {value} does not refer to a stop code.') + super().__init__(f"[Error] Major type {value} does not refer to a stop code.") class AdditionalInfoException(Exception): """This exception indicates that a CBOR object has an unexpected value for its Additional Info.""" + def __init__(self): - super().__init__('[Error] Invalid additional info.') + super().__init__("[Error] Invalid additional info.") class UnhandledTypeException(Exception): @@ -113,13 +111,13 @@ def get_value(add_info, b): val = b[1] elif add_info == 25: val_length = 3 # 1 byte head + 2 byte argument - val = int.from_bytes(b[1:3], byteorder='big') + val = int.from_bytes(b[1:3], byteorder="big") elif add_info == 26: val_length = 5 # 4 byte argument - val = int.from_bytes(b[1:5], byteorder='big') + val = int.from_bytes(b[1:5], byteorder="big") elif add_info == 27: val_length = 9 # 8 byte argument - val = int.from_bytes(b[1:9], byteorder='big') + val = int.from_bytes(b[1:9], byteorder="big") else: raise AdditionalInfoException() @@ -154,9 +152,9 @@ def get_value(add_info, b): # size of argument is known now, so # get value of the argument, which contains the size of the data - data_size = int.from_bytes(b[1:1 + arg_size], byteorder='big') + data_size = int.from_bytes(b[1 : 1 + arg_size], byteorder="big") val_length = 1 + arg_size + data_size - val = b[1 + arg_size:val_length] + val = b[1 + arg_size : val_length] return b[val_length:], val @@ -195,7 +193,7 @@ def getfield(self, pkt, s): class CBORStopCode(CBORBase): def addfield(self, pkt, s, val): - return s + b'\xff' + return s + b"\xff" @staticmethod def get_value(add_info, b): @@ -240,13 +238,13 @@ def count_additional_fields(self) -> int: return len(self.default_fields) - head_field_count def set_additional_fields(self, pkt: Packet) -> bytes: - """ For an, the add field is set to the number of elements minus the two head fields. """ + """For an, the add field is set to the number of elements minus the two head fields.""" # pylint: disable=W0201 # field (instance variable) initialization is handled via "fields_desc" self.add = self.count_additional_fields() head = (self.major_type << 5) | self.add - return head.to_bytes(1, 'big') + pkt[1:] + return head.to_bytes(1, "big") + pkt[1:] def post_build(self, pkt: bytes, pay: bytes) -> bytes: return self.set_additional_fields(pkt) + pay @@ -277,6 +275,7 @@ class CBORPacketFieldWithRemain(CBORPacketField): The regular Packet.getfield() never returns the remaining bytes, so the CRC or other following fields get lost. This getfield does return the remaining bytes. """ + def m2i(self, pkt: Packet, m): _, add_info = CBORBase.static_get_head_info(m) remain, decoded_m = CBORStringBase.get_value(add_info, m) diff --git a/scapy/contrib/dtn/common.py b/scapy/contrib/dtn/common.py index 12f0c33f492..58d7fcad9e0 100644 --- a/scapy/contrib/dtn/common.py +++ b/scapy/contrib/dtn/common.py @@ -38,7 +38,9 @@ class FieldPacket(NoPayloadPacket): FieldsTemplate = Dict[str, Field] -def template_replace(template: FieldsTemplate, new_values: FieldsTemplate) -> FieldsTemplate: +def template_replace( + template: FieldsTemplate, new_values: FieldsTemplate +) -> FieldsTemplate: return {**template, **new_values} diff --git a/scapy/contrib/dtn/tcpcl.py b/scapy/contrib/dtn/tcpcl.py index c01fb96f7d9..60e5e6a53be 100644 --- a/scapy/contrib/dtn/tcpcl.py +++ b/scapy/contrib/dtn/tcpcl.py @@ -6,10 +6,10 @@ # scapy.contrib.status = loads """ - TCP Convergence Layer version 4 (TCPCLv4) layer - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +TCP Convergence Layer version 4 (TCPCLv4) layer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :author: Timothy Recker, timothy.recker@nasa.gov +:author: Timothy Recker, timothy.recker@nasa.gov """ from scapy.packet import Packet, Raw, bind_layers @@ -21,7 +21,7 @@ ConditionalField, PacketListField, StrLenField, - FieldLenField + FieldLenField, ) import struct @@ -35,12 +35,15 @@ class MagicValueError(Exception): """ Exception raised when a ContactHeader is dissected and the magic value is incorrect. """ + def __init__(self, value): - super().__init__(f"Tried to decode ContactHeader with invalid magic value of {value}") + super().__init__( + f"Tried to decode ContactHeader with invalid magic value of {value}" + ) class ContactHeader(ControlPacket): - MAGIC_VALUE = 0x64746e21 + MAGIC_VALUE = 0x64746E21 class Flag(IntEnum): CAN_TLS = 0x01 @@ -48,7 +51,7 @@ class Flag(IntEnum): fields_desc = [ BitField("magic", MAGIC_VALUE, 32), BitField("version", 4, 8), - XByteEnumField("flags", 0, {Flag.CAN_TLS: "can tls"}) + XByteEnumField("flags", 0, {Flag.CAN_TLS: "can tls"}), ] def post_dissect(self, s): @@ -68,15 +71,19 @@ class MsgType(IntEnum): MSG_REJECT = 0x06 fields_desc = [ - XByteEnumField("type", MsgType.XFER_SEGMENT, { - MsgType.SESS_INIT: "sess_init", - MsgType.SESS_TERM: "sess_term", - MsgType.XFER_SEGMENT: "xfer_segment", - MsgType.XFER_ACK: "xfer_ack", - MsgType.XFER_REFUSE: "xfer_refuse", - MsgType.KEEPALIVE: "keepalive", - MsgType.MSG_REJECT: "msg_reject" - }) + XByteEnumField( + "type", + MsgType.XFER_SEGMENT, + { + MsgType.SESS_INIT: "sess_init", + MsgType.SESS_TERM: "sess_term", + MsgType.XFER_SEGMENT: "xfer_segment", + MsgType.XFER_ACK: "xfer_ack", + MsgType.XFER_REFUSE: "xfer_refuse", + MsgType.KEEPALIVE: "keepalive", + MsgType.MSG_REJECT: "msg_reject", + }, + ) ] @@ -84,6 +91,7 @@ class Ext(FieldPacket): """ Class definition for an Extension Item in the format of a Type-Length-Value container. """ + class Flag(IntEnum): CRITICAL = 0x01 @@ -94,7 +102,7 @@ class Type(IntEnum): XByteEnumField("flags", 0, {Flag.CRITICAL: "critical"}), BitField("type", 0, 16), BitFieldLenField("length", default=0, size=16, length_of="data"), - StrLenField("data", 0, length_from=lambda pkt: pkt.length) + StrLenField("data", 0, length_from=lambda pkt: pkt.length), ] @@ -103,15 +111,19 @@ class SessInit(ControlPacket): BitField("keepalive", 0, 16), BitField("segment_mru", 0, 64), BitField("transfer_mru", 0, 64), - FieldLenField("id_length", None, length_of="id", fmt="H"), # Node ID Length (U16) - StrLenField("id", b"", length_from=lambda pkt: pkt.id_length), # Node ID Data (variable) + FieldLenField( + "id_length", None, length_of="id", fmt="H" + ), # Node ID Length (U16) + StrLenField( + "id", b"", length_from=lambda pkt: pkt.id_length + ), # Node ID Data (variable) BitFieldLenField("ext_length", 0, 32, length_of="ext_items"), ConditionalField( - PacketListField("ext_items", - [], - Ext, - length_from=lambda pkt: pkt.ext_length), - lambda pkt: pkt.ext_length > 0) + PacketListField( + "ext_items", [], Ext, length_from=lambda pkt: pkt.ext_length + ), + lambda pkt: pkt.ext_length > 0, + ), ] @@ -128,12 +140,16 @@ class ReasonCode(IntEnum): UNEXPECTED = 0x03 fields_desc = [ - XByteEnumField("reason", ReasonCode.UNSUPPORTED, { - ReasonCode.UNKNOWN: "message type unknown", - ReasonCode.UNSUPPORTED: "message unsupported", - ReasonCode.UNEXPECTED: "message unexpected" - }), - PacketField("header", MsgHeader(), MsgHeader) + XByteEnumField( + "reason", + ReasonCode.UNSUPPORTED, + { + ReasonCode.UNKNOWN: "message type unknown", + ReasonCode.UNSUPPORTED: "message unsupported", + ReasonCode.UNEXPECTED: "message unexpected", + }, + ), + PacketField("header", MsgHeader(), MsgHeader), ] @@ -141,17 +157,18 @@ class Xfer(Packet): """ Abstract class containing fields and flags common to Xfer messages """ + class Flag(IntEnum): END = 0x01 START = 0x02 fields_desc = [ - XByteEnumField("flags", 0, { - Flag.END: "END", - Flag.START: "START", - Flag.START | Flag.END: "START|END" - }), - BitField("id", 0, 64) + XByteEnumField( + "flags", + 0, + {Flag.END: "END", Flag.START: "START", Flag.START | Flag.END: "START|END"}, + ), + BitField("id", 0, 64), ] @@ -160,25 +177,33 @@ class InvalidPayloadError(Exception): This error indicates that an XferSegment contains raw bytes instead of a properly formatted Bundle as its payload. """ + def __init__(self, payload_bytes): - super().__init__(f"Failed to fully parse Bundle from Xfer payload: bundle={payload_bytes}") + super().__init__( + f"Failed to fully parse Bundle from Xfer payload: bundle={payload_bytes}" + ) class XferSegment(Xfer): """ Packet for transferring a data segment """ + fields_desc = Xfer.fields_desc + [ ConditionalField( BitFieldLenField("ext_length", default=0, size=32, length_of="ext_items"), - lambda pkt: pkt.flags & Xfer.Flag.START), + lambda pkt: pkt.flags & Xfer.Flag.START, + ), ConditionalField( - PacketListField("ext_items", - [Ext(type=Ext.Type.LENGTH)], - Ext, - length_from=lambda pkt: pkt.ext_length), - lambda pkt: (pkt.flags & Xfer.Flag.START) and (pkt.ext_length > 0)), - BitField("length", default=0, size=64) + PacketListField( + "ext_items", + [Ext(type=Ext.Type.LENGTH)], + Ext, + length_from=lambda pkt: pkt.ext_length, + ), + lambda pkt: (pkt.flags & Xfer.Flag.START) and (pkt.ext_length > 0), + ), + BitField("length", default=0, size=64), ] def post_build(self, pkt, pay): @@ -186,7 +211,7 @@ def post_build(self, pkt, pay): if not self.length: index = len(pkt) - 8 # size of length is 8 bytes, thus position=len(pkt)-8 length = len(pay) - pkt = pkt[:index] + struct.pack('!Q', length) + pkt = pkt[:index] + struct.pack("!Q", length) return pkt + pay def post_dissect(self, s): @@ -201,9 +226,7 @@ def post_dissect(self, s): class XferAck(ControlPacket): - fields_desc = Xfer.fields_desc + [ - BitField("length", default=0, size=64) - ] + fields_desc = Xfer.fields_desc + [BitField("length", default=0, size=64)] class XferRefuse(ControlPacket): @@ -217,16 +240,20 @@ class ReasonCode(IntEnum): SESS_TERM = 0x06 fields_desc = [ - XByteEnumField("reason", ReasonCode.UNKNOWN, { - ReasonCode.UNKNOWN: "unknown", - ReasonCode.COMPLETED: "complete bundle received", - ReasonCode.NO_RESOURCES: "resources exhausted", - ReasonCode.RETRANSMIT: "retransmit bundle", - ReasonCode.NOT_ACCEPTABLE: "bundle not acceptable", - ReasonCode.EXT_FAIL: "failed to process extensions", - ReasonCode.SESS_TERM: "session is terminating" - }), - BitField("id", 0, 64) + XByteEnumField( + "reason", + ReasonCode.UNKNOWN, + { + ReasonCode.UNKNOWN: "unknown", + ReasonCode.COMPLETED: "complete bundle received", + ReasonCode.NO_RESOURCES: "resources exhausted", + ReasonCode.RETRANSMIT: "retransmit bundle", + ReasonCode.NOT_ACCEPTABLE: "bundle not acceptable", + ReasonCode.EXT_FAIL: "failed to process extensions", + ReasonCode.SESS_TERM: "session is terminating", + }, + ), + BitField("id", 0, 64), ] @@ -244,15 +271,18 @@ class ReasonCode(IntEnum): fields_desc = [ XByteEnumField("flags", 0, {Flag.REPLY: "reply"}), - XByteEnumField("reason", ReasonCode.UNKNOWN, { - ReasonCode.UNKNOWN: "unknown", - ReasonCode.TIMEOUT: "idle timeout", - ReasonCode.MISMATCH: "version mismatch", - ReasonCode.BUSY: "entity busy", - ReasonCode.CONTACT_FAIL: "failed to process contact header or sess init", - ReasonCode.NO_RESOURCES: "entity resource exhaustion" - }), - + XByteEnumField( + "reason", + ReasonCode.UNKNOWN, + { + ReasonCode.UNKNOWN: "unknown", + ReasonCode.TIMEOUT: "idle timeout", + ReasonCode.MISMATCH: "version mismatch", + ReasonCode.BUSY: "entity busy", + ReasonCode.CONTACT_FAIL: "failed to process contact header or sess init", + ReasonCode.NO_RESOURCES: "entity resource exhaustion", + }, + ), ] diff --git a/scapy/contrib/dtn/tcpcl_session.py b/scapy/contrib/dtn/tcpcl_session.py index 876ec2cec86..a2442e1b838 100644 --- a/scapy/contrib/dtn/tcpcl_session.py +++ b/scapy/contrib/dtn/tcpcl_session.py @@ -16,6 +16,7 @@ class Session: """ TCPCL messages are conventionally, but not necessarily, sent on port 4556. Since this cannot be relied upon, especially on a localhost session, the best way to bind TCP packets to TCPCL message is to track the state of a TCPCL session. Once Contact Headers are successfuly exchanged, TCP packets can be assumed to carry payloads of TCPCL messages until the session ends. """ + def __init__(self): self.contact_init = False self.contact_ack = False @@ -36,7 +37,9 @@ def split_messages(sport, dport): def activate(self): if not (self.contact_init and self.contact_ack): - raise Exception("tried to activate a session before initialization and acknowledgement") + raise Exception( + "tried to activate a session before initialization and acknowledgement" + ) self.is_active = self.contact_init and self.contact_ack @@ -69,13 +72,15 @@ def proc_term_ack(self): self.terminate() -class TestTcpcl(): +class TestTcpcl: @staticmethod def check_pkt(pkt: Packet, options: List[Packet]): """Asserts that pkt is equal to one of the packets in options (according to the raw representation)""" for opt in options: - assert raw(pkt) in list(map(raw, options)), "Failed to build a properly formatted TCPCL message" + assert raw(pkt) in list( + map(raw, options) + ), "Failed to build a properly formatted TCPCL message" @staticmethod def make_prn(): @@ -89,14 +94,18 @@ def process(pkt): if not sess.is_active: try: # try to find a Contact Header pay = pkt[Raw].load - contact = TCPCL.ContactHeader(pay) # should raise unhandled error if + contact = TCPCL.ContactHeader( + pay + ) # should raise unhandled error if # the TCP payload does not fit ContactHeader # replace pkt's raw payload with a ContactHeader formatted payload pkt[TCP].remove_payload() pkt = pkt / contact # process ContactHeader - if sess.contact_init: # session aready initialized, Header is an ack + if ( + sess.contact_init + ): # session aready initialized, Header is an ack sess.proc_ack() print("BEGIN TCPCL SESSION") else: From 4a6cb57edb54dfed8b026e7462d31ab67fe69f26 Mon Sep 17 00:00:00 2001 From: T-recks Date: Sun, 24 Aug 2025 11:39:06 -0700 Subject: [PATCH 13/16] ignore E231 inside of string --- scapy/contrib/dtn/bpv7.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scapy/contrib/dtn/bpv7.py b/scapy/contrib/dtn/bpv7.py index 74493a2efa0..4e21eeb7ca0 100644 --- a/scapy/contrib/dtn/bpv7.py +++ b/scapy/contrib/dtn/bpv7.py @@ -111,7 +111,7 @@ def from_string(self, ipn_str: str): return self def __str__(self): - return f"ipn:{self.node_id}.{self.service_number}" + return f"ipn:{self.node_id}.{self.service_number}" # noqa: E231 def __eq__(self, other): if isinstance(other, IPN): From df5c0bf34fe532f3a99ff1f75b7034f0c9e53d60 Mon Sep 17 00:00:00 2001 From: T-recks Date: Sun, 24 Aug 2025 12:22:33 -0700 Subject: [PATCH 14/16] fix E501 line too long --- scapy/contrib/dtn/bpv7.py | 30 +++++++++++++++++------------- scapy/contrib/dtn/cbor.py | 17 ++++++++++------- scapy/contrib/dtn/common.py | 5 +++-- scapy/contrib/dtn/tcpcl.py | 11 ++++++----- scapy/contrib/dtn/tcpcl_session.py | 13 ++++++++++--- 5 files changed, 46 insertions(+), 30 deletions(-) diff --git a/scapy/contrib/dtn/bpv7.py b/scapy/contrib/dtn/bpv7.py index 4e21eeb7ca0..91a28c01997 100644 --- a/scapy/contrib/dtn/bpv7.py +++ b/scapy/contrib/dtn/bpv7.py @@ -360,12 +360,13 @@ def get_header(self) -> bytes: def post_dissect(self, s): """ - Because some block elements--such as CRCs and CBOR array headers--are added to the - raw representation via overriding the post_build method (and correspondingly removed - during pre_dissect), the raw packet cache must be cleared. Otherwise, some important - methods will be broken for Blocks built from sniffed packets; for example, - `raw(Bundle(raw_bytes_received_from_socket))` will not produce valid bundle bytes. - See the comment linked below and the subsequent comment with a solution copied here. + Because some block elements--such as CRCs and CBOR array headers--are added to + the raw representation via overriding the post_build method (and correspondingly + removed during pre_dissect), the raw packet cache must be cleared. Otherwise, + some important methods will be broken for Blocks built from sniffed packets; + for example, `raw(Bundle(raw_bytes_received_from_socket))` will not produce + valid bundle bytes. See the comment linked below and the subsequent comment + with a solution copied here. https://github.com/secdev/scapy/issues/1021#issuecomment-704472941 """ @@ -441,9 +442,10 @@ class EncryptedPreviousNodeBlock(PreviousNodeBlock): fields_template = Common.template_replace( PreviousNodeBlock.fields_template, { - # The data field defintion from the parent class cannot be used here. That data - # is now encrypted and cannot be decrypted to its original bytes (within the - # scope of this module), so the Packet that it represents cannot be dissected. + # The data field defintion from the parent class cannot be used here. + # That data is now encrypted and cannot be decrypted to its original bytes + # (within the scope of this module), so the Packet that it represents + # cannot be dissected. # Instead, the data field defintion from CanonicalBlock is used. "data": CBORByteString("data", b"\xde\xad\xbe\xef") }, @@ -617,7 +619,8 @@ class UnassignedExtensionBlock(CanonicalBlock): class EncryptedUnassignedExtensionBlock(CanonicalBlock): - """An extension block with an unassigned type code < 192. The data field is encrypted.""" + """An extension block with an unassigned type code < 192. + The data field is encrypted.""" encrypted = True @@ -801,8 +804,9 @@ def find_block_by_type( self, block_type, excluded_block_nums: List[int] = None ) -> CanonicalBlock: """ - Find the first canonical block matching the specified type, with a block number not - in the excluded list.""" + Find the first canonical block matching the specified type, + with a block number not in the excluded list. + """ if excluded_block_nums is None: excluded_block_nums = [] try: @@ -865,7 +869,7 @@ def add_block( block_num_to_insert_above=1, select_block_number=False, ) -> CanonicalBlock: - """Insert an extension block just before the block with the specified block number.""" + """Insert an extension block before the block with specified block number.""" if select_block_number: block.block_number = self.get_new_block_number() diff --git a/scapy/contrib/dtn/cbor.py b/scapy/contrib/dtn/cbor.py index 4d49210d220..7167c1a2afd 100644 --- a/scapy/contrib/dtn/cbor.py +++ b/scapy/contrib/dtn/cbor.py @@ -32,11 +32,12 @@ class MajorTypeException(Exception): - """This exception indicates that a CBOR object has an unexpected value for its Major Type. + """This exception indicates that a CBOR object has an unexpected + value for its Major Type. Attributes: actual -- the integer value of the actual Major Type - expected -- an integer or list of integers indicating the acceptable Major Type values + expected -- an integer or list indicating the acceptable Major Type values """ def __init__(self, actual: int, expected: Union[int, List[int]]): @@ -61,7 +62,8 @@ def __init__(self, value): class AdditionalInfoException(Exception): - """This exception indicates that a CBOR object has an unexpected value for its Additional Info.""" + """This exception indicates that a CBOR object has an unexpected value for + its Additional Info.""" def __init__(self): super().__init__("[Error] Invalid additional info.") @@ -74,9 +76,9 @@ def __init__(self, value, cls): # CBOR definitions class CBORNull(Field): - """This class exists so that it can be used in a MultipleTypeField containing CBOR values. - Every option given to a MultipleTypeField must be a field with at least a name. - Thus, if one of the MultipleType options should be that no field whatsoever is present, + """This class exists so that it can be used in a MultipleTypeField containing CBOR + values. Every option given to a MultipleTypeField must be a field with at least a + name. Thus, if one of the MultipleType options should be that no field is present, you need a field that produces no bytes when added to the packet. CBORNull can serve this purpose.""" @@ -238,7 +240,8 @@ def count_additional_fields(self) -> int: return len(self.default_fields) - head_field_count def set_additional_fields(self, pkt: Packet) -> bytes: - """For an, the add field is set to the number of elements minus the two head fields.""" + """For an array, the add field is set to the number of elements minus + the two head fields.""" # pylint: disable=W0201 # field (instance variable) initialization is handled via "fields_desc" self.add = self.count_additional_fields() diff --git a/scapy/contrib/dtn/common.py b/scapy/contrib/dtn/common.py index 58d7fcad9e0..84a94517f8c 100644 --- a/scapy/contrib/dtn/common.py +++ b/scapy/contrib/dtn/common.py @@ -31,8 +31,9 @@ class ControlPacket(NoPayloadPacket): class FieldPacket(NoPayloadPacket): """A packet intended for use as a field (i.e. PacketField or PacketListField) - in another Packet, rather than one sent or received on the wire. Useful when you need - heterogeneous, compound data similar to a record/struct within another Packet.""" + in another Packet, rather than one sent or received on the wire. Useful when you + need heterogeneous, compound data similar to a record/struct within + another Packet.""" FieldsTemplate = Dict[str, Field] diff --git a/scapy/contrib/dtn/tcpcl.py b/scapy/contrib/dtn/tcpcl.py index 60e5e6a53be..4ed0539605f 100644 --- a/scapy/contrib/dtn/tcpcl.py +++ b/scapy/contrib/dtn/tcpcl.py @@ -89,7 +89,7 @@ class MsgType(IntEnum): class Ext(FieldPacket): """ - Class definition for an Extension Item in the format of a Type-Length-Value container. + Definition for an Extension Item in the format of a Type-Length-Value container. """ class Flag(IntEnum): @@ -215,7 +215,8 @@ def post_build(self, pkt, pay): return pkt + pay def post_dissect(self, s): - "An XferSegment message should have a Bundle as payload. If it has raw bytes instead, raise an error." + """An XferSegment message should have a Bundle as payload. + If it has raw bytes instead, raise an error.""" try: if self[Raw].load is not None: raise InvalidPayloadError(self[Raw].load) @@ -279,7 +280,7 @@ class ReasonCode(IntEnum): ReasonCode.TIMEOUT: "idle timeout", ReasonCode.MISMATCH: "version mismatch", ReasonCode.BUSY: "entity busy", - ReasonCode.CONTACT_FAIL: "failed to process contact header or sess init", + ReasonCode.CONTACT_FAIL: "failed to process contact header or sess init", # noqa: E501 ReasonCode.NO_RESOURCES: "entity resource exhaustion", }, ), @@ -287,8 +288,8 @@ class ReasonCode(IntEnum): # Bind all TCPCL message headers to TCPCL messages. -# This way, if `some_bytes` consists of the raw representation of a TCPCL message, -# you can evaluate e.g. `x=MsgHeader(some_bytes)` and `x` will be a Packet consisting of a TCPCL +# This way, if `some_bytes` consists of a raw TCPCL message, you can evaluate +# e.g. `x=MsgHeader(some_bytes)` and `x` will be a Packet consisting of a TCPCL # MsgHeader with the correct type code plus a payload of the correct TCPCL message type. bind_layers(MsgHeader, SessInit, type=MsgHeader.MsgType.SESS_INIT) bind_layers(MsgHeader, Keepalive, type=MsgHeader.MsgType.KEEPALIVE) diff --git a/scapy/contrib/dtn/tcpcl_session.py b/scapy/contrib/dtn/tcpcl_session.py index a2442e1b838..0367f0b82e1 100644 --- a/scapy/contrib/dtn/tcpcl_session.py +++ b/scapy/contrib/dtn/tcpcl_session.py @@ -5,7 +5,9 @@ # scapy.contrib.description = TCP Convergence Layer version 4 (TCPCLv4) # scapy.contrib.status = loads -# These classes support unit testing of the TCPCL scapy layer (scapy.contrib.dtn.tcpcl) and illustrate how the protocol messages may be used to emulate a TCPCL session. +# These classes support unit testing of the TCPCL scapy layer +# (scapy.contrib.dtn.tcpcl) and illustrate how the protocol messages may +# be used to emulate a TCPCL session. from scapy.all import Raw, raw, TCP, Packet, bind_layers, split_layers import scapy.contrib.dtn.tcpcl as TCPCL @@ -14,7 +16,11 @@ class Session: """ - TCPCL messages are conventionally, but not necessarily, sent on port 4556. Since this cannot be relied upon, especially on a localhost session, the best way to bind TCP packets to TCPCL message is to track the state of a TCPCL session. Once Contact Headers are successfuly exchanged, TCP packets can be assumed to carry payloads of TCPCL messages until the session ends. + TCPCL messages are conventionally, but not necessarily, sent on port 4556. + Since this cannot be relied upon, especially on a localhost session, the best + way to bind TCP packets to TCPCL message is to track the state of a TCPCL session. + Once Contact Headers are successfuly exchanged, TCP packets can be assumed to + carry payloads of TCPCL messages until the session ends. """ def __init__(self): @@ -76,7 +82,8 @@ class TestTcpcl: @staticmethod def check_pkt(pkt: Packet, options: List[Packet]): - """Asserts that pkt is equal to one of the packets in options (according to the raw representation)""" + """Asserts that pkt is equal to one of the packets in options + (according to the raw representation)""" for opt in options: assert raw(pkt) in list( map(raw, options) From 72116473faf18d38b7353d465173af18d2b23bac Mon Sep 17 00:00:00 2001 From: T-recks Date: Sun, 24 Aug 2025 12:23:52 -0700 Subject: [PATCH 15/16] add contrib.dtn runtime deps --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index d47bddf8b9d..57de8fb57e0 100644 --- a/tox.ini +++ b/tox.ini @@ -33,6 +33,8 @@ deps = cryptography coverage[toml] python-can + flynn + crcmod # disabled on windows because they require c++ dependencies # brotli 1.1.0 broken https://github.com/google/brotli/issues/1072 brotli < 1.1.0 ; sys_platform != 'win32' From 4812c62af700dde6aecd5df6777c30f457a7fbe1 Mon Sep 17 00:00:00 2001 From: T-recks Date: Sun, 24 Aug 2025 12:43:10 -0700 Subject: [PATCH 16/16] fix spell err --- scapy/contrib/dtn/bpv7.py | 4 ++-- scapy/contrib/dtn/tcpcl_session.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scapy/contrib/dtn/bpv7.py b/scapy/contrib/dtn/bpv7.py index 91a28c01997..b70fe7664e4 100644 --- a/scapy/contrib/dtn/bpv7.py +++ b/scapy/contrib/dtn/bpv7.py @@ -442,11 +442,11 @@ class EncryptedPreviousNodeBlock(PreviousNodeBlock): fields_template = Common.template_replace( PreviousNodeBlock.fields_template, { - # The data field defintion from the parent class cannot be used here. + # The data field definition from the parent class cannot be used here. # That data is now encrypted and cannot be decrypted to its original bytes # (within the scope of this module), so the Packet that it represents # cannot be dissected. - # Instead, the data field defintion from CanonicalBlock is used. + # Instead, the data field definition from CanonicalBlock is used. "data": CBORByteString("data", b"\xde\xad\xbe\xef") }, ) diff --git a/scapy/contrib/dtn/tcpcl_session.py b/scapy/contrib/dtn/tcpcl_session.py index 0367f0b82e1..6fc194ef01e 100644 --- a/scapy/contrib/dtn/tcpcl_session.py +++ b/scapy/contrib/dtn/tcpcl_session.py @@ -19,7 +19,7 @@ class Session: TCPCL messages are conventionally, but not necessarily, sent on port 4556. Since this cannot be relied upon, especially on a localhost session, the best way to bind TCP packets to TCPCL message is to track the state of a TCPCL session. - Once Contact Headers are successfuly exchanged, TCP packets can be assumed to + Once Contact Headers are successfully exchanged, TCP packets can be assumed to carry payloads of TCPCL messages until the session ends. """ @@ -112,7 +112,7 @@ def process(pkt): # process ContactHeader if ( sess.contact_init - ): # session aready initialized, Header is an ack + ): # session already initialized, Header is an ack sess.proc_ack() print("BEGIN TCPCL SESSION") else: