Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions doc/scapy/layers/canxl.rst
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.
80 changes: 69 additions & 11 deletions scapy/contrib/cansocket_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
Expand Down Expand Up @@ -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]
):
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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)"""
Expand All @@ -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 \

Copy link
Copy Markdown
Contributor

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.

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))
Expand All @@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Expand Down
10 changes: 8 additions & 2 deletions scapy/contrib/cansocket_python_can.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = python-can CANSocket
# scapy.contrib.status = loads
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading