diff --git a/doc/scapy/layers/canxl.rst b/doc/scapy/layers/canxl.rst new file mode 100644 index 00000000000..e42218fdfd9 --- /dev/null +++ b/doc/scapy/layers/canxl.rst @@ -0,0 +1,178 @@ +.. + Note: Copyright (c) 2026, Robert Bosch GmbH. + The content of this documentation file is contributed by and + copyright of Robert Bosch GmbH, created by Friedrich Wiemer. + +###### +CAN XL +###### + +CAN XL (ISO 11898-1:2024) is the newest member of the CAN protocol family, +offering up to 2048 bytes of payload per frame and a priority-based +arbitration field. The CiA 613-1 specification defines a simple extended +content (SEC) flag and an "add-on services" framework that allows optional +features to be layered on top of plain CAN XL. Two add-on services are +currently standardised in dedicated documents: CANsec for authenticated and +encrypted communication (CiA 613-2) and fragmentation of payloads (CiA 613-3). + +Scapy provides the ``CANXL`` packet class in ``scapy.layers.can`` and +supports sending/receiving CAN XL frames through ``NativeCANSocket`` +on Linux (kernel 6.2 or later). + +Quick start +=========== + +Setting up a virtual CAN interface +----------------------------------- + +CAN XL works over standard Linux virtual CAN (vcan) interfaces. +Make sure your kernel is 6.2 or newer:: + + $ sudo modprobe vcan + $ sudo ip link add dev vcan0 type vcan + $ sudo ip link set vcan0 up + +Building and inspecting frames +------------------------------- + +.. code-block:: python + + from scapy.layers.can import CANXL + + # Create a basic CAN XL frame + pkt = CANXL(priority=0x42, sdt=3, af=0xDEAD) / b'\x01\x02\x03' + pkt.show() + + # ISO 11898-1 field names (Priority, Format, FTYPE, SDT, SEC, DLC, etc.) + pkt.show_iso() + + # Access payload data (same API as classic CAN) + pkt.data # b'\x01\x02\x03' + + # ISO properties + pkt.dlc # 2 (length - 1) + pkt.sec # False + pkt.xlf # True + pkt.fdf # True + pkt.ftype # False + pkt.frame_format # 6 (XLF+FDF) + +Sending and receiving over a socket +------------------------------------ + +.. code-block:: python + + from scapy.contrib.cansocket_native import NativeCANSocket + from scapy.layers.can import CANXL + + # Open a CAN XL socket (xl=True enables CAN_RAW_XL_FRAMES) + sock = NativeCANSocket(channel="vcan0", xl=True) + + # Send a frame + sock.send(CANXL(priority=0x42, sdt=3, af=0xDEAD) / b'\x01\x02\x03') + + # Receive a frame (in another terminal or Scapy session) + pkt = sock.recv() + pkt.show() + pkt.show_iso() + + sock.close() + +Kernel requirements: + +- CAN XL frames need Linux **kernel >= 6.2** (``CAN_RAW_XL_FRAMES`` socket option). +- VCID pass-through needs Linux **kernel >= 6.11** (``CAN_RAW_XL_VCID_OPTS``). + Scapy handles older kernels gracefully -- VCID just stays at zero. + + +Field naming: Linux vs ISO +=========================== + +CAN XL field names differ between the Linux kernel's ``struct canxl_frame`` +(used in Scapy's ``fields_desc``) and the ISO 11898-1:2024 specification. +Use ``pkt.show_iso()`` to see ISO names, or access via +properties: + ++-------------+---------------------+----------------------+ +| ISO name | Linux / Scapy name | Access via | ++=============+=====================+======================+ +| Priority | ``priority`` | ``pkt.priority`` | ++-------------+---------------------+----------------------+ +| Format | ``flags`` bits 7-5 | ``pkt.frame_format`` | ++-------------+---------------------+----------------------+ +| XLF | ``flags.xlf`` | ``pkt.xlf`` | ++-------------+---------------------+----------------------+ +| FDF | ``flags.fdf`` | ``pkt.fdf`` | ++-------------+---------------------+----------------------+ +| IDE | ``flags.ide`` | ``pkt.ide`` | ++-------------+---------------------+----------------------+ +| SEC | ``flags.sec`` | ``pkt.sec`` | ++-------------+---------------------+----------------------+ +| FTYPE / RRS | ``flags.rrs`` | ``pkt.ftype`` | ++-------------+---------------------+----------------------+ +| SDT | ``sdt`` | ``pkt.sdt`` | ++-------------+---------------------+----------------------+ +| DLC | ``length`` (len-1) | ``pkt.dlc`` | ++-------------+---------------------+----------------------+ +| VCID | ``vcid`` | ``pkt.vcid`` | ++-------------+---------------------+----------------------+ +| AF | ``af`` | ``pkt.af`` | ++-------------+---------------------+----------------------+ +| Data | (sub-layer payload) | ``pkt.data`` | ++-------------+---------------------+----------------------+ + + +Byte-order handling +==================== + +In PCAP captures (``DLT_CAN_SOCKETCAN``), CAN XL follows the Wireshark and +``libpcap`` specification: the 4-byte Priority word (VCID + Priority) is stored +in big-endian (network) byte order, matching classic CAN and CAN FD, while the +Length (2 bytes) and Acceptance Field (4 bytes) are stored in little-endian byte +order. + +Linux SocketCAN (``struct canxl_frame``) uses host byte order (little-endian on +x86/ARM) for all multi-byte fields. When using ``NativeCANSocket``, Scapy +automatically swaps the 4-byte Priority word between Linux SocketCAN and Scapy +when ``conf.contribs['CAN']['swap-bytes']`` is ``False`` (default), exactly as +it does for classic CAN and CAN FD. Setting +``conf.contribs['CAN']['swap-bytes'] = True`` configures Scapy to parse and +build CAN XL frames directly in Linux SocketCAN host byte order. + + +Interop with can-utils +====================== + +On Linux you can send and receive CAN XL frames using the ``can-utils`` +package (``cansend``, ``candump``, etc.) alongside Scapy. Start +``candump`` in one terminal and use Scapy to send:: + + # Terminal 1: + $ candump vcan0 + + # Terminal 2 (Scapy): + >>> from scapy.contrib.cansocket_native import NativeCANSocket + >>> from scapy.layers.can import CANXL + >>> sock = NativeCANSocket(channel="vcan0", xl=True) + >>> sock.send(CANXL(priority=0x42, sdt=3, af=0xDEAD) / b'\x01\x02') + +The ``candump`` output should show the CAN XL frame with its priority, +SDT, and payload. + + +Known limitations +================== + +- **pcap read/write:** CAN XL frames are part of ``LINKTYPE_CAN_SOCKETCAN`` + (DLT 227) and Wireshark supports them since version 4.2.3. However, Scapy + does not yet handle the mixed-endian pcap wire format for CAN XL correctly + (Priority is big-endian in pcap, while Length and AF are little-endian). + This will be addressed in a future release. + +- **CandumpReader:** The ``rdcandump`` / ``CandumpReader`` utilities do not + parse CAN XL frames yet. + +- **No SDT-based payload dispatch:** The ``guess_payload_class`` override + currently returns raw bytes. Sub-dissectors for specific SDT values can + be added via ``bind_layers`` or by monkey-patching, as the CANsec contrib + demonstrates. diff --git a/scapy/contrib/cansocket_native.py b/scapy/contrib/cansocket_native.py index 49efacd457c..43de45fcdab 100644 --- a/scapy/contrib/cansocket_native.py +++ b/scapy/contrib/cansocket_native.py @@ -2,6 +2,9 @@ # This file is part of Scapy # See https://scapy.net/ for more information # Copyright (C) Nils Weiss +# +# The CAN XL parts are created by Friedrich Wiemer +# Copyright (C) 2026, Robert Bosch GmbH # scapy.contrib.description = Native CANSocket # scapy.contrib.status = loads @@ -19,7 +22,7 @@ from scapy.supersocket import SuperSocket from scapy.error import Scapy_Exception, warning, log_runtime from scapy.packet import Packet -from scapy.layers.can import CAN, CAN_MTU, CAN_FD_MTU +from scapy.layers.can import CAN, CANFD, CANXL, CAN_MTU, CAN_FD_MTU, CANXL_MTU from scapy.compat import raw from typing import ( @@ -51,11 +54,21 @@ class NativeCANSocket(SuperSocket): """ # noqa: E501 desc = "read/write packets at a given CAN interface using PF_CAN sockets" + # Socket option constants for CAN XL (not yet in Python's socket module) + CAN_RAW_XL_FRAMES = 7 # enable CAN XL frames (kernel >= 6.2) + CAN_RAW_XL_VCID_OPTS = 8 # VCID pass-through opts (kernel >= 6.11) + + # can_raw_vcid_options.flags bits + CAN_RAW_XL_VCID_TX_SET = 0x01 + CAN_RAW_XL_VCID_TX_PASS = 0x02 + CAN_RAW_XL_VCID_RX_FILTER = 0x04 + def __init__(self, channel=None, # type: Optional[str] receive_own_messages=False, # type: bool can_filters=None, # type: Optional[List[Dict[str, int]]] fd=False, # type: bool + xl=False, # type: bool basecls=CAN, # type: Type[Packet] **kwargs # type: Dict[str, Any] ): @@ -67,8 +80,12 @@ def __init__(self, "the correct one to achieve compatibility with python-can" "/PythonCANSocket. \n'bustype=socketcan'") + if fd and xl: + raise Scapy_Exception("fd and xl are mutually exclusive") + self.MTU = CAN_MTU self.fd = fd + self.xl = xl self.basecls = basecls self.channel = conf.contribs['NativeCANSocket']['channel'] if \ channel is None else channel @@ -109,6 +126,34 @@ def __init__(self, "Could not enable CAN FD support (%s)", exception ) + if self.xl: + # CAN_RAW_XL_FRAMES - required, kernel >= 6.2 + try: + self.ins.setsockopt(socket.SOL_CAN_RAW, + self.CAN_RAW_XL_FRAMES, + struct.pack("i", 1)) + self.MTU = CANXL_MTU + except OSError as exc: + raise Scapy_Exception( + "Could not enable CAN XL frames " + "(kernel >= 6.2 required): %s" % exc + ) + + # CAN_RAW_XL_VCID_OPTS - optional, kernel >= 6.11 + # RX_FILTER with mask=0 passes all VCIDs; TX_PASS forwards + # the VCID from the frame to the bus. + vcid_flags = (self.CAN_RAW_XL_VCID_RX_FILTER | + self.CAN_RAW_XL_VCID_TX_PASS) + try: + vcid_opts = struct.pack("BBBB", vcid_flags, 0, 0, 0) + self.ins.setsockopt(socket.SOL_CAN_RAW, + self.CAN_RAW_XL_VCID_OPTS, + vcid_opts) + except OSError: + warning("CAN_RAW_XL_VCID_OPTS not available " + "(kernel >= 6.11 required). " + "Frames with non-zero VCID may not be received.") + if can_filters is None: can_filters = [{ "can_id": 0, @@ -143,12 +188,8 @@ def recv_raw(self, x=CAN_MTU): # something bad happened (e.g. the interface went down) warning("Captured no data.") - # need to change the byte order of the first four bytes, - # required by the underlying Linux SocketCAN frame format if not conf.contribs['CAN']['swap-bytes'] and pkt: - pack_fmt = " +# +# The CAN XL parts are created by Friedrich Wiemer +# Copyright (C) 2026, Robert Bosch GmbH # scapy.contrib.description = python-can CANSocket # scapy.contrib.status = loads @@ -21,9 +24,9 @@ from scapy.config import conf from scapy.supersocket import SuperSocket -from scapy.layers.can import CAN +from scapy.layers.can import CAN, CANXL from scapy.packet import Packet -from scapy.error import warning, log_runtime +from scapy.error import Scapy_Exception, warning, log_runtime from typing import ( List, Type, @@ -433,6 +436,9 @@ def recv_raw(self, x=0xffff): def send(self, x): # type: (Packet) -> int + if isinstance(x, CANXL): + raise Scapy_Exception( + "PythonCANSocket does not support CAN XL frames") bx = bytes(x) msg = can_Message(is_remote_frame=x.flags == 0x2, is_extended_id=x.flags == 0x4, diff --git a/scapy/layers/can.py b/scapy/layers/can.py index c7d517f9898..878493c3757 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -2,6 +2,9 @@ # This file is part of Scapy # See https://scapy.net/ for more information # Copyright (C) Philippe Biondi +# +# The CAN XL parts are created by Friedrich Wiemer +# Copyright (C) 2026, Robert Bosch GmbH """A minimal implementation of the CANopen protocol, based on @@ -16,12 +19,13 @@ from scapy.config import conf from scapy.compat import chb, hex_bytes from scapy.data import DLT_CAN_SOCKETCAN -from scapy.fields import FieldLenField, FlagsField, StrLenField, \ - ThreeBytesField, XBitField, ScalingField, ConditionalField, LenField, ShortField +from scapy.fields import BitField, FieldLenField, FlagsField, StrLenField, \ + ThreeBytesField, XBitField, XByteField, XLEIntField, ScalingField, \ + ConditionalField, LenField, LEShortField, ShortField from scapy.volatile import RandFloat, RandBinFloat from scapy.packet import Packet, bind_layers from scapy.layers.l2 import CookedLinux -from scapy.error import Scapy_Exception +from scapy.error import Scapy_Exception, log_runtime from scapy.plist import PacketList from scapy.supersocket import SuperSocket from scapy.utils import _ByteStream @@ -44,7 +48,9 @@ "BESignedSignalField", "BEUnsignedSignalField", "rdcandump", "CandumpReader", "SignalHeader", "CAN_MTU", "CAN_MAX_IDENTIFIER", "CAN_MAX_DLEN", "CAN_INV_FILTER", "CANFD", "CAN_FD_MTU", - "CAN_FD_MAX_DLEN"] + "CAN_FD_MAX_DLEN", "CANXL", "CANXL_MTU", "CANXL_MAX_DLEN", + "CANXL_MIN_DLEN", "CANXL_HDR_SIZE", "CANXL_XLF", "CANXL_FDF", + "CANXL_IDE", "CANXL_SEC", "CANXL_RRS"] # CONSTANTS CAN_MAX_IDENTIFIER = (1 << 29) - 1 # Maximum 29-bit identifier @@ -53,6 +59,15 @@ CAN_INV_FILTER = 0x20000000 CAN_FD_MTU = 72 CAN_FD_MAX_DLEN = 64 +CANXL_MTU = 2060 +CANXL_HDR_SIZE = 12 +CANXL_MAX_DLEN = 2048 +CANXL_MIN_DLEN = 1 +CANXL_XLF = 0x80 # XL Frame flag (flags bit 7, must be set) +CANXL_FDF = 0x40 # FD Frame flag (flags bit 6, must be set) +CANXL_IDE = 0x20 # Identifier Extension (flags bit 5, must be clear) +CANXL_SEC = 0x01 # Security / SEC bit +CANXL_RRS = 0x02 # Remote Request Substitution / Frame Type bit # Mimics the Wireshark CAN dissector parameter # 'Byte-swap the CAN ID/flags field'. @@ -111,6 +126,8 @@ def dispatch_hook(cls, **kargs # type: Any ): # type: (...) -> Type[Packet] if _pkt: + if CANXL.is_canxl_frame(_pkt): + return CANXL fdf_set = len(_pkt) > 5 and _pkt[5] & 0x04 and \ not _pkt[5] & 0xf8 if fdf_set: @@ -211,6 +228,190 @@ def post_build(self, pkt, pay): bind_layers(CookedLinux, CANFD, proto=13) +conf.l2types.register_layer2num(DLT_CAN_SOCKETCAN, CANFD) + + +class CANXL(CAN): + """CAN XL frame - wire-format compatible with Linux struct canxl_frame. + + Uses the Linux kernel data representation (``struct canxl_frame``) for + field names and layout. ISO 11898-1:2024 field accessors are available + via ``@property`` methods (``dlc``, ``xlf``, ``sec``, ``ftype``, + ``frame_format``), and ``show_iso()`` renders using ISO terminology. + + Example:: + + >>> from scapy.layers.can import CANXL + >>> pkt = CANXL(priority=0x42, vcid=0x10, sdt=3, af=0xDEAD) / b'\\x01\\x02' + >>> pkt.show() + >>> pkt.show_iso() + """ + name = "CAN XL" + + @staticmethod + def is_canxl_frame(pkt): + # type: (bytes) -> bool + """Detect CAN XL frame by XLF flag (bit 7 of byte 4). + + CAN XL: byte 4 is the flags byte with XLF (bit 7) always set. + In CAN/CANFD byte 4 is the length field (max 64 = 0x40), + so bit 7 is never set - this is an unambiguous discriminator. + """ + return len(pkt) > 4 and bool(pkt[4] & 0x80) + + fields_desc = [ + # prio word: 4 bytes, big endian in PCAP (matches classic CAN / CAN FD), + # swapped with CAN.inv_endianness for Linux SocketCAN (struct canxl_frame.prio) + BitField('reserved2', 0, 8), # bits 31-24 + XBitField('vcid', 0, 8), # bits 23-16 + BitField('reserved1', 0, 5), # bits 15-11 + XBitField('priority', 0, 11), # bits 10-0 + # ISO 11898-1:2024: CAN XL requires XLF=1, FDF=1, IDE=0 + FlagsField('flags', CANXL_XLF | CANXL_FDF, 8, + ['sec', 'rrs', 'res_f2', 'res_f3', + 'res_f4', 'ide', 'fdf', 'xlf']), + XByteField('sdt', 0), + # Auto-computed from the payload in post_build. + # ISO 11898-1:2024 defines this as an 11-bit field (range 1-2048), + # but Linux struct canxl_frame uses a full 16-bit field. + # For kernel compatibility we use a 16-bit field; post_build warns + # if the computed length falls outside the valid range. + LEShortField('length', 0), + XLEIntField('af', 0), + # NO data field - payload carried as sub-layers + ] + + def post_build(self, pkt, pay): + # type: (bytes, bytes) -> bytes + # Auto-compute length from payload + length = len(pay) + if length < CANXL_MIN_DLEN: + log_runtime.warning( + "CAN XL payload length %d is below the minimum of %d", + length, CANXL_MIN_DLEN) + elif length > CANXL_MAX_DLEN: + log_runtime.warning( + "CAN XL payload length %d exceeds the ISO 11898-1 " + "maximum of %d (11-bit field)", length, CANXL_MAX_DLEN) + pkt = pkt[:6] + struct.pack(' Tuple[bytes, Optional[bytes]] + data_len = min(int(self.length), CANXL_MAX_DLEN) if self.length else 0 + # Return None (not p[data_len:]) as the padding element so + # that trailing bytes beyond the stated length are silently + # dropped rather than preserved as a Padding layer. CAN XL + # frames from a native socket have exact-length data; any + # trailing garbage is safely discarded. + return p[:data_len], None + + # -- ISO 11898-1:2024 property accessors --------------------------------- + + @property + def dlc(self): + # type: () -> int + """ISO 11898-1 Data Length Code (length - 1, range 0..2047).""" + return max(0, self.length - 1) if self.length else 0 + + @property + def xlf(self): + # type: () -> bool + """XL Frame flag (flags bit 7). Always 1 for valid CAN XL.""" + return bool(self.flags.xlf) + + @property + def fdf(self): + # type: () -> bool + """FD Frame flag (flags bit 6). Always 1 for valid CAN XL.""" + return bool(self.flags.fdf) + + @property + def ide(self): + # type: () -> bool + """Identifier Extension flag (flags bit 5). Always 0 for CAN XL.""" + return bool(self.flags.ide) + + @property + def sec(self): + # type: () -> bool + """Simple Extended Content / security flag (flags bit 0).""" + return bool(self.flags.sec) + + @property + def ftype(self): + # type: () -> bool + """Frame Type / RRS (flags bit 1).""" + return bool(self.flags.rrs) + + @property + def frame_format(self): + # type: () -> int + """ISO 11898-1:2024 3-bit format field (XLF:FDF:IDE), bits 7-5.""" + return (int(self.flags) >> 5) & 0x07 + + # -- ISO 11898-1:2024 rendering ------------------------------------------ + + @property + def data(self): + # type: () -> bytes + """Access payload data as bytes, for API consistency with CAN/CANFD. + + CAN and CAN FD use ``pkt.data``; CAN XL carries its payload as + Scapy sub-layers, so this property provides the same interface:: + + >>> pkt = CANXL(priority=0x42) / b'\\x01\\x02\\x03' + >>> pkt.data # equivalent to bytes(pkt.payload) + b'\\x01\\x02\\x03' + """ + return bytes(self.payload) + + def show_iso(self, dump=False): + # type: (bool) -> Optional[str] + """Render the frame using ISO 11898-1:2024 field names. + + Field order follows ISO 11898-1:2024 Table 4. The values that the + standard derives from the flags byte (Format, FTYPE, SEC) and from + the length (DLC) are not fields of their own, so they are resolved + through the matching properties rather than by ``sprintf``. + + :param dump: return the string instead of printing it + """ + fmt_val = self.frame_format + fmt_str = "+".join( + name for bit, name in ((0x04, "XLF"), (0x02, "FDF"), (0x01, "IDE")) + if fmt_val & bit) or "0" + + s = self.sprintf( + "###[ CAN XL (ISO 11898-1) ]###\n" + " Priority = %CANXL.priority%\n" + " Format = " + fmt_str + " (" + hex(fmt_val) + ")\n" + " FTYPE = " + str(int(self.ftype)) + "\n" + " SDT = %CANXL.sdt%\n" + " SEC = " + str(int(self.sec)) + "\n" + " DLC = " + str(self.dlc) + "\n" + " VCID = %CANXL.vcid%\n" + " AF = %CANXL.af%\n") + # Keep the payload out of sprintf - raw bytes may contain '%'. + s += " Data = %r\n" % bytes(self.payload) + + if dump: + return s + print(s) + return None + + +bind_layers(CookedLinux, CANXL, proto=14) +conf.l2types.register_layer2num(DLT_CAN_SOCKETCAN, CANXL) class SignalField(ScalingField): diff --git a/test/contrib/canxlsocket_testsocket.uts b/test/contrib/canxlsocket_testsocket.uts new file mode 100644 index 00000000000..c841a884a67 --- /dev/null +++ b/test/contrib/canxlsocket_testsocket.uts @@ -0,0 +1,247 @@ +% Regression tests for CAN XL via TestSocket +% Tests CAN XL frame send/recv through scapy's in-memory TestSocket, +% Created by Friedrich Wiemer +# Copyright (C) 2026, Robert Bosch GmbH + +############ +############ ++ Configuration + += Imports + +conf.contribs['CAN'] = {'swap-bytes': False, 'remove-padding': True} +load_layer("can", globals_dict=globals()) +from scapy.layers.can import CANXL, CANXL_MTU, CANXL_HDR_SIZE, \ + CANXL_MAX_DLEN, CANXL_MIN_DLEN, CANXL_XLF, CANXL_FDF, CANXL_SEC, \ + CANXL_RRS +from test.testsocket import TestSocket, cleanup_testsockets + +############ +############ ++ Basic CAN XL send/recv via TestSocket + += CAN XL minimal frame send and recv + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + s1.send(CANXL(priority=0x042, sdt=3, af=0xDEAD) / b'\x01\x02') + rx = s2.recv() + assert isinstance(rx, CANXL) + assert rx.priority == 0x042 + assert rx.sdt == 3 + assert rx.af == 0xDEAD + assert rx.length == 2 + assert bytes(rx.payload) == b'\x01\x02' + assert rx.flags.xlf == 1 + assert rx.flags.fdf == 1 + assert rx.flags.ide == 0 + += CAN XL all fields set + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + tx = CANXL(priority=0x7FF, vcid=0xFF, flags=0xC3, sdt=0xFF, + af=0xFFFFFFFF) / b'\xCA\xFE' + s1.send(tx) + rx = s2.recv() + assert isinstance(rx, CANXL) + assert rx.priority == 0x7FF + assert rx.vcid == 0xFF + assert rx.sdt == 0xFF + assert rx.af == 0xFFFFFFFF + assert rx.flags.sec == 1 + assert rx.flags.rrs == 1 + assert rx.flags.xlf == 1 + assert rx.flags.fdf == 1 + assert rx.flags.ide == 0 + assert bytes(rx.payload) == b'\xCA\xFE' + += CAN XL minimum payload (1 byte) + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + s1.send(CANXL(priority=0x001) / b'\xAA') + rx = s2.recv() + assert isinstance(rx, CANXL) + assert rx.length == 1 + assert bytes(rx.payload) == b'\xAA' + += CAN XL maximum payload (2048 bytes) + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + payload = bytes(range(256)) * 8 + assert len(payload) == CANXL_MAX_DLEN + s1.send(CANXL(priority=0x100, vcid=0x10, sdt=0x07, + af=0x12345678) / payload) + rx = s2.recv() + assert isinstance(rx, CANXL) + assert rx.length == 2048 + assert bytes(rx.payload) == payload + assert rx.priority == 0x100 + assert rx.vcid == 0x10 + += CAN XL round-trip field equality + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + orig = CANXL(priority=0x1AB, vcid=0xCC, sdt=0x05, + af=0xDEADBEEF) / b'round-trip' + s1.send(orig) + rx = s2.recv() + assert isinstance(rx, CANXL) + assert rx.priority == orig.priority + assert rx.vcid == orig.vcid + assert rx.sdt == orig.sdt + assert rx.af == orig.af + assert rx.length == len(b'round-trip') + assert bytes(rx.payload) == b'round-trip' + +############ +############ ++ ISO flag enforcement through TestSocket + += CAN XL flags enforced after send/recv (XLF+FDF set, IDE cleared) + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + # Intentionally set wrong flags: IDE on, XLF/FDF off + s1.send(CANXL(flags=0x20) / b'\xBB') + rx = s2.recv() + assert isinstance(rx, CANXL) + # post_build enforces correct flags + assert rx.flags.xlf == 1 + assert rx.flags.fdf == 1 + assert rx.flags.ide == 0 + +############ +############ ++ dispatch_hook differentiation via TestSocket + += Mixed CAN, CANFD, CANXL on same paired sockets + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + # Send one of each type + s1.send(CAN(identifier=0x100, length=3, data=b'\x01\x02\x03')) + s1.send(CANFD(identifier=0x200, length=12, + data=b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C')) + s1.send(CANXL(priority=0x042, sdt=1, af=0xBEEF) / b'\xDE\xAD') + rx1 = s2.recv() + rx2 = s2.recv() + rx3 = s2.recv() + # dispatch_hook must route each to the correct class + assert type(rx1) == CAN, "Expected CAN, got %s" % type(rx1).__name__ + assert type(rx2) == CANFD, "Expected CANFD, got %s" % type(rx2).__name__ + assert type(rx3) == CANXL, "Expected CANXL, got %s" % type(rx3).__name__ + # Verify fields survived + assert rx1.identifier == 0x100 + assert rx1.length == 3 + assert rx2.identifier == 0x200 + assert rx2.length == 12 + assert rx3.priority == 0x042 + assert rx3.af == 0xBEEF + +############ +############ ++ ISO 11898-1 properties after send/recv + += CAN XL ISO properties on received frame + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + s1.send(CANXL(priority=0x042, flags=0xC1, sdt=3, + af=0xDEAD) / b'\x01\x02\x03') + rx = s2.recv() + assert isinstance(rx, CANXL) + # dlc = length - 1 + assert rx.dlc == 2 + # Flag properties + assert rx.xlf == True + assert rx.fdf == True + assert rx.ide == False + assert rx.sec == True + assert rx.ftype == False + # frame_format: XLF+FDF = 0b110 = 6 + assert rx.frame_format == 6 + += CAN XL dlc edge cases via TestSocket + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + # 1-byte payload -> dlc = 0 + s1.send(CANXL() / b'\xAA') + rx = s2.recv() + assert rx.dlc == 0 + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + # 2048-byte payload -> dlc = 2047 + s1.send(CANXL() / (b'\xBB' * 2048)) + rx = s2.recv() + assert rx.dlc == 2047 + +############ +############ ++ CAN XL SEC and RRS flags via TestSocket + += CAN XL SEC flag round-trip + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + s1.send(CANXL(flags=0xC1) / b'\x11\x22') + rx = s2.recv() + assert isinstance(rx, CANXL) + assert rx.flags.sec == 1 + assert rx.sec == True + assert rx.flags.rrs == 0 + += CAN XL RRS flag round-trip + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + s1.send(CANXL(flags=0xC2) / b'\x33\x44') + rx = s2.recv() + assert isinstance(rx, CANXL) + assert rx.flags.rrs == 1 + assert rx.ftype == True + assert rx.flags.sec == 0 + += CAN XL SEC+RRS flags together + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + s1.send(CANXL(flags=0xC3) / b'\x55') + rx = s2.recv() + assert isinstance(rx, CANXL) + assert rx.flags.sec == 1 + assert rx.flags.rrs == 1 + +############ +############ ++ Bidirectional CAN XL communication + += CAN XL send in both directions + +with TestSocket(CAN) as s1, TestSocket(CAN) as s2: + s1.pair(s2) + # s1 -> s2 + s1.send(CANXL(priority=0x001, af=0x11111111) / b'\xAA') + rx_at_s2 = s2.recv() + assert isinstance(rx_at_s2, CANXL) + assert rx_at_s2.priority == 0x001 + assert rx_at_s2.af == 0x11111111 + # s2 -> s1 + s2.send(CANXL(priority=0x002, af=0x22222222) / b'\xBB') + rx_at_s1 = s1.recv() + assert isinstance(rx_at_s1, CANXL) + assert rx_at_s1.priority == 0x002 + assert rx_at_s1.af == 0x22222222 + +############ +############ ++ Cleanup + += Close all test sockets + +cleanup_testsockets() diff --git a/test/scapy/layers/can.uts b/test/scapy/layers/can.uts index a40c0fff215..9310782f2b7 100644 --- a/test/scapy/layers/can.uts +++ b/test/scapy/layers/can.uts @@ -1541,3 +1541,365 @@ remote = CandumpReader(BytesIO(b"(1.000000) vcan0 123#R8\n")).read_packet() assert "remote_transmission_request" in remote.flags assert remote.length == 8 assert remote.data == b"" + +############ +############ + ++ CAN XL basic operations + += CAN XL constants + +from scapy.layers.can import CAN, CANFD, CANXL, CANXL_MTU, CANXL_HDR_SIZE, \ + CANXL_MAX_DLEN, CANXL_MIN_DLEN, CANXL_XLF, CANXL_FDF, CANXL_IDE, \ + CANXL_SEC, CANXL_RRS + +assert CANXL_HDR_SIZE == 12 +assert CANXL_MTU == 2060 +assert CANXL_XLF == 0x80 +assert CANXL_FDF == 0x40 +assert CANXL_IDE == 0x20 +assert CANXL_SEC == 0x01 +assert CANXL_RRS == 0x02 +assert CANXL_MIN_DLEN == 1 +assert CANXL_MAX_DLEN == 2048 + += CAN XL default field values + +pkt = CANXL() +assert pkt.priority == 0 +assert pkt.vcid == 0 +assert pkt.reserved1 == 0 +assert pkt.reserved2 == 0 +assert pkt.sdt == 0 +assert pkt.af == 0 +assert int(pkt.flags) == 0xC0 # XLF+FDF set by default (ISO 11898-1) +assert pkt.flags.xlf == 1 +assert pkt.flags.fdf == 1 +assert pkt.flags.ide == 0 +assert pkt.length == 0 + += CAN XL build with default ISO flags (XLF+FDF, IDE=0) + +pkt = CANXL() / b'\xde' +wire = raw(pkt) +assert wire[4] & CANXL_XLF # XLF must be set +assert wire[4] & CANXL_FDF # FDF must be set +assert not (wire[4] & CANXL_IDE) # IDE must be clear + += CAN XL TV1: minimal frame wire format +# priority=0x042, flags=0xC0 (XLF+FDF), sdt=0x00, af=0, payload=b'\xde' +# Expected wire bytes (BE for Priority/VCID, LE for len/af): +# prio BE : 00 00 00 42 +# flags : c0 +# sdt : 00 +# len LE : 01 00 +# af LE : 00 00 00 00 +# data : de + +pkt = CANXL(priority=0x042) / b'\xde' +wire = raw(pkt) +assert wire == bytes.fromhex('00000042' 'c0' '00' '0100' '00000000' 'de'), wire.hex() + += CAN XL TV2: all fields set + +pkt = CANXL(priority=0x123, vcid=0x45, sdt=0x07, af=0x12345678) / \ + b'\xde\xad\xbe\xef' +wire = raw(pkt) +assert wire == bytes.fromhex('00450123' 'c0' '07' '0400' '78563412' 'deadbeef'), wire.hex() + += CAN XL TV3: SEC flag set + +pkt = CANXL(priority=0x000, flags=0xC1, sdt=0x00, af=0) / b'\x11\x22' +wire = raw(pkt) +assert wire == bytes.fromhex('00000000' 'c1' '00' '0200' '00000000' '1122'), wire.hex() + += CAN XL TV4: RRS flag and max identifier + vcid + +pkt = CANXL(priority=0x7ff, vcid=0xff, flags=0xC2, sdt=0xff, + af=0xffffffff) / b'\xff' +wire = raw(pkt) +# prio = (0xff << 16) | 0x7ff -> 0x00ff07ff -> BE: 00 ff 07 ff +assert wire == bytes.fromhex('00ff07ff' 'c2' 'ff' '0100' 'ffffffff' 'ff'), wire.hex() + += CAN XL TV5: 2048-byte payload (max) + +payload = bytes(range(256)) * 8 +pkt = CANXL(priority=0x001) / payload +wire = raw(pkt) +assert len(wire) == CANXL_HDR_SIZE + 2048 +# len field LE at bytes 6-7 = 0x0800 (len=2048) +assert wire[6:8] == b'\x00\x08', wire[6:8].hex() + += CAN XL ISO flags enforced even when cleared or wrong + +pkt = CANXL(flags=0x00) / b'\xaa' +wire = raw(pkt) +assert wire[4] & CANXL_XLF # post_build must set XLF +assert wire[4] & CANXL_FDF # post_build must set FDF +assert not (wire[4] & CANXL_IDE) # post_build must clear IDE + +# Even if IDE is explicitly set, post_build clears it +pkt = CANXL(flags=0xE0) / b'\xaa' +wire = raw(pkt) +assert not (wire[4] & CANXL_IDE) # IDE forced clear + +############ +############ + ++ CAN XL dispatch_hook + += dispatch_hook returns CANXL for XL frame bytes + +# Byte 4 has bit 7 set (flags=0xC0) -> CANXL +xl_bytes = bytes.fromhex('00000042' 'c0' '00' '0100' '00000000' 'de') +assert CAN.dispatch_hook(_pkt=xl_bytes) == CANXL + += dispatch_hook still returns CAN for classic CAN + +can_bytes = bytes(16) # All zeros, length byte (byte 4) = 0 +assert CAN.dispatch_hook(_pkt=can_bytes) == CAN + += dispatch_hook still returns CANFD for FD frames + +# CANFD: byte 4 = 12 (length > 8), byte 5 = 0x04 (fd_frame flag) +canfd_bytes = b'\x00\x00\x00\x00\x0c\x04\x00\x00' + b'\x00' * 12 +assert CAN.dispatch_hook(_pkt=canfd_bytes) == CANFD + +############ +############ + ++ CAN XL dissection + += CAN XL TV1 dissect + +wire = bytes.fromhex('00000042' 'c0' '00' '0100' '00000000' 'de') +pkt = CANXL(wire) +assert pkt.priority == 0x042 +assert pkt.vcid == 0x00 +assert pkt.sdt == 0x00 +assert pkt.af == 0x00000000 +assert pkt.length == 1 +assert bytes(pkt.payload) == b'\xde' +assert pkt.flags.xlf == 1 +assert pkt.flags.fdf == 1 +assert pkt.flags.ide == 0 + += CAN XL TV2 dissect + +wire = bytes.fromhex('00450123' 'c0' '07' '0400' '78563412' 'deadbeef') +pkt = CANXL(wire) +assert pkt.priority == 0x123 +assert pkt.vcid == 0x45 +assert pkt.sdt == 0x07 +assert pkt.af == 0x12345678 +assert pkt.length == 4 +assert bytes(pkt.payload) == b'\xde\xad\xbe\xef' + += CAN XL TV3 dissect (SEC flag) + +wire = bytes.fromhex('00000000' 'c1' '00' '0200' '00000000' '1122') +pkt = CANXL(wire) +assert pkt.flags.sec == 1 +assert pkt.flags.xlf == 1 +assert pkt.flags.fdf == 1 +assert pkt.flags.rrs == 0 + += CAN XL TV4 dissect (RRS + max fields) + +wire = bytes.fromhex('00ff07ff' 'c2' 'ff' '0100' 'ffffffff' 'ff') +pkt = CANXL(wire) +assert pkt.priority == 0x7ff +assert pkt.vcid == 0xff +assert pkt.sdt == 0xff +assert pkt.af == 0xffffffff +assert pkt.flags.rrs == 1 +assert pkt.flags.fdf == 1 +assert pkt.flags.sec == 0 + +############ +############ + ++ CAN XL round-trip tests + += CAN XL round-trip: simple frame + +orig = CANXL(priority=0x1ab, vcid=0xcc, sdt=0x05, af=0xdeadbeef) / \ + b'round-trip' +rx = CANXL(raw(orig)) +assert rx.priority == orig.priority +assert rx.vcid == orig.vcid +assert rx.sdt == orig.sdt +assert rx.af == orig.af +assert bytes(rx.payload) == b'round-trip' + += CAN XL round-trip: auto-computed length from payload + +pkt = CANXL(priority=0x042) / b'\x01\x02\x03' +rx = CANXL(raw(pkt)) +assert rx.length == 3 +assert bytes(rx.payload) == b'\x01\x02\x03' + += CAN XL round-trip: 1-byte payload (minimum) + +pkt = CANXL() / b'\xaa' +rx = CANXL(raw(pkt)) +assert rx.length == 1 +assert bytes(rx.payload) == b'\xaa' + += CAN XL round-trip: 2048-byte payload (maximum) + +payload = bytes(range(256)) * 8 +pkt = CANXL() / payload +rx = CANXL(raw(pkt)) +assert rx.length == 2048 +assert bytes(rx.payload) == payload + +############ +############ + ++ CAN XL ISO 11898-1 property accessors + += CAN XL dlc property + +pkt = CANXL() / b'\x01\x02\x03' +rx = CANXL(raw(pkt)) +assert rx.dlc == 2 # length=3 -> dlc=2 + +pkt = CANXL() / b'\xaa' +rx = CANXL(raw(pkt)) +assert rx.dlc == 0 # length=1 -> dlc=0 + +payload = bytes(range(256)) * 8 +pkt = CANXL() / payload +rx = CANXL(raw(pkt)) +assert rx.dlc == 2047 # length=2048 -> dlc=2047 + += CAN XL xlf property + +pkt = CANXL() / b'\x01' +assert pkt.xlf == True + += CAN XL fdf property + +pkt = CANXL() / b'\x01' +assert pkt.fdf == True + += CAN XL ide property + +pkt = CANXL() / b'\x01' +assert pkt.ide == False + +# Even when explicitly set at field level, ide reads from flags +pkt = CANXL(flags=0xE0) / b'\x01' +assert pkt.ide == True + += CAN XL sec property + +pkt = CANXL(flags=0xC1) / b'\x01' +assert pkt.sec == True +pkt = CANXL(flags=0xC0) / b'\x01' +assert pkt.sec == False + += CAN XL ftype property + +pkt = CANXL(flags=0xC2) / b'\x01' +assert pkt.ftype == True +pkt = CANXL(flags=0xC0) / b'\x01' +assert pkt.ftype == False + += CAN XL frame_format property + +# Default CAN XL: XLF+FDF = 0b110 = 6 +pkt = CANXL() / b'\x01' +assert pkt.frame_format == 6 # XLF+FDF: 0b110 + +# Explicit flags (pre-enforcement, at field level) +pkt = CANXL(flags=0xC0) / b'\x01' +assert pkt.frame_format == 6 # XLF+FDF: 0b110 + +pkt = CANXL(flags=0xE0) / b'\x01' +assert pkt.frame_format == 7 # XLF+FDF+IDE: 0b111 + +############ +############ + ++ CAN XL show_iso() + += CAN XL show ISO style contains expected field names + +pkt = CANXL(priority=0x123, vcid=0x45, sdt=0x07, + af=0x12345678) / b'\xde\xad\xbe\xef' +output = pkt.show_iso(dump=True) +assert "Priority" in output +assert "VCID" in output +assert "Format" in output +assert "SEC" in output +assert "FTYPE" in output +assert "SDT" in output +assert "DLC" in output +assert "AF" in output +assert "Data" in output +assert "ISO 11898-1" in output + += CAN XL show default style works + +pkt = CANXL(priority=0x42) / b'\x01' +output = pkt.show(dump=True) +assert "CAN XL" in output +assert "priority" in output +assert "flags" in output + +############ +############ + ++ CAN XL swap-bytes and pcap interactions + += CAN XL swap-bytes configuration + +conf.contribs['CAN']['swap-bytes'] = True +pkt = CANXL(priority=0x123, vcid=0x45, sdt=0x07, af=0x12345678) / b'\xde\xad\xbe\xef' +wire = raw(pkt) +# When swap-bytes=True, priority word is swapped to SocketCAN LE format +assert wire[:4] == bytes.fromhex('23014500') +assert wire[4:] == bytes.fromhex('c007040078563412deadbeef') +# Dissect back with swap-bytes=True +rx = CANXL(wire) +assert rx.priority == 0x123 +assert rx.vcid == 0x45 +assert rx.sdt == 0x07 +assert rx.af == 0x12345678 +assert bytes(rx.payload) == b'\xde\xad\xbe\xef' + +# Dispatch hook with swap-bytes=True +rx_can = CAN(wire) +assert isinstance(rx_can, CANXL) +assert rx_can.priority == 0x123 +assert rx_can.vcid == 0x45 + +conf.contribs['CAN']['swap-bytes'] = False + += CAN XL pcap write and read + +from scapy.utils import get_temp_file +tmp_pcap = get_temp_file(autoext=".pcap") +orig = CANXL(priority=0x123, vcid=0x45, sdt=0x07, af=0x12345678) / b'\xde\xad\xbe\xef' +wrpcap(tmp_pcap, [orig]) +pkts = rdpcap(tmp_pcap) +assert len(pkts) == 1 +assert isinstance(pkts[0], CANXL) +assert pkts[0].priority == 0x123 +assert pkts[0].vcid == 0x45 +assert pkts[0].sdt == 0x07 +assert pkts[0].af == 0x12345678 +assert bytes(pkts[0].payload) == b'\xde\xad\xbe\xef' + += CAN XL CookedLinux binding + +orig = CookedLinux(proto=14) / CANXL(priority=0x123, vcid=0x45) / b'\x01\x02' +wire = raw(orig) +rx = CookedLinux(wire) +assert CANXL in rx +assert rx[CANXL].priority == 0x123 +assert rx[CANXL].vcid == 0x45 +assert bytes(rx[CANXL].payload) == b'\x01\x02' +