From e51c4fbd578ed5aa806b7da38e201ca2a92a8de5 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Thu, 5 Mar 2026 12:49:21 +0100 Subject: [PATCH 01/19] Add CAN XL frame support Introduce the CANXL class in scapy.layers.can, following the Linux struct canxl_frame wire format and inheriting from the CAN class. Provides additional ISO 11898-1:2024 property accessors and show(style='11898-1') for ISO field-name rendering. AI-Assisted: yes (Claude Opus and Sonnet) --- scapy/layers/can.py | 268 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 264 insertions(+), 4 deletions(-) diff --git a/scapy/layers/can.py b/scapy/layers/can.py index c7d517f9898..af4180d91ea 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -16,12 +16,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 +45,8 @@ "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"] # CONSTANTS CAN_MAX_IDENTIFIER = (1 << 29) - 1 # Maximum 29-bit identifier @@ -53,6 +55,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 +122,11 @@ def dispatch_hook(cls, **kargs # type: Any ): # type: (...) -> Type[Packet] if _pkt: + # 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. + if len(_pkt) > 4 and _pkt[4] & 0x80: + return CANXL fdf_set = len(_pkt) > 5 and _pkt[5] & 0x04 and \ not _pkt[5] & 0xf8 if fdf_set: @@ -213,6 +229,250 @@ 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" + 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 SEC+AOT; the CANsec + # contrib monkey-patches this method to add SEC-based dispatch. + 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 From ba6d26bd81f33decd07ada1791f034d2396bffd0 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Thu, 5 Mar 2026 12:51:54 +0100 Subject: [PATCH 02/19] Add CAN XL unit tests AI-Assisted: yes (Claude Opus and Sonnet) --- test/scapy/layers/can.uts | 307 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) 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 From 142943ea3e7017e2d29fe8c99a0da599fb5ec22c Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Thu, 5 Mar 2026 20:55:06 +0100 Subject: [PATCH 03/19] Add CAN XL support to NativeCANSocket AI-Assisted: yes (Claude Opus and Sonnet) --- scapy/contrib/cansocket_native.py | 74 ++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/scapy/contrib/cansocket_native.py b/scapy/contrib/cansocket_native.py index 49efacd457c..282f6c0d9a6 100644 --- a/scapy/contrib/cansocket_native.py +++ b/scapy/contrib/cansocket_native.py @@ -19,7 +19,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, CANXL, CAN_MTU, CAN_FD_MTU, CANXL_MTU from scapy.compat import raw from typing import ( @@ -51,11 +51,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] ): @@ -69,6 +79,7 @@ def __init__(self, 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 +120,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 +167,12 @@ def __init__(self, self.ins.bind((self.channel,)) self.outs = self.ins + @staticmethod + def _is_canxl(pkt): + # type: (bytes) -> bool + """Detect CAN XL frame by XLF flag (bit 7 of byte 4).""" + return len(pkt) > 4 and bool(pkt[4] & 0x80) + 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 +188,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 = " Date: Fri, 6 Mar 2026 07:28:15 +0100 Subject: [PATCH 04/19] test: add CAN XL TestSocket regression tests Tests pass on Linux. On Windows, tests appear to timeout -- needs further investigation. AI-Assisted: yes (Claude Opus and Sonnet) --- test/contrib/canxlsocket_testsocket.uts | 250 ++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 test/contrib/canxlsocket_testsocket.uts diff --git a/test/contrib/canxlsocket_testsocket.uts b/test/contrib/canxlsocket_testsocket.uts new file mode 100644 index 00000000000..923a3ab9b46 --- /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, +% exercising the full build -> wire -> dispatch_hook -> dissect chain. +% No platform restrictions: runs on Windows, Linux, macOS without root. + +############ +############ ++ 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() From ffb7293d5ebe865779924e27d4cfb4dade810f06 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Fri, 6 Mar 2026 07:30:04 +0100 Subject: [PATCH 05/19] doc: add CAN XL layer documentation Covers quick start, field naming (Linux vs ISO), byte-order handling, NativeCANSocket usage, can-utils interop, and known limitations. AI-Assisted: yes (Claude Opus and Sonnet) --- doc/scapy/layers/canxl.rst | 172 +++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 doc/scapy/layers/canxl.rst diff --git a/doc/scapy/layers/canxl.rst b/doc/scapy/layers/canxl.rst new file mode 100644 index 00000000000..77bf9c8bedd --- /dev/null +++ b/doc/scapy/layers/canxl.rst @@ -0,0 +1,172 @@ +.. note:: This document is under a `Creative Commons Attribution - Non-Commercial - Share Alike 2.5 `_ license. + +##### +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. From 122114df0e1fae8577d0ae9da8868088ac7b5d5c Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Mon, 7 Sep 2026 14:34:39 +0200 Subject: [PATCH 06/19] Add copyright notes AI-Assisted: yes (Claude Opus and Sonnet) --- doc/scapy/layers/canxl.rst | 9 +++++--- scapy/contrib/cansocket_native.py | 16 +++++++++----- scapy/contrib/cansocket_python_can.py | 10 +++++++-- scapy/layers/can.py | 28 ++++++++++++++++++------- test/contrib/canxlsocket_testsocket.uts | 4 ++-- 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/doc/scapy/layers/canxl.rst b/doc/scapy/layers/canxl.rst index 77bf9c8bedd..43c4d4255ca 100644 --- a/doc/scapy/layers/canxl.rst +++ b/doc/scapy/layers/canxl.rst @@ -1,8 +1,11 @@ -.. note:: This document is under a `Creative Commons Attribution - Non-Commercial - Share Alike 2.5 `_ license. +.. + 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 diff --git a/scapy/contrib/cansocket_native.py b/scapy/contrib/cansocket_native.py index 282f6c0d9a6..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, CANXL, CAN_MTU, CAN_FD_MTU, CANXL_MTU +from scapy.layers.can import CAN, CANFD, CANXL, CAN_MTU, CAN_FD_MTU, CANXL_MTU from scapy.compat import raw from typing import ( @@ -77,6 +80,9 @@ 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 @@ -170,8 +176,7 @@ def __init__(self, @staticmethod def _is_canxl(pkt): # type: (bytes) -> bool - """Detect CAN XL frame by XLF flag (bit 7 of byte 4).""" - return len(pkt) > 4 and bool(pkt[4] & 0x80) + 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 @@ -225,8 +230,9 @@ def send(self, x): 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 af4180d91ea..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 @@ -46,7 +49,8 @@ "CandumpReader", "SignalHeader", "CAN_MTU", "CAN_MAX_IDENTIFIER", "CAN_MAX_DLEN", "CAN_INV_FILTER", "CANFD", "CAN_FD_MTU", "CAN_FD_MAX_DLEN", "CANXL", "CANXL_MTU", "CANXL_MAX_DLEN", - "CANXL_MIN_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 @@ -122,10 +126,7 @@ def dispatch_hook(cls, **kargs # type: Any ): # type: (...) -> Type[Packet] if _pkt: - # 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. - if len(_pkt) > 4 and _pkt[4] & 0x80: + if CANXL.is_canxl_frame(_pkt): return CANXL fdf_set = len(_pkt) > 5 and _pkt[5] & 0x04 and \ not _pkt[5] & 0xf8 @@ -246,6 +247,18 @@ class CANXL(CAN): >>> 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 @@ -339,8 +352,9 @@ 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 SEC+AOT; the CANsec - # contrib monkey-patches this method to add SEC-based dispatch. + # 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 --------------------------------- diff --git a/test/contrib/canxlsocket_testsocket.uts b/test/contrib/canxlsocket_testsocket.uts index 923a3ab9b46..bd8a0ae7a68 100644 --- a/test/contrib/canxlsocket_testsocket.uts +++ b/test/contrib/canxlsocket_testsocket.uts @@ -1,7 +1,7 @@ % Regression tests for CAN XL via TestSocket % Tests CAN XL frame send/recv through scapy's in-memory TestSocket, -% exercising the full build -> wire -> dispatch_hook -> dissect chain. -% No platform restrictions: runs on Windows, Linux, macOS without root. +% Created by Friedrich Wiemer +# Copyright (C) 2026, Robert Bosch GmbH ############ ############ From a4cfea8c271e5d5e441b8372ea865ba51abdec69 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 09:10:23 +0200 Subject: [PATCH 07/19] fix CI findings AI-Assisted: no --- doc/scapy/layers/canxl.rst | 54 ++++++++++++------------- scapy/contrib/cansocket_native.py | 4 +- scapy/contrib/cansocket_python_can.py | 2 +- scapy/layers/can.py | 4 +- test/contrib/canxlsocket_testsocket.uts | 3 -- 5 files changed, 32 insertions(+), 35 deletions(-) diff --git a/doc/scapy/layers/canxl.rst b/doc/scapy/layers/canxl.rst index 43c4d4255ca..f2c546dcd51 100644 --- a/doc/scapy/layers/canxl.rst +++ b/doc/scapy/layers/canxl.rst @@ -93,33 +93,33 @@ CAN XL field names differ between the Linux kernel's ``struct canxl_frame`` 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`` | -+--------------+--------------------+--------------------+ ++-------------+---------------------+----------------------+ +| 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 diff --git a/scapy/contrib/cansocket_native.py b/scapy/contrib/cansocket_native.py index 20521e22c6c..71c8b1303fe 100644 --- a/scapy/contrib/cansocket_native.py +++ b/scapy/contrib/cansocket_native.py @@ -2,7 +2,7 @@ # 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 @@ -225,7 +225,7 @@ def send(self, x): # No MTU padding - kernel expects exact HDR_SIZE + len. pass else: - # CAN/CANFD: swap first 4 bytes (CAN ID) big endian to litte endian + # CAN/CANFD: swap first 4 bytes (CAN ID) big endian to little endian if not conf.contribs['CAN']['swap-bytes']: pack_fmt = " -# +# # The CAN XL parts are created by Friedrich Wiemer # Copyright (C) 2026, Robert Bosch GmbH diff --git a/scapy/layers/can.py b/scapy/layers/can.py index 863faa09a69..ade559445a6 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -2,7 +2,7 @@ # 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 @@ -439,7 +439,7 @@ def _show_iso(self, dump=False, indent=3, lvl="", label_lvl=""): """Render using ISO 11898-1:2024 field names.""" if dump: from scapy.themes import ColorTheme, AnsiColorTheme - ct = AnsiColorTheme() + ct: ColorTheme = AnsiColorTheme() # No color for dump output else: ct = conf.color_theme diff --git a/test/contrib/canxlsocket_testsocket.uts b/test/contrib/canxlsocket_testsocket.uts index bd8a0ae7a68..c841a884a67 100644 --- a/test/contrib/canxlsocket_testsocket.uts +++ b/test/contrib/canxlsocket_testsocket.uts @@ -127,16 +127,13 @@ with TestSocket(CAN) as s1, TestSocket(CAN) as s2: 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 From 54b84788654db20c220ba6ab43885ab22c945b05 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 09:56:56 +0200 Subject: [PATCH 08/19] can: use little endian fields for CAN XL Declare the CAN XL header as little endian via BitField's tot_size / end_tot_size, LEShortField and XLEIntField, instead of swapping the prio, length and af regions by hand. Removes CANXL.inv_endianness() and the swap in pre_dissect() and post_build(). AI-Assisted: yes (Claude Opus and Sonnet) --- scapy/layers/can.py | 61 +++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 41 deletions(-) diff --git a/scapy/layers/can.py b/scapy/layers/can.py index ade559445a6..3f91ab8dd33 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -20,8 +20,8 @@ from scapy.compat import chb, hex_bytes from scapy.data import DLT_CAN_SOCKETCAN from scapy.fields import BitField, FieldLenField, FlagsField, StrLenField, \ - ThreeBytesField, XBitField, XByteField, XIntField, ScalingField, \ - ConditionalField, LenField, ShortField + 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 @@ -260,59 +260,38 @@ def is_canxl_frame(pkt): 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) + # prio word: 4 bytes, little endian (struct canxl_frame.prio) + BitField('reserved2', 0, 8, tot_size=-4), # bits 31-24 + XBitField('vcid', 0, 8), # bits 23-16 + BitField('reserved1', 0, 5), # bits 15-11 + XBitField('priority', 0, 11, end_tot_size=-4), # 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']), - # 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. + # 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 ShortField; post_build warns + # For kernel compatibility we use a 16-bit field; 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 + LEShortField('length', 0), + XLEIntField('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) + # The fields above already describe the SocketCAN wire layout, so + # no swap is needed here. CAN.pre_dissect's swap-bytes handling + # covers the 4-byte CAN ID only and would corrupt an XL header; + # the pcap byte order for CAN XL is still to be determined. + return 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. + # through do_build() -> post_build(), which recomputes the length. self.raw_packet_cache = None return s @@ -328,7 +307,7 @@ def post_build(self, pkt, pay): 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:] + pkt = pkt[:6] + struct.pack(' Tuple[bytes, Optional[bytes]] From 319004a030afca19948c8a5dcbecfc94748c959e Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 10:00:50 +0200 Subject: [PATCH 09/19] can: drop unnecessary CANXL overrides post_dissect duplicated CAN.post_dissect, and the default guess_payload_class already returns conf.raw_layer when no payload class is bound. AI-Assisted: yes (Claude Opus and Sonnet) --- scapy/layers/can.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/scapy/layers/can.py b/scapy/layers/can.py index 3f91ab8dd33..95cbed36419 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -288,13 +288,6 @@ def pre_dissect(self, s): # the pcap byte order for CAN XL is still to be determined. return 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 recomputes the length. - self.raw_packet_cache = None - return s - def post_build(self, pkt, pay): # type: (bytes, bytes) -> bytes # Auto-compute length from payload @@ -327,15 +320,6 @@ def extract_padding(self, p): # 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 From 8a2d544bfb9db0d8ed57344dac7ecf609981bc7c Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 10:03:59 +0200 Subject: [PATCH 10/19] can: replace CANXL show(style=...) with show_iso() Keep Packet.show()'s signature unchanged and expose the ISO 11898-1 rendering as its own method. AI-Assisted: yes (Claude Opus and Sonnet) --- doc/scapy/layers/canxl.rst | 6 +++--- scapy/layers/can.py | 31 ++++++++----------------------- test/scapy/layers/can.uts | 4 ++-- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/doc/scapy/layers/canxl.rst b/doc/scapy/layers/canxl.rst index f2c546dcd51..c804fdf2953 100644 --- a/doc/scapy/layers/canxl.rst +++ b/doc/scapy/layers/canxl.rst @@ -44,7 +44,7 @@ Building and inspecting frames pkt.show() # ISO 11898-1 field names (Priority, Format, FTYPE, SDT, SEC, DLC, etc.) - pkt.show(style="11898-1") + pkt.show_iso() # Access payload data (same API as classic CAN) pkt.data # b'\x01\x02\x03' @@ -74,7 +74,7 @@ Sending and receiving over a socket # Receive a frame (in another terminal or Scapy session) pkt = sock.recv() pkt.show() - pkt.show(style="11898-1") + pkt.show_iso() sock.close() @@ -90,7 +90,7 @@ 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 +Use ``pkt.show_iso()`` to see ISO names, or access via properties: +-------------+---------------------+----------------------+ diff --git a/scapy/layers/can.py b/scapy/layers/can.py index 95cbed36419..f78184bcf35 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -236,15 +236,14 @@ class CANXL(CAN): 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. + ``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(style="11898-1") + >>> pkt.show_iso() """ name = "CAN XL" @@ -364,7 +363,7 @@ def frame_format(self): """ISO 11898-1:2024 3-bit format field (XLF:FDF:IDE), bits 7-5.""" return (int(self.flags) >> 5) & 0x07 - # -- show(style="11898-1") ----------------------------------------------- + # -- ISO 11898-1:2024 rendering ------------------------------------------ @property def data(self): @@ -380,26 +379,12 @@ def data(self): """ 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). + def show_iso(self, dump=False, lvl="", label_lvl=""): + # type: (bool, str, str) -> Optional[str] + """Render the frame using ISO 11898-1:2024 field names. + + :param dump: return the string instead of printing it """ - 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: ColorTheme = AnsiColorTheme() # No color for dump output diff --git a/test/scapy/layers/can.uts b/test/scapy/layers/can.uts index 9e0df40b8d8..eef1ab25696 100644 --- a/test/scapy/layers/can.uts +++ b/test/scapy/layers/can.uts @@ -1823,13 +1823,13 @@ assert pkt.frame_format == 7 # XLF+FDF+IDE: 0b111 ############ ############ -+ CAN XL show(style="11898-1") ++ 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(dump=True, style="11898-1") +output = pkt.show_iso(dump=True) assert "Priority" in output assert "VCID" in output assert "Format" in output From f69adcb7492a5c8e30541660db58657b8d252223 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 10:07:08 +0200 Subject: [PATCH 11/19] can: render CANXL ISO field names with sprintf Use Packet.sprintf() for the fields it can resolve instead of formatting each one against the color theme by hand. Format, FTYPE, SEC and DLC are properties rather than fields, so they stay resolved in Python. AI-Assisted: yes (Claude Opus and Sonnet) --- scapy/layers/can.py | 77 +++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 49 deletions(-) diff --git a/scapy/layers/can.py b/scapy/layers/can.py index f78184bcf35..54fb2b0a290 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -379,60 +379,39 @@ def data(self): """ return bytes(self.payload) - def show_iso(self, dump=False, lvl="", label_lvl=""): - # type: (bool, str, str) -> Optional[str] + 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 """ - if dump: - from scapy.themes import ColorTheme, AnsiColorTheme - ct: ColorTheme = AnsiColorTheme() # No color for dump output - 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 + 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 class SignalField(ScalingField): From 2c391a911d5fbe26d38260cf422fbb64df0cc095 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 10:10:43 +0200 Subject: [PATCH 12/19] cansocket_native: reuse CAN.inv_endianness for the CAN ID swap Both recv_raw() and send() open-coded the same four-byte swap that CAN.inv_endianness() already performs. Also drops _is_canxl(), a single-use wrapper around CANXL.is_canxl_frame(). AI-Assisted: yes (Claude Opus and Sonnet) --- scapy/contrib/cansocket_native.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/scapy/contrib/cansocket_native.py b/scapy/contrib/cansocket_native.py index 71c8b1303fe..7e4b1f87a52 100644 --- a/scapy/contrib/cansocket_native.py +++ b/scapy/contrib/cansocket_native.py @@ -173,11 +173,6 @@ 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)""" @@ -193,14 +188,11 @@ def recv_raw(self, x=CAN_MTU): # something bad happened (e.g. the interface went down) warning("Captured no data.") - # 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. + # CAN XL describes its little endian layout in its fields_desc, + # so it needs no swap here. CAN/CANFD still need the CAN ID swap. if not conf.contribs['CAN']['swap-bytes'] and pkt \ - and not self._is_canxl(pkt): - pack_fmt = " Date: Thu, 24 Sep 2026 23:31:38 +0200 Subject: [PATCH 13/19] fix CAN XL pcap support AI-Assisted: yes (Gemini 3.8 Flash) --- doc/scapy/layers/canxl.rst | 23 +++++---- scapy/contrib/cansocket_native.py | 14 ++---- scapy/layers/can.py | 22 ++++----- test/scapy/layers/can.uts | 77 ++++++++++++++++++++++++++----- 4 files changed, 95 insertions(+), 41 deletions(-) diff --git a/doc/scapy/layers/canxl.rst b/doc/scapy/layers/canxl.rst index c804fdf2953..e42218fdfd9 100644 --- a/doc/scapy/layers/canxl.rst +++ b/doc/scapy/layers/canxl.rst @@ -125,16 +125,19 @@ properties: 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. +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 diff --git a/scapy/contrib/cansocket_native.py b/scapy/contrib/cansocket_native.py index 7e4b1f87a52..43de45fcdab 100644 --- a/scapy/contrib/cansocket_native.py +++ b/scapy/contrib/cansocket_native.py @@ -188,10 +188,7 @@ def recv_raw(self, x=CAN_MTU): # something bad happened (e.g. the interface went down) warning("Captured no data.") - # CAN XL describes its little endian layout in its fields_desc, - # so it needs no swap here. CAN/CANFD still need the CAN ID swap. - if not conf.contribs['CAN']['swap-bytes'] and pkt \ - and not CANXL.is_canxl_frame(pkt): + if not conf.contribs['CAN']['swap-bytes'] and pkt: pkt = CAN.inv_endianness(pkt) if pkt and ts is None: @@ -212,12 +209,11 @@ def send(self, x): bs = raw(x) - # CAN XL builds its little endian wire bytes from fields_desc, and - # the kernel expects exactly HDR_SIZE + len bytes, so no padding. + if not conf.contribs['CAN']['swap-bytes']: + bs = CAN.inv_endianness(bs) + + # CAN XL: the kernel expects exactly HDR_SIZE + len bytes, so no padding. if not isinstance(x, CANXL): - # CAN/CANFD: swap first 4 bytes (CAN ID) big endian to little endian - if not conf.contribs['CAN']['swap-bytes']: - bs = CAN.inv_endianness(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)) diff --git a/scapy/layers/can.py b/scapy/layers/can.py index 54fb2b0a290..878493c3757 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -228,6 +228,7 @@ def post_build(self, pkt, pay): bind_layers(CookedLinux, CANFD, proto=13) +conf.l2types.register_layer2num(DLT_CAN_SOCKETCAN, CANFD) class CANXL(CAN): @@ -259,11 +260,12 @@ def is_canxl_frame(pkt): return len(pkt) > 4 and bool(pkt[4] & 0x80) fields_desc = [ - # prio word: 4 bytes, little endian (struct canxl_frame.prio) - BitField('reserved2', 0, 8, tot_size=-4), # bits 31-24 + # 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, end_tot_size=-4), # bits 10-0 + 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', @@ -279,14 +281,6 @@ def is_canxl_frame(pkt): # NO data field - payload carried as sub-layers ] - def pre_dissect(self, s): - # type: (bytes) -> bytes - # The fields above already describe the SocketCAN wire layout, so - # no swap is needed here. CAN.pre_dissect's swap-bytes handling - # covers the 4-byte CAN ID only and would corrupt an XL header; - # the pcap byte order for CAN XL is still to be determined. - return s - def post_build(self, pkt, pay): # type: (bytes, bytes) -> bytes # Auto-compute length from payload @@ -307,6 +301,8 @@ def post_build(self, pkt, pay): "(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:] + if conf.contribs['CAN']['swap-bytes']: + pkt = CAN.inv_endianness(pkt) return pkt + pay def extract_padding(self, p): @@ -414,6 +410,10 @@ def show_iso(self, dump=False): return None +bind_layers(CookedLinux, CANXL, proto=14) +conf.l2types.register_layer2num(DLT_CAN_SOCKETCAN, CANXL) + + 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/scapy/layers/can.uts b/test/scapy/layers/can.uts index eef1ab25696..9310782f2b7 100644 --- a/test/scapy/layers/can.uts +++ b/test/scapy/layers/can.uts @@ -1549,7 +1549,7 @@ assert remote.data == b"" = CAN XL constants -from scapy.layers.can import CANXL, CANXL_MTU, CANXL_HDR_SIZE, \ +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 @@ -1588,8 +1588,8 @@ 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 +# 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 @@ -1598,14 +1598,14 @@ assert not (wire[4] & CANXL_IDE) # IDE must be clear pkt = CANXL(priority=0x042) / b'\xde' wire = raw(pkt) -assert wire == bytes.fromhex('42000000' 'c0' '00' '0100' '00000000' 'de'), wire.hex() +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('23014500' 'c0' '07' '0400' '78563412' 'deadbeef'), wire.hex() +assert wire == bytes.fromhex('00450123' 'c0' '07' '0400' '78563412' 'deadbeef'), wire.hex() = CAN XL TV3: SEC flag set @@ -1618,8 +1618,8 @@ assert wire == bytes.fromhex('00000000' 'c1' '00' '0200' '00000000' '1122'), wir 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() +# 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) @@ -1651,7 +1651,7 @@ assert not (wire[4] & CANXL_IDE) # IDE forced clear = 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') +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 @@ -1672,7 +1672,7 @@ assert CAN.dispatch_hook(_pkt=canfd_bytes) == CANFD = CAN XL TV1 dissect -wire = bytes.fromhex('42000000' 'c0' '00' '0100' '00000000' 'de') +wire = bytes.fromhex('00000042' 'c0' '00' '0100' '00000000' 'de') pkt = CANXL(wire) assert pkt.priority == 0x042 assert pkt.vcid == 0x00 @@ -1686,7 +1686,7 @@ assert pkt.flags.ide == 0 = CAN XL TV2 dissect -wire = bytes.fromhex('23014500' 'c0' '07' '0400' '78563412' 'deadbeef') +wire = bytes.fromhex('00450123' 'c0' '07' '0400' '78563412' 'deadbeef') pkt = CANXL(wire) assert pkt.priority == 0x123 assert pkt.vcid == 0x45 @@ -1706,7 +1706,7 @@ assert pkt.flags.rrs == 0 = CAN XL TV4 dissect (RRS + max fields) -wire = bytes.fromhex('ff07ff00' 'c2' 'ff' '0100' 'ffffffff' 'ff') +wire = bytes.fromhex('00ff07ff' 'c2' 'ff' '0100' 'ffffffff' 'ff') pkt = CANXL(wire) assert pkt.priority == 0x7ff assert pkt.vcid == 0xff @@ -1848,3 +1848,58 @@ 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' + From d82fb3e86810b449781a52edd7281d163bb36c0c Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Thu, 8 Oct 2026 20:17:47 +0200 Subject: [PATCH 14/19] cansocket_native: pad sent frames to the socket frame size CAN XL support changed send() to choose the padding size from the packet type, so a classic CAN frame sent on a socket opened with fd=True was transmitted as a 16 byte frame instead of a CAN FD frame. AI-Assisted: yes (Claude Opus and Sonnet) --- scapy/contrib/cansocket_native.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scapy/contrib/cansocket_native.py b/scapy/contrib/cansocket_native.py index 43de45fcdab..de7f7fb7c0d 100644 --- a/scapy/contrib/cansocket_native.py +++ b/scapy/contrib/cansocket_native.py @@ -22,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, CANFD, CANXL, CAN_MTU, CAN_FD_MTU, CANXL_MTU +from scapy.layers.can import CAN, CANXL, CAN_MTU, CAN_FD_MTU, CANXL_MTU from scapy.compat import raw from typing import ( @@ -214,8 +214,9 @@ def send(self, x): # CAN XL: the kernel expects exactly HDR_SIZE + len bytes, so no padding. if not isinstance(x, CANXL): - # CAN/CANFD: pad to correct MTU per frame type - mtu = CAN_FD_MTU if isinstance(x, CANFD) else CAN_MTU + # CAN/CANFD: pad to the frame size the socket was opened with, so + # that a classic CAN frame sent on an fd socket stays a CAN FD frame + mtu = CAN_FD_MTU if self.fd else CAN_MTU bs = bs + b"\x00" * (mtu - len(bs)) return super(NativeCANSocket, self).send(bs) # type: ignore From d8d25dfd372b819dbbd9b9b9e18109478410bd62 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Thu, 8 Oct 2026 20:17:52 +0200 Subject: [PATCH 15/19] test: use parentheses for the CAN XL imports Python 3.7 rejects the backslash continuation when UTScapy compiles the test, with "SyntaxError: trailing comma not allowed without surrounding parentheses". AI-Assisted: yes (Claude Opus and Sonnet) --- test/contrib/canxlsocket_testsocket.uts | 8 +++++--- test/scapy/layers/can.uts | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/test/contrib/canxlsocket_testsocket.uts b/test/contrib/canxlsocket_testsocket.uts index c841a884a67..77d7d0ce70d 100644 --- a/test/contrib/canxlsocket_testsocket.uts +++ b/test/contrib/canxlsocket_testsocket.uts @@ -11,9 +11,11 @@ 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 scapy.layers.can import ( + CANXL, + CANXL_FDF, CANXL_HDR_SIZE, CANXL_MAX_DLEN, CANXL_MIN_DLEN, + CANXL_MTU, CANXL_RRS, CANXL_SEC, CANXL_XLF, +) from test.testsocket import TestSocket, cleanup_testsockets ############ diff --git a/test/scapy/layers/can.uts b/test/scapy/layers/can.uts index 9310782f2b7..a004b38a78d 100644 --- a/test/scapy/layers/can.uts +++ b/test/scapy/layers/can.uts @@ -1549,9 +1549,11 @@ assert remote.data == b"" = 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 +from scapy.layers.can import ( + CAN, CANFD, CANXL, + CANXL_FDF, CANXL_HDR_SIZE, CANXL_IDE, CANXL_MAX_DLEN, + CANXL_MIN_DLEN, CANXL_MTU, CANXL_RRS, CANXL_SEC, CANXL_XLF, +) assert CANXL_HDR_SIZE == 12 assert CANXL_MTU == 2060 From 32b78d31a2900ecfe9f815d588eb3abb74d437a5 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Fri, 9 Oct 2026 08:04:00 +0200 Subject: [PATCH 16/19] cansocket_native: pad sent frames to self.MTU self.MTU is CANXL_MTU on a socket opened with xl=True, which is not a valid frame size for a classic CAN or CAN FD frame, so those are padded to CAN_MTU there. AI-Assisted: yes (Claude Opus and Sonnet) --- scapy/contrib/cansocket_native.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scapy/contrib/cansocket_native.py b/scapy/contrib/cansocket_native.py index de7f7fb7c0d..6af05dab789 100644 --- a/scapy/contrib/cansocket_native.py +++ b/scapy/contrib/cansocket_native.py @@ -214,10 +214,11 @@ def send(self, x): # CAN XL: the kernel expects exactly HDR_SIZE + len bytes, so no padding. if not isinstance(x, CANXL): - # CAN/CANFD: pad to the frame size the socket was opened with, so - # that a classic CAN frame sent on an fd socket stays a CAN FD frame - mtu = CAN_FD_MTU if self.fd else CAN_MTU - bs = bs + b"\x00" * (mtu - len(bs)) + # This is a classic CAN or CAN FD frame. Pad it to the frame size + # the socket was opened with; on an xl socket self.MTU is the CAN XL + # receive size, which is not a valid size for such a frame. + classic_mtu = CAN_MTU if self.xl else self.MTU + bs = bs + b"\x00" * (classic_mtu - len(bs)) return super(NativeCANSocket, self).send(bs) # type: ignore From 5de79be4ce47df5a50c0a6a5daa7cdc2d7b4f099 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Fri, 9 Oct 2026 08:04:24 +0200 Subject: [PATCH 17/19] can: compute the CAN XL length only when it is not set Default the length field to None and compute it from the payload in post_build only in that case, so that a value set by the user reaches the wire unchanged. AI-Assisted: yes (Claude Opus and Sonnet) --- scapy/layers/can.py | 28 +++++++++++++++------------- test/scapy/layers/can.uts | 12 +++++++++++- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/scapy/layers/can.py b/scapy/layers/can.py index 878493c3757..7b40c2006cc 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -271,29 +271,31 @@ def is_canxl_frame(pkt): ['sec', 'rrs', 'res_f2', 'res_f3', 'res_f4', 'ide', 'fdf', 'xlf']), XByteField('sdt', 0), - # Auto-computed from the payload in post_build. + # Computed from the payload in post_build when left at None. # 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), + LEShortField('length', None), 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(' 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(' Date: Fri, 9 Oct 2026 08:04:57 +0200 Subject: [PATCH 18/19] can: send explicit CAN XL flags unchanged post_build forced XLF and FDF on and IDE off, overriding a value set by the user. The field default still follows ISO 11898-1. AI-Assisted: yes (Claude Opus and Sonnet) --- scapy/layers/can.py | 7 ------- test/contrib/canxlsocket_testsocket.uts | 23 +++++++++++++++++------ test/scapy/layers/can.uts | 20 ++++++++++---------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/scapy/layers/can.py b/scapy/layers/can.py index 7b40c2006cc..18324ed898f 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -296,13 +296,6 @@ def post_build(self, pkt, pay): "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(' Date: Fri, 9 Oct 2026 08:05:20 +0200 Subject: [PATCH 19/19] can: respect remove-padding in CANXL.extract_padding Trailing bytes beyond the stated length were always discarded. They are now kept as Padding when remove-padding is disabled, as for CAN and CAN FD. AI-Assisted: yes (Claude Opus and Sonnet) --- scapy/layers/can.py | 10 ++++------ test/scapy/layers/can.uts | 12 ++++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/scapy/layers/can.py b/scapy/layers/can.py index 18324ed898f..9453eeca910 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -303,12 +303,10 @@ def post_build(self, 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 + if conf.contribs['CAN']['remove-padding']: + return p[:data_len], None + else: + return p[:data_len], p[data_len:] # -- ISO 11898-1:2024 property accessors --------------------------------- diff --git a/test/scapy/layers/can.uts b/test/scapy/layers/can.uts index 9a0d6ec0655..927e3f1aeee 100644 --- a/test/scapy/layers/can.uts +++ b/test/scapy/layers/can.uts @@ -1655,6 +1655,18 @@ wire = raw(CANXL(length=0x40) / b'\x01\x02') assert wire[6:8] == b'\x40\x00' assert CANXL(wire).length == 0x40 += CAN XL respects the remove-padding configuration + +trailing = raw(CANXL() / b'\x01\x02') + b'\x00' * 6 + +conf.contribs['CAN']['remove-padding'] = True +assert bytes(CANXL(trailing).payload) == b'\x01\x02' + +conf.contribs['CAN']['remove-padding'] = False +assert bytes(CANXL(trailing).payload) == b'\x01\x02' + b'\x00' * 6 + +conf.contribs['CAN']['remove-padding'] = True + ############ ############