From d27cef846e887d10575b9793fe6b983d2797e261 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Thu, 5 Mar 2026 12:49:21 +0100 Subject: [PATCH 01/12] 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 0c02c3c4496..8c6c0d720c2 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 e821deeedc97f6c51b46b766fab215c1f7ecddc7 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Thu, 5 Mar 2026 12:51:54 +0100 Subject: [PATCH 02/12] Add CAN XL unit tests AI-Assisted: yes (Claude Opus and Sonnet) --- test/scapy/layers/can.uts | 310 +++++++++++++++++++++++++++++++++++++- 1 file changed, 309 insertions(+), 1 deletion(-) diff --git a/test/scapy/layers/can.uts b/test/scapy/layers/can.uts index 8e4a2652c53..9a84e718ace 100644 --- a/test/scapy/layers/can.uts +++ b/test/scapy/layers/can.uts @@ -1531,4 +1531,312 @@ if conf.crypto_valid: c = cmac.CMAC(algorithms.AES128(b"\x00" * 16)) c.update(bytes.fromhex("1122334455667788AABBCCDDEEFF0011") + bytes.fromhex("00000000")) mac = c.finalize() - assert pkt.tmac == mac[:3] \ No newline at end of file + assert pkt.tmac == mac[:3] + + +############ +############ + ++ 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 \ No newline at end of file From 59f946e808775ac93ff2865abe12563b3ba25cc3 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Thu, 5 Mar 2026 20:55:06 +0100 Subject: [PATCH 03/12] 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/12] 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 92be2071f0370acac45c4028f5ca7b1bda49606b Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Fri, 6 Mar 2026 07:30:04 +0100 Subject: [PATCH 05/12] 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 6bb512f2f4dfc425011c2fecef36981b46e8f141 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Mon, 7 Sep 2026 14:34:39 +0200 Subject: [PATCH 06/12] 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 +from scapy.error import Scapy_Exception, warning from typing import ( List, Type, @@ -354,6 +357,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 8c6c0d720c2..1e59f6d77e0 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 d2a5cb66ab3a93bb4517a5a7893dd694810395d6 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 09:10:23 +0200 Subject: [PATCH 07/12] fix CI findings --- 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 5d6179ed16b449358005055e41f572072d7bae15 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 09:56:56 +0200 Subject: [PATCH 08/12] 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 035053f9e66a9a65e691991230b78635afeef921 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 10:00:50 +0200 Subject: [PATCH 09/12] 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 1751e2ec1efc0a1b1a44586dd3967663a0e25998 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 10:03:59 +0200 Subject: [PATCH 10/12] 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 0f86e342eca17dc24a7b49cb78e4d9e1e29246f3 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 10:07:08 +0200 Subject: [PATCH 11/12] 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 3ebf3a5be2cf31193792e213396b6fd85d723d65 Mon Sep 17 00:00:00 2001 From: Friedrich Wiemer Date: Tue, 22 Sep 2026 10:10:43 +0200 Subject: [PATCH 12/12] 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 = "