-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
[Draft] Feature: CAN XL Support #5152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d27cef8
e821dee
59f946e
5411954
92be207
6bb512f
87ae595
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,9 @@ | |
| # This file is part of Scapy | ||
| # See https://scapy.net/ for more information | ||
| # Copyright (C) Nils Weiss <nils@we155.de> | ||
| # | ||
| # 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 = "<I%ds" % (len(pkt) - 4) | ||
| unpack_fmt = ">I%ds" % (len(pkt) - 4) | ||
| pkt = struct.pack(pack_fmt, *struct.unpack(unpack_fmt, pkt)) | ||
|
|
@@ -166,15 +218,21 @@ def send(self, x): | |
| except AttributeError: | ||
| pass | ||
|
|
||
| # need to change the byte order of the first four bytes, | ||
| # required by the underlying Linux SocketCAN frame format | ||
| bs = raw(x) | ||
| if not conf.contribs['CAN']['swap-bytes']: | ||
| pack_fmt = "<I%ds" % (len(bs) - 4) | ||
| unpack_fmt = ">I%ds" % (len(bs) - 4) | ||
| bs = struct.pack(pack_fmt, *struct.unpack(unpack_fmt, bs)) | ||
|
|
||
| bs = bs + b"\x00" * (self.MTU - len(bs)) | ||
| if isinstance(x, CANXL): | ||
| # CANXL.post_build already produces little endian wire bytes. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we needed this byte-swap, since Wireshark (pcap) has a different byte order than the linux kernel. Did you checked if this also applies for CANXL?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so the byte-order inside a pcap file differs from what we get from the linux kernel. If we read a pcap file with scapy we need to swap bytes compared to receiving a CAN-Frame from the socket. |
||
| # No MTU padding - kernel expects exact HDR_SIZE + len. | ||
| pass | ||
| else: | ||
| # CAN/CANFD: swap first 4 bytes (CAN ID) big endian to litte endian | ||
| if not conf.contribs['CAN']['swap-bytes']: | ||
| pack_fmt = "<I%ds" % (len(bs) - 4) | ||
| unpack_fmt = ">I%ds" % (len(bs) - 4) | ||
| bs = struct.pack(pack_fmt, *struct.unpack(unpack_fmt, bs)) | ||
| # CAN/CANFD: pad to correct MTU per frame type | ||
| mtu = CAN_FD_MTU if isinstance(x, CANFD) else CAN_MTU | ||
| bs = bs + b"\x00" * (mtu - len(bs)) | ||
|
|
||
| return super(NativeCANSocket, self).send(bs) # type: ignore | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
create a helper function for the byteswap, so that we don't have duplicated code here.