diff --git a/doc/scapy/layers/canxl.rst b/doc/scapy/layers/canxl.rst new file mode 100644 index 00000000000..43c4d4255ca --- /dev/null +++ b/doc/scapy/layers/canxl.rst @@ -0,0 +1,175 @@ +.. + 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(style="11898-1") + + # 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(style="11898-1") + + 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(style="11898-1")`` 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 +==================== + +CAN XL uses a multi-region byte swap: the Priority word (4 bytes), +Length (2 bytes), and Acceptance Field (4 bytes) are in little-endian +order on the Linux socket but stored as big-endian inside Scapy. +The swap happens automatically in ``pre_dissect`` (receive) and +``post_build`` (send). + +Unlike classic CAN and CAN FD, CAN XL **ignores** the +``conf.contribs['CAN']['swap-bytes']`` setting -- the swap always happens +because CAN XL frames only come from PF_CAN sockets which are always LE. +You do *not* need to touch this config for CAN XL. + + +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..20521e22c6c 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, @@ -128,6 +173,11 @@ def __init__(self, self.ins.bind((self.channel,)) self.outs = self.ins + @staticmethod + def _is_canxl(pkt): + # type: (bytes) -> bool + return CANXL.is_canxl_frame(pkt) + def recv_raw(self, x=CAN_MTU): # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 """Returns a tuple containing (cls, pkt_data, time)""" @@ -143,9 +193,11 @@ 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: + # CAN XL frames handle their own byte swapping in + # CANXL.pre_dissect - skip the first-4-byte swap here. + # CAN/CANFD still need the first-4-byte swap. + if not conf.contribs['CAN']['swap-bytes'] and pkt \ + and not self._is_canxl(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..863faa09a69 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, XIntField, ScalingField, \ + ConditionalField, LenField, 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: @@ -213,6 +230,263 @@ def post_build(self, pkt, pay): bind_layers(CookedLinux, CANFD, proto=13) +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(style="11898-1")`` 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(style="11898-1") + """ + 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, LE on socket, swapped to BE by pre_dissect) + 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 + # flags byte (1 byte, no swap needed) + # 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']), + # sdt (1 byte, no swap needed) + XByteField('sdt', 0), + # length (2 bytes, LE on socket, swapped to BE by pre_dissect) + # Auto-computed from 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 ShortField; post_build warns + # if the computed length falls outside the valid range. + ShortField('length', 0), + # af (4 bytes, LE on socket, swapped to BE by pre_dissect) + XIntField('af', 0), + # NO data field — payload carried as sub-layers + ] + + # -- Byte-order conversion ----------------------------------------------- + # CAN XL needs 3 regions swapped between LE (socket) and BE (scapy): + # bytes 0-3 (prio), bytes 6-7 (length), bytes 8-11 (af) + # This is independent of conf.contribs['CAN']['swap-bytes'] - CANXL + # always performs its own full swap. + + @staticmethod + def inv_endianness(pkt): + # type: (bytes) -> bytes + """Swap the three LE multi-byte fields in a CAN XL header.""" + if len(pkt) < CANXL_HDR_SIZE: + return pkt + b = bytearray(pkt) + b[0:4] = b[0:4][::-1] # prio + b[6:8] = b[6:8][::-1] # length + b[8:12] = b[8:12][::-1] # af + return bytes(b) + + def pre_dissect(self, s): + # type: (bytes) -> bytes + return CANXL.inv_endianness(s) + + def post_dissect(self, s): + # type: (bytes) -> bytes + # Clear the raw byte cache so that self_build() always goes + # through do_build() -> post_build(), which applies the + # BE -> LE byte-order swap via inv_endianness(). Without + # this, self_build() would return the cached LE wire bytes + # directly and skip post_build, producing incorrect output. + self.raw_packet_cache = None + return s + + 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('>H', length) + pkt[8:] + # ISO 11898-1:2024: enforce XLF=1, FDF=1, IDE=0 + if pkt[4] & CANXL_IDE: + log_runtime.warning( + "CAN XL frame has IDE set; clearing it " + "(IDE is always 0 for CAN XL per ISO 11898-1)") + flags = (pkt[4] | CANXL_XLF | CANXL_FDF) & ~CANXL_IDE + pkt = pkt[:4] + bytes([flags]) + pkt[5:] + return CANXL.inv_endianness(pkt) + pay + + def extract_padding(self, p): + # type: (bytes) -> 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 + + def guess_payload_class(self, payload): + # type: (bytes) -> Type[Packet] + # Override the default to unconditionally return raw_layer, + # bypassing any bind_layers() registrations. CAN XL payload + # dispatch should be based on SDT or on add-on service flags + # (e.g. SEC); contrib modules implementing an add-on service + # may monkey-patch this method to add their own dispatch logic. + return conf.raw_layer + + # -- 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 + + # -- show(style="11898-1") ----------------------------------------------- + + @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(self, dump=False, indent=3, lvl="", label_lvl="", + style=None): + # type: (bool, int, str, str, Optional[str]) -> Optional[Any] + # Return type is Optional[Any] because show() returns None when + # printing to stdout (dump=False) and str when dump=True. + # Using Any avoids mypy complaints across subclass overrides. + """Show packet fields. + + :param style: If ``"11898-1"``, render using ISO 11898-1:2024 + field names (Priority, VCID, Format, SEC, FTYPE, + SDT, DLC, AF, Data). + """ + if style == "11898-1": + return self._show_iso(dump, indent, lvl, label_lvl) + return super(CANXL, self).show( + dump=dump, indent=indent, lvl=lvl, label_lvl=label_lvl) + + def _show_iso(self, dump=False, indent=3, lvl="", label_lvl=""): + # type: (bool, int, str, str) -> Optional[str] + """Render using ISO 11898-1:2024 field names.""" + if dump: + from scapy.themes import ColorTheme, AnsiColorTheme + ct = AnsiColorTheme() + else: + ct = conf.color_theme + + fmt_val = self.frame_format + fmt_names = [] + if fmt_val & 0x04: + fmt_names.append("XLF") + if fmt_val & 0x02: + fmt_names.append("FDF") + if fmt_val & 0x01: + fmt_names.append("IDE") + fmt_str = "+".join(fmt_names) if fmt_names else "0" + + s = "%s%s %s %s\n" % ( + label_lvl, + ct.punct("###["), + ct.layer_name("CAN XL (ISO 11898-1)"), + ct.punct("]###")) + + # Field order follows ISO 11898-1:2024 Table 4 + fields = [ + ("Priority", "0x%x" % self.priority), + ("Format", "%s (0x%x)" % (fmt_str, fmt_val)), + ("FTYPE", "%d" % int(self.ftype)), + ("SDT", "0x%x" % self.sdt), + ("SEC", "%d" % int(self.sec)), + ("DLC", "%d" % self.dlc), + ("VCID", "0x%x" % self.vcid), + ("AF", "0x%08x" % self.af), + ("Data", "%r" % bytes(self.payload)), + ] + + for name, val in fields: + pad = max(0, 10 - len(name)) * " " + s += "%s %s%s%s %s\n" % ( + label_lvl + lvl, + ct.field_name(name), + pad, + ct.punct("="), + ct.field_value(val)) + + if not dump: + print(s) + return None + return s + + class SignalField(ScalingField): """SignalField is a base class for signal data, usually transmitted from CAN messages in automotive applications. Most vehicle manufacturers diff --git a/test/contrib/canxlsocket_testsocket.uts b/test/contrib/canxlsocket_testsocket.uts new file mode 100644 index 00000000000..bd8a0ae7a68 --- /dev/null +++ b/test/contrib/canxlsocket_testsocket.uts @@ -0,0 +1,250 @@ +% 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..9e0df40b8d8 100644 --- a/test/scapy/layers/can.uts +++ b/test/scapy/layers/can.uts @@ -1541,3 +1541,310 @@ 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 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 (LE): +# prio LE : 42 00 00 00 +# 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('42000000' '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('23014500' '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 = 0x00ff07ff -> LE: ff 07 ff 00 +assert wire == bytes.fromhex('ff07ff00' '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('42000000' '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('42000000' '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('23014500' '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('ff07ff00' '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(style="11898-1") + += 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(dump=True, style="11898-1") +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