From ff9a9a487aef9f9656b7569ec3d39c5bca96bc06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:45:00 +0000 Subject: [PATCH 01/16] Add single_layer_UDS config option to UDS contrib - Add split_layers import and orb from scapy.compat - Add 'single_layer_UDS': False to default UDS contribs config - Add _uds_service_cls dict and dispatch_hook classmethod to UDS to support direct dispatch in single layer mode - Add _uds_service(service_id) class decorator that: - Prepends a conditional 'service' field (shown only in single layer mode) - Registers the class in UDS._uds_service_cls dispatch table - Binds the layer to UDS in multi-layer mode - Injects a hashret for single layer mode - Add uds_single_layer_mode(enable) function to toggle modes at runtime - Replace all 55 bind_layers(UDS, ...) calls with @_uds_service(...) decorators Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- scapy/contrib/automotive/uds.py | 252 ++++++++++++++++++-------------- 1 file changed, 140 insertions(+), 112 deletions(-) diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 978bf6a0483..ea06e2b9ec7 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -19,13 +19,15 @@ ShortField, ObservableDict, XShortEnumField, XByteEnumField, StrLenField, \ FieldLenField, XStrFixedLenField, XStrLenField, FlagsField, PacketListField, \ PacketField -from scapy.packet import Packet, bind_layers, NoPayload, Raw +from scapy.packet import Packet, bind_layers, split_layers, NoPayload, Raw +from scapy.compat import orb from scapy.config import conf from scapy.utils import PeriodicSenderThread from scapy.contrib.isotp import ISOTP # Typing imports from typing import ( + Any, Dict, Union, ) @@ -39,7 +41,8 @@ # "a negative response 'requestCorrectlyReceived-" # "ResponsePending' as answer of a request. \n" # "The default value is False.") - conf.contribs['UDS'] = {'treat-response-pending-as-answer': False} + conf.contribs['UDS'] = {'treat-response-pending-as-answer': False, + 'single_layer_UDS': False} conf.debug_dissector = True @@ -126,8 +129,89 @@ def hashret(self): return struct.pack('B', bytes(self)[1] & ~0x40) return struct.pack('B', self.service & ~0x40) + _uds_service_cls = {} # type: Dict[int, type] + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (bytes, Any, Any) -> type + """Dispatch to the correct UDS service class in single layer mode.""" + if conf.contribs['UDS'].get('single_layer_UDS', False) and _pkt: + service = orb(_pkt[0]) + return cls._uds_service_cls.get(service, cls) + return cls + + +def _uds_service(service_id): + # type: (int) -> Any + """Class decorator to register a UDS service layer. + + Prepends a conditional 'service' field (active only in single layer mode), + registers the class in the UDS dispatch table, and conditionally binds + the layer to UDS based on the 'single_layer_UDS' config flag. + """ + def decorator(cls): + # type: (type) -> type + # Prepend conditional service field to the class + svc_field = ConditionalField( + XByteEnumField('service', service_id, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_UDS', False) + ) + cls.fields_desc = [svc_field] + list(cls.fields_desc) + # Register in UDS dispatch table for single layer mode + UDS._uds_service_cls[service_id] = cls + # In multi-layer mode, bind to UDS (for backward compatibility) + if not conf.contribs['UDS'].get('single_layer_UDS', False): + bind_layers(UDS, cls, service=service_id) + # Inject hashret for single layer mode + _sid = service_id + + def _hashret(self): + # type: () -> bytes + if conf.contribs['UDS'].get('single_layer_UDS', False): + return struct.pack('B', _sid & ~0x40) + return Packet.hashret(self) + + if 'hashret' not in cls.__dict__: + cls.hashret = _hashret + return cls + return decorator + + +def uds_single_layer_mode(enable=True): + # type: (bool) -> None + """Enable or disable single layer UDS mode. + + In single layer mode, each UDS service is a standalone packet with a + conditional 'service' field, rather than being nested inside a UDS() + layer. When dissecting raw bytes, UDS() will directly return the + appropriate subpacket class (e.g., UDS_RDBI) instead of UDS / UDS_RDBI. + + This function can be called after the module is loaded to switch modes. + + Args: + enable: If True, enable single layer mode. If False, revert to + multi-layer mode (default). + + Example:: + + >>> conf.contribs['UDS'] = {'single_layer_UDS': True} + >>> load_contrib('automotive.uds') + # OR after loading: + >>> from scapy.contrib.automotive.uds import uds_single_layer_mode + >>> uds_single_layer_mode(True) + >>> UDS(b'\\x10\\x01') + + """ + conf.contribs['UDS']['single_layer_UDS'] = enable + for service_id, cls in UDS._uds_service_cls.items(): + if enable: + split_layers(UDS, cls, service=service_id) + else: + bind_layers(UDS, cls, service=service_id) + # ########################DSC################################### +@_uds_service(0x10) class UDS_DSC(Packet): diagnosticSessionTypes = ObservableDict({ 0x00: 'ISOSAEReserved', @@ -142,9 +226,8 @@ class UDS_DSC(Packet): ] -bind_layers(UDS, UDS_DSC, service=0x10) - +@_uds_service(0x50) class UDS_DSCPR(Packet): name = 'DiagnosticSessionControlPositiveResponse' fields_desc = [ @@ -158,10 +241,9 @@ def answers(self, other): other.diagnosticSessionType == self.diagnosticSessionType -bind_layers(UDS, UDS_DSCPR, service=0x50) - # #########################ER################################### +@_uds_service(0x11) class UDS_ER(Packet): resetTypes = { 0x00: 'ISOSAEReserved', @@ -178,9 +260,8 @@ class UDS_ER(Packet): ] -bind_layers(UDS, UDS_ER, service=0x11) - +@_uds_service(0x51) class UDS_ERPR(Packet): name = 'ECUResetPositiveResponse' fields_desc = [ @@ -193,10 +274,9 @@ def answers(self, other): return isinstance(other, UDS_ER) and other.resetType == self.resetType -bind_layers(UDS, UDS_ERPR, service=0x51) - # #########################SA################################### +@_uds_service(0x27) class UDS_SA(Packet): name = 'SecurityAccess' fields_desc = [ @@ -208,9 +288,8 @@ class UDS_SA(Packet): ] -bind_layers(UDS, UDS_SA, service=0x27) - +@_uds_service(0x67) class UDS_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ @@ -224,10 +303,9 @@ def answers(self, other): and other.securityAccessType == self.securityAccessType -bind_layers(UDS, UDS_SAPR, service=0x67) - # #########################CC################################### +@_uds_service(0x28) class UDS_CC(Packet): controlTypes = { 0x00: 'enableRxAndTx', @@ -265,9 +343,8 @@ class UDS_CC(Packet): ] -bind_layers(UDS, UDS_CC, service=0x28) - +@_uds_service(0x68) class UDS_CCPR(Packet): name = 'CommunicationControlPositiveResponse' fields_desc = [ @@ -279,10 +356,9 @@ def answers(self, other): and other.controlType == self.controlType -bind_layers(UDS, UDS_CCPR, service=0x68) - # #########################AUTH################################### +@_uds_service(0x29) class UDS_AUTH(Packet): subFunctions = { 0x00: 'deAuthenticate', @@ -355,9 +431,8 @@ class UDS_AUTH(Packet): ] -bind_layers(UDS, UDS_AUTH, service=0x29) - +@_uds_service(0x69) class UDS_AUTHPR(Packet): authenticationReturnParameterTypes = { 0x00: 'requestAccepted', @@ -435,10 +510,9 @@ def answers(self, other): and other.subFunction == self.subFunction -bind_layers(UDS, UDS_AUTHPR, service=0x69) - # #########################TP################################### +@_uds_service(0x3E) class UDS_TP(Packet): name = 'TesterPresent' fields_desc = [ @@ -446,9 +520,8 @@ class UDS_TP(Packet): ] -bind_layers(UDS, UDS_TP, service=0x3E) - +@_uds_service(0x7E) class UDS_TPPR(Packet): name = 'TesterPresentPositiveResponse' fields_desc = [ @@ -459,10 +532,9 @@ def answers(self, other): return isinstance(other, UDS_TP) -bind_layers(UDS, UDS_TPPR, service=0x7E) - # #########################ATP################################### +@_uds_service(0x83) class UDS_ATP(Packet): timingParameterAccessTypes = { 0: 'ISOSAEReserved', @@ -480,9 +552,8 @@ class UDS_ATP(Packet): ] -bind_layers(UDS, UDS_ATP, service=0x83) - +@_uds_service(0xC3) class UDS_ATPPR(Packet): name = 'AccessTimingParameterPositiveResponse' fields_desc = [ @@ -498,12 +569,11 @@ def answers(self, other): self.timingParameterAccessType -bind_layers(UDS, UDS_ATPPR, service=0xC3) - # #########################SDT################################### # TODO: Implement correct internal message service handling here, # instead of using just the dataRecord +@_uds_service(0x84) class UDS_SDT(Packet): name = 'SecuredDataTransmission' fields_desc = [ @@ -522,9 +592,8 @@ class UDS_SDT(Packet): ] -bind_layers(UDS, UDS_SDT, service=0x84) - +@_uds_service(0xC4) class UDS_SDTPR(Packet): name = 'SecuredDataTransmissionPositiveResponse' fields_desc = [ @@ -546,10 +615,9 @@ def answers(self, other): return isinstance(other, UDS_SDT) -bind_layers(UDS, UDS_SDTPR, service=0xC4) - # #########################CDTCS################################### +@_uds_service(0x85) class UDS_CDTCS(Packet): DTCSettingTypes = { 0: 'ISOSAEReserved', @@ -563,9 +631,8 @@ class UDS_CDTCS(Packet): ] -bind_layers(UDS, UDS_CDTCS, service=0x85) - +@_uds_service(0xC5) class UDS_CDTCSPR(Packet): name = 'ControlDTCSettingPositiveResponse' fields_desc = [ @@ -576,11 +643,10 @@ def answers(self, other): return isinstance(other, UDS_CDTCS) -bind_layers(UDS, UDS_CDTCSPR, service=0xC5) - # #########################ROE################################### # TODO: improve this protocol implementation +@_uds_service(0x86) class UDS_ROE(Packet): eventTypes = { 0: 'doNotStoreEvent', @@ -594,9 +660,8 @@ class UDS_ROE(Packet): ] -bind_layers(UDS, UDS_ROE, service=0x86) - +@_uds_service(0xC6) class UDS_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' fields_desc = [ @@ -611,10 +676,9 @@ def answers(self, other): and other.eventType == self.eventType -bind_layers(UDS, UDS_ROEPR, service=0xC6) - # #########################LC################################### +@_uds_service(0x87) class UDS_LC(Packet): linkControlTypes = { 0: 'ISOSAEReserved', @@ -636,9 +700,8 @@ class UDS_LC(Packet): ] -bind_layers(UDS, UDS_LC, service=0x87) - +@_uds_service(0xC7) class UDS_LCPR(Packet): name = 'LinkControlPositiveResponse' fields_desc = [ @@ -650,10 +713,9 @@ def answers(self, other): and other.linkControlType == self.linkControlType -bind_layers(UDS, UDS_LCPR, service=0xC7) - # #########################RDBI################################### +@_uds_service(0x22) class UDS_RDBI(Packet): dataIdentifiers = ObservableDict() name = 'ReadDataByIdentifier' @@ -664,9 +726,8 @@ class UDS_RDBI(Packet): ] -bind_layers(UDS, UDS_RDBI, service=0x22) - +@_uds_service(0x62) class UDS_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ @@ -679,10 +740,9 @@ def answers(self, other): and self.dataIdentifier in other.identifiers -bind_layers(UDS, UDS_RDBIPR, service=0x62) - # #########################RMBA################################### +@_uds_service(0x23) class UDS_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ @@ -707,9 +767,8 @@ class UDS_RMBA(Packet): ] -bind_layers(UDS, UDS_RMBA, service=0x23) - +@_uds_service(0x63) class UDS_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ @@ -720,10 +779,9 @@ def answers(self, other): return isinstance(other, UDS_RMBA) -bind_layers(UDS, UDS_RMBAPR, service=0x63) - # #########################RSDBI################################### +@_uds_service(0x24) class UDS_RSDBI(Packet): name = 'ReadScalingDataByIdentifier' dataIdentifiers = ObservableDict() @@ -732,10 +790,9 @@ class UDS_RSDBI(Packet): ] -bind_layers(UDS, UDS_RSDBI, service=0x24) - # TODO: Implement correct scaling here, instead of using just the dataRecord +@_uds_service(0x64) class UDS_RSDBIPR(Packet): name = 'ReadScalingDataByIdentifierPositiveResponse' fields_desc = [ @@ -749,10 +806,9 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier -bind_layers(UDS, UDS_RSDBIPR, service=0x64) - # #########################RDBPI################################### +@_uds_service(0x2A) class UDS_RDBPI(Packet): periodicDataIdentifiers = ObservableDict() transmissionModes = { @@ -770,10 +826,9 @@ class UDS_RDBPI(Packet): ] -bind_layers(UDS, UDS_RDBPI, service=0x2A) - # TODO: Implement correct scaling here, instead of using just the dataRecord +@_uds_service(0x6A) class UDS_RDBPIPR(Packet): name = 'ReadDataByPeriodicIdentifierPositiveResponse' fields_desc = [ @@ -786,12 +841,11 @@ def answers(self, other): and other.periodicDataIdentifier == self.periodicDataIdentifier -bind_layers(UDS, UDS_RDBPIPR, service=0x6A) - # #########################DDDI################################### # TODO: Implement correct interpretation here, # instead of using just the dataRecord +@_uds_service(0x2C) class UDS_DDDI(Packet): name = 'DynamicallyDefineDataIdentifier' subFunctions = {0x1: "defineByIdentifier", @@ -803,9 +857,8 @@ class UDS_DDDI(Packet): ] -bind_layers(UDS, UDS_DDDI, service=0x2C) - +@_uds_service(0x6C) class UDS_DDDIPR(Packet): name = 'DynamicallyDefineDataIdentifierPositiveResponse' fields_desc = [ @@ -818,10 +871,9 @@ def answers(self, other): and other.subFunction == self.subFunction -bind_layers(UDS, UDS_DDDIPR, service=0x6C) - # #########################WDBI################################### +@_uds_service(0x2E) class UDS_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ @@ -830,9 +882,8 @@ class UDS_WDBI(Packet): ] -bind_layers(UDS, UDS_WDBI, service=0x2E) - +@_uds_service(0x6E) class UDS_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ @@ -845,10 +896,9 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier -bind_layers(UDS, UDS_WDBIPR, service=0x6E) - # #########################WMBA################################### +@_uds_service(0x3D) class UDS_WMBA(Packet): name = 'WriteMemoryByAddress' fields_desc = [ @@ -875,9 +925,8 @@ class UDS_WMBA(Packet): ] -bind_layers(UDS, UDS_WMBA, service=0x3D) - +@_uds_service(0x7D) class UDS_WMBAPR(Packet): name = 'WriteMemoryByAddressPositiveResponse' fields_desc = [ @@ -907,8 +956,6 @@ def answers(self, other): and other.memoryAddressLen == self.memoryAddressLen -bind_layers(UDS, UDS_WMBAPR, service=0x7D) - # ##########################DTC##################################### class DTC(Packet): @@ -935,6 +982,7 @@ def extract_padding(self, s): # #########################CDTCI################################### +@_uds_service(0x14) class UDS_CDTCI(Packet): name = 'ClearDiagnosticInformation' fields_desc = [ @@ -944,9 +992,8 @@ class UDS_CDTCI(Packet): ] -bind_layers(UDS, UDS_CDTCI, service=0x14) - +@_uds_service(0x54) class UDS_CDTCIPR(Packet): name = 'ClearDiagnosticInformationPositiveResponse' @@ -954,10 +1001,9 @@ def answers(self, other): return isinstance(other, UDS_CDTCI) -bind_layers(UDS, UDS_CDTCIPR, service=0x54) - # #########################RDTCI################################### +@_uds_service(0x19) class UDS_RDTCI(Packet): reportTypes = { 0: 'ISOSAEReserved', @@ -1028,8 +1074,6 @@ class UDS_RDTCI(Packet): ] -bind_layers(UDS, UDS_RDTCI, service=0x19) - class DTCAndStatusRecord(Packet): name = 'DTC and status record' @@ -1088,6 +1132,7 @@ class DTCSnapshotRecord(Packet): ] +@_uds_service(0x59) class UDS_RDTCIPR(Packet): name = 'ReadDTCInformationPositiveResponse' fields_desc = [ @@ -1139,10 +1184,9 @@ def answers(self, other): return True -bind_layers(UDS, UDS_RDTCIPR, service=0x59) - # #########################RC################################### +@_uds_service(0x31) class UDS_RC(Packet): routineControlTypes = { 0: 'ISOSAEReserved', @@ -1158,9 +1202,8 @@ class UDS_RC(Packet): ] -bind_layers(UDS, UDS_RC, service=0x31) - +@_uds_service(0x71) class UDS_RCPR(Packet): name = 'RoutineControlPositiveResponse' fields_desc = [ @@ -1180,10 +1223,9 @@ def answers(self, other): return False -bind_layers(UDS, UDS_RCPR, service=0x71) - # #########################RD################################### +@_uds_service(0x34) class UDS_RD(Packet): dataFormatIdentifiers = ObservableDict({ 0: 'noCompressionNoEncryption' @@ -1212,9 +1254,8 @@ class UDS_RD(Packet): ] -bind_layers(UDS, UDS_RD, service=0x34) - +@_uds_service(0x74) class UDS_RDPR(Packet): name = 'RequestDownloadPositiveResponse' fields_desc = [ @@ -1227,10 +1268,9 @@ def answers(self, other): return isinstance(other, UDS_RD) -bind_layers(UDS, UDS_RDPR, service=0x74) - # #########################RU################################### +@_uds_service(0x35) class UDS_RU(Packet): name = 'RequestUpload' fields_desc = [ @@ -1257,9 +1297,8 @@ class UDS_RU(Packet): ] -bind_layers(UDS, UDS_RU, service=0x35) - +@_uds_service(0x75) class UDS_RUPR(Packet): name = 'RequestUploadPositiveResponse' fields_desc = [ @@ -1272,10 +1311,9 @@ def answers(self, other): return isinstance(other, UDS_RU) -bind_layers(UDS, UDS_RUPR, service=0x75) - # #########################TD################################### +@_uds_service(0x36) class UDS_TD(Packet): name = 'TransferData' fields_desc = [ @@ -1284,9 +1322,8 @@ class UDS_TD(Packet): ] -bind_layers(UDS, UDS_TD, service=0x36) - +@_uds_service(0x76) class UDS_TDPR(Packet): name = 'TransferDataPositiveResponse' fields_desc = [ @@ -1299,10 +1336,9 @@ def answers(self, other): and other.blockSequenceCounter == self.blockSequenceCounter -bind_layers(UDS, UDS_TDPR, service=0x76) - # #########################RTE################################### +@_uds_service(0x37) class UDS_RTE(Packet): name = 'RequestTransferExit' fields_desc = [ @@ -1310,9 +1346,8 @@ class UDS_RTE(Packet): ] -bind_layers(UDS, UDS_RTE, service=0x37) - +@_uds_service(0x77) class UDS_RTEPR(Packet): name = 'RequestTransferExitPositiveResponse' fields_desc = [ @@ -1323,10 +1358,9 @@ def answers(self, other): return isinstance(other, UDS_RTE) -bind_layers(UDS, UDS_RTEPR, service=0x77) - # #########################RFT################################### +@_uds_service(0x38) class UDS_RFT(Packet): name = 'RequestFileTransfer' @@ -1368,9 +1402,8 @@ def _contains_file_size(packet): ] -bind_layers(UDS, UDS_RFT, service=0x38) - +@_uds_service(0x78) class UDS_RFTPR(Packet): name = 'RequestFileTransferPositiveResponse' @@ -1410,10 +1443,9 @@ def answers(self, other): return isinstance(other, UDS_RFT) -bind_layers(UDS, UDS_RFTPR, service=0x78) - # #########################IOCBI################################### +@_uds_service(0x2F) class UDS_IOCBI(Packet): name = 'InputOutputControlByIdentifier' fields_desc = [ @@ -1421,9 +1453,8 @@ class UDS_IOCBI(Packet): ] -bind_layers(UDS, UDS_IOCBI, service=0x2F) - +@_uds_service(0x6F) class UDS_IOCBIPR(Packet): name = 'InputOutputControlByIdentifierPositiveResponse' fields_desc = [ @@ -1435,10 +1466,9 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier -bind_layers(UDS, UDS_IOCBIPR, service=0x6F) - # #########################NR################################### +@_uds_service(0x7f) class UDS_NR(Packet): negativeResponseCodes = { 0x00: 'positiveResponse', @@ -1515,8 +1545,6 @@ def answers(self, other): conf.contribs['UDS']['treat-response-pending-as-answer']) -bind_layers(UDS, UDS_NR, service=0x7f) - # ################################################################## # ######################## UTILS ################################### From f28bd737e3855ab0e3a349d13f5e096a70720632 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:47:28 +0000 Subject: [PATCH 02/16] Address code review: explicit length check and closure comment - Use len(_pkt) >= 1 instead of truthiness check in dispatch_hook - Add comment explaining _sid closure variable captures by value Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- scapy/contrib/automotive/uds.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index ea06e2b9ec7..c44559f444b 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -135,7 +135,7 @@ def hashret(self): def dispatch_hook(cls, _pkt=b"", *args, **kwargs): # type: (bytes, Any, Any) -> type """Dispatch to the correct UDS service class in single layer mode.""" - if conf.contribs['UDS'].get('single_layer_UDS', False) and _pkt: + if conf.contribs['UDS'].get('single_layer_UDS', False) and len(_pkt) >= 1: service = orb(_pkt[0]) return cls._uds_service_cls.get(service, cls) return cls @@ -162,7 +162,7 @@ def decorator(cls): # In multi-layer mode, bind to UDS (for backward compatibility) if not conf.contribs['UDS'].get('single_layer_UDS', False): bind_layers(UDS, cls, service=service_id) - # Inject hashret for single layer mode + # _sid captures service_id by value to avoid late-binding closure issues _sid = service_id def _hashret(self): From e9467d819e40ecf90d1c381c255520902f4acd8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:50:04 +0000 Subject: [PATCH 03/16] Add single layer UDS config, tests, and documentation Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/8125374f-f4ee-41eb-b908-21bc9363a30f Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- doc/scapy/layers/automotive.rst | 55 ++++++++++++++++ test/contrib/automotive/uds.uts | 111 ++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) diff --git a/doc/scapy/layers/automotive.rst b/doc/scapy/layers/automotive.rst index 6abee5daa66..2e6792c8fb9 100644 --- a/doc/scapy/layers/automotive.rst +++ b/doc/scapy/layers/automotive.rst @@ -1081,6 +1081,61 @@ to the Scapy interpreter:: .. image:: ../graphics/animations/animation-scapy-uds3.svg + +Single Layer UDS Mode +--------------------- + +By default, UDS packets consist of two layers: ``UDS`` (outer) and a service-specific +subpacket (e.g. ``UDS_RDBI``). This is the standard multi-layer mode: + +.. code-block:: python + + >>> pkt = UDS() / UDS_DSC(diagnosticSessionType=0x01) + >>> pkt.show() + ###[ UDS ]### + service= DiagnosticSessionControl + ###[ DiagnosticSessionControl ]### + diagnosticSessionType= defaultSession + + >>> UDS(b'\x10\x01') + > + +Single layer mode makes each UDS service packet a standalone ``Packet`` with its own +``service`` field. This can be more ergonomic in some use cases. + +To enable single layer mode **before** loading the module:: + + >>> conf.contribs['UDS'] = {'treat-response-pending-as-answer': False, + ... 'single_layer_UDS': True} + >>> load_contrib('automotive.uds') + +To switch modes at runtime after loading:: + + >>> load_contrib('automotive.uds') + >>> from scapy.contrib.automotive.uds import uds_single_layer_mode + >>> uds_single_layer_mode(True) # enable single layer mode + >>> UDS(b'\x10\x01') + + + >>> UDS_DSC(diagnosticSessionType=0x01) + + + >>> bytes(UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x01' + + >>> uds_single_layer_mode(False) # revert to multi-layer mode + +In single layer mode: + +- The ``UDS()`` class acts as a dispatcher: it reads the first byte (service ID) + and directly returns the corresponding service packet (e.g. ``UDS_DSC``). +- Each service packet has a conditional ``service`` field that is only present + (for building and dissection) when single layer mode is active. +- ``bind_layers`` between ``UDS`` and service classes are disabled; dissection + is handled via ``UDS.dispatch_hook``. +- Service packets' ``answers()`` and ``hashret()`` methods work correctly in + both modes. + GMLAN ===== diff --git a/test/contrib/automotive/uds.uts b/test/contrib/automotive/uds.uts index 1545076039d..df38f36c453 100644 --- a/test/contrib/automotive/uds.uts +++ b/test/contrib/automotive/uds.uts @@ -1438,3 +1438,114 @@ nrc = UDS(b'\x7f\x22\x33') assert nrc.service == 0x7f assert nrc.requestServiceId == 0x22 assert nrc.negativeResponseCode == 0x33 + ++ Single layer UDS mode + += Single layer mode: load helper + +from scapy.contrib.automotive.uds import uds_single_layer_mode + += Single layer mode: enable and basic dissect + +uds_single_layer_mode(True) + +dsc = UDS(b'\x10\x01') +assert isinstance(dsc, UDS_DSC), "Expected UDS_DSC, got %s" % type(dsc) +assert dsc.service == 0x10 +assert dsc.diagnosticSessionType == 0x01 + += Single layer mode: build UDS_DSC + +dsc_built = UDS_DSC(diagnosticSessionType=0x01) +assert bytes(dsc_built) == b'\x10\x01', "Expected b'\\x10\\x01', got %s" % bytes(dsc_built).hex() + += Single layer mode: UDS() / UDS_DSC() still works in single layer mode + +dsc_two = UDS_DSC(service=0x10, diagnosticSessionType=0x01) +assert dsc_two.service == 0x10 +assert dsc_two.diagnosticSessionType == 0x01 + += Single layer mode: dissect positive response + +dscpr = UDS(b'\x50\x01beef') +assert isinstance(dscpr, UDS_DSCPR), "Expected UDS_DSCPR, got %s" % type(dscpr) +assert dscpr.service == 0x50 +assert dscpr.diagnosticSessionType == 0x01 +assert dscpr.sessionParameterRecord == b"beef" + += Single layer mode: answers() between subpackets + +dsc = UDS_DSC(diagnosticSessionType=0x01) +dscpr = UDS_DSCPR(diagnosticSessionType=0x01, sessionParameterRecord=b"beef") +assert dscpr.answers(dsc) + += Single layer mode: answers() negative (different session type) + +dsc2 = UDS_DSC(diagnosticSessionType=0x02) +dscpr2 = UDS_DSCPR(diagnosticSessionType=0x01) +assert not dscpr2.answers(dsc2) + += Single layer mode: NegativeResponse dissect + +nr = UDS(b'\x7f\x10\x22') +assert isinstance(nr, UDS_NR), "Expected UDS_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.requestServiceId == 0x10 +assert nr.negativeResponseCode == 0x22 + += Single layer mode: NegativeResponse answers() + +dsc3 = UDS_DSC(diagnosticSessionType=0x01) +nr2 = UDS_NR(requestServiceId=0x10, negativeResponseCode=0x22) +assert nr2.answers(dsc3) + += Single layer mode: NegativeResponse does not answer wrong service + +er = UDS_ER(resetType=0x01) +assert not nr2.answers(er) + += Single layer mode: hashret consistency between request and positive response + +dsc4 = UDS_DSC(diagnosticSessionType=0x01) +dscpr4 = UDS_DSCPR(diagnosticSessionType=0x01) +assert dsc4.hashret() == dscpr4.hashret(), \ + "hashret mismatch: %s vs %s" % (dsc4.hashret().hex(), dscpr4.hashret().hex()) + += Single layer mode: UDS_RDBI dissect + +rdbi = UDS(b'\x22\x01\x02\x03\x04') +assert isinstance(rdbi, UDS_RDBI), "Expected UDS_RDBI, got %s" % type(rdbi) +assert rdbi.service == 0x22 + += Single layer mode: unknown service falls back to UDS + +unknown = UDS(b'\xAA\x01\x02') +assert isinstance(unknown, UDS), "Expected UDS fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +uds_single_layer_mode(False) + +dsc5 = UDS(b'\x10\x01') +assert dsc5.__class__ == UDS +assert dsc5.service == 0x10 +assert dsc5[UDS_DSC].diagnosticSessionType == 0x01 + +dscpr5 = UDS(b'\x50\x01beef') +assert dscpr5.__class__ == UDS +assert dscpr5.service == 0x50 +assert dscpr5[UDS_DSCPR].diagnosticSessionType == 0x01 + += Single layer mode: enable via conf before loading + +conf.contribs['UDS']['single_layer_UDS'] = True + +er6 = UDS(b'\x11\x01') +assert isinstance(er6, UDS_ER), "Expected UDS_ER, got %s" % type(er6) +assert er6.service == 0x11 +assert er6.resetType == 0x01 + += Single layer mode: cleanup - restore default multi-layer mode + +uds_single_layer_mode(False) +assert not conf.contribs['UDS']['single_layer_UDS'] From 4354e39253be18332e3e67ea5e62398b72c34341 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:52:51 +0000 Subject: [PATCH 04/16] Fix uds_single_layer_mode idempotency and clarify test naming Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/8125374f-f4ee-41eb-b908-21bc9363a30f Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- scapy/contrib/automotive/uds.py | 6 +++--- test/contrib/automotive/uds.uts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index c44559f444b..88ed042524f 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -204,9 +204,9 @@ def uds_single_layer_mode(enable=True): """ conf.contribs['UDS']['single_layer_UDS'] = enable for service_id, cls in UDS._uds_service_cls.items(): - if enable: - split_layers(UDS, cls, service=service_id) - else: + # Always split first to ensure idempotency (no duplicate bindings) + split_layers(UDS, cls, service=service_id) + if not enable: bind_layers(UDS, cls, service=service_id) diff --git a/test/contrib/automotive/uds.uts b/test/contrib/automotive/uds.uts index df38f36c453..d0b2d8056e0 100644 --- a/test/contrib/automotive/uds.uts +++ b/test/contrib/automotive/uds.uts @@ -1536,7 +1536,7 @@ assert dscpr5.__class__ == UDS assert dscpr5.service == 0x50 assert dscpr5[UDS_DSCPR].diagnosticSessionType == 0x01 -= Single layer mode: enable via conf before loading += Single layer mode: enable via conf directly (without uds_single_layer_mode) conf.contribs['UDS']['single_layer_UDS'] = True @@ -1545,7 +1545,7 @@ assert isinstance(er6, UDS_ER), "Expected UDS_ER, got %s" % type(er6) assert er6.service == 0x11 assert er6.resetType == 0x01 -= Single layer mode: cleanup - restore default multi-layer mode += Single layer mode: final cleanup - restore default multi-layer mode uds_single_layer_mode(False) assert not conf.contribs['UDS']['single_layer_UDS'] From db73b4a785eaecfa004e5163a0da913eba39a510 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:31:50 +0000 Subject: [PATCH 05/16] Make service decorator and single layer mode generic for KWP, OBD, GMLAN Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/98843579-9ca6-43b4-958a-457a46b29a63 Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- doc/scapy/layers/automotive.rst | 67 ++++++----- scapy/contrib/automotive/gm/gmlan.py | 75 +++++++----- scapy/contrib/automotive/kwp.py | 130 +++++++++++--------- scapy/contrib/automotive/obd/obd.py | 69 +++++++---- scapy/contrib/automotive/uds.py | 55 ++------- scapy/contrib/automotive/utils.py | 174 +++++++++++++++++++++++++++ test/contrib/automotive/gm/gmlan.uts | 73 ++++++++++- test/contrib/automotive/kwp.uts | 88 ++++++++++++++ test/contrib/automotive/obd/obd.uts | 65 ++++++++++ 9 files changed, 616 insertions(+), 180 deletions(-) create mode 100644 scapy/contrib/automotive/utils.py diff --git a/doc/scapy/layers/automotive.rst b/doc/scapy/layers/automotive.rst index 2e6792c8fb9..7258dc37ac9 100644 --- a/doc/scapy/layers/automotive.rst +++ b/doc/scapy/layers/automotive.rst @@ -1082,60 +1082,71 @@ to the Scapy interpreter:: .. image:: ../graphics/animations/animation-scapy-uds3.svg -Single Layer UDS Mode ---------------------- +Single Layer Mode +----------------- -By default, UDS packets consist of two layers: ``UDS`` (outer) and a service-specific -subpacket (e.g. ``UDS_RDBI``). This is the standard multi-layer mode: +UDS, KWP, OBD, and GMLAN all support a *single layer mode* that makes each +service packet a standalone ``Packet`` rather than a nested sublayer. The +feature is backed by the generic helpers in +:mod:`scapy.contrib.automotive.utils` and works identically for every protocol. + +**Default (multi-layer) mode** .. code-block:: python >>> pkt = UDS() / UDS_DSC(diagnosticSessionType=0x01) - >>> pkt.show() - ###[ UDS ]### - service= DiagnosticSessionControl - ###[ DiagnosticSessionControl ]### - diagnosticSessionType= defaultSession - >>> UDS(b'\x10\x01') > -Single layer mode makes each UDS service packet a standalone ``Packet`` with its own -``service`` field. This can be more ergonomic in some use cases. +**Single layer mode** -To enable single layer mode **before** loading the module:: +To enable before loading a module:: >>> conf.contribs['UDS'] = {'treat-response-pending-as-answer': False, ... 'single_layer_UDS': True} >>> load_contrib('automotive.uds') -To switch modes at runtime after loading:: +To toggle at runtime after loading:: - >>> load_contrib('automotive.uds') >>> from scapy.contrib.automotive.uds import uds_single_layer_mode - >>> uds_single_layer_mode(True) # enable single layer mode + >>> uds_single_layer_mode(True) >>> UDS(b'\x10\x01') - - >>> UDS_DSC(diagnosticSessionType=0x01) - - >>> bytes(UDS_DSC(diagnosticSessionType=0x01)) b'\x10\x01' - - >>> uds_single_layer_mode(False) # revert to multi-layer mode + >>> uds_single_layer_mode(False) # revert to multi-layer mode + +The same API is available for the other protocols: + ++----------+-----------------------+----------------------------+------------------------------------+ +| Protocol | Config flag | Toggle function | Module | ++==========+=======================+============================+====================================+ +| UDS | ``single_layer_UDS`` | ``uds_single_layer_mode`` | ``scapy.contrib.automotive.uds`` | ++----------+-----------------------+----------------------------+------------------------------------+ +| KWP | ``single_layer_KWP`` | ``kwp_single_layer_mode`` | ``scapy.contrib.automotive.kwp`` | ++----------+-----------------------+----------------------------+------------------------------------+ +| OBD | ``single_layer_OBD`` | ``obd_single_layer_mode`` | ``scapy.contrib.automotive.obd`` | ++----------+-----------------------+----------------------------+------------------------------------+ +| GMLAN | ``single_layer_GMLAN``| ``gmlan_single_layer_mode``| ``scapy.contrib.automotive.gm.gmlan`` | ++----------+-----------------------+----------------------------+------------------------------------+ In single layer mode: -- The ``UDS()`` class acts as a dispatcher: it reads the first byte (service ID) - and directly returns the corresponding service packet (e.g. ``UDS_DSC``). -- Each service packet has a conditional ``service`` field that is only present - (for building and dissection) when single layer mode is active. -- ``bind_layers`` between ``UDS`` and service classes are disabled; dissection - is handled via ``UDS.dispatch_hook``. +- The base class (e.g. ``KWP``) acts as a dispatcher via ``dispatch_hook``: + it reads the first byte and returns the correct service class directly. +- Each service packet has a conditional ``service`` field that is present + (for building and dissection) only when single layer mode is active. +- ``bind_layers`` between the base class and service classes are disabled; + dissection is handled via ``dispatch_hook``. - Service packets' ``answers()`` and ``hashret()`` methods work correctly in both modes. +The underlying helpers that power this feature are +:func:`~scapy.contrib.automotive.utils._make_service_decorator` and +:func:`~scapy.contrib.automotive.utils._make_single_layer_mode` in +:mod:`scapy.contrib.automotive.utils`. OEM-specific protocol extensions can +use the same helpers to add single layer support to custom protocol classes. + GMLAN ===== diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index 76c9cf110f1..44322cc050e 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -32,6 +32,11 @@ from scapy.packet import Packet, bind_layers, NoPayload from scapy.config import conf from scapy.contrib.isotp import ISOTP +from scapy.contrib.automotive.utils import ( + _make_service_decorator, + _make_single_layer_mode, +) +from scapy.compat import orb """ GMLAN @@ -46,7 +51,8 @@ # "a negative response 'RequestCorrectlyReceived-" # "ResponsePending' as answer of a request. \n" # "The default value is False.") - conf.contribs['GMLAN'] = {'treat-response-pending-as-answer': False} + conf.contribs['GMLAN'] = {'treat-response-pending-as-answer': False, + 'single_layer_GMLAN': False} conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = None @@ -130,8 +136,24 @@ def hashret(self): return struct.pack('B', self.requestServiceId & ~0x40) return struct.pack('B', self.service & ~0x40) + _service_cls = {} # type: ignore + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (...) -> type + """Dispatch to the correct GMLAN service class in single layer mode.""" + if conf.contribs['GMLAN'].get('single_layer_GMLAN', False) and len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls + + +_gmlan_service = _make_service_decorator(GMLAN, 'GMLAN', 'single_layer_GMLAN') +gmlan_single_layer_mode = _make_single_layer_mode(GMLAN, 'GMLAN', 'single_layer_GMLAN') + # ########################IDO################################### +@_gmlan_service(0x10) class GMLAN_IDO(Packet): subfunctions = { 0x02: 'disableAllDTCs', @@ -143,7 +165,6 @@ class GMLAN_IDO(Packet): ] -bind_layers(GMLAN, GMLAN_IDO, service=0x10) # ########################RFRD################################### @@ -160,6 +181,7 @@ def extract_padding(self, p): return "", p +@_gmlan_service(0x12) class GMLAN_RFRD(Packet): subfunctions = { 0x01: 'readFailureRecordIdentifiers', @@ -172,9 +194,9 @@ class GMLAN_RFRD(Packet): ] -bind_layers(GMLAN, GMLAN_RFRD, service=0x12) +@_gmlan_service(0x52) class GMLAN_RFRDPR(Packet): name = 'ReadFailureRecordDataPositiveResponse' fields_desc = [ @@ -186,7 +208,6 @@ def answers(self, other): other.subfunction == self.subfunction -bind_layers(GMLAN, GMLAN_RFRDPR, service=0x52) class GMLAN_RFRDPR_RFRI(Packet): @@ -216,6 +237,7 @@ class GMLAN_RFRDPR_RFRP(Packet): # ########################RDBI################################### +@_gmlan_service(0x1A) class GMLAN_RDBI(Packet): dataIdentifiers = ObservableDict({ 0x90: "$90: VehicleIdentificationNumber (VIN)", @@ -308,9 +330,9 @@ class GMLAN_RDBI(Packet): ] -bind_layers(GMLAN, GMLAN_RDBI, service=0x1A) +@_gmlan_service(0x5A) class GMLAN_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ @@ -322,10 +344,10 @@ def answers(self, other): other.dataIdentifier == self.dataIdentifier -bind_layers(GMLAN, GMLAN_RDBIPR, service=0x5A) # ########################RDBI################################### +@_gmlan_service(0x22) class GMLAN_RDBPI(Packet): dataIdentifiers = ObservableDict({ 0x0005: "OBD_EngineCoolantTemperature", @@ -340,9 +362,9 @@ class GMLAN_RDBPI(Packet): ] -bind_layers(GMLAN, GMLAN_RDBPI, service=0x22) +@_gmlan_service(0x62) class GMLAN_RDBPIPR(Packet): name = 'ReadDataByParameterIdentifierPositiveResponse' fields_desc = [ @@ -354,10 +376,10 @@ def answers(self, other): self.parameterIdentifier in other.identifiers -bind_layers(GMLAN, GMLAN_RDBPIPR, service=0x62) # ########################RDBPKTI################################### +@_gmlan_service(0xAA) class GMLAN_RDBPKTI(Packet): name = 'ReadDataByPacketIdentifier' subfunctions = { @@ -376,10 +398,10 @@ class GMLAN_RDBPKTI(Packet): ] -bind_layers(GMLAN, GMLAN_RDBPKTI, service=0xAA) # ########################RMBA################################### +@_gmlan_service(0x23) class GMLAN_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ @@ -397,9 +419,9 @@ class GMLAN_RMBA(Packet): ] -bind_layers(GMLAN, GMLAN_RMBA, service=0x23) +@_gmlan_service(0x63) class GMLAN_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ @@ -421,10 +443,10 @@ def answers(self, other): other.memoryAddress == self.memoryAddress -bind_layers(GMLAN, GMLAN_RMBAPR, service=0x63) # ########################SA################################### +@_gmlan_service(0x27) class GMLAN_SA(Packet): subfunctions = { 0: 'ReservedByDocument', @@ -449,9 +471,9 @@ class GMLAN_SA(Packet): ] -bind_layers(GMLAN, GMLAN_SA, service=0x27) +@_gmlan_service(0x67) class GMLAN_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ @@ -465,10 +487,10 @@ def answers(self, other): and other.subfunction == self.subfunction -bind_layers(GMLAN, GMLAN_SAPR, service=0x67) # ########################DDM################################### +@_gmlan_service(0x2C) class GMLAN_DDM(Packet): name = 'DynamicallyDefineMessage' fields_desc = [ @@ -477,9 +499,9 @@ class GMLAN_DDM(Packet): ] -bind_layers(GMLAN, GMLAN_DDM, service=0x2C) +@_gmlan_service(0x6C) class GMLAN_DDMPR(Packet): name = 'DynamicallyDefineMessagePositiveResponse' fields_desc = [ @@ -491,10 +513,10 @@ def answers(self, other): and other.DPIDIdentifier == self.DPIDIdentifier -bind_layers(GMLAN, GMLAN_DDMPR, service=0x6C) # ########################DPBA################################### +@_gmlan_service(0x2D) class GMLAN_DPBA(Packet): name = 'DefinePIDByAddress' fields_desc = [ @@ -513,9 +535,9 @@ class GMLAN_DPBA(Packet): ] -bind_layers(GMLAN, GMLAN_DPBA, service=0x2D) +@_gmlan_service(0x6D) class GMLAN_DPBAPR(Packet): name = 'DefinePIDByAddressPositiveResponse' fields_desc = [ @@ -527,10 +549,10 @@ def answers(self, other): and other.parameterIdentifier == self.parameterIdentifier -bind_layers(GMLAN, GMLAN_DPBAPR, service=0x6D) # ########################RD################################### +@_gmlan_service(0x34) class GMLAN_RD(Packet): name = 'RequestDownload' fields_desc = [ @@ -548,10 +570,10 @@ class GMLAN_RD(Packet): ] -bind_layers(GMLAN, GMLAN_RD, service=0x34) # ########################TD################################### +@_gmlan_service(0x36) class GMLAN_TD(Packet): subfunctions = { 0x00: "download", @@ -574,10 +596,10 @@ class GMLAN_TD(Packet): ] -bind_layers(GMLAN, GMLAN_TD, service=0x36) # ########################WDBI################################### +@_gmlan_service(0x3B) class GMLAN_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ @@ -586,9 +608,9 @@ class GMLAN_WDBI(Packet): ] -bind_layers(GMLAN, GMLAN_WDBI, service=0x3B) +@_gmlan_service(0x7B) class GMLAN_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ @@ -600,10 +622,10 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier -bind_layers(GMLAN, GMLAN_WDBIPR, service=0x7B) # ########################RPSPR################################### +@_gmlan_service(0xE2) class GMLAN_RPSPR(Packet): programmedStates = { 0x00: "fully programmed", @@ -623,10 +645,10 @@ class GMLAN_RPSPR(Packet): ] -bind_layers(GMLAN, GMLAN_RPSPR, service=0xE2) # ########################PM################################### +@_gmlan_service(0xA5) class GMLAN_PM(Packet): subfunctions = { 0x01: "requestProgrammingMode", @@ -639,10 +661,10 @@ class GMLAN_PM(Packet): ] -bind_layers(GMLAN, GMLAN_PM, service=0xA5) # ########################RDI################################### +@_gmlan_service(0xA9) class GMLAN_RDI(Packet): subfunctions = { 0x80: 'readStatusOfDTCByDTCNumber', @@ -655,7 +677,6 @@ class GMLAN_RDI(Packet): ] -bind_layers(GMLAN, GMLAN_RDI, service=0xA9) class GMLAN_RDI_BN(Packet): @@ -694,6 +715,7 @@ class GMLAN_RDI_BC(Packet): # ########################DC################################### +@_gmlan_service(0xAE) class GMLAN_DC(Packet): name = 'DeviceControl' fields_desc = [ @@ -702,9 +724,9 @@ class GMLAN_DC(Packet): ] -bind_layers(GMLAN, GMLAN_DC, service=0xAE) +@_gmlan_service(0xEE) class GMLAN_DCPR(Packet): name = 'DeviceControlPositiveResponse' fields_desc = [ @@ -716,10 +738,10 @@ def answers(self, other): and other.CPIDNumber == self.CPIDNumber -bind_layers(GMLAN, GMLAN_DCPR, service=0xEE) # ########################NRC################################### +@_gmlan_service(0x7f) class GMLAN_NR(Packet): negativeResponseCodes = { 0x11: 'ServiceNotSupported', @@ -751,4 +773,3 @@ def answers(self, other): conf.contribs['GMLAN']['treat-response-pending-as-answer']) -bind_layers(GMLAN, GMLAN_NR, service=0x7f) diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index 5617c1d7a23..15d03d89790 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -27,10 +27,14 @@ from scapy.utils import PeriodicSenderThread from scapy.plist import _PacketIterable from scapy.contrib.isotp import ISOTP +from scapy.contrib.automotive.utils import ( + _make_service_decorator, + _make_single_layer_mode, +) +from scapy.compat import orb from typing import ( Dict, - Any, ) @@ -43,7 +47,8 @@ "a negative response 'requestCorrectlyReceived-" "ResponsePending' as answer of a request. \n" "The default value is False.") - conf.contribs['KWP'] = {'treat-response-pending-as-answer': False} + conf.contribs['KWP'] = {'treat-response-pending-as-answer': False, + 'single_layer_KWP': False} class KWP(ISOTP): @@ -129,8 +134,24 @@ def hashret(self): else: return struct.pack('B', self.service & ~0x40) + _service_cls = {} # type: Dict[int, type] + + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (...) -> type + """Dispatch to the correct KWP service class in single layer mode.""" + if conf.contribs['KWP'].get('single_layer_KWP', False) and len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls + + +_kwp_service = _make_service_decorator(KWP, 'KWP', 'single_layer_KWP') +kwp_single_layer_mode = _make_single_layer_mode(KWP, 'KWP', 'single_layer_KWP') + # ########################SDS################################### +@_kwp_service(0x10) class KWP_SDS(Packet): diagnosticSessionTypes = ObservableDict({ 0x81: 'defaultSession', @@ -144,9 +165,9 @@ class KWP_SDS(Packet): ] -bind_layers(KWP, KWP_SDS, service=0x10) +@_kwp_service(0x50) class KWP_SDSPR(Packet): name = 'StartDiagnosticSessionPositiveResponse' fields_desc = [ @@ -160,10 +181,10 @@ def answers(self, other): other.diagnosticSession == self.diagnosticSession -bind_layers(KWP, KWP_SDSPR, service=0x50) # ######################### KWP_ER ################################### +@_kwp_service(0x11) class KWP_ER(Packet): resetModes = { 0x00: 'reserved', @@ -175,9 +196,9 @@ class KWP_ER(Packet): ] -bind_layers(KWP, KWP_ER, service=0x11) +@_kwp_service(0x51) class KWP_ERPR(Packet): name = 'ECUResetPositiveResponse' @@ -186,10 +207,10 @@ def answers(self, other): return isinstance(other, KWP_ER) -bind_layers(KWP, KWP_ERPR, service=0x51) # ######################### KWP_SA ################################### +@_kwp_service(0x27) class KWP_SA(Packet): name = 'SecurityAccess' fields_desc = [ @@ -199,9 +220,9 @@ class KWP_SA(Packet): ] -bind_layers(KWP, KWP_SA, service=0x27) +@_kwp_service(0x67) class KWP_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ @@ -216,10 +237,10 @@ def answers(self, other): and other.accessMode == self.accessMode -bind_layers(KWP, KWP_SAPR, service=0x67) # ######################### KWP_IOCBLI ################################### +@_kwp_service(0x30) class KWP_IOCBLI(Packet): name = 'InputOutputControlByLocalIdentifier' inputOutputControlParameters = { @@ -238,9 +259,9 @@ class KWP_IOCBLI(Packet): ] -bind_layers(KWP, KWP_IOCBLI, service=0x30) +@_kwp_service(0x70) class KWP_IOCBLIPR(Packet): name = 'InputOutputControlByLocalIdentifierPositiveResponse' fields_desc = [ @@ -256,10 +277,10 @@ def answers(self, other): and other.localIdentifier == self.localIdentifier -bind_layers(KWP, KWP_IOCBLIPR, service=0x70) # ######################### KWP_DNMT ################################### +@_kwp_service(0x28) class KWP_DNMT(Packet): responseTypes = { 0x01: 'responseRequired', @@ -271,9 +292,9 @@ class KWP_DNMT(Packet): ] -bind_layers(KWP, KWP_DNMT, service=0x28) +@_kwp_service(0x68) class KWP_DNMTPR(Packet): name = 'DisableNormalMessageTransmissionPositiveResponse' @@ -282,10 +303,10 @@ def answers(self, other): return isinstance(other, KWP_DNMT) -bind_layers(KWP, KWP_DNMTPR, service=0x68) # ######################### KWP_ENMT ################################### +@_kwp_service(0x29) class KWP_ENMT(Packet): responseTypes = { 0x01: 'responseRequired', @@ -297,9 +318,9 @@ class KWP_ENMT(Packet): ] -bind_layers(KWP, KWP_ENMT, service=0x29) +@_kwp_service(0x69) class KWP_ENMTPR(Packet): name = 'EnableNormalMessageTransmissionPositiveResponse' @@ -308,10 +329,10 @@ def answers(self, other): return isinstance(other, KWP_DNMT) -bind_layers(KWP, KWP_ENMTPR, service=0x69) # ######################### KWP_TP ################################### +@_kwp_service(0x3E) class KWP_TP(Packet): responseTypes = { 0x01: 'responseRequired', @@ -323,9 +344,9 @@ class KWP_TP(Packet): ] -bind_layers(KWP, KWP_TP, service=0x3E) +@_kwp_service(0x7E) class KWP_TPPR(Packet): name = 'TesterPresentPositiveResponse' @@ -334,10 +355,10 @@ def answers(self, other): return isinstance(other, KWP_TP) -bind_layers(KWP, KWP_TPPR, service=0x7E) # ######################### KWP_CDTCS ################################### +@_kwp_service(0x85) class KWP_CDTCS(Packet): responseTypes = { 0x01: 'responseRequired', @@ -363,9 +384,9 @@ class KWP_CDTCS(Packet): ] -bind_layers(KWP, KWP_CDTCS, service=0x85) +@_kwp_service(0xC5) class KWP_CDTCSPR(Packet): name = 'ControlDTCSettingPositiveResponse' @@ -374,10 +395,10 @@ def answers(self, other): return isinstance(other, KWP_CDTCS) -bind_layers(KWP, KWP_CDTCSPR, service=0xC5) # ######################### KWP_ROE ################################### +@_kwp_service(0x86) class KWP_ROE(Packet): responseTypes = { 0x01: 'responseRequired', @@ -409,9 +430,9 @@ class KWP_ROE(Packet): ] -bind_layers(KWP, KWP_ROE, service=0x86) +@_kwp_service(0xC6) class KWP_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' fields_desc = [ @@ -427,10 +448,10 @@ def answers(self, other): and other.eventType == self.eventType -bind_layers(KWP, KWP_ROEPR, service=0xC6) # ######################### KWP_RDBLI ################################### +@_kwp_service(0x21) class KWP_RDBLI(Packet): localIdentifiers = ObservableDict({ 0xE0: "Development Data", @@ -452,9 +473,9 @@ class KWP_RDBLI(Packet): ] -bind_layers(KWP, KWP_RDBLI, service=0x21) +@_kwp_service(0x61) class KWP_RDBLIPR(Packet): name = 'ReadDataByLocalIdentifierPositiveResponse' fields_desc = [ @@ -467,10 +488,10 @@ def answers(self, other): and self.recordLocalIdentifier == other.recordLocalIdentifier -bind_layers(KWP, KWP_RDBLIPR, service=0x61) # ######################### KWP_WDBLI ################################### +@_kwp_service(0x3B) class KWP_WDBLI(Packet): name = 'WriteDataByLocalIdentifier' fields_desc = [ @@ -478,9 +499,9 @@ class KWP_WDBLI(Packet): ] -bind_layers(KWP, KWP_WDBLI, service=0x3B) +@_kwp_service(0x7B) class KWP_WDBLIPR(Packet): name = 'WriteDataByLocalIdentifierPositiveResponse' fields_desc = [ @@ -493,10 +514,10 @@ def answers(self, other): and self.recordLocalIdentifier == other.recordLocalIdentifier -bind_layers(KWP, KWP_WDBLIPR, service=0x7B) # ######################### KWP_RDBI ################################### +@_kwp_service(0x22) class KWP_RDBI(Packet): dataIdentifiers = ObservableDict() name = 'ReadDataByIdentifier' @@ -505,9 +526,9 @@ class KWP_RDBI(Packet): ] -bind_layers(KWP, KWP_RDBI, service=0x22) +@_kwp_service(0x62) class KWP_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ @@ -520,10 +541,10 @@ def answers(self, other): and self.identifier == other.identifier -bind_layers(KWP, KWP_RDBIPR, service=0x62) # ######################### KWP_RMBA ################################### +@_kwp_service(0x23) class KWP_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ @@ -532,9 +553,9 @@ class KWP_RMBA(Packet): ] -bind_layers(KWP, KWP_RMBA, service=0x23) +@_kwp_service(0x63) class KWP_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ @@ -546,12 +567,12 @@ def answers(self, other): return isinstance(other, KWP_RMBA) -bind_layers(KWP, KWP_RMBAPR, service=0x63) # ######################### KWP_DDLI ################################### # TODO: Implement correct interpretation here, # instead of using just the dataRecord +@_kwp_service(0x2C) class KWP_DDLI(Packet): name = 'DynamicallyDefineLocalIdentifier' definitionModes = {0x1: "defineByLocalIdentifier", @@ -565,9 +586,9 @@ class KWP_DDLI(Packet): ] -bind_layers(KWP, KWP_DDLI, service=0x2C) +@_kwp_service(0x6C) class KWP_DDLIPR(Packet): name = 'DynamicallyDefineLocalIdentifierPositiveResponse' fields_desc = [ @@ -580,10 +601,10 @@ def answers(self, other): other.dynamicallyDefineLocalIdentifier == self.dynamicallyDefineLocalIdentifier # noqa: E501 -bind_layers(KWP, KWP_DDLIPR, service=0x6C) # ######################### KWP_WDBI ################################### +@_kwp_service(0x2E) class KWP_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ @@ -591,9 +612,9 @@ class KWP_WDBI(Packet): ] -bind_layers(KWP, KWP_WDBI, service=0x2E) +@_kwp_service(0x6E) class KWP_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ @@ -606,10 +627,10 @@ def answers(self, other): and other.identifier == self.identifier -bind_layers(KWP, KWP_WDBIPR, service=0x6E) # ######################### KWP_WMBA ################################### +@_kwp_service(0x3D) class KWP_WMBA(Packet): name = 'WriteMemoryByAddress' fields_desc = [ @@ -619,9 +640,9 @@ class KWP_WMBA(Packet): ] -bind_layers(KWP, KWP_WMBA, service=0x3D) +@_kwp_service(0x7D) class KWP_WMBAPR(Packet): name = 'WriteMemoryByAddressPositiveResponse' fields_desc = [ @@ -634,10 +655,10 @@ def answers(self, other): other.memoryAddress == self.memoryAddress -bind_layers(KWP, KWP_WMBAPR, service=0x7D) # ######################### KWP_CDI ################################### +@_kwp_service(0x14) class KWP_CDI(Packet): DTCGroups = { 0x0000: 'allPowertrainDTCs', @@ -652,9 +673,9 @@ class KWP_CDI(Packet): ] -bind_layers(KWP, KWP_CDI, service=0x14) +@_kwp_service(0x54) class KWP_CDIPR(Packet): name = 'ClearDiagnosticInformationPositiveResponse' @@ -668,10 +689,10 @@ def answers(self, other): self.groupOfDTC == other.groupOfDTC -bind_layers(KWP, KWP_CDIPR, service=0x54) # ######################### KWP_RSODTC ################################### +@_kwp_service(0x17) class KWP_RSODTC(Packet): name = 'ReadStatusOfDiagnosticTroubleCodes' fields_desc = [ @@ -679,9 +700,9 @@ class KWP_RSODTC(Packet): ] -bind_layers(KWP, KWP_RSODTC, service=0x17) +@_kwp_service(0x57) class KWP_RSODTCPR(Packet): name = 'ReadStatusOfDiagnosticTroubleCodesPositiveResponse' @@ -694,10 +715,10 @@ def answers(self, other): return isinstance(other, KWP_RSODTC) -bind_layers(KWP, KWP_RSODTCPR, service=0x57) # ######################### KWP_RECUI ################################### +@_kwp_service(0x1A) class KWP_RECUI(Packet): name = 'ReadECUIdentification' localIdentifiers = ObservableDict({ @@ -720,9 +741,9 @@ class KWP_RECUI(Packet): ] -bind_layers(KWP, KWP_RECUI, service=0x1A) +@_kwp_service(0x5A) class KWP_RECUIPR(Packet): name = 'ReadECUIdentificationPositiveResponse' @@ -736,10 +757,10 @@ def answers(self, other): self.localIdentifier == other.localIdentifier -bind_layers(KWP, KWP_RECUIPR, service=0x5A) # ######################### KWP_SRBLI ################################### +@_kwp_service(0x31) class KWP_SRBLI(Packet): routineLocalIdentifiers = ObservableDict({ 0xE0: "FlashEraseRoutine", @@ -759,9 +780,9 @@ class KWP_SRBLI(Packet): ] -bind_layers(KWP, KWP_SRBLI, service=0x31) +@_kwp_service(0x71) class KWP_SRBLIPR(Packet): name = 'StartRoutineByLocalIdentifierPositiveResponse' fields_desc = [ @@ -775,10 +796,10 @@ def answers(self, other): and other.routineLocalIdentifier == self.routineLocalIdentifier -bind_layers(KWP, KWP_SRBLIPR, service=0x71) # ######################### KWP_STRBLI ################################### +@_kwp_service(0x32) class KWP_STRBLI(Packet): name = 'StopRoutineByLocalIdentifier' fields_desc = [ @@ -787,9 +808,9 @@ class KWP_STRBLI(Packet): ] -bind_layers(KWP, KWP_STRBLI, service=0x32) +@_kwp_service(0x72) class KWP_STRBLIPR(Packet): name = 'StopRoutineByLocalIdentifierPositiveResponse' fields_desc = [ @@ -803,10 +824,10 @@ def answers(self, other): and other.routineLocalIdentifier == self.routineLocalIdentifier -bind_layers(KWP, KWP_STRBLIPR, service=0x72) # ######################### KWP_RRRBLI ################################### +@_kwp_service(0x33) class KWP_RRRBLI(Packet): name = 'RequestRoutineResultsByLocalIdentifier' fields_desc = [ @@ -815,9 +836,9 @@ class KWP_RRRBLI(Packet): ] -bind_layers(KWP, KWP_RRRBLI, service=0x33) +@_kwp_service(0x73) class KWP_RRRBLIPR(Packet): name = 'RequestRoutineResultsByLocalIdentifierPositiveResponse' fields_desc = [ @@ -831,10 +852,10 @@ def answers(self, other): and other.routineLocalIdentifier == self.routineLocalIdentifier -bind_layers(KWP, KWP_RRRBLIPR, service=0x73) # ######################### KWP_RD ################################### +@_kwp_service(0x34) class KWP_RD(Packet): name = 'RequestDownload' fields_desc = [ @@ -845,9 +866,9 @@ class KWP_RD(Packet): ] -bind_layers(KWP, KWP_RD, service=0x34) +@_kwp_service(0x74) class KWP_RDPR(Packet): name = 'RequestDownloadPositiveResponse' fields_desc = [ @@ -859,10 +880,10 @@ def answers(self, other): return isinstance(other, KWP_RD) -bind_layers(KWP, KWP_RDPR, service=0x74) # ######################### KWP_RU ################################### +@_kwp_service(0x35) class KWP_RU(Packet): name = 'RequestUpload' fields_desc = [ @@ -873,9 +894,9 @@ class KWP_RU(Packet): ] -bind_layers(KWP, KWP_RU, service=0x35) +@_kwp_service(0x75) class KWP_RUPR(Packet): name = 'RequestUploadPositiveResponse' fields_desc = [ @@ -887,10 +908,10 @@ def answers(self, other): return isinstance(other, KWP_RU) -bind_layers(KWP, KWP_RUPR, service=0x75) # ######################### KWP_TD ################################### +@_kwp_service(0x36) class KWP_TD(Packet): name = 'TransferData' fields_desc = [ @@ -899,9 +920,9 @@ class KWP_TD(Packet): ] -bind_layers(KWP, KWP_TD, service=0x36) +@_kwp_service(0x76) class KWP_TDPR(Packet): name = 'TransferDataPositiveResponse' fields_desc = [ @@ -915,10 +936,10 @@ def answers(self, other): and other.blockSequenceCounter == self.blockSequenceCounter -bind_layers(KWP, KWP_TDPR, service=0x76) # ######################### KWP_RTE ################################### +@_kwp_service(0x37) class KWP_RTE(Packet): name = 'RequestTransferExit' fields_desc = [ @@ -926,9 +947,9 @@ class KWP_RTE(Packet): ] -bind_layers(KWP, KWP_RTE, service=0x37) +@_kwp_service(0x77) class KWP_RTEPR(Packet): name = 'RequestTransferExitPositiveResponse' fields_desc = [ @@ -940,10 +961,10 @@ def answers(self, other): return isinstance(other, KWP_RTE) -bind_layers(KWP, KWP_RTEPR, service=0x77) # ######################### KWP_NR ################################### +@_kwp_service(0x7f) class KWP_NR(Packet): negativeResponseCodes = { 0x00: 'positiveResponse', @@ -982,7 +1003,6 @@ def answers(self, other): conf.contribs['KWP']['treat-response-pending-as-answer']) -bind_layers(KWP, KWP_NR, service=0x7f) # ################################################################## diff --git a/scapy/contrib/automotive/obd/obd.py b/scapy/contrib/automotive/obd/obd.py index a165935d2c3..32c6c903f95 100644 --- a/scapy/contrib/automotive/obd/obd.py +++ b/scapy/contrib/automotive/obd/obd.py @@ -14,10 +14,15 @@ from scapy.contrib.automotive.obd.pid.pids import * from scapy.contrib.automotive.obd.tid.tids import * from scapy.contrib.automotive.obd.services import * -from scapy.packet import bind_layers, NoPayload +from scapy.packet import NoPayload from scapy.config import conf from scapy.fields import XByteEnumField from scapy.contrib.isotp import ISOTP +from scapy.contrib.automotive.utils import ( + _make_service_decorator, + _make_single_layer_mode, +) +from scapy.compat import orb try: if conf.contribs['OBD']['treat-response-pending-as-answer']: @@ -28,7 +33,8 @@ # "a negative response 'requestCorrectlyReceived-" # "ResponsePending' as answer of a request. \n" # "The default value is False.") - conf.contribs['OBD'] = {'treat-response-pending-as-answer': False} + conf.contribs['OBD'] = {'treat-response-pending-as-answer': False, + 'single_layer_OBD': False} class OBD(ISOTP): @@ -79,26 +85,41 @@ def answers(self, other): return self.payload.answers(other.payload) return False + _service_cls = {} # type: ignore -# Service Bindings - -bind_layers(OBD, OBD_S01, service=0x01) -bind_layers(OBD, OBD_S02, service=0x02) -bind_layers(OBD, OBD_S03, service=0x03) -bind_layers(OBD, OBD_S04, service=0x04) -bind_layers(OBD, OBD_S06, service=0x06) -bind_layers(OBD, OBD_S07, service=0x07) -bind_layers(OBD, OBD_S08, service=0x08) -bind_layers(OBD, OBD_S09, service=0x09) -bind_layers(OBD, OBD_S0A, service=0x0A) - -bind_layers(OBD, OBD_S01_PR, service=0x41) -bind_layers(OBD, OBD_S02_PR, service=0x42) -bind_layers(OBD, OBD_S03_PR, service=0x43) -bind_layers(OBD, OBD_S04_PR, service=0x44) -bind_layers(OBD, OBD_S06_PR, service=0x46) -bind_layers(OBD, OBD_S07_PR, service=0x47) -bind_layers(OBD, OBD_S08_PR, service=0x48) -bind_layers(OBD, OBD_S09_PR, service=0x49) -bind_layers(OBD, OBD_S0A_PR, service=0x4A) -bind_layers(OBD, OBD_NR, service=0x7F) + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + # type: (...) -> type + """Dispatch to the correct OBD service class in single layer mode.""" + if conf.contribs['OBD'].get('single_layer_OBD', False) and len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls + + +_obd_service = _make_service_decorator(OBD, 'OBD', 'single_layer_OBD') +obd_single_layer_mode = _make_single_layer_mode(OBD, 'OBD', 'single_layer_OBD') + +# Service Bindings — applied via the generic decorator (functional form, +# since the service classes are defined in a separate module) + +_obd_service(0x01)(OBD_S01) +_obd_service(0x02)(OBD_S02) +_obd_service(0x03)(OBD_S03) +_obd_service(0x04)(OBD_S04) +_obd_service(0x06)(OBD_S06) +_obd_service(0x07)(OBD_S07) +_obd_service(0x08)(OBD_S08) +_obd_service(0x09)(OBD_S09) +_obd_service(0x0A)(OBD_S0A) + +_obd_service(0x41)(OBD_S01_PR) +_obd_service(0x42)(OBD_S02_PR) +_obd_service(0x43)(OBD_S03_PR) +_obd_service(0x44)(OBD_S04_PR) +_obd_service(0x46)(OBD_S06_PR) +_obd_service(0x47)(OBD_S07_PR) +_obd_service(0x48)(OBD_S08_PR) +_obd_service(0x49)(OBD_S09_PR) +_obd_service(0x4A)(OBD_S0A_PR) +_obd_service(0x7F)(OBD_NR) diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 88ed042524f..663575852ad 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -19,15 +19,18 @@ ShortField, ObservableDict, XShortEnumField, XByteEnumField, StrLenField, \ FieldLenField, XStrFixedLenField, XStrLenField, FlagsField, PacketListField, \ PacketField -from scapy.packet import Packet, bind_layers, split_layers, NoPayload, Raw +from scapy.packet import Packet, bind_layers, NoPayload, Raw from scapy.compat import orb from scapy.config import conf from scapy.utils import PeriodicSenderThread from scapy.contrib.isotp import ISOTP +from scapy.contrib.automotive.utils import ( + _make_service_decorator, + _make_single_layer_mode, +) # Typing imports from typing import ( - Any, Dict, Union, ) @@ -129,52 +132,19 @@ def hashret(self): return struct.pack('B', bytes(self)[1] & ~0x40) return struct.pack('B', self.service & ~0x40) - _uds_service_cls = {} # type: Dict[int, type] + _service_cls = {} # type: Dict[int, type] @classmethod def dispatch_hook(cls, _pkt=b"", *args, **kwargs): - # type: (bytes, Any, Any) -> type + # type: (...) -> type """Dispatch to the correct UDS service class in single layer mode.""" if conf.contribs['UDS'].get('single_layer_UDS', False) and len(_pkt) >= 1: service = orb(_pkt[0]) - return cls._uds_service_cls.get(service, cls) + return cls._service_cls.get(service, cls) return cls -def _uds_service(service_id): - # type: (int) -> Any - """Class decorator to register a UDS service layer. - - Prepends a conditional 'service' field (active only in single layer mode), - registers the class in the UDS dispatch table, and conditionally binds - the layer to UDS based on the 'single_layer_UDS' config flag. - """ - def decorator(cls): - # type: (type) -> type - # Prepend conditional service field to the class - svc_field = ConditionalField( - XByteEnumField('service', service_id, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_UDS', False) - ) - cls.fields_desc = [svc_field] + list(cls.fields_desc) - # Register in UDS dispatch table for single layer mode - UDS._uds_service_cls[service_id] = cls - # In multi-layer mode, bind to UDS (for backward compatibility) - if not conf.contribs['UDS'].get('single_layer_UDS', False): - bind_layers(UDS, cls, service=service_id) - # _sid captures service_id by value to avoid late-binding closure issues - _sid = service_id - - def _hashret(self): - # type: () -> bytes - if conf.contribs['UDS'].get('single_layer_UDS', False): - return struct.pack('B', _sid & ~0x40) - return Packet.hashret(self) - - if 'hashret' not in cls.__dict__: - cls.hashret = _hashret - return cls - return decorator +_uds_service = _make_service_decorator(UDS, 'UDS', 'single_layer_UDS') def uds_single_layer_mode(enable=True): @@ -202,12 +172,7 @@ def uds_single_layer_mode(enable=True): >>> UDS(b'\\x10\\x01') """ - conf.contribs['UDS']['single_layer_UDS'] = enable - for service_id, cls in UDS._uds_service_cls.items(): - # Always split first to ensure idempotency (no duplicate bindings) - split_layers(UDS, cls, service=service_id) - if not enable: - bind_layers(UDS, cls, service=service_id) + _make_single_layer_mode(UDS, 'UDS', 'single_layer_UDS')(enable) # ########################DSC################################### diff --git a/scapy/contrib/automotive/utils.py b/scapy/contrib/automotive/utils.py new file mode 100644 index 00000000000..34457802de8 --- /dev/null +++ b/scapy/contrib/automotive/utils.py @@ -0,0 +1,174 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# scapy.contrib.status = skip + +""" +Generic utilities for automotive protocol single layer mode support. + +These helpers allow automotive protocol base classes (UDS, KWP, OBD, GMLAN …) +to offer a *single layer mode* where every service packet (e.g. ``UDS_DSC``) is +returned directly by the base-class dispatcher instead of being nested inside +the parent layer. + +To add single layer support to a protocol: + +1. Add a ``_service_cls = {}`` class attribute and a ``dispatch_hook`` + classmethod to the base class (see :func:`_make_dispatch_hook`):: + + class KWP(ISOTP): + _service_cls = {} + ... + @classmethod + def dispatch_hook(cls, _pkt=b"", *args, **kwargs): + return _make_dispatch_hook('KWP', 'single_layer_KWP')( + cls, _pkt, *args, **kwargs) + +2. Create the per-protocol decorator and mode-toggle with the factories:: + + _kwp_service = _make_service_decorator(KWP, 'KWP', 'single_layer_KWP') + kwp_single_layer_mode = _make_single_layer_mode(KWP, 'KWP', 'single_layer_KWP') + +3. Decorate every service subpacket instead of calling ``bind_layers``:: + + @_kwp_service(0x10) + class KWP_SDS(Packet): + ... + + For classes defined in a separate module (e.g. OBD), apply the decorator + post-definition using the functional form:: + + _obd_service(0x01)(OBD_S01) +""" + +import struct + +from typing import Any + +from scapy.compat import orb +from scapy.config import conf +from scapy.fields import ConditionalField, XByteEnumField +from scapy.packet import Packet, bind_layers, split_layers + + +def _make_service_decorator(base_cls, conf_contrib_key, single_layer_flag): + # type: (type, str, str) -> Any + """Return a class-decorator factory for an automotive protocol service. + + The returned decorator factory accepts a *service_id* integer and returns + a class decorator that: + + 1. Prepends a conditional ``service`` field (visible only in single layer + mode) to the subpacket's ``fields_desc``. + 2. Registers the class in ``base_cls._service_cls`` so the + ``dispatch_hook`` can route raw bytes to the correct type. + 3. Calls :func:`~scapy.packet.bind_layers` to link the subpacket to + *base_cls* (multi-layer mode). When the module is loaded with + single layer mode already enabled the binding is skipped because + ``dispatch_hook`` handles dissection. + 4. Injects a ``hashret`` method that returns the correct value for + request/response matching in single layer mode (unless the class + already defines its own ``hashret``). + + Args: + base_cls: The base protocol class (e.g. ``UDS``, ``KWP``). + conf_contrib_key: Key used in :attr:`~scapy.config.conf.contribs` + (e.g. ``'UDS'``, ``'KWP'``). + single_layer_flag: Flag name inside + ``conf.contribs[conf_contrib_key]`` + (e.g. ``'single_layer_UDS'``). + + Returns: + A ``service_decorator(service_id)`` factory function. + + Example:: + + _kwp_service = _make_service_decorator(KWP, 'KWP', 'single_layer_KWP') + + @_kwp_service(0x10) + class KWP_SDS(Packet): + ... + """ + _base = base_cls + _key = conf_contrib_key + _flag = single_layer_flag + + def service_decorator(service_id): + # type: (int) -> Any + def decorator(cls): + # type: (type) -> type + # Prepend a conditional service field so that in single layer mode + # the service byte is part of the subpacket itself. + svc_field = ConditionalField( + XByteEnumField('service', service_id, _base.services), + lambda pkt: conf.contribs[_key].get(_flag, False) + ) + cls.fields_desc = [svc_field] + list(cls.fields_desc) + # Register in base class dispatch table for single layer mode. + _base._service_cls[service_id] = cls + # In multi-layer mode bind to base class for backward compatibility. + if not conf.contribs[_key].get(_flag, False): + bind_layers(_base, cls, service=service_id) + # Capture service_id by value to avoid late-binding closure issues. + _sid = service_id + + def _hashret(self): + # type: () -> bytes + if conf.contribs[_key].get(_flag, False): + return struct.pack('B', _sid & ~0x40) + return Packet.hashret(self) + + if 'hashret' not in cls.__dict__: + cls.hashret = _hashret + return cls + return decorator + return service_decorator + + +def _make_single_layer_mode(base_cls, conf_contrib_key, single_layer_flag): + # type: (type, str, str) -> Any + """Return a function that enables or disables single layer mode. + + The returned function, when called with ``enable=True`` (default), removes + the :func:`~scapy.packet.bind_layers` associations between *base_cls* and + its service subclasses so that ``dispatch_hook`` takes over dissection. + When called with ``enable=False``, the traditional multi-layer bindings + are restored. + + The function is idempotent: calling it multiple times with the same + argument is safe (no duplicate bindings are created). + + Args: + base_cls: The base protocol class (e.g. ``UDS``, ``KWP``). + conf_contrib_key: Key used in :attr:`~scapy.config.conf.contribs`. + single_layer_flag: Flag name inside + ``conf.contribs[conf_contrib_key]``. + + Returns: + A ``single_layer_mode(enable=True)`` toggle function. + + Example:: + + kwp_single_layer_mode = _make_single_layer_mode( + KWP, 'KWP', 'single_layer_KWP') + + >>> kwp_single_layer_mode(True) + >>> KWP(b'\\x10\\x01') + + >>> kwp_single_layer_mode(False) # revert to multi-layer mode + """ + _base = base_cls + _key = conf_contrib_key + _flag = single_layer_flag + + def single_layer_mode(enable=True): + # type: (bool) -> None + conf.contribs[_key][_flag] = enable + for service_id, cls in _base._service_cls.items(): + # Always split first to ensure idempotency (no duplicate bindings). + split_layers(_base, cls, service=service_id) + if not enable: + bind_layers(_base, cls, service=service_id) + + return single_layer_mode diff --git a/test/contrib/automotive/gm/gmlan.uts b/test/contrib/automotive/gm/gmlan.uts index 722b2bc974d..d994ffa0370 100644 --- a/test/contrib/automotive/gm/gmlan.uts +++ b/test/contrib/automotive/gm/gmlan.uts @@ -612,4 +612,75 @@ log = get_log(pkt) print(log) assert len(log) == 2 assert log[1] == "0x80" -assert log[0] == "DeviceControlPositiveResponse" \ No newline at end of file +assert log[0] == "DeviceControlPositiveResponse" ++ Single layer GMLAN mode + += Single layer mode: load helper + +from scapy.contrib.automotive.gm.gmlan import gmlan_single_layer_mode + += Single layer mode: enable and basic dissect + +gmlan_single_layer_mode(True) + +ido = GMLAN(b'\x10\x02') +assert isinstance(ido, GMLAN_IDO), "Expected GMLAN_IDO, got %s" % type(ido) +assert ido.service == 0x10 +assert ido.subfunction == 0x02 + += Single layer mode: build GMLAN_IDO + +ido_built = GMLAN_IDO(subfunction=0x02) +assert bytes(ido_built) == b'\x10\x02', "Expected b'\\x10\\x02', got %s" % bytes(ido_built).hex() + += Single layer mode: dissect positive response (using SA which has a PR class) + +sapr = GMLAN(b'\x67\x01\xde\xad') +assert isinstance(sapr, GMLAN_SAPR), "Expected GMLAN_SAPR, got %s" % type(sapr) +assert sapr.service == 0x67 +assert sapr.subfunction == 0x01 + += Single layer mode: NegativeResponse dissect + +nr = GMLAN(b'\x7f\x10\x22') +assert isinstance(nr, GMLAN_NR), "Expected GMLAN_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.requestServiceId == 0x10 +assert nr.returnCode == 0x22 + += Single layer mode: NegativeResponse answers() + +ido2 = GMLAN_IDO(subfunction=0x02) +nr2 = GMLAN_NR(requestServiceId=0x10, returnCode=0x22) +assert nr2.answers(ido2) + += Single layer mode: hashret consistency between request and positive response (SA) + +sa3 = GMLAN_SA(subfunction=0x01) +sapr3 = GMLAN_SAPR(subfunction=0x01) +assert sa3.hashret() == sapr3.hashret(), \ + "hashret mismatch: %s vs %s" % (sa3.hashret().hex(), sapr3.hashret().hex()) + += Single layer mode: sub-subpacket bindings are unaffected + +rfrdpr = GMLAN(b'\x52\x01\x00\x01\x02\x03') +assert isinstance(rfrdpr, GMLAN_RFRDPR), "Expected GMLAN_RFRDPR, got %s" % type(rfrdpr) + += Single layer mode: unknown service falls back to GMLAN + +unknown = GMLAN(b'\xBB\x01\x02') +assert isinstance(unknown, GMLAN), "Expected GMLAN fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +gmlan_single_layer_mode(False) + +ido4 = GMLAN(b'\x10\x02') +assert ido4.__class__ == GMLAN +assert ido4.service == 0x10 +assert ido4[GMLAN_IDO].subfunction == 0x02 + += Single layer mode: cleanup + +gmlan_single_layer_mode(False) +assert not conf.contribs['GMLAN']['single_layer_GMLAN'] diff --git a/test/contrib/automotive/kwp.uts b/test/contrib/automotive/kwp.uts index d525b8554e6..32a365a012b 100644 --- a/test/contrib/automotive/kwp.uts +++ b/test/contrib/automotive/kwp.uts @@ -507,3 +507,91 @@ nrc = KWP(b'\x7f\x22\x33') assert nrc.service == 0x7f assert nrc.requestServiceId == 0x22 assert nrc.negativeResponseCode == 0x33 + ++ Single layer KWP mode + += Single layer mode: load helper + +from scapy.contrib.automotive.kwp import kwp_single_layer_mode + += Single layer mode: enable and basic dissect + +kwp_single_layer_mode(True) + +sds = KWP(b'\x10\x01') +assert isinstance(sds, KWP_SDS), "Expected KWP_SDS, got %s" % type(sds) +assert sds.service == 0x10 +assert sds.diagnosticSession == 0x01 + += Single layer mode: build KWP_SDS + +sds_built = KWP_SDS(diagnosticSession=0x01) +assert bytes(sds_built) == b'\x10\x01', "Expected b'\\x10\\x01', got %s" % bytes(sds_built).hex() + += Single layer mode: dissect positive response + +sdspr = KWP(b'\x50\x01\xbe\xef') +assert isinstance(sdspr, KWP_SDSPR), "Expected KWP_SDSPR, got %s" % type(sdspr) +assert sdspr.service == 0x50 +assert sdspr.diagnosticSession == 0x01 + += Single layer mode: answers() between subpackets + +sds2 = KWP_SDS(diagnosticSession=0x01) +sdspr2 = KWP_SDSPR(diagnosticSession=0x01) +assert sdspr2.answers(sds2) + += Single layer mode: NegativeResponse dissect + +nr = KWP(b'\x7f\x10\x22') +assert isinstance(nr, KWP_NR), "Expected KWP_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.requestServiceId == 0x10 +assert nr.negativeResponseCode == 0x22 + += Single layer mode: NegativeResponse answers() + +sds3 = KWP_SDS(diagnosticSession=0x01) +nr2 = KWP_NR(requestServiceId=0x10, negativeResponseCode=0x22) +assert nr2.answers(sds3) + += Single layer mode: hashret consistency between request and positive response + +sds4 = KWP_SDS(diagnosticSession=0x01) +sdspr4 = KWP_SDSPR(diagnosticSession=0x01) +assert sds4.hashret() == sdspr4.hashret(), \ + "hashret mismatch: %s vs %s" % (sds4.hashret().hex(), sdspr4.hashret().hex()) + += Single layer mode: unknown service falls back to KWP + +unknown = KWP(b'\xAA\x01\x02') +assert isinstance(unknown, KWP), "Expected KWP fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +kwp_single_layer_mode(False) + +sds5 = KWP(b'\x10\x01') +assert sds5.__class__ == KWP +assert sds5.service == 0x10 +assert sds5[KWP_SDS].diagnosticSession == 0x01 + += Single layer mode: idempotency + +kwp_single_layer_mode(True) +kwp_single_layer_mode(True) +sds6 = KWP(b'\x10\x01') +assert isinstance(sds6, KWP_SDS) + +kwp_single_layer_mode(False) +kwp_single_layer_mode(False) +sds7 = KWP(b'\x10\x01') +assert sds7.__class__ == KWP +count = sum(1 for fval, cls in KWP.payload_guess + if fval.get('service') == 0x10 and cls == KWP_SDS) +assert count == 1, "Expected 1 binding for KWP_SDS, got %d" % count + += Single layer mode: cleanup + +kwp_single_layer_mode(False) +assert not conf.contribs['KWP']['single_layer_KWP'] diff --git a/test/contrib/automotive/obd/obd.uts b/test/contrib/automotive/obd/obd.uts index fa65e95e447..fca51e3821c 100644 --- a/test/contrib/automotive/obd/obd.uts +++ b/test/contrib/automotive/obd/obd.uts @@ -1031,3 +1031,68 @@ assert b[22:] == b'ABCDEFGHIJKLMNOP' r = OBD(b'\x09\x02\x04') assert p.answers(r) + ++ Single layer OBD mode + += Single layer mode: load helper + +from scapy.contrib.automotive.obd.obd import obd_single_layer_mode + += Single layer mode: enable and basic dissect + +obd_single_layer_mode(True) + +s01 = OBD(b'\x01\x0c') +assert isinstance(s01, OBD_S01), "Expected OBD_S01, got %s" % type(s01) +assert s01.service == 0x01 + += Single layer mode: build OBD_S01 + +s01_built = OBD_S01(pid=[0x0c]) +assert bytes(s01_built) == b'\x01\x0c', "Expected b'\\x01\\x0c', got %s" % bytes(s01_built).hex() + += Single layer mode: dissect positive response + +s01pr = OBD(b'\x41\x0c\x0f\xa0') +assert isinstance(s01pr, OBD_S01_PR), "Expected OBD_S01_PR, got %s" % type(s01pr) +assert s01pr.service == 0x41 + += Single layer mode: NegativeResponse dissect + +nr = OBD(b'\x7f\x01\x22') +assert isinstance(nr, OBD_NR), "Expected OBD_NR, got %s" % type(nr) +assert nr.service == 0x7f +assert nr.request_service_id == 0x01 +assert nr.response_code == 0x22 + += Single layer mode: NegativeResponse answers() + +s01_2 = OBD_S01(pid=[0x0c]) +nr2 = OBD_NR(request_service_id=0x01, response_code=0x22) +assert nr2.answers(s01_2) + += Single layer mode: hashret consistency between request and positive response + +s09 = OBD_S09(iid=[0x02]) +s09pr = OBD_S09_PR() +assert s09.hashret() == s09pr.hashret(), \ + "hashret mismatch: %s vs %s" % (s09.hashret().hex(), s09pr.hashret().hex()) + += Single layer mode: unknown service falls back to OBD + +unknown = OBD(b'\xBB\x01\x02') +assert isinstance(unknown, OBD), "Expected OBD fallback, got %s" % type(unknown) + += Single layer mode: switch back to multi-layer mode + +obd_single_layer_mode(False) + +s01_3 = OBD(b'\x01\x0c') +assert s01_3.__class__ == OBD +assert s01_3.service == 0x01 +assert isinstance(s01_3[OBD_S01], OBD_S01) + += Single layer mode: cleanup + +obd_single_layer_mode(False) +assert not conf.contribs['OBD']['single_layer_OBD'] From 7ea604d5b00481841c0103cd812b6a31c5c62b8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:49:07 +0000 Subject: [PATCH 06/16] Fix flake8 and mypy issues introduced by generic service decorator changes Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/84433859-6054-4e3c-a058-5bd51c1fcb56 Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- scapy/contrib/automotive/gm/gmlan.py | 54 +------------ scapy/contrib/automotive/kwp.py | 111 +-------------------------- scapy/contrib/automotive/obd/obd.py | 2 +- scapy/contrib/automotive/uds.py | 2 +- scapy/contrib/automotive/utils.py | 20 ++--- 5 files changed, 13 insertions(+), 176 deletions(-) diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index 44322cc050e..9df34e5dde9 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -52,7 +52,7 @@ # "ResponsePending' as answer of a request. \n" # "The default value is False.") conf.contribs['GMLAN'] = {'treat-response-pending-as-answer': False, - 'single_layer_GMLAN': False} + 'single_layer_GMLAN': False} conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = None @@ -165,8 +165,6 @@ class GMLAN_IDO(Packet): ] - - # ########################RFRD################################### class GMLAN_DTC(Packet): name = 'GMLAN DTC information' @@ -194,8 +192,6 @@ class GMLAN_RFRD(Packet): ] - - @_gmlan_service(0x52) class GMLAN_RFRDPR(Packet): name = 'ReadFailureRecordDataPositiveResponse' @@ -208,8 +204,6 @@ def answers(self, other): other.subfunction == self.subfunction - - class GMLAN_RFRDPR_RFRI(Packet): failureRecordDataStructureIdentifiers = { 0x00: "PID", @@ -330,8 +324,6 @@ class GMLAN_RDBI(Packet): ] - - @_gmlan_service(0x5A) class GMLAN_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' @@ -344,8 +336,6 @@ def answers(self, other): other.dataIdentifier == self.dataIdentifier - - # ########################RDBI################################### @_gmlan_service(0x22) class GMLAN_RDBPI(Packet): @@ -362,8 +352,6 @@ class GMLAN_RDBPI(Packet): ] - - @_gmlan_service(0x62) class GMLAN_RDBPIPR(Packet): name = 'ReadDataByParameterIdentifierPositiveResponse' @@ -376,8 +364,6 @@ def answers(self, other): self.parameterIdentifier in other.identifiers - - # ########################RDBPKTI################################### @_gmlan_service(0xAA) class GMLAN_RDBPKTI(Packet): @@ -398,8 +384,6 @@ class GMLAN_RDBPKTI(Packet): ] - - # ########################RMBA################################### @_gmlan_service(0x23) class GMLAN_RMBA(Packet): @@ -419,8 +403,6 @@ class GMLAN_RMBA(Packet): ] - - @_gmlan_service(0x63) class GMLAN_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' @@ -443,8 +425,6 @@ def answers(self, other): other.memoryAddress == self.memoryAddress - - # ########################SA################################### @_gmlan_service(0x27) class GMLAN_SA(Packet): @@ -471,8 +451,6 @@ class GMLAN_SA(Packet): ] - - @_gmlan_service(0x67) class GMLAN_SAPR(Packet): name = 'SecurityAccessPositiveResponse' @@ -487,8 +465,6 @@ def answers(self, other): and other.subfunction == self.subfunction - - # ########################DDM################################### @_gmlan_service(0x2C) class GMLAN_DDM(Packet): @@ -499,8 +475,6 @@ class GMLAN_DDM(Packet): ] - - @_gmlan_service(0x6C) class GMLAN_DDMPR(Packet): name = 'DynamicallyDefineMessagePositiveResponse' @@ -513,8 +487,6 @@ def answers(self, other): and other.DPIDIdentifier == self.DPIDIdentifier - - # ########################DPBA################################### @_gmlan_service(0x2D) class GMLAN_DPBA(Packet): @@ -535,8 +507,6 @@ class GMLAN_DPBA(Packet): ] - - @_gmlan_service(0x6D) class GMLAN_DPBAPR(Packet): name = 'DefinePIDByAddressPositiveResponse' @@ -549,8 +519,6 @@ def answers(self, other): and other.parameterIdentifier == self.parameterIdentifier - - # ########################RD################################### @_gmlan_service(0x34) class GMLAN_RD(Packet): @@ -570,8 +538,6 @@ class GMLAN_RD(Packet): ] - - # ########################TD################################### @_gmlan_service(0x36) class GMLAN_TD(Packet): @@ -596,8 +562,6 @@ class GMLAN_TD(Packet): ] - - # ########################WDBI################################### @_gmlan_service(0x3B) class GMLAN_WDBI(Packet): @@ -608,8 +572,6 @@ class GMLAN_WDBI(Packet): ] - - @_gmlan_service(0x7B) class GMLAN_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' @@ -622,8 +584,6 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier - - # ########################RPSPR################################### @_gmlan_service(0xE2) class GMLAN_RPSPR(Packet): @@ -645,8 +605,6 @@ class GMLAN_RPSPR(Packet): ] - - # ########################PM################################### @_gmlan_service(0xA5) class GMLAN_PM(Packet): @@ -661,8 +619,6 @@ class GMLAN_PM(Packet): ] - - # ########################RDI################################### @_gmlan_service(0xA9) class GMLAN_RDI(Packet): @@ -677,8 +633,6 @@ class GMLAN_RDI(Packet): ] - - class GMLAN_RDI_BN(Packet): name = 'ReadStatusOfDTCByDTCNumber' fields_desc = [ @@ -724,8 +678,6 @@ class GMLAN_DC(Packet): ] - - @_gmlan_service(0xEE) class GMLAN_DCPR(Packet): name = 'DeviceControlPositiveResponse' @@ -738,8 +690,6 @@ def answers(self, other): and other.CPIDNumber == self.CPIDNumber - - # ########################NRC################################### @_gmlan_service(0x7f) class GMLAN_NR(Packet): @@ -771,5 +721,3 @@ def answers(self, other): return self.requestServiceId == other.service and \ (self.returnCode != 0x78 or conf.contribs['GMLAN']['treat-response-pending-as-answer']) - - diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index 15d03d89790..18706798f39 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -21,7 +21,7 @@ XByteField, XShortEnumField, ) -from scapy.packet import Packet, bind_layers, NoPayload +from scapy.packet import Packet, NoPayload from scapy.config import conf from scapy.error import log_loading from scapy.utils import PeriodicSenderThread @@ -34,6 +34,7 @@ from scapy.compat import orb from typing import ( + Any, Dict, ) @@ -48,7 +49,7 @@ "ResponsePending' as answer of a request. \n" "The default value is False.") conf.contribs['KWP'] = {'treat-response-pending-as-answer': False, - 'single_layer_KWP': False} + 'single_layer_KWP': False} class KWP(ISOTP): @@ -165,8 +166,6 @@ class KWP_SDS(Packet): ] - - @_kwp_service(0x50) class KWP_SDSPR(Packet): name = 'StartDiagnosticSessionPositiveResponse' @@ -181,8 +180,6 @@ def answers(self, other): other.diagnosticSession == self.diagnosticSession - - # ######################### KWP_ER ################################### @_kwp_service(0x11) class KWP_ER(Packet): @@ -196,8 +193,6 @@ class KWP_ER(Packet): ] - - @_kwp_service(0x51) class KWP_ERPR(Packet): name = 'ECUResetPositiveResponse' @@ -207,8 +202,6 @@ def answers(self, other): return isinstance(other, KWP_ER) - - # ######################### KWP_SA ################################### @_kwp_service(0x27) class KWP_SA(Packet): @@ -220,8 +213,6 @@ class KWP_SA(Packet): ] - - @_kwp_service(0x67) class KWP_SAPR(Packet): name = 'SecurityAccessPositiveResponse' @@ -237,8 +228,6 @@ def answers(self, other): and other.accessMode == self.accessMode - - # ######################### KWP_IOCBLI ################################### @_kwp_service(0x30) class KWP_IOCBLI(Packet): @@ -259,8 +248,6 @@ class KWP_IOCBLI(Packet): ] - - @_kwp_service(0x70) class KWP_IOCBLIPR(Packet): name = 'InputOutputControlByLocalIdentifierPositiveResponse' @@ -277,8 +264,6 @@ def answers(self, other): and other.localIdentifier == self.localIdentifier - - # ######################### KWP_DNMT ################################### @_kwp_service(0x28) class KWP_DNMT(Packet): @@ -292,8 +277,6 @@ class KWP_DNMT(Packet): ] - - @_kwp_service(0x68) class KWP_DNMTPR(Packet): name = 'DisableNormalMessageTransmissionPositiveResponse' @@ -303,8 +286,6 @@ def answers(self, other): return isinstance(other, KWP_DNMT) - - # ######################### KWP_ENMT ################################### @_kwp_service(0x29) class KWP_ENMT(Packet): @@ -318,8 +299,6 @@ class KWP_ENMT(Packet): ] - - @_kwp_service(0x69) class KWP_ENMTPR(Packet): name = 'EnableNormalMessageTransmissionPositiveResponse' @@ -329,8 +308,6 @@ def answers(self, other): return isinstance(other, KWP_DNMT) - - # ######################### KWP_TP ################################### @_kwp_service(0x3E) class KWP_TP(Packet): @@ -344,8 +321,6 @@ class KWP_TP(Packet): ] - - @_kwp_service(0x7E) class KWP_TPPR(Packet): name = 'TesterPresentPositiveResponse' @@ -355,8 +330,6 @@ def answers(self, other): return isinstance(other, KWP_TP) - - # ######################### KWP_CDTCS ################################### @_kwp_service(0x85) class KWP_CDTCS(Packet): @@ -384,8 +357,6 @@ class KWP_CDTCS(Packet): ] - - @_kwp_service(0xC5) class KWP_CDTCSPR(Packet): name = 'ControlDTCSettingPositiveResponse' @@ -395,8 +366,6 @@ def answers(self, other): return isinstance(other, KWP_CDTCS) - - # ######################### KWP_ROE ################################### @_kwp_service(0x86) class KWP_ROE(Packet): @@ -430,8 +399,6 @@ class KWP_ROE(Packet): ] - - @_kwp_service(0xC6) class KWP_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' @@ -448,8 +415,6 @@ def answers(self, other): and other.eventType == self.eventType - - # ######################### KWP_RDBLI ################################### @_kwp_service(0x21) class KWP_RDBLI(Packet): @@ -473,8 +438,6 @@ class KWP_RDBLI(Packet): ] - - @_kwp_service(0x61) class KWP_RDBLIPR(Packet): name = 'ReadDataByLocalIdentifierPositiveResponse' @@ -488,8 +451,6 @@ def answers(self, other): and self.recordLocalIdentifier == other.recordLocalIdentifier - - # ######################### KWP_WDBLI ################################### @_kwp_service(0x3B) class KWP_WDBLI(Packet): @@ -499,8 +460,6 @@ class KWP_WDBLI(Packet): ] - - @_kwp_service(0x7B) class KWP_WDBLIPR(Packet): name = 'WriteDataByLocalIdentifierPositiveResponse' @@ -514,8 +473,6 @@ def answers(self, other): and self.recordLocalIdentifier == other.recordLocalIdentifier - - # ######################### KWP_RDBI ################################### @_kwp_service(0x22) class KWP_RDBI(Packet): @@ -526,8 +483,6 @@ class KWP_RDBI(Packet): ] - - @_kwp_service(0x62) class KWP_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' @@ -541,8 +496,6 @@ def answers(self, other): and self.identifier == other.identifier - - # ######################### KWP_RMBA ################################### @_kwp_service(0x23) class KWP_RMBA(Packet): @@ -553,8 +506,6 @@ class KWP_RMBA(Packet): ] - - @_kwp_service(0x63) class KWP_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' @@ -567,8 +518,6 @@ def answers(self, other): return isinstance(other, KWP_RMBA) - - # ######################### KWP_DDLI ################################### # TODO: Implement correct interpretation here, # instead of using just the dataRecord @@ -586,8 +535,6 @@ class KWP_DDLI(Packet): ] - - @_kwp_service(0x6C) class KWP_DDLIPR(Packet): name = 'DynamicallyDefineLocalIdentifierPositiveResponse' @@ -601,8 +548,6 @@ def answers(self, other): other.dynamicallyDefineLocalIdentifier == self.dynamicallyDefineLocalIdentifier # noqa: E501 - - # ######################### KWP_WDBI ################################### @_kwp_service(0x2E) class KWP_WDBI(Packet): @@ -612,8 +557,6 @@ class KWP_WDBI(Packet): ] - - @_kwp_service(0x6E) class KWP_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' @@ -627,8 +570,6 @@ def answers(self, other): and other.identifier == self.identifier - - # ######################### KWP_WMBA ################################### @_kwp_service(0x3D) class KWP_WMBA(Packet): @@ -640,8 +581,6 @@ class KWP_WMBA(Packet): ] - - @_kwp_service(0x7D) class KWP_WMBAPR(Packet): name = 'WriteMemoryByAddressPositiveResponse' @@ -655,8 +594,6 @@ def answers(self, other): other.memoryAddress == self.memoryAddress - - # ######################### KWP_CDI ################################### @_kwp_service(0x14) class KWP_CDI(Packet): @@ -673,8 +610,6 @@ class KWP_CDI(Packet): ] - - @_kwp_service(0x54) class KWP_CDIPR(Packet): name = 'ClearDiagnosticInformationPositiveResponse' @@ -689,8 +624,6 @@ def answers(self, other): self.groupOfDTC == other.groupOfDTC - - # ######################### KWP_RSODTC ################################### @_kwp_service(0x17) class KWP_RSODTC(Packet): @@ -700,8 +633,6 @@ class KWP_RSODTC(Packet): ] - - @_kwp_service(0x57) class KWP_RSODTCPR(Packet): name = 'ReadStatusOfDiagnosticTroubleCodesPositiveResponse' @@ -715,8 +646,6 @@ def answers(self, other): return isinstance(other, KWP_RSODTC) - - # ######################### KWP_RECUI ################################### @_kwp_service(0x1A) class KWP_RECUI(Packet): @@ -741,8 +670,6 @@ class KWP_RECUI(Packet): ] - - @_kwp_service(0x5A) class KWP_RECUIPR(Packet): name = 'ReadECUIdentificationPositiveResponse' @@ -757,8 +684,6 @@ def answers(self, other): self.localIdentifier == other.localIdentifier - - # ######################### KWP_SRBLI ################################### @_kwp_service(0x31) class KWP_SRBLI(Packet): @@ -780,8 +705,6 @@ class KWP_SRBLI(Packet): ] - - @_kwp_service(0x71) class KWP_SRBLIPR(Packet): name = 'StartRoutineByLocalIdentifierPositiveResponse' @@ -796,8 +719,6 @@ def answers(self, other): and other.routineLocalIdentifier == self.routineLocalIdentifier - - # ######################### KWP_STRBLI ################################### @_kwp_service(0x32) class KWP_STRBLI(Packet): @@ -808,8 +729,6 @@ class KWP_STRBLI(Packet): ] - - @_kwp_service(0x72) class KWP_STRBLIPR(Packet): name = 'StopRoutineByLocalIdentifierPositiveResponse' @@ -824,8 +743,6 @@ def answers(self, other): and other.routineLocalIdentifier == self.routineLocalIdentifier - - # ######################### KWP_RRRBLI ################################### @_kwp_service(0x33) class KWP_RRRBLI(Packet): @@ -836,8 +753,6 @@ class KWP_RRRBLI(Packet): ] - - @_kwp_service(0x73) class KWP_RRRBLIPR(Packet): name = 'RequestRoutineResultsByLocalIdentifierPositiveResponse' @@ -852,8 +767,6 @@ def answers(self, other): and other.routineLocalIdentifier == self.routineLocalIdentifier - - # ######################### KWP_RD ################################### @_kwp_service(0x34) class KWP_RD(Packet): @@ -866,8 +779,6 @@ class KWP_RD(Packet): ] - - @_kwp_service(0x74) class KWP_RDPR(Packet): name = 'RequestDownloadPositiveResponse' @@ -880,8 +791,6 @@ def answers(self, other): return isinstance(other, KWP_RD) - - # ######################### KWP_RU ################################### @_kwp_service(0x35) class KWP_RU(Packet): @@ -894,8 +803,6 @@ class KWP_RU(Packet): ] - - @_kwp_service(0x75) class KWP_RUPR(Packet): name = 'RequestUploadPositiveResponse' @@ -908,8 +815,6 @@ def answers(self, other): return isinstance(other, KWP_RU) - - # ######################### KWP_TD ################################### @_kwp_service(0x36) class KWP_TD(Packet): @@ -920,8 +825,6 @@ class KWP_TD(Packet): ] - - @_kwp_service(0x76) class KWP_TDPR(Packet): name = 'TransferDataPositiveResponse' @@ -936,8 +839,6 @@ def answers(self, other): and other.blockSequenceCounter == self.blockSequenceCounter - - # ######################### KWP_RTE ################################### @_kwp_service(0x37) class KWP_RTE(Packet): @@ -947,8 +848,6 @@ class KWP_RTE(Packet): ] - - @_kwp_service(0x77) class KWP_RTEPR(Packet): name = 'RequestTransferExitPositiveResponse' @@ -961,8 +860,6 @@ def answers(self, other): return isinstance(other, KWP_RTE) - - # ######################### KWP_NR ################################### @_kwp_service(0x7f) class KWP_NR(Packet): @@ -1003,8 +900,6 @@ def answers(self, other): conf.contribs['KWP']['treat-response-pending-as-answer']) - - # ################################################################## # ######################## UTILS ################################### # ################################################################## diff --git a/scapy/contrib/automotive/obd/obd.py b/scapy/contrib/automotive/obd/obd.py index 32c6c903f95..e81108530e6 100644 --- a/scapy/contrib/automotive/obd/obd.py +++ b/scapy/contrib/automotive/obd/obd.py @@ -34,7 +34,7 @@ # "ResponsePending' as answer of a request. \n" # "The default value is False.") conf.contribs['OBD'] = {'treat-response-pending-as-answer': False, - 'single_layer_OBD': False} + 'single_layer_OBD': False} class OBD(ISOTP): diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 663575852ad..cf0951509c5 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -19,7 +19,7 @@ ShortField, ObservableDict, XShortEnumField, XByteEnumField, StrLenField, \ FieldLenField, XStrFixedLenField, XStrLenField, FlagsField, PacketListField, \ PacketField -from scapy.packet import Packet, bind_layers, NoPayload, Raw +from scapy.packet import Packet, NoPayload, Raw from scapy.compat import orb from scapy.config import conf from scapy.utils import PeriodicSenderThread diff --git a/scapy/contrib/automotive/utils.py b/scapy/contrib/automotive/utils.py index 34457802de8..b7e27466758 100644 --- a/scapy/contrib/automotive/utils.py +++ b/scapy/contrib/automotive/utils.py @@ -43,17 +43,15 @@ class KWP_SDS(Packet): """ import struct +from typing import Any, Callable -from typing import Any - -from scapy.compat import orb from scapy.config import conf from scapy.fields import ConditionalField, XByteEnumField from scapy.packet import Packet, bind_layers, split_layers def _make_service_decorator(base_cls, conf_contrib_key, single_layer_flag): - # type: (type, str, str) -> Any + # type: (Any, str, str) -> Callable[[int], Any] """Return a class-decorator factory for an automotive protocol service. The returned decorator factory accepts a *service_id* integer and returns @@ -94,10 +92,8 @@ class KWP_SDS(Packet): _key = conf_contrib_key _flag = single_layer_flag - def service_decorator(service_id): - # type: (int) -> Any - def decorator(cls): - # type: (type) -> type + def service_decorator(service_id: int) -> Callable[[Any], Any]: + def decorator(cls: Any) -> Any: # Prepend a conditional service field so that in single layer mode # the service byte is part of the subpacket itself. svc_field = ConditionalField( @@ -113,8 +109,7 @@ def decorator(cls): # Capture service_id by value to avoid late-binding closure issues. _sid = service_id - def _hashret(self): - # type: () -> bytes + def _hashret(self: Any) -> bytes: if conf.contribs[_key].get(_flag, False): return struct.pack('B', _sid & ~0x40) return Packet.hashret(self) @@ -127,7 +122,7 @@ def _hashret(self): def _make_single_layer_mode(base_cls, conf_contrib_key, single_layer_flag): - # type: (type, str, str) -> Any + # type: (Any, str, str) -> Callable[[bool], None] """Return a function that enables or disables single layer mode. The returned function, when called with ``enable=True`` (default), removes @@ -162,8 +157,7 @@ def _make_single_layer_mode(base_cls, conf_contrib_key, single_layer_flag): _key = conf_contrib_key _flag = single_layer_flag - def single_layer_mode(enable=True): - # type: (bool) -> None + def single_layer_mode(enable: bool = True) -> None: conf.contribs[_key][_flag] = enable for service_id, cls in _base._service_cls.items(): # Always split first to ensure idempotency (no duplicate bindings). From a7517ffea92bacdf81c3afff764b912600c46109 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 19:04:04 +0000 Subject: [PATCH 07/16] Fix remaining flake8 and mypy issues in automotive protocol files Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/6cbead0f-e508-411e-a433-5ba980dfbc5d Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- scapy/contrib/automotive/gm/gmlan.py | 4 +- scapy/contrib/automotive/kwp.py | 8 +- .../contrib/automotive/obd/pid/pids_00_1F.py | 6 +- scapy/contrib/automotive/uds.py | 80 ++++--------------- 4 files changed, 23 insertions(+), 75 deletions(-) diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index 9df34e5dde9..ab2bcae1798 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -187,7 +187,7 @@ class GMLAN_RFRD(Packet): name = 'ReadFailureRecordData' fields_desc = [ ByteEnumField('subfunction', 0, subfunctions), - ConditionalField(PacketField("dtc", b'', GMLAN_DTC), + ConditionalField(PacketField("dtc", None, GMLAN_DTC), lambda pkt: pkt.subfunction == 0x02) ] @@ -223,7 +223,7 @@ class GMLAN_RFRDPR_RFRI(Packet): class GMLAN_RFRDPR_RFRP(Packet): name = 'ReadFailureRecordDataPositiveResponse_readFailureRecordParameters' fields_desc = [ - PacketField("dtc", b'', GMLAN_DTC) + PacketField("dtc", None, GMLAN_DTC) ] diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index 18706798f39..6ed97efbde4 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -25,7 +25,7 @@ from scapy.config import conf from scapy.error import log_loading from scapy.utils import PeriodicSenderThread -from scapy.plist import _PacketIterable +from scapy.plist import _PacketIterable # noqa: F401 from scapy.contrib.isotp import ISOTP from scapy.contrib.automotive.utils import ( _make_service_decorator, @@ -33,7 +33,7 @@ ) from scapy.compat import orb -from typing import ( +from typing import ( # noqa: F401 Any, Dict, ) @@ -119,13 +119,13 @@ def answers(self, other): if not isinstance(other, type(self)): return False if self.service == 0x7f: - return self.payload.answers(other) + return bool(self.payload.answers(other)) if self.service == (other.service + 0x40): if isinstance(self.payload, NoPayload) or \ isinstance(other.payload, NoPayload): return len(self) <= len(other) else: - return self.payload.answers(other.payload) + return bool(self.payload.answers(other.payload)) return False def hashret(self): diff --git a/scapy/contrib/automotive/obd/pid/pids_00_1F.py b/scapy/contrib/automotive/obd/pid/pids_00_1F.py index 4cfc483021b..829aac6e5ec 100644 --- a/scapy/contrib/automotive/obd/pid/pids_00_1F.py +++ b/scapy/contrib/automotive/obd/pid/pids_00_1F.py @@ -19,7 +19,7 @@ class OBD_PID00(OBD_Packet): name = "PID_00_PIDsSupported" fields_desc = [ - FlagsField('supported_pids', b'', 32, [ + FlagsField('supported_pids', 0, 32, [ 'PID20', 'PID1F', 'PID1E', @@ -109,7 +109,7 @@ class OBD_PID01(OBD_Packet): class OBD_PID02(OBD_Packet): name = "PID_02_FreezeDtc" fields_desc = [ - PacketField('dtc', b'', OBD_DTC) + PacketField('dtc', None, OBD_DTC) ] @@ -250,7 +250,7 @@ class OBD_PID12(OBD_Packet): class OBD_PID13(OBD_Packet): name = "PID_13_OxygenSensorsPresent" fields_desc = [ - FlagsField('sensors_present', b'', 8, [ + FlagsField('sensors_present', 0, 8, [ 'Bank1Sensor1', 'Bank1Sensor2', 'Bank1Sensor3', diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index cf0951509c5..a4de637f49c 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -30,7 +30,7 @@ ) # Typing imports -from typing import ( +from typing import ( # noqa: F401 Dict, Union, ) @@ -45,7 +45,7 @@ # "ResponsePending' as answer of a request. \n" # "The default value is False.") conf.contribs['UDS'] = {'treat-response-pending-as-answer': False, - 'single_layer_UDS': False} + 'single_layer_UDS': False} conf.debug_dissector = True @@ -117,13 +117,13 @@ def answers(self, other): if other.__class__ != self.__class__: return False if self.service == 0x7f: - return self.payload.answers(other) + return bool(self.payload.answers(other)) if self.service == (other.service + 0x40): if isinstance(self.payload, NoPayload) or \ isinstance(other.payload, NoPayload): return len(self) <= len(other) else: - return self.payload.answers(other.payload) + return bool(self.payload.answers(other.payload)) return False def hashret(self): @@ -170,7 +170,8 @@ def uds_single_layer_mode(enable=True): >>> from scapy.contrib.automotive.uds import uds_single_layer_mode >>> uds_single_layer_mode(True) >>> UDS(b'\\x10\\x01') - + """ _make_single_layer_mode(UDS, 'UDS', 'single_layer_UDS')(enable) @@ -191,7 +192,6 @@ class UDS_DSC(Packet): ] - @_uds_service(0x50) class UDS_DSCPR(Packet): name = 'DiagnosticSessionControlPositiveResponse' @@ -206,7 +206,6 @@ def answers(self, other): other.diagnosticSessionType == self.diagnosticSessionType - # #########################ER################################### @_uds_service(0x11) class UDS_ER(Packet): @@ -225,7 +224,6 @@ class UDS_ER(Packet): ] - @_uds_service(0x51) class UDS_ERPR(Packet): name = 'ECUResetPositiveResponse' @@ -239,7 +237,6 @@ def answers(self, other): return isinstance(other, UDS_ER) and other.resetType == self.resetType - # #########################SA################################### @_uds_service(0x27) class UDS_SA(Packet): @@ -253,7 +250,6 @@ class UDS_SA(Packet): ] - @_uds_service(0x67) class UDS_SAPR(Packet): name = 'SecurityAccessPositiveResponse' @@ -268,7 +264,6 @@ def answers(self, other): and other.securityAccessType == self.securityAccessType - # #########################CC################################### @_uds_service(0x28) class UDS_CC(Packet): @@ -308,7 +303,6 @@ class UDS_CC(Packet): ] - @_uds_service(0x68) class UDS_CCPR(Packet): name = 'CommunicationControlPositiveResponse' @@ -321,7 +315,6 @@ def answers(self, other): and other.controlType == self.controlType - # #########################AUTH################################### @_uds_service(0x29) class UDS_AUTH(Packet): @@ -344,8 +337,9 @@ class UDS_AUTH(Packet): lambda pkt: pkt.subFunction in [0x01, 0x02, 0x5]), ConditionalField(XShortField('certificateEvaluationId', 0), lambda pkt: pkt.subFunction == 0x04), - ConditionalField(XStrFixedLenField('algorithmIndicator', 0, length=16), - lambda pkt: pkt.subFunction in [0x05, 0x06, 0x07]), + ConditionalField( + XStrFixedLenField('algorithmIndicator', b'\x00' * 16, length=16), + lambda pkt: pkt.subFunction in [0x05, 0x06, 0x07]), ConditionalField(FieldLenField('lengthOfCertificateClient', None, fmt="H", length_of='certificateClient'), lambda pkt: pkt.subFunction in [0x01, 0x02]), @@ -396,7 +390,6 @@ class UDS_AUTH(Packet): ] - @_uds_service(0x69) class UDS_AUTHPR(Packet): authenticationReturnParameterTypes = { @@ -421,8 +414,9 @@ class UDS_AUTHPR(Packet): fields_desc = [ ByteEnumField('subFunction', 0, UDS_AUTH.subFunctions), ByteEnumField('returnValue', 0, authenticationReturnParameterTypes), - ConditionalField(XStrFixedLenField('algorithmIndicator', 0, length=16), - lambda pkt: pkt.subFunction in [0x05, 0x06, 0x07]), + ConditionalField( + XStrFixedLenField('algorithmIndicator', b'\x00' * 16, length=16), + lambda pkt: pkt.subFunction in [0x05, 0x06, 0x07]), ConditionalField(FieldLenField('lengthOfChallengeServer', None, fmt="H", length_of='challengeServer'), lambda pkt: pkt.subFunction in [0x01, 0x02, 0x05]), @@ -475,7 +469,6 @@ def answers(self, other): and other.subFunction == self.subFunction - # #########################TP################################### @_uds_service(0x3E) class UDS_TP(Packet): @@ -485,7 +478,6 @@ class UDS_TP(Packet): ] - @_uds_service(0x7E) class UDS_TPPR(Packet): name = 'TesterPresentPositiveResponse' @@ -497,7 +489,6 @@ def answers(self, other): return isinstance(other, UDS_TP) - # #########################ATP################################### @_uds_service(0x83) class UDS_ATP(Packet): @@ -517,7 +508,6 @@ class UDS_ATP(Packet): ] - @_uds_service(0xC3) class UDS_ATPPR(Packet): name = 'AccessTimingParameterPositiveResponse' @@ -534,7 +524,6 @@ def answers(self, other): self.timingParameterAccessType - # #########################SDT################################### # TODO: Implement correct internal message service handling here, # instead of using just the dataRecord @@ -557,7 +546,6 @@ class UDS_SDT(Packet): ] - @_uds_service(0xC4) class UDS_SDTPR(Packet): name = 'SecuredDataTransmissionPositiveResponse' @@ -580,7 +568,6 @@ def answers(self, other): return isinstance(other, UDS_SDT) - # #########################CDTCS################################### @_uds_service(0x85) class UDS_CDTCS(Packet): @@ -596,7 +583,6 @@ class UDS_CDTCS(Packet): ] - @_uds_service(0xC5) class UDS_CDTCSPR(Packet): name = 'ControlDTCSettingPositiveResponse' @@ -608,7 +594,6 @@ def answers(self, other): return isinstance(other, UDS_CDTCS) - # #########################ROE################################### # TODO: improve this protocol implementation @_uds_service(0x86) @@ -625,7 +610,6 @@ class UDS_ROE(Packet): ] - @_uds_service(0xC6) class UDS_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' @@ -641,7 +625,6 @@ def answers(self, other): and other.eventType == self.eventType - # #########################LC################################### @_uds_service(0x87) class UDS_LC(Packet): @@ -665,7 +648,6 @@ class UDS_LC(Packet): ] - @_uds_service(0xC7) class UDS_LCPR(Packet): name = 'LinkControlPositiveResponse' @@ -678,7 +660,6 @@ def answers(self, other): and other.linkControlType == self.linkControlType - # #########################RDBI################################### @_uds_service(0x22) class UDS_RDBI(Packet): @@ -691,7 +672,6 @@ class UDS_RDBI(Packet): ] - @_uds_service(0x62) class UDS_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' @@ -705,7 +685,6 @@ def answers(self, other): and self.dataIdentifier in other.identifiers - # #########################RMBA################################### @_uds_service(0x23) class UDS_RMBA(Packet): @@ -732,7 +711,6 @@ class UDS_RMBA(Packet): ] - @_uds_service(0x63) class UDS_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' @@ -744,7 +722,6 @@ def answers(self, other): return isinstance(other, UDS_RMBA) - # #########################RSDBI################################### @_uds_service(0x24) class UDS_RSDBI(Packet): @@ -755,7 +732,6 @@ class UDS_RSDBI(Packet): ] - # TODO: Implement correct scaling here, instead of using just the dataRecord @_uds_service(0x64) class UDS_RSDBIPR(Packet): @@ -771,7 +747,6 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier - # #########################RDBPI################################### @_uds_service(0x2A) class UDS_RDBPI(Packet): @@ -791,7 +766,6 @@ class UDS_RDBPI(Packet): ] - # TODO: Implement correct scaling here, instead of using just the dataRecord @_uds_service(0x6A) class UDS_RDBPIPR(Packet): @@ -806,7 +780,6 @@ def answers(self, other): and other.periodicDataIdentifier == self.periodicDataIdentifier - # #########################DDDI################################### # TODO: Implement correct interpretation here, # instead of using just the dataRecord @@ -822,7 +795,6 @@ class UDS_DDDI(Packet): ] - @_uds_service(0x6C) class UDS_DDDIPR(Packet): name = 'DynamicallyDefineDataIdentifierPositiveResponse' @@ -836,7 +808,6 @@ def answers(self, other): and other.subFunction == self.subFunction - # #########################WDBI################################### @_uds_service(0x2E) class UDS_WDBI(Packet): @@ -847,7 +818,6 @@ class UDS_WDBI(Packet): ] - @_uds_service(0x6E) class UDS_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' @@ -861,7 +831,6 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier - # #########################WMBA################################### @_uds_service(0x3D) class UDS_WMBA(Packet): @@ -890,7 +859,6 @@ class UDS_WMBA(Packet): ] - @_uds_service(0x7D) class UDS_WMBAPR(Packet): name = 'WriteMemoryByAddressPositiveResponse' @@ -921,11 +889,10 @@ def answers(self, other): and other.memoryAddressLen == self.memoryAddressLen - # ##########################DTC##################################### class DTC(Packet): name = 'Diagnostic Trouble Code' - dtc_descriptions = {} # Customize this dictionary for each individual ECU / OEM + dtc_descriptions = {} # type: Dict[int, str] fields_desc = [ BitEnumField("system", 0, 2, { @@ -957,7 +924,6 @@ class UDS_CDTCI(Packet): ] - @_uds_service(0x54) class UDS_CDTCIPR(Packet): name = 'ClearDiagnosticInformationPositiveResponse' @@ -966,7 +932,6 @@ def answers(self, other): return isinstance(other, UDS_CDTCI) - # #########################RDTCI################################### @_uds_service(0x19) class UDS_RDTCI(Packet): @@ -1039,7 +1004,6 @@ class UDS_RDTCI(Packet): ] - class DTCAndStatusRecord(Packet): name = 'DTC and status record' fields_desc = [ @@ -1071,7 +1035,7 @@ class DTCExtendedDataRecord(Packet): class DTCSnapshot(Packet): - identifiers = defaultdict(list) # for later extension + identifiers = defaultdict(list) # type: Dict[int, list] # for later extension @staticmethod def next_identifier_cb(pkt, lst, cur, remain): @@ -1149,7 +1113,6 @@ def answers(self, other): return True - # #########################RC################################### @_uds_service(0x31) class UDS_RC(Packet): @@ -1167,7 +1130,6 @@ class UDS_RC(Packet): ] - @_uds_service(0x71) class UDS_RCPR(Packet): name = 'RoutineControlPositiveResponse' @@ -1188,7 +1150,6 @@ def answers(self, other): return False - # #########################RD################################### @_uds_service(0x34) class UDS_RD(Packet): @@ -1219,7 +1180,6 @@ class UDS_RD(Packet): ] - @_uds_service(0x74) class UDS_RDPR(Packet): name = 'RequestDownloadPositiveResponse' @@ -1233,7 +1193,6 @@ def answers(self, other): return isinstance(other, UDS_RD) - # #########################RU################################### @_uds_service(0x35) class UDS_RU(Packet): @@ -1262,7 +1221,6 @@ class UDS_RU(Packet): ] - @_uds_service(0x75) class UDS_RUPR(Packet): name = 'RequestUploadPositiveResponse' @@ -1276,7 +1234,6 @@ def answers(self, other): return isinstance(other, UDS_RU) - # #########################TD################################### @_uds_service(0x36) class UDS_TD(Packet): @@ -1287,7 +1244,6 @@ class UDS_TD(Packet): ] - @_uds_service(0x76) class UDS_TDPR(Packet): name = 'TransferDataPositiveResponse' @@ -1301,7 +1257,6 @@ def answers(self, other): and other.blockSequenceCounter == self.blockSequenceCounter - # #########################RTE################################### @_uds_service(0x37) class UDS_RTE(Packet): @@ -1311,7 +1266,6 @@ class UDS_RTE(Packet): ] - @_uds_service(0x77) class UDS_RTEPR(Packet): name = 'RequestTransferExitPositiveResponse' @@ -1323,7 +1277,6 @@ def answers(self, other): return isinstance(other, UDS_RTE) - # #########################RFT################################### @_uds_service(0x38) class UDS_RFT(Packet): @@ -1367,7 +1320,6 @@ def _contains_file_size(packet): ] - @_uds_service(0x78) class UDS_RFTPR(Packet): name = 'RequestFileTransferPositiveResponse' @@ -1408,7 +1360,6 @@ def answers(self, other): return isinstance(other, UDS_RFT) - # #########################IOCBI################################### @_uds_service(0x2F) class UDS_IOCBI(Packet): @@ -1418,7 +1369,6 @@ class UDS_IOCBI(Packet): ] - @_uds_service(0x6F) class UDS_IOCBIPR(Packet): name = 'InputOutputControlByIdentifierPositiveResponse' @@ -1431,7 +1381,6 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier - # #########################NR################################### @_uds_service(0x7f) class UDS_NR(Packet): @@ -1510,7 +1459,6 @@ def answers(self, other): conf.contribs['UDS']['treat-response-pending-as-answer']) - # ################################################################## # ######################## UTILS ################################### # ################################################################## From 0b079061d20bfb6d8321764ff1c2925fa02b0ed2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 05:45:35 +0000 Subject: [PATCH 08/16] Remove _make_single_layer_mode helper; rename config key to single_layer_mode; use config directly Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/68821df8-a572-42b8-a882-d8026ab945fc Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- scapy/contrib/automotive/gm/gmlan.py | 8 +-- scapy/contrib/automotive/kwp.py | 8 +-- scapy/contrib/automotive/obd/obd.py | 8 +-- scapy/contrib/automotive/uds.py | 36 +---------- scapy/contrib/automotive/utils.py | 92 ++++++++-------------------- test/contrib/automotive/gm/gmlan.uts | 12 ++-- test/contrib/automotive/kwp.uts | 20 +++--- test/contrib/automotive/obd/obd.uts | 12 ++-- test/contrib/automotive/uds.uts | 16 ++--- 9 files changed, 59 insertions(+), 153 deletions(-) diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index ab2bcae1798..854863e11f3 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -34,7 +34,6 @@ from scapy.contrib.isotp import ISOTP from scapy.contrib.automotive.utils import ( _make_service_decorator, - _make_single_layer_mode, ) from scapy.compat import orb @@ -52,7 +51,7 @@ # "ResponsePending' as answer of a request. \n" # "The default value is False.") conf.contribs['GMLAN'] = {'treat-response-pending-as-answer': False, - 'single_layer_GMLAN': False} + 'single_layer_mode': False} conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = None @@ -142,14 +141,13 @@ def hashret(self): def dispatch_hook(cls, _pkt=b"", *args, **kwargs): # type: (...) -> type """Dispatch to the correct GMLAN service class in single layer mode.""" - if conf.contribs['GMLAN'].get('single_layer_GMLAN', False) and len(_pkt) >= 1: + if conf.contribs['GMLAN'].get('single_layer_mode', False) and len(_pkt) >= 1: service = orb(_pkt[0]) return cls._service_cls.get(service, cls) return cls -_gmlan_service = _make_service_decorator(GMLAN, 'GMLAN', 'single_layer_GMLAN') -gmlan_single_layer_mode = _make_single_layer_mode(GMLAN, 'GMLAN', 'single_layer_GMLAN') +_gmlan_service = _make_service_decorator(GMLAN, 'GMLAN') # ########################IDO################################### diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index 6ed97efbde4..c5ece3c5dc0 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -29,7 +29,6 @@ from scapy.contrib.isotp import ISOTP from scapy.contrib.automotive.utils import ( _make_service_decorator, - _make_single_layer_mode, ) from scapy.compat import orb @@ -49,7 +48,7 @@ "ResponsePending' as answer of a request. \n" "The default value is False.") conf.contribs['KWP'] = {'treat-response-pending-as-answer': False, - 'single_layer_KWP': False} + 'single_layer_mode': False} class KWP(ISOTP): @@ -141,14 +140,13 @@ def hashret(self): def dispatch_hook(cls, _pkt=b"", *args, **kwargs): # type: (...) -> type """Dispatch to the correct KWP service class in single layer mode.""" - if conf.contribs['KWP'].get('single_layer_KWP', False) and len(_pkt) >= 1: + if conf.contribs['KWP'].get('single_layer_mode', False) and len(_pkt) >= 1: service = orb(_pkt[0]) return cls._service_cls.get(service, cls) return cls -_kwp_service = _make_service_decorator(KWP, 'KWP', 'single_layer_KWP') -kwp_single_layer_mode = _make_single_layer_mode(KWP, 'KWP', 'single_layer_KWP') +_kwp_service = _make_service_decorator(KWP, 'KWP') # ########################SDS################################### diff --git a/scapy/contrib/automotive/obd/obd.py b/scapy/contrib/automotive/obd/obd.py index e81108530e6..a6cb9b785cf 100644 --- a/scapy/contrib/automotive/obd/obd.py +++ b/scapy/contrib/automotive/obd/obd.py @@ -20,7 +20,6 @@ from scapy.contrib.isotp import ISOTP from scapy.contrib.automotive.utils import ( _make_service_decorator, - _make_single_layer_mode, ) from scapy.compat import orb @@ -34,7 +33,7 @@ # "ResponsePending' as answer of a request. \n" # "The default value is False.") conf.contribs['OBD'] = {'treat-response-pending-as-answer': False, - 'single_layer_OBD': False} + 'single_layer_mode': False} class OBD(ISOTP): @@ -91,14 +90,13 @@ def answers(self, other): def dispatch_hook(cls, _pkt=b"", *args, **kwargs): # type: (...) -> type """Dispatch to the correct OBD service class in single layer mode.""" - if conf.contribs['OBD'].get('single_layer_OBD', False) and len(_pkt) >= 1: + if conf.contribs['OBD'].get('single_layer_mode', False) and len(_pkt) >= 1: service = orb(_pkt[0]) return cls._service_cls.get(service, cls) return cls -_obd_service = _make_service_decorator(OBD, 'OBD', 'single_layer_OBD') -obd_single_layer_mode = _make_single_layer_mode(OBD, 'OBD', 'single_layer_OBD') +_obd_service = _make_service_decorator(OBD, 'OBD') # Service Bindings — applied via the generic decorator (functional form, # since the service classes are defined in a separate module) diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index a4de637f49c..21f4ec7ad01 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -26,7 +26,6 @@ from scapy.contrib.isotp import ISOTP from scapy.contrib.automotive.utils import ( _make_service_decorator, - _make_single_layer_mode, ) # Typing imports @@ -45,7 +44,7 @@ # "ResponsePending' as answer of a request. \n" # "The default value is False.") conf.contribs['UDS'] = {'treat-response-pending-as-answer': False, - 'single_layer_UDS': False} + 'single_layer_mode': False} conf.debug_dissector = True @@ -138,42 +137,13 @@ def hashret(self): def dispatch_hook(cls, _pkt=b"", *args, **kwargs): # type: (...) -> type """Dispatch to the correct UDS service class in single layer mode.""" - if conf.contribs['UDS'].get('single_layer_UDS', False) and len(_pkt) >= 1: + if conf.contribs['UDS'].get('single_layer_mode', False) and len(_pkt) >= 1: service = orb(_pkt[0]) return cls._service_cls.get(service, cls) return cls -_uds_service = _make_service_decorator(UDS, 'UDS', 'single_layer_UDS') - - -def uds_single_layer_mode(enable=True): - # type: (bool) -> None - """Enable or disable single layer UDS mode. - - In single layer mode, each UDS service is a standalone packet with a - conditional 'service' field, rather than being nested inside a UDS() - layer. When dissecting raw bytes, UDS() will directly return the - appropriate subpacket class (e.g., UDS_RDBI) instead of UDS / UDS_RDBI. - - This function can be called after the module is loaded to switch modes. - - Args: - enable: If True, enable single layer mode. If False, revert to - multi-layer mode (default). - - Example:: - - >>> conf.contribs['UDS'] = {'single_layer_UDS': True} - >>> load_contrib('automotive.uds') - # OR after loading: - >>> from scapy.contrib.automotive.uds import uds_single_layer_mode - >>> uds_single_layer_mode(True) - >>> UDS(b'\\x10\\x01') - - """ - _make_single_layer_mode(UDS, 'UDS', 'single_layer_UDS')(enable) +_uds_service = _make_service_decorator(UDS, 'UDS') # ########################DSC################################### diff --git a/scapy/contrib/automotive/utils.py b/scapy/contrib/automotive/utils.py index b7e27466758..89ef3e5b715 100644 --- a/scapy/contrib/automotive/utils.py +++ b/scapy/contrib/automotive/utils.py @@ -15,20 +15,22 @@ To add single layer support to a protocol: 1. Add a ``_service_cls = {}`` class attribute and a ``dispatch_hook`` - classmethod to the base class (see :func:`_make_dispatch_hook`):: + classmethod to the base class:: class KWP(ISOTP): _service_cls = {} ... @classmethod def dispatch_hook(cls, _pkt=b"", *args, **kwargs): - return _make_dispatch_hook('KWP', 'single_layer_KWP')( - cls, _pkt, *args, **kwargs) + if conf.contribs['KWP'].get('single_layer_mode', False) and \ + len(_pkt) >= 1: + service = orb(_pkt[0]) + return cls._service_cls.get(service, cls) + return cls -2. Create the per-protocol decorator and mode-toggle with the factories:: +2. Create the per-protocol service decorator:: - _kwp_service = _make_service_decorator(KWP, 'KWP', 'single_layer_KWP') - kwp_single_layer_mode = _make_single_layer_mode(KWP, 'KWP', 'single_layer_KWP') + _kwp_service = _make_service_decorator(KWP, 'KWP') 3. Decorate every service subpacket instead of calling ``bind_layers``:: @@ -40,6 +42,12 @@ class KWP_SDS(Packet): post-definition using the functional form:: _obd_service(0x01)(OBD_S01) + +4. To enable or disable single layer mode at runtime, set the config flag + directly:: + + conf.contribs['KWP']['single_layer_mode'] = True # enable + conf.contribs['KWP']['single_layer_mode'] = False # disable """ import struct @@ -47,11 +55,11 @@ class KWP_SDS(Packet): from scapy.config import conf from scapy.fields import ConditionalField, XByteEnumField -from scapy.packet import Packet, bind_layers, split_layers +from scapy.packet import Packet, bind_layers -def _make_service_decorator(base_cls, conf_contrib_key, single_layer_flag): - # type: (Any, str, str) -> Callable[[int], Any] +def _make_service_decorator(base_cls, conf_contrib_key): + # type: (Any, str) -> Callable[[int], Any] """Return a class-decorator factory for an automotive protocol service. The returned decorator factory accepts a *service_id* integer and returns @@ -62,27 +70,25 @@ def _make_service_decorator(base_cls, conf_contrib_key, single_layer_flag): 2. Registers the class in ``base_cls._service_cls`` so the ``dispatch_hook`` can route raw bytes to the correct type. 3. Calls :func:`~scapy.packet.bind_layers` to link the subpacket to - *base_cls* (multi-layer mode). When the module is loaded with - single layer mode already enabled the binding is skipped because - ``dispatch_hook`` handles dissection. + *base_cls* (multi-layer mode). 4. Injects a ``hashret`` method that returns the correct value for request/response matching in single layer mode (unless the class already defines its own ``hashret``). + The single layer mode is controlled at runtime via + ``conf.contribs[conf_contrib_key]['single_layer_mode']``. + Args: base_cls: The base protocol class (e.g. ``UDS``, ``KWP``). conf_contrib_key: Key used in :attr:`~scapy.config.conf.contribs` (e.g. ``'UDS'``, ``'KWP'``). - single_layer_flag: Flag name inside - ``conf.contribs[conf_contrib_key]`` - (e.g. ``'single_layer_UDS'``). Returns: A ``service_decorator(service_id)`` factory function. Example:: - _kwp_service = _make_service_decorator(KWP, 'KWP', 'single_layer_KWP') + _kwp_service = _make_service_decorator(KWP, 'KWP') @_kwp_service(0x10) class KWP_SDS(Packet): @@ -90,7 +96,7 @@ class KWP_SDS(Packet): """ _base = base_cls _key = conf_contrib_key - _flag = single_layer_flag + _flag = 'single_layer_mode' def service_decorator(service_id: int) -> Callable[[Any], Any]: def decorator(cls: Any) -> Any: @@ -103,9 +109,8 @@ def decorator(cls: Any) -> Any: cls.fields_desc = [svc_field] + list(cls.fields_desc) # Register in base class dispatch table for single layer mode. _base._service_cls[service_id] = cls - # In multi-layer mode bind to base class for backward compatibility. - if not conf.contribs[_key].get(_flag, False): - bind_layers(_base, cls, service=service_id) + # Bind to base class for multi-layer mode payload routing. + bind_layers(_base, cls, service=service_id) # Capture service_id by value to avoid late-binding closure issues. _sid = service_id @@ -119,50 +124,3 @@ def _hashret(self: Any) -> bytes: return cls return decorator return service_decorator - - -def _make_single_layer_mode(base_cls, conf_contrib_key, single_layer_flag): - # type: (Any, str, str) -> Callable[[bool], None] - """Return a function that enables or disables single layer mode. - - The returned function, when called with ``enable=True`` (default), removes - the :func:`~scapy.packet.bind_layers` associations between *base_cls* and - its service subclasses so that ``dispatch_hook`` takes over dissection. - When called with ``enable=False``, the traditional multi-layer bindings - are restored. - - The function is idempotent: calling it multiple times with the same - argument is safe (no duplicate bindings are created). - - Args: - base_cls: The base protocol class (e.g. ``UDS``, ``KWP``). - conf_contrib_key: Key used in :attr:`~scapy.config.conf.contribs`. - single_layer_flag: Flag name inside - ``conf.contribs[conf_contrib_key]``. - - Returns: - A ``single_layer_mode(enable=True)`` toggle function. - - Example:: - - kwp_single_layer_mode = _make_single_layer_mode( - KWP, 'KWP', 'single_layer_KWP') - - >>> kwp_single_layer_mode(True) - >>> KWP(b'\\x10\\x01') - - >>> kwp_single_layer_mode(False) # revert to multi-layer mode - """ - _base = base_cls - _key = conf_contrib_key - _flag = single_layer_flag - - def single_layer_mode(enable: bool = True) -> None: - conf.contribs[_key][_flag] = enable - for service_id, cls in _base._service_cls.items(): - # Always split first to ensure idempotency (no duplicate bindings). - split_layers(_base, cls, service=service_id) - if not enable: - bind_layers(_base, cls, service=service_id) - - return single_layer_mode diff --git a/test/contrib/automotive/gm/gmlan.uts b/test/contrib/automotive/gm/gmlan.uts index d994ffa0370..822ce5c3448 100644 --- a/test/contrib/automotive/gm/gmlan.uts +++ b/test/contrib/automotive/gm/gmlan.uts @@ -615,13 +615,9 @@ assert log[1] == "0x80" assert log[0] == "DeviceControlPositiveResponse" + Single layer GMLAN mode -= Single layer mode: load helper - -from scapy.contrib.automotive.gm.gmlan import gmlan_single_layer_mode - = Single layer mode: enable and basic dissect -gmlan_single_layer_mode(True) +conf.contribs['GMLAN']['single_layer_mode'] = True ido = GMLAN(b'\x10\x02') assert isinstance(ido, GMLAN_IDO), "Expected GMLAN_IDO, got %s" % type(ido) @@ -673,7 +669,7 @@ assert isinstance(unknown, GMLAN), "Expected GMLAN fallback, got %s" % type(unkn = Single layer mode: switch back to multi-layer mode -gmlan_single_layer_mode(False) +conf.contribs['GMLAN']['single_layer_mode'] = False ido4 = GMLAN(b'\x10\x02') assert ido4.__class__ == GMLAN @@ -682,5 +678,5 @@ assert ido4[GMLAN_IDO].subfunction == 0x02 = Single layer mode: cleanup -gmlan_single_layer_mode(False) -assert not conf.contribs['GMLAN']['single_layer_GMLAN'] +conf.contribs['GMLAN']['single_layer_mode'] = False +assert not conf.contribs['GMLAN']['single_layer_mode'] diff --git a/test/contrib/automotive/kwp.uts b/test/contrib/automotive/kwp.uts index 32a365a012b..ddc83373d28 100644 --- a/test/contrib/automotive/kwp.uts +++ b/test/contrib/automotive/kwp.uts @@ -510,13 +510,9 @@ assert nrc.negativeResponseCode == 0x33 + Single layer KWP mode -= Single layer mode: load helper - -from scapy.contrib.automotive.kwp import kwp_single_layer_mode - = Single layer mode: enable and basic dissect -kwp_single_layer_mode(True) +conf.contribs['KWP']['single_layer_mode'] = True sds = KWP(b'\x10\x01') assert isinstance(sds, KWP_SDS), "Expected KWP_SDS, got %s" % type(sds) @@ -569,7 +565,7 @@ assert isinstance(unknown, KWP), "Expected KWP fallback, got %s" % type(unknown) = Single layer mode: switch back to multi-layer mode -kwp_single_layer_mode(False) +conf.contribs['KWP']['single_layer_mode'] = False sds5 = KWP(b'\x10\x01') assert sds5.__class__ == KWP @@ -578,13 +574,13 @@ assert sds5[KWP_SDS].diagnosticSession == 0x01 = Single layer mode: idempotency -kwp_single_layer_mode(True) -kwp_single_layer_mode(True) +conf.contribs['KWP']['single_layer_mode'] = True +conf.contribs['KWP']['single_layer_mode'] = True sds6 = KWP(b'\x10\x01') assert isinstance(sds6, KWP_SDS) -kwp_single_layer_mode(False) -kwp_single_layer_mode(False) +conf.contribs['KWP']['single_layer_mode'] = False +conf.contribs['KWP']['single_layer_mode'] = False sds7 = KWP(b'\x10\x01') assert sds7.__class__ == KWP count = sum(1 for fval, cls in KWP.payload_guess @@ -593,5 +589,5 @@ assert count == 1, "Expected 1 binding for KWP_SDS, got %d" % count = Single layer mode: cleanup -kwp_single_layer_mode(False) -assert not conf.contribs['KWP']['single_layer_KWP'] +conf.contribs['KWP']['single_layer_mode'] = False +assert not conf.contribs['KWP']['single_layer_mode'] diff --git a/test/contrib/automotive/obd/obd.uts b/test/contrib/automotive/obd/obd.uts index fca51e3821c..f9e577c4200 100644 --- a/test/contrib/automotive/obd/obd.uts +++ b/test/contrib/automotive/obd/obd.uts @@ -1034,13 +1034,9 @@ assert p.answers(r) + Single layer OBD mode -= Single layer mode: load helper - -from scapy.contrib.automotive.obd.obd import obd_single_layer_mode - = Single layer mode: enable and basic dissect -obd_single_layer_mode(True) +conf.contribs['OBD']['single_layer_mode'] = True s01 = OBD(b'\x01\x0c') assert isinstance(s01, OBD_S01), "Expected OBD_S01, got %s" % type(s01) @@ -1085,7 +1081,7 @@ assert isinstance(unknown, OBD), "Expected OBD fallback, got %s" % type(unknown) = Single layer mode: switch back to multi-layer mode -obd_single_layer_mode(False) +conf.contribs['OBD']['single_layer_mode'] = False s01_3 = OBD(b'\x01\x0c') assert s01_3.__class__ == OBD @@ -1094,5 +1090,5 @@ assert isinstance(s01_3[OBD_S01], OBD_S01) = Single layer mode: cleanup -obd_single_layer_mode(False) -assert not conf.contribs['OBD']['single_layer_OBD'] +conf.contribs['OBD']['single_layer_mode'] = False +assert not conf.contribs['OBD']['single_layer_mode'] diff --git a/test/contrib/automotive/uds.uts b/test/contrib/automotive/uds.uts index d0b2d8056e0..90a90962d64 100644 --- a/test/contrib/automotive/uds.uts +++ b/test/contrib/automotive/uds.uts @@ -1441,13 +1441,9 @@ assert nrc.negativeResponseCode == 0x33 + Single layer UDS mode -= Single layer mode: load helper - -from scapy.contrib.automotive.uds import uds_single_layer_mode - = Single layer mode: enable and basic dissect -uds_single_layer_mode(True) +conf.contribs['UDS']['single_layer_mode'] = True dsc = UDS(b'\x10\x01') assert isinstance(dsc, UDS_DSC), "Expected UDS_DSC, got %s" % type(dsc) @@ -1524,7 +1520,7 @@ assert isinstance(unknown, UDS), "Expected UDS fallback, got %s" % type(unknown) = Single layer mode: switch back to multi-layer mode -uds_single_layer_mode(False) +conf.contribs['UDS']['single_layer_mode'] = False dsc5 = UDS(b'\x10\x01') assert dsc5.__class__ == UDS @@ -1536,9 +1532,9 @@ assert dscpr5.__class__ == UDS assert dscpr5.service == 0x50 assert dscpr5[UDS_DSCPR].diagnosticSessionType == 0x01 -= Single layer mode: enable via conf directly (without uds_single_layer_mode) += Single layer mode: enable via conf directly -conf.contribs['UDS']['single_layer_UDS'] = True +conf.contribs['UDS']['single_layer_mode'] = True er6 = UDS(b'\x11\x01') assert isinstance(er6, UDS_ER), "Expected UDS_ER, got %s" % type(er6) @@ -1547,5 +1543,5 @@ assert er6.resetType == 0x01 = Single layer mode: final cleanup - restore default multi-layer mode -uds_single_layer_mode(False) -assert not conf.contribs['UDS']['single_layer_UDS'] +conf.contribs['UDS']['single_layer_mode'] = False +assert not conf.contribs['UDS']['single_layer_mode'] From 58162cf51d59642bb0c2f5e4097b346f88bd881d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 05:48:01 +0000 Subject: [PATCH 09/16] Update documentation to reflect single_layer_mode rename and removal of helper functions Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/68821df8-a572-42b8-a882-d8026ab945fc Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- doc/scapy/layers/automotive.rst | 45 +++++++++++++++------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/doc/scapy/layers/automotive.rst b/doc/scapy/layers/automotive.rst index 7258dc37ac9..3db163b1042 100644 --- a/doc/scapy/layers/automotive.rst +++ b/doc/scapy/layers/automotive.rst @@ -1103,32 +1103,32 @@ feature is backed by the generic helpers in To enable before loading a module:: >>> conf.contribs['UDS'] = {'treat-response-pending-as-answer': False, - ... 'single_layer_UDS': True} + ... 'single_layer_mode': True} >>> load_contrib('automotive.uds') To toggle at runtime after loading:: - >>> from scapy.contrib.automotive.uds import uds_single_layer_mode - >>> uds_single_layer_mode(True) + >>> conf.contribs['UDS']['single_layer_mode'] = True >>> UDS(b'\x10\x01') >>> bytes(UDS_DSC(diagnosticSessionType=0x01)) b'\x10\x01' - >>> uds_single_layer_mode(False) # revert to multi-layer mode - -The same API is available for the other protocols: - -+----------+-----------------------+----------------------------+------------------------------------+ -| Protocol | Config flag | Toggle function | Module | -+==========+=======================+============================+====================================+ -| UDS | ``single_layer_UDS`` | ``uds_single_layer_mode`` | ``scapy.contrib.automotive.uds`` | -+----------+-----------------------+----------------------------+------------------------------------+ -| KWP | ``single_layer_KWP`` | ``kwp_single_layer_mode`` | ``scapy.contrib.automotive.kwp`` | -+----------+-----------------------+----------------------------+------------------------------------+ -| OBD | ``single_layer_OBD`` | ``obd_single_layer_mode`` | ``scapy.contrib.automotive.obd`` | -+----------+-----------------------+----------------------------+------------------------------------+ -| GMLAN | ``single_layer_GMLAN``| ``gmlan_single_layer_mode``| ``scapy.contrib.automotive.gm.gmlan`` | -+----------+-----------------------+----------------------------+------------------------------------+ + >>> conf.contribs['UDS']['single_layer_mode'] = False # revert to multi-layer mode + +The same API is available for the other protocols — use the ``single_layer_mode`` +config key in the corresponding ``conf.contribs`` entry: + ++----------+-----------------------------------------------+ +| Protocol | Config entry | ++==========+===============================================+ +| UDS | ``conf.contribs['UDS']['single_layer_mode']`` | ++----------+-----------------------------------------------+ +| KWP | ``conf.contribs['KWP']['single_layer_mode']`` | ++----------+-----------------------------------------------+ +| OBD | ``conf.contribs['OBD']['single_layer_mode']`` | ++----------+-----------------------------------------------+ +| GMLAN | ``conf.contribs['GMLAN']['single_layer_mode']`` | ++----------+-----------------------------------------------+ In single layer mode: @@ -1136,16 +1136,13 @@ In single layer mode: it reads the first byte and returns the correct service class directly. - Each service packet has a conditional ``service`` field that is present (for building and dissection) only when single layer mode is active. -- ``bind_layers`` between the base class and service classes are disabled; - dissection is handled via ``dispatch_hook``. - Service packets' ``answers()`` and ``hashret()`` methods work correctly in both modes. -The underlying helpers that power this feature are -:func:`~scapy.contrib.automotive.utils._make_service_decorator` and -:func:`~scapy.contrib.automotive.utils._make_single_layer_mode` in +The underlying helper that powers this feature is +:func:`~scapy.contrib.automotive.utils._make_service_decorator` in :mod:`scapy.contrib.automotive.utils`. OEM-specific protocol extensions can -use the same helpers to add single layer support to custom protocol classes. +use the same helper to add single layer support to custom protocol classes. GMLAN ===== From 3fc4bd53d1f4baaa34a80cefe26e782badb7eca5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:21:32 +0000 Subject: [PATCH 10/16] Remove _make_service_decorator; add ConditionalField explicitly in every service packet Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/5dbfa702-3238-4f56-9d29-7868afb27512 Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- doc/scapy/layers/automotive.rst | 5 - scapy/contrib/automotive/gm/gmlan.py | 214 +++++++++++-- scapy/contrib/automotive/kwp.py | 442 ++++++++++++++++++++++---- scapy/contrib/automotive/obd/obd.py | 64 ++-- scapy/contrib/automotive/uds.py | 450 +++++++++++++++++++++++---- scapy/contrib/automotive/utils.py | 121 +------ 6 files changed, 1001 insertions(+), 295 deletions(-) diff --git a/doc/scapy/layers/automotive.rst b/doc/scapy/layers/automotive.rst index 3db163b1042..75c2b69abc0 100644 --- a/doc/scapy/layers/automotive.rst +++ b/doc/scapy/layers/automotive.rst @@ -1139,11 +1139,6 @@ In single layer mode: - Service packets' ``answers()`` and ``hashret()`` methods work correctly in both modes. -The underlying helper that powers this feature is -:func:`~scapy.contrib.automotive.utils._make_service_decorator` in -:mod:`scapy.contrib.automotive.utils`. OEM-specific protocol extensions can -use the same helper to add single layer support to custom protocol classes. - GMLAN ===== diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index 854863e11f3..9be1fbd4010 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -32,9 +32,6 @@ from scapy.packet import Packet, bind_layers, NoPayload from scapy.config import conf from scapy.contrib.isotp import ISOTP -from scapy.contrib.automotive.utils import ( - _make_service_decorator, -) from scapy.compat import orb """ @@ -147,11 +144,7 @@ def dispatch_hook(cls, _pkt=b"", *args, **kwargs): return cls -_gmlan_service = _make_service_decorator(GMLAN, 'GMLAN') - - # ########################IDO################################### -@_gmlan_service(0x10) class GMLAN_IDO(Packet): subfunctions = { 0x02: 'disableAllDTCs', @@ -159,10 +152,17 @@ class GMLAN_IDO(Packet): 0x04: 'wakeUpLinks'} name = 'InitiateDiagnosticOperation' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x10, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), ByteEnumField('subfunction', 0, subfunctions) ] +bind_layers(GMLAN, GMLAN_IDO, service=0x10) +GMLAN._service_cls[0x10] = GMLAN_IDO + + # ########################RFRD################################### class GMLAN_DTC(Packet): name = 'GMLAN DTC information' @@ -177,23 +177,31 @@ def extract_padding(self, p): return "", p -@_gmlan_service(0x12) class GMLAN_RFRD(Packet): subfunctions = { 0x01: 'readFailureRecordIdentifiers', 0x02: 'readFailureRecordParameters'} name = 'ReadFailureRecordData' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x12, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), ByteEnumField('subfunction', 0, subfunctions), ConditionalField(PacketField("dtc", None, GMLAN_DTC), lambda pkt: pkt.subfunction == 0x02) ] -@_gmlan_service(0x52) +bind_layers(GMLAN, GMLAN_RFRD, service=0x12) +GMLAN._service_cls[0x12] = GMLAN_RFRD + + class GMLAN_RFRDPR(Packet): name = 'ReadFailureRecordDataPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x52, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), ByteEnumField('subfunction', 0, GMLAN_RFRD.subfunctions) ] @@ -202,6 +210,10 @@ def answers(self, other): other.subfunction == self.subfunction +bind_layers(GMLAN, GMLAN_RFRDPR, service=0x52) +GMLAN._service_cls[0x52] = GMLAN_RFRDPR + + class GMLAN_RFRDPR_RFRI(Packet): failureRecordDataStructureIdentifiers = { 0x00: "PID", @@ -229,7 +241,6 @@ class GMLAN_RFRDPR_RFRP(Packet): # ########################RDBI################################### -@_gmlan_service(0x1A) class GMLAN_RDBI(Packet): dataIdentifiers = ObservableDict({ 0x90: "$90: VehicleIdentificationNumber (VIN)", @@ -318,14 +329,23 @@ class GMLAN_RDBI(Packet): name = 'ReadDataByIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x1a, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XByteEnumField('dataIdentifier', 0, dataIdentifiers) ] -@_gmlan_service(0x5A) +bind_layers(GMLAN, GMLAN_RDBI, service=0x1a) +GMLAN._service_cls[0x1a] = GMLAN_RDBI + + class GMLAN_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x5a, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers), ] @@ -334,8 +354,11 @@ def answers(self, other): other.dataIdentifier == self.dataIdentifier +bind_layers(GMLAN, GMLAN_RDBIPR, service=0x5a) +GMLAN._service_cls[0x5a] = GMLAN_RDBIPR + + # ########################RDBI################################### -@_gmlan_service(0x22) class GMLAN_RDBPI(Packet): dataIdentifiers = ObservableDict({ 0x0005: "OBD_EngineCoolantTemperature", @@ -344,16 +367,25 @@ class GMLAN_RDBPI(Packet): }) name = 'ReadDataByParameterIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x22, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), FieldListField("identifiers", [], XShortEnumField('parameterIdentifier', 0, dataIdentifiers)) ] -@_gmlan_service(0x62) +bind_layers(GMLAN, GMLAN_RDBPI, service=0x22) +GMLAN._service_cls[0x22] = GMLAN_RDBPI + + class GMLAN_RDBPIPR(Packet): name = 'ReadDataByParameterIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x62, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XShortEnumField('parameterIdentifier', 0, GMLAN_RDBPI.dataIdentifiers), ] @@ -362,8 +394,11 @@ def answers(self, other): self.parameterIdentifier in other.identifiers +bind_layers(GMLAN, GMLAN_RDBPIPR, service=0x62) +GMLAN._service_cls[0x62] = GMLAN_RDBPIPR + + # ########################RDBPKTI################################### -@_gmlan_service(0xAA) class GMLAN_RDBPKTI(Packet): name = 'ReadDataByPacketIdentifier' subfunctions = { @@ -375,6 +410,9 @@ class GMLAN_RDBPKTI(Packet): } fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xaa, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XByteEnumField('subfunction', 0, subfunctions), ConditionalField(FieldListField('request_DPIDs', [], XByteField("", 0)), @@ -382,11 +420,17 @@ class GMLAN_RDBPKTI(Packet): ] +bind_layers(GMLAN, GMLAN_RDBPKTI, service=0xaa) +GMLAN._service_cls[0xaa] = GMLAN_RDBPKTI + + # ########################RMBA################################### -@_gmlan_service(0x23) class GMLAN_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x23, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), MultipleTypeField( [ (XShortField('memoryAddress', 0), @@ -401,10 +445,16 @@ class GMLAN_RMBA(Packet): ] -@_gmlan_service(0x63) +bind_layers(GMLAN, GMLAN_RMBA, service=0x23) +GMLAN._service_cls[0x23] = GMLAN_RMBA + + class GMLAN_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x63, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), MultipleTypeField( [ (XShortField('memoryAddress', 0), @@ -423,8 +473,11 @@ def answers(self, other): other.memoryAddress == self.memoryAddress +bind_layers(GMLAN, GMLAN_RMBAPR, service=0x63) +GMLAN._service_cls[0x63] = GMLAN_RMBAPR + + # ########################SA################################### -@_gmlan_service(0x27) class GMLAN_SA(Packet): subfunctions = { 0: 'ReservedByDocument', @@ -443,16 +496,25 @@ class GMLAN_SA(Packet): name = 'SecurityAccess' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x27, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), ByteEnumField('subfunction', 0, subfunctions), ConditionalField(XShortField('securityKey', 0), lambda pkt: pkt.subfunction % 2 == 0) ] -@_gmlan_service(0x67) +bind_layers(GMLAN, GMLAN_SA, service=0x27) +GMLAN._service_cls[0x27] = GMLAN_SA + + class GMLAN_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x67, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), ByteEnumField('subfunction', 0, GMLAN_SA.subfunctions), ConditionalField(XShortField('securitySeed', 0), lambda pkt: pkt.subfunction % 2 == 1), @@ -463,20 +525,32 @@ def answers(self, other): and other.subfunction == self.subfunction +bind_layers(GMLAN, GMLAN_SAPR, service=0x67) +GMLAN._service_cls[0x67] = GMLAN_SAPR + + # ########################DDM################################### -@_gmlan_service(0x2C) class GMLAN_DDM(Packet): name = 'DynamicallyDefineMessage' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x2c, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XByteField('DPIDIdentifier', 0), StrField('PIDData', b'\x00\x00') ] -@_gmlan_service(0x6C) +bind_layers(GMLAN, GMLAN_DDM, service=0x2c) +GMLAN._service_cls[0x2c] = GMLAN_DDM + + class GMLAN_DDMPR(Packet): name = 'DynamicallyDefineMessagePositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x6c, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XByteField('DPIDIdentifier', 0) ] @@ -485,11 +559,17 @@ def answers(self, other): and other.DPIDIdentifier == self.DPIDIdentifier +bind_layers(GMLAN, GMLAN_DDMPR, service=0x6c) +GMLAN._service_cls[0x6c] = GMLAN_DDMPR + + # ########################DPBA################################### -@_gmlan_service(0x2D) class GMLAN_DPBA(Packet): name = 'DefinePIDByAddress' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x2d, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XShortField('parameterIdentifier', 0), MultipleTypeField( [ @@ -505,10 +585,16 @@ class GMLAN_DPBA(Packet): ] -@_gmlan_service(0x6D) +bind_layers(GMLAN, GMLAN_DPBA, service=0x2d) +GMLAN._service_cls[0x2d] = GMLAN_DPBA + + class GMLAN_DPBAPR(Packet): name = 'DefinePIDByAddressPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x6d, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XShortField('parameterIdentifier', 0), ] @@ -517,11 +603,17 @@ def answers(self, other): and other.parameterIdentifier == self.parameterIdentifier +bind_layers(GMLAN, GMLAN_DPBAPR, service=0x6d) +GMLAN._service_cls[0x6d] = GMLAN_DPBAPR + + # ########################RD################################### -@_gmlan_service(0x34) class GMLAN_RD(Packet): name = 'RequestDownload' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x34, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XByteField('dataFormatIdentifier', 0), MultipleTypeField( [ @@ -536,8 +628,11 @@ class GMLAN_RD(Packet): ] +bind_layers(GMLAN, GMLAN_RD, service=0x34) +GMLAN._service_cls[0x34] = GMLAN_RD + + # ########################TD################################### -@_gmlan_service(0x36) class GMLAN_TD(Packet): subfunctions = { 0x00: "download", @@ -545,6 +640,9 @@ class GMLAN_TD(Packet): } name = 'TransferData' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x36, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), ByteEnumField('subfunction', 0, subfunctions), MultipleTypeField( [ @@ -560,20 +658,32 @@ class GMLAN_TD(Packet): ] +bind_layers(GMLAN, GMLAN_TD, service=0x36) +GMLAN._service_cls[0x36] = GMLAN_TD + + # ########################WDBI################################### -@_gmlan_service(0x3B) class GMLAN_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x3b, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers), StrField("dataRecord", b'') ] -@_gmlan_service(0x7B) +bind_layers(GMLAN, GMLAN_WDBI, service=0x3b) +GMLAN._service_cls[0x3b] = GMLAN_WDBI + + class GMLAN_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x7b, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers) ] @@ -582,8 +692,11 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier +bind_layers(GMLAN, GMLAN_WDBIPR, service=0x7b) +GMLAN._service_cls[0x7b] = GMLAN_WDBIPR + + # ########################RPSPR################################### -@_gmlan_service(0xE2) class GMLAN_RPSPR(Packet): programmedStates = { 0x00: "fully programmed", @@ -599,12 +712,18 @@ class GMLAN_RPSPR(Packet): } name = 'ReportProgrammedStatePositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xe2, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), ByteEnumField('programmedState', 0, programmedStates), ] +bind_layers(GMLAN, GMLAN_RPSPR, service=0xe2) +GMLAN._service_cls[0xe2] = GMLAN_RPSPR + + # ########################PM################################### -@_gmlan_service(0xA5) class GMLAN_PM(Packet): subfunctions = { 0x01: "requestProgrammingMode", @@ -613,12 +732,18 @@ class GMLAN_PM(Packet): } name = 'ProgrammingMode' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xa5, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), ByteEnumField('subfunction', 0, subfunctions), ] +bind_layers(GMLAN, GMLAN_PM, service=0xa5) +GMLAN._service_cls[0xa5] = GMLAN_PM + + # ########################RDI################################### -@_gmlan_service(0xA9) class GMLAN_RDI(Packet): subfunctions = { 0x80: 'readStatusOfDTCByDTCNumber', @@ -627,10 +752,17 @@ class GMLAN_RDI(Packet): } name = 'ReadDiagnosticInformation' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xa9, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), ByteEnumField('subfunction', 0, subfunctions) ] +bind_layers(GMLAN, GMLAN_RDI, service=0xa9) +GMLAN._service_cls[0xa9] = GMLAN_RDI + + class GMLAN_RDI_BN(Packet): name = 'ReadStatusOfDTCByDTCNumber' fields_desc = [ @@ -667,19 +799,27 @@ class GMLAN_RDI_BC(Packet): # ########################DC################################### -@_gmlan_service(0xAE) class GMLAN_DC(Packet): name = 'DeviceControl' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xae, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XByteField('CPIDNumber', 0), StrFixedLenField('CPIDControlBytes', b"", 5) ] -@_gmlan_service(0xEE) +bind_layers(GMLAN, GMLAN_DC, service=0xae) +GMLAN._service_cls[0xae] = GMLAN_DC + + class GMLAN_DCPR(Packet): name = 'DeviceControlPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xee, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XByteField('CPIDNumber', 0) ] @@ -688,8 +828,11 @@ def answers(self, other): and other.CPIDNumber == self.CPIDNumber +bind_layers(GMLAN, GMLAN_DCPR, service=0xee) +GMLAN._service_cls[0xee] = GMLAN_DCPR + + # ########################NRC################################### -@_gmlan_service(0x7f) class GMLAN_NR(Packet): negativeResponseCodes = { 0x11: 'ServiceNotSupported', @@ -709,6 +852,9 @@ class GMLAN_NR(Packet): } name = 'NegativeResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x7f, GMLAN.services), + lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), XByteEnumField('requestServiceId', 0, GMLAN.services), MayEnd(ByteEnumField('returnCode', 0, negativeResponseCodes)), # XXX Is this MayEnd correct? Why is the field below also 0xe3 ? @@ -719,3 +865,7 @@ def answers(self, other): return self.requestServiceId == other.service and \ (self.returnCode != 0x78 or conf.contribs['GMLAN']['treat-response-pending-as-answer']) + + +bind_layers(GMLAN, GMLAN_NR, service=0x7f) +GMLAN._service_cls[0x7f] = GMLAN_NR diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index c5ece3c5dc0..e97abdb6850 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -21,15 +21,12 @@ XByteField, XShortEnumField, ) -from scapy.packet import Packet, NoPayload +from scapy.packet import Packet, NoPayload, bind_layers from scapy.config import conf from scapy.error import log_loading from scapy.utils import PeriodicSenderThread from scapy.plist import _PacketIterable # noqa: F401 from scapy.contrib.isotp import ISOTP -from scapy.contrib.automotive.utils import ( - _make_service_decorator, -) from scapy.compat import orb from typing import ( # noqa: F401 @@ -146,11 +143,7 @@ def dispatch_hook(cls, _pkt=b"", *args, **kwargs): return cls -_kwp_service = _make_service_decorator(KWP, 'KWP') - - # ########################SDS################################### -@_kwp_service(0x10) class KWP_SDS(Packet): diagnosticSessionTypes = ObservableDict({ 0x81: 'defaultSession', @@ -160,14 +153,23 @@ class KWP_SDS(Packet): 0x92: 'extendedDiagnosticSession'}) name = 'StartDiagnosticSession' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x10, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteEnumField('diagnosticSession', 0, diagnosticSessionTypes) ] -@_kwp_service(0x50) +bind_layers(KWP, KWP_SDS, service=0x10) +KWP._service_cls[0x10] = KWP_SDS + + class KWP_SDSPR(Packet): name = 'StartDiagnosticSessionPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x50, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteEnumField('diagnosticSession', 0, KWP_SDS.diagnosticSessionTypes), ] @@ -178,8 +180,11 @@ def answers(self, other): other.diagnosticSession == self.diagnosticSession +bind_layers(KWP, KWP_SDSPR, service=0x50) +KWP._service_cls[0x50] = KWP_SDSPR + + # ######################### KWP_ER ################################### -@_kwp_service(0x11) class KWP_ER(Packet): resetModes = { 0x00: 'reserved', @@ -187,12 +192,23 @@ class KWP_ER(Packet): 0x82: 'nonvolatileMemoryReset'} name = 'ECUReset' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x11, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteEnumField('resetMode', 0, resetModes) ] -@_kwp_service(0x51) +bind_layers(KWP, KWP_ER, service=0x11) +KWP._service_cls[0x11] = KWP_ER + + class KWP_ERPR(Packet): + fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x51, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ] name = 'ECUResetPositiveResponse' def answers(self, other): @@ -200,21 +216,33 @@ def answers(self, other): return isinstance(other, KWP_ER) +bind_layers(KWP, KWP_ERPR, service=0x51) +KWP._service_cls[0x51] = KWP_ERPR + + # ######################### KWP_SA ################################### -@_kwp_service(0x27) class KWP_SA(Packet): name = 'SecurityAccess' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x27, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteField('accessMode', 0), ConditionalField(StrField('key', b""), lambda pkt: pkt.accessMode % 2 == 0) ] -@_kwp_service(0x67) +bind_layers(KWP, KWP_SA, service=0x27) +KWP._service_cls[0x27] = KWP_SA + + class KWP_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x67, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteField('accessMode', 0), ConditionalField(StrField('seed', b""), lambda pkt: pkt.accessMode % 2 == 1), @@ -226,8 +254,11 @@ def answers(self, other): and other.accessMode == self.accessMode +bind_layers(KWP, KWP_SAPR, service=0x67) +KWP._service_cls[0x67] = KWP_SAPR + + # ######################### KWP_IOCBLI ################################### -@_kwp_service(0x30) class KWP_IOCBLI(Packet): name = 'InputOutputControlByLocalIdentifier' inputOutputControlParameters = { @@ -239,6 +270,9 @@ class KWP_IOCBLI(Packet): 0x08: "Long Term Adjustment" } fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x30, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteField('localIdentifier', 0), XByteEnumField('inputOutputControlParameter', 0, inputOutputControlParameters), @@ -246,10 +280,16 @@ class KWP_IOCBLI(Packet): ] -@_kwp_service(0x70) +bind_layers(KWP, KWP_IOCBLI, service=0x30) +KWP._service_cls[0x30] = KWP_IOCBLI + + class KWP_IOCBLIPR(Packet): name = 'InputOutputControlByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x70, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteField('localIdentifier', 0), XByteEnumField('inputOutputControlParameter', 0, KWP_IOCBLI.inputOutputControlParameters), @@ -262,8 +302,11 @@ def answers(self, other): and other.localIdentifier == self.localIdentifier +bind_layers(KWP, KWP_IOCBLIPR, service=0x70) +KWP._service_cls[0x70] = KWP_IOCBLIPR + + # ######################### KWP_DNMT ################################### -@_kwp_service(0x28) class KWP_DNMT(Packet): responseTypes = { 0x01: 'responseRequired', @@ -271,12 +314,23 @@ class KWP_DNMT(Packet): } name = 'DisableNormalMessageTransmission' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x28, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteEnumField('responseRequired', 0, responseTypes) ] -@_kwp_service(0x68) +bind_layers(KWP, KWP_DNMT, service=0x28) +KWP._service_cls[0x28] = KWP_DNMT + + class KWP_DNMTPR(Packet): + fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x68, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ] name = 'DisableNormalMessageTransmissionPositiveResponse' def answers(self, other): @@ -284,8 +338,11 @@ def answers(self, other): return isinstance(other, KWP_DNMT) +bind_layers(KWP, KWP_DNMTPR, service=0x68) +KWP._service_cls[0x68] = KWP_DNMTPR + + # ######################### KWP_ENMT ################################### -@_kwp_service(0x29) class KWP_ENMT(Packet): responseTypes = { 0x01: 'responseRequired', @@ -293,12 +350,23 @@ class KWP_ENMT(Packet): } name = 'EnableNormalMessageTransmission' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x29, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteEnumField('responseRequired', 1, responseTypes) ] -@_kwp_service(0x69) +bind_layers(KWP, KWP_ENMT, service=0x29) +KWP._service_cls[0x29] = KWP_ENMT + + class KWP_ENMTPR(Packet): + fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x69, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ] name = 'EnableNormalMessageTransmissionPositiveResponse' def answers(self, other): @@ -306,8 +374,11 @@ def answers(self, other): return isinstance(other, KWP_DNMT) +bind_layers(KWP, KWP_ENMTPR, service=0x69) +KWP._service_cls[0x69] = KWP_ENMTPR + + # ######################### KWP_TP ################################### -@_kwp_service(0x3E) class KWP_TP(Packet): responseTypes = { 0x01: 'responseRequired', @@ -315,12 +386,23 @@ class KWP_TP(Packet): } name = 'TesterPresent' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x3e, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteEnumField('responseRequired', 1, responseTypes) ] -@_kwp_service(0x7E) +bind_layers(KWP, KWP_TP, service=0x3e) +KWP._service_cls[0x3e] = KWP_TP + + class KWP_TPPR(Packet): + fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x7e, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ] name = 'TesterPresentPositiveResponse' def answers(self, other): @@ -328,8 +410,11 @@ def answers(self, other): return isinstance(other, KWP_TP) +bind_layers(KWP, KWP_TPPR, service=0x7e) +KWP._service_cls[0x7e] = KWP_TPPR + + # ######################### KWP_CDTCS ################################### -@_kwp_service(0x85) class KWP_CDTCS(Packet): responseTypes = { 0x01: 'responseRequired', @@ -349,14 +434,25 @@ class KWP_CDTCS(Packet): } name = 'ControlDTCSetting' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x85, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteEnumField('responseRequired', 1, responseTypes), XShortEnumField('groupOfDTC', 0, DTCGroups), ByteEnumField('DTCSettingMode', 0, DTCSettingModes), ] -@_kwp_service(0xC5) +bind_layers(KWP, KWP_CDTCS, service=0x85) +KWP._service_cls[0x85] = KWP_CDTCS + + class KWP_CDTCSPR(Packet): + fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xc5, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ] name = 'ControlDTCSettingPositiveResponse' def answers(self, other): @@ -364,8 +460,11 @@ def answers(self, other): return isinstance(other, KWP_CDTCS) +bind_layers(KWP, KWP_CDTCSPR, service=0xc5) +KWP._service_cls[0xc5] = KWP_CDTCSPR + + # ######################### KWP_ROE ################################### -@_kwp_service(0x86) class KWP_ROE(Packet): responseTypes = { 0x01: 'responseRequired', @@ -387,6 +486,9 @@ class KWP_ROE(Packet): } name = 'ResponseOnEvent' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x86, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteEnumField('responseRequired', 1, responseTypes), ByteEnumField('eventWindowTime', 0, eventWindowTimes), MayEnd(ByteEnumField('eventType', 0, eventTypes)), @@ -397,10 +499,16 @@ class KWP_ROE(Packet): ] -@_kwp_service(0xC6) +bind_layers(KWP, KWP_ROE, service=0x86) +KWP._service_cls[0x86] = KWP_ROE + + class KWP_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xc6, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteField("numberOfActivatedEvents", 0), MayEnd(ByteEnumField('eventWindowTime', 0, KWP_ROE.eventWindowTimes)), # XXX Is this MayEnd correct? @@ -413,8 +521,11 @@ def answers(self, other): and other.eventType == self.eventType +bind_layers(KWP, KWP_ROEPR, service=0xc6) +KWP._service_cls[0xc6] = KWP_ROEPR + + # ######################### KWP_RDBLI ################################### -@_kwp_service(0x21) class KWP_RDBLI(Packet): localIdentifiers = ObservableDict({ 0xE0: "Development Data", @@ -432,14 +543,23 @@ class KWP_RDBLI(Packet): }) name = 'ReadDataByLocalIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x21, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('recordLocalIdentifier', 0, localIdentifiers) ] -@_kwp_service(0x61) +bind_layers(KWP, KWP_RDBLI, service=0x21) +KWP._service_cls[0x21] = KWP_RDBLI + + class KWP_RDBLIPR(Packet): name = 'ReadDataByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x61, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) ] @@ -449,19 +569,31 @@ def answers(self, other): and self.recordLocalIdentifier == other.recordLocalIdentifier +bind_layers(KWP, KWP_RDBLIPR, service=0x61) +KWP._service_cls[0x61] = KWP_RDBLIPR + + # ######################### KWP_WDBLI ################################### -@_kwp_service(0x3B) class KWP_WDBLI(Packet): name = 'WriteDataByLocalIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x3b, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) ] -@_kwp_service(0x7B) +bind_layers(KWP, KWP_WDBLI, service=0x3b) +KWP._service_cls[0x3b] = KWP_WDBLI + + class KWP_WDBLIPR(Packet): name = 'WriteDataByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x7b, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) ] @@ -471,20 +603,32 @@ def answers(self, other): and self.recordLocalIdentifier == other.recordLocalIdentifier +bind_layers(KWP, KWP_WDBLIPR, service=0x7b) +KWP._service_cls[0x7b] = KWP_WDBLIPR + + # ######################### KWP_RDBI ################################### -@_kwp_service(0x22) class KWP_RDBI(Packet): dataIdentifiers = ObservableDict() name = 'ReadDataByIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x22, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XShortEnumField('identifier', 0, dataIdentifiers) ] -@_kwp_service(0x62) +bind_layers(KWP, KWP_RDBI, service=0x22) +KWP._service_cls[0x22] = KWP_RDBI + + class KWP_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x62, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers), ] @@ -494,20 +638,32 @@ def answers(self, other): and self.identifier == other.identifier +bind_layers(KWP, KWP_RDBIPR, service=0x62) +KWP._service_cls[0x62] = KWP_RDBIPR + + # ######################### KWP_RMBA ################################### -@_kwp_service(0x23) class KWP_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x23, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), X3BytesField('memoryAddress', 0), ByteField('memorySize', 0) ] -@_kwp_service(0x63) +bind_layers(KWP, KWP_RMBA, service=0x23) +KWP._service_cls[0x23] = KWP_RMBA + + class KWP_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x63, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), StrField('dataRecord', b"", fmt="B") ] @@ -516,10 +672,13 @@ def answers(self, other): return isinstance(other, KWP_RMBA) +bind_layers(KWP, KWP_RMBAPR, service=0x63) +KWP._service_cls[0x63] = KWP_RMBAPR + + # ######################### KWP_DDLI ################################### # TODO: Implement correct interpretation here, # instead of using just the dataRecord -@_kwp_service(0x2C) class KWP_DDLI(Packet): name = 'DynamicallyDefineLocalIdentifier' definitionModes = {0x1: "defineByLocalIdentifier", @@ -527,16 +686,25 @@ class KWP_DDLI(Packet): 0x3: "defineByIdentifier", 0x4: "clearDynamicallyDefinedLocalIdentifier"} fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x2c, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteField('dynamicallyDefineLocalIdentifier', 0), ByteEnumField('definitionMode', 0, definitionModes), StrField('dataRecord', b"", fmt="B") ] -@_kwp_service(0x6C) +bind_layers(KWP, KWP_DDLI, service=0x2c) +KWP._service_cls[0x2c] = KWP_DDLI + + class KWP_DDLIPR(Packet): name = 'DynamicallyDefineLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x6c, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteField('dynamicallyDefineLocalIdentifier', 0) ] @@ -546,19 +714,31 @@ def answers(self, other): other.dynamicallyDefineLocalIdentifier == self.dynamicallyDefineLocalIdentifier # noqa: E501 +bind_layers(KWP, KWP_DDLIPR, service=0x6c) +KWP._service_cls[0x6c] = KWP_DDLIPR + + # ######################### KWP_WDBI ################################### -@_kwp_service(0x2E) class KWP_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x2e, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers) ] -@_kwp_service(0x6E) +bind_layers(KWP, KWP_WDBI, service=0x2e) +KWP._service_cls[0x2e] = KWP_WDBI + + class KWP_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x6e, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers), ] @@ -568,21 +748,33 @@ def answers(self, other): and other.identifier == self.identifier +bind_layers(KWP, KWP_WDBIPR, service=0x6e) +KWP._service_cls[0x6e] = KWP_WDBIPR + + # ######################### KWP_WMBA ################################### -@_kwp_service(0x3D) class KWP_WMBA(Packet): name = 'WriteMemoryByAddress' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x3d, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), X3BytesField('memoryAddress', 0), ByteField('memorySize', 0), StrField('dataRecord', b'', fmt="B") ] -@_kwp_service(0x7D) +bind_layers(KWP, KWP_WMBA, service=0x3d) +KWP._service_cls[0x3d] = KWP_WMBA + + class KWP_WMBAPR(Packet): name = 'WriteMemoryByAddressPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x7d, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), X3BytesField('memoryAddress', 0) ] @@ -592,8 +784,11 @@ def answers(self, other): other.memoryAddress == self.memoryAddress +bind_layers(KWP, KWP_WMBAPR, service=0x7d) +KWP._service_cls[0x7d] = KWP_WMBAPR + + # ######################### KWP_CDI ################################### -@_kwp_service(0x14) class KWP_CDI(Packet): DTCGroups = { 0x0000: 'allPowertrainDTCs', @@ -604,15 +799,24 @@ class KWP_CDI(Packet): } name = 'ClearDiagnosticInformation' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x14, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XShortEnumField('groupOfDTC', 0, DTCGroups) ] -@_kwp_service(0x54) +bind_layers(KWP, KWP_CDI, service=0x14) +KWP._service_cls[0x14] = KWP_CDI + + class KWP_CDIPR(Packet): name = 'ClearDiagnosticInformationPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x54, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XShortEnumField('groupOfDTC', 0, KWP_CDI.DTCGroups) ] @@ -622,20 +826,32 @@ def answers(self, other): self.groupOfDTC == other.groupOfDTC +bind_layers(KWP, KWP_CDIPR, service=0x54) +KWP._service_cls[0x54] = KWP_CDIPR + + # ######################### KWP_RSODTC ################################### -@_kwp_service(0x17) class KWP_RSODTC(Packet): name = 'ReadStatusOfDiagnosticTroubleCodes' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x17, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XShortEnumField('groupOfDTC', 0, KWP_CDI.DTCGroups) ] -@_kwp_service(0x57) +bind_layers(KWP, KWP_RSODTC, service=0x17) +KWP._service_cls[0x17] = KWP_RSODTC + + class KWP_RSODTCPR(Packet): name = 'ReadStatusOfDiagnosticTroubleCodesPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x57, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteField('numberOfDTC', 0), ] @@ -644,8 +860,11 @@ def answers(self, other): return isinstance(other, KWP_RSODTC) +bind_layers(KWP, KWP_RSODTCPR, service=0x57) +KWP._service_cls[0x57] = KWP_RSODTCPR + + # ######################### KWP_RECUI ################################### -@_kwp_service(0x1A) class KWP_RECUI(Packet): name = 'ReadECUIdentification' localIdentifiers = ObservableDict({ @@ -664,15 +883,24 @@ class KWP_RECUI(Packet): 0x9F: "ECU Boot Fingerprint" }) fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x1a, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('localIdentifier', 0, localIdentifiers) ] -@_kwp_service(0x5A) +bind_layers(KWP, KWP_RECUI, service=0x1a) +KWP._service_cls[0x1a] = KWP_RECUI + + class KWP_RECUIPR(Packet): name = 'ReadECUIdentificationPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x5a, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('localIdentifier', 0, KWP_RECUI.localIdentifiers) ] @@ -682,8 +910,11 @@ def answers(self, other): self.localIdentifier == other.localIdentifier +bind_layers(KWP, KWP_RECUIPR, service=0x5a) +KWP._service_cls[0x5a] = KWP_RECUIPR + + # ######################### KWP_SRBLI ################################### -@_kwp_service(0x31) class KWP_SRBLI(Packet): routineLocalIdentifiers = ObservableDict({ 0xE0: "FlashEraseRoutine", @@ -699,14 +930,23 @@ class KWP_SRBLI(Packet): }) name = 'StartRoutineByLocalIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x31, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('routineLocalIdentifier', 0, routineLocalIdentifiers) ] -@_kwp_service(0x71) +bind_layers(KWP, KWP_SRBLI, service=0x31) +KWP._service_cls[0x31] = KWP_SRBLI + + class KWP_SRBLIPR(Packet): name = 'StartRoutineByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x71, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] @@ -717,20 +957,32 @@ def answers(self, other): and other.routineLocalIdentifier == self.routineLocalIdentifier +bind_layers(KWP, KWP_SRBLIPR, service=0x71) +KWP._service_cls[0x71] = KWP_SRBLIPR + + # ######################### KWP_STRBLI ################################### -@_kwp_service(0x32) class KWP_STRBLI(Packet): name = 'StopRoutineByLocalIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x32, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] -@_kwp_service(0x72) +bind_layers(KWP, KWP_STRBLI, service=0x32) +KWP._service_cls[0x32] = KWP_STRBLI + + class KWP_STRBLIPR(Packet): name = 'StopRoutineByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x72, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] @@ -741,20 +993,32 @@ def answers(self, other): and other.routineLocalIdentifier == self.routineLocalIdentifier +bind_layers(KWP, KWP_STRBLIPR, service=0x72) +KWP._service_cls[0x72] = KWP_STRBLIPR + + # ######################### KWP_RRRBLI ################################### -@_kwp_service(0x33) class KWP_RRRBLI(Packet): name = 'RequestRoutineResultsByLocalIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x33, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] -@_kwp_service(0x73) +bind_layers(KWP, KWP_RRRBLI, service=0x33) +KWP._service_cls[0x33] = KWP_RRRBLI + + class KWP_RRRBLIPR(Packet): name = 'RequestRoutineResultsByLocalIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x73, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] @@ -765,11 +1029,17 @@ def answers(self, other): and other.routineLocalIdentifier == self.routineLocalIdentifier +bind_layers(KWP, KWP_RRRBLIPR, service=0x73) +KWP._service_cls[0x73] = KWP_RRRBLIPR + + # ######################### KWP_RD ################################### -@_kwp_service(0x34) class KWP_RD(Packet): name = 'RequestDownload' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x34, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), X3BytesField('memoryAddress', 0), BitField('compression', 0, 4), BitField('encryption', 0, 4), @@ -777,10 +1047,16 @@ class KWP_RD(Packet): ] -@_kwp_service(0x74) +bind_layers(KWP, KWP_RD, service=0x34) +KWP._service_cls[0x34] = KWP_RD + + class KWP_RDPR(Packet): name = 'RequestDownloadPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x74, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), StrField('maxNumberOfBlockLength', b"", fmt="B"), ] @@ -789,11 +1065,17 @@ def answers(self, other): return isinstance(other, KWP_RD) +bind_layers(KWP, KWP_RDPR, service=0x74) +KWP._service_cls[0x74] = KWP_RDPR + + # ######################### KWP_RU ################################### -@_kwp_service(0x35) class KWP_RU(Packet): name = 'RequestUpload' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x35, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), X3BytesField('memoryAddress', 0), BitField('compression', 0, 4), BitField('encryption', 0, 4), @@ -801,10 +1083,16 @@ class KWP_RU(Packet): ] -@_kwp_service(0x75) +bind_layers(KWP, KWP_RU, service=0x35) +KWP._service_cls[0x35] = KWP_RU + + class KWP_RUPR(Packet): name = 'RequestUploadPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x75, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), StrField('maxNumberOfBlockLength', b"", fmt="B"), ] @@ -813,20 +1101,32 @@ def answers(self, other): return isinstance(other, KWP_RU) +bind_layers(KWP, KWP_RUPR, service=0x75) +KWP._service_cls[0x75] = KWP_RUPR + + # ######################### KWP_TD ################################### -@_kwp_service(0x36) class KWP_TD(Packet): name = 'TransferData' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x36, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteField('blockSequenceCounter', 0), StrField('transferDataRequestParameter', b"", fmt="B") ] -@_kwp_service(0x76) +bind_layers(KWP, KWP_TD, service=0x36) +KWP._service_cls[0x36] = KWP_TD + + class KWP_TDPR(Packet): name = 'TransferDataPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x76, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), ByteField('blockSequenceCounter', 0), StrField('transferDataRequestParameter', b"", fmt="B") ] @@ -837,19 +1137,31 @@ def answers(self, other): and other.blockSequenceCounter == self.blockSequenceCounter +bind_layers(KWP, KWP_TDPR, service=0x76) +KWP._service_cls[0x76] = KWP_TDPR + + # ######################### KWP_RTE ################################### -@_kwp_service(0x37) class KWP_RTE(Packet): name = 'RequestTransferExit' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x37, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), StrField('transferDataRequestParameter', b"", fmt="B") ] -@_kwp_service(0x77) +bind_layers(KWP, KWP_RTE, service=0x37) +KWP._service_cls[0x37] = KWP_RTE + + class KWP_RTEPR(Packet): name = 'RequestTransferExitPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x77, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), StrField('transferDataRequestParameter', b"", fmt="B") ] @@ -858,8 +1170,11 @@ def answers(self, other): return isinstance(other, KWP_RTE) +bind_layers(KWP, KWP_RTEPR, service=0x77) +KWP._service_cls[0x77] = KWP_RTEPR + + # ######################### KWP_NR ################################### -@_kwp_service(0x7f) class KWP_NR(Packet): negativeResponseCodes = { 0x00: 'positiveResponse', @@ -886,6 +1201,9 @@ class KWP_NR(Packet): } name = 'NegativeResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x7f, KWP.services), + lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), MayEnd(XByteEnumField('requestServiceId', 0, KWP.services)), # XXX Is this MayEnd correct? ByteEnumField('negativeResponseCode', 0, negativeResponseCodes) @@ -898,6 +1216,10 @@ def answers(self, other): conf.contribs['KWP']['treat-response-pending-as-answer']) +bind_layers(KWP, KWP_NR, service=0x7f) +KWP._service_cls[0x7f] = KWP_NR + + # ################################################################## # ######################## UTILS ################################### # ################################################################## diff --git a/scapy/contrib/automotive/obd/obd.py b/scapy/contrib/automotive/obd/obd.py index a6cb9b785cf..a59211cc116 100644 --- a/scapy/contrib/automotive/obd/obd.py +++ b/scapy/contrib/automotive/obd/obd.py @@ -14,13 +14,10 @@ from scapy.contrib.automotive.obd.pid.pids import * from scapy.contrib.automotive.obd.tid.tids import * from scapy.contrib.automotive.obd.services import * -from scapy.packet import NoPayload +from scapy.packet import NoPayload, bind_layers from scapy.config import conf -from scapy.fields import XByteEnumField +from scapy.fields import ConditionalField, XByteEnumField from scapy.contrib.isotp import ISOTP -from scapy.contrib.automotive.utils import ( - _make_service_decorator, -) from scapy.compat import orb try: @@ -96,28 +93,35 @@ def dispatch_hook(cls, _pkt=b"", *args, **kwargs): return cls -_obd_service = _make_service_decorator(OBD, 'OBD') - -# Service Bindings — applied via the generic decorator (functional form, -# since the service classes are defined in a separate module) - -_obd_service(0x01)(OBD_S01) -_obd_service(0x02)(OBD_S02) -_obd_service(0x03)(OBD_S03) -_obd_service(0x04)(OBD_S04) -_obd_service(0x06)(OBD_S06) -_obd_service(0x07)(OBD_S07) -_obd_service(0x08)(OBD_S08) -_obd_service(0x09)(OBD_S09) -_obd_service(0x0A)(OBD_S0A) - -_obd_service(0x41)(OBD_S01_PR) -_obd_service(0x42)(OBD_S02_PR) -_obd_service(0x43)(OBD_S03_PR) -_obd_service(0x44)(OBD_S04_PR) -_obd_service(0x46)(OBD_S06_PR) -_obd_service(0x47)(OBD_S07_PR) -_obd_service(0x48)(OBD_S08_PR) -_obd_service(0x49)(OBD_S09_PR) -_obd_service(0x4A)(OBD_S0A_PR) -_obd_service(0x7F)(OBD_NR) +_OBD_SERVICES = [ + (0x01, OBD_S01), + (0x02, OBD_S02), + (0x03, OBD_S03), + (0x04, OBD_S04), + (0x06, OBD_S06), + (0x07, OBD_S07), + (0x08, OBD_S08), + (0x09, OBD_S09), + (0x0A, OBD_S0A), + (0x41, OBD_S01_PR), + (0x42, OBD_S02_PR), + (0x43, OBD_S03_PR), + (0x44, OBD_S04_PR), + (0x46, OBD_S06_PR), + (0x47, OBD_S07_PR), + (0x48, OBD_S08_PR), + (0x49, OBD_S09_PR), + (0x4A, OBD_S0A_PR), + (0x7F, OBD_NR), +] + +for _sid, _cls in _OBD_SERVICES: + _cls.fields_desc = [ + ConditionalField( + XByteEnumField('service', _sid, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) + ] + list(_cls.fields_desc) + bind_layers(OBD, _cls, service=_sid) + OBD._service_cls[_sid] = _cls + +del _sid, _cls diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 21f4ec7ad01..76b204f3007 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -19,14 +19,11 @@ ShortField, ObservableDict, XShortEnumField, XByteEnumField, StrLenField, \ FieldLenField, XStrFixedLenField, XStrLenField, FlagsField, PacketListField, \ PacketField -from scapy.packet import Packet, NoPayload, Raw +from scapy.packet import Packet, NoPayload, Raw, bind_layers from scapy.compat import orb from scapy.config import conf from scapy.utils import PeriodicSenderThread from scapy.contrib.isotp import ISOTP -from scapy.contrib.automotive.utils import ( - _make_service_decorator, -) # Typing imports from typing import ( # noqa: F401 @@ -143,11 +140,7 @@ def dispatch_hook(cls, _pkt=b"", *args, **kwargs): return cls -_uds_service = _make_service_decorator(UDS, 'UDS') - - # ########################DSC################################### -@_uds_service(0x10) class UDS_DSC(Packet): diagnosticSessionTypes = ObservableDict({ 0x00: 'ISOSAEReserved', @@ -158,14 +151,23 @@ class UDS_DSC(Packet): 0x7F: 'ISOSAEReserved'}) name = 'DiagnosticSessionControl' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x10, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('diagnosticSessionType', 0, diagnosticSessionTypes) ] -@_uds_service(0x50) +bind_layers(UDS, UDS_DSC, service=0x10) +UDS._service_cls[0x10] = UDS_DSC + + class UDS_DSCPR(Packet): name = 'DiagnosticSessionControlPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x50, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('diagnosticSessionType', 0, UDS_DSC.diagnosticSessionTypes), StrField('sessionParameterRecord', b"") @@ -176,8 +178,11 @@ def answers(self, other): other.diagnosticSessionType == self.diagnosticSessionType +bind_layers(UDS, UDS_DSCPR, service=0x50) +UDS._service_cls[0x50] = UDS_DSCPR + + # #########################ER################################### -@_uds_service(0x11) class UDS_ER(Packet): resetTypes = { 0x00: 'ISOSAEReserved', @@ -190,14 +195,23 @@ class UDS_ER(Packet): 0x7F: 'ISOSAEReserved'} name = 'ECUReset' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x11, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('resetType', 0, resetTypes) ] -@_uds_service(0x51) +bind_layers(UDS, UDS_ER, service=0x11) +UDS._service_cls[0x11] = UDS_ER + + class UDS_ERPR(Packet): name = 'ECUResetPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x51, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('resetType', 0, UDS_ER.resetTypes), ConditionalField(ByteField('powerDownTime', 0), lambda pkt: pkt.resetType == 0x04) @@ -207,11 +221,17 @@ def answers(self, other): return isinstance(other, UDS_ER) and other.resetType == self.resetType +bind_layers(UDS, UDS_ERPR, service=0x51) +UDS._service_cls[0x51] = UDS_ERPR + + # #########################SA################################### -@_uds_service(0x27) class UDS_SA(Packet): name = 'SecurityAccess' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x27, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteField('securityAccessType', 0), ConditionalField(StrField('securityAccessDataRecord', b""), lambda pkt: pkt.securityAccessType % 2 == 1), @@ -220,10 +240,16 @@ class UDS_SA(Packet): ] -@_uds_service(0x67) +bind_layers(UDS, UDS_SA, service=0x27) +UDS._service_cls[0x27] = UDS_SA + + class UDS_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x67, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteField('securityAccessType', 0), ConditionalField(StrField('securitySeed', b""), lambda pkt: pkt.securityAccessType % 2 == 1), @@ -234,8 +260,11 @@ def answers(self, other): and other.securityAccessType == self.securityAccessType +bind_layers(UDS, UDS_SAPR, service=0x67) +UDS._service_cls[0x67] = UDS_SAPR + + # #########################CC################################### -@_uds_service(0x28) class UDS_CC(Packet): controlTypes = { 0x00: 'enableRxAndTx', @@ -245,6 +274,9 @@ class UDS_CC(Packet): } name = 'CommunicationControl' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x28, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('controlType', 0, controlTypes), BitEnumField('communicationType0', 0, 2, {0: 'ISOSAEReserved', @@ -273,10 +305,16 @@ class UDS_CC(Packet): ] -@_uds_service(0x68) +bind_layers(UDS, UDS_CC, service=0x28) +UDS._service_cls[0x28] = UDS_CC + + class UDS_CCPR(Packet): name = 'CommunicationControlPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x68, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('controlType', 0, UDS_CC.controlTypes) ] @@ -285,8 +323,11 @@ def answers(self, other): and other.controlType == self.controlType +bind_layers(UDS, UDS_CCPR, service=0x68) +UDS._service_cls[0x68] = UDS_CCPR + + # #########################AUTH################################### -@_uds_service(0x29) class UDS_AUTH(Packet): subFunctions = { 0x00: 'deAuthenticate', @@ -302,6 +343,9 @@ class UDS_AUTH(Packet): } name = "Authentication" fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x29, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('subFunction', 0, subFunctions), ConditionalField(XByteField('communicationConfiguration', 0), lambda pkt: pkt.subFunction in [0x01, 0x02, 0x5]), @@ -360,7 +404,10 @@ class UDS_AUTH(Packet): ] -@_uds_service(0x69) +bind_layers(UDS, UDS_AUTH, service=0x29) +UDS._service_cls[0x29] = UDS_AUTH + + class UDS_AUTHPR(Packet): authenticationReturnParameterTypes = { 0x00: 'requestAccepted', @@ -382,6 +429,9 @@ class UDS_AUTHPR(Packet): } name = 'AuthenticationPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x69, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('subFunction', 0, UDS_AUTH.subFunctions), ByteEnumField('returnValue', 0, authenticationReturnParameterTypes), ConditionalField( @@ -439,19 +489,31 @@ def answers(self, other): and other.subFunction == self.subFunction +bind_layers(UDS, UDS_AUTHPR, service=0x69) +UDS._service_cls[0x69] = UDS_AUTHPR + + # #########################TP################################### -@_uds_service(0x3E) class UDS_TP(Packet): name = 'TesterPresent' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x3e, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteField('subFunction', 0) ] -@_uds_service(0x7E) +bind_layers(UDS, UDS_TP, service=0x3e) +UDS._service_cls[0x3e] = UDS_TP + + class UDS_TPPR(Packet): name = 'TesterPresentPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x7e, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteField('zeroSubFunction', 0) ] @@ -459,8 +521,11 @@ def answers(self, other): return isinstance(other, UDS_TP) +bind_layers(UDS, UDS_TPPR, service=0x7e) +UDS._service_cls[0x7e] = UDS_TPPR + + # #########################ATP################################### -@_uds_service(0x83) class UDS_ATP(Packet): timingParameterAccessTypes = { 0: 'ISOSAEReserved', @@ -471,6 +536,9 @@ class UDS_ATP(Packet): } name = 'AccessTimingParameter' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x83, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('timingParameterAccessType', 0, timingParameterAccessTypes), ConditionalField(StrField('timingParameterRequestRecord', b""), @@ -478,10 +546,16 @@ class UDS_ATP(Packet): ] -@_uds_service(0xC3) +bind_layers(UDS, UDS_ATP, service=0x83) +UDS._service_cls[0x83] = UDS_ATP + + class UDS_ATPPR(Packet): name = 'AccessTimingParameterPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xc3, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('timingParameterAccessType', 0, UDS_ATP.timingParameterAccessTypes), ConditionalField(StrField('timingParameterResponseRecord', b""), @@ -494,13 +568,19 @@ def answers(self, other): self.timingParameterAccessType +bind_layers(UDS, UDS_ATPPR, service=0xc3) +UDS._service_cls[0xc3] = UDS_ATPPR + + # #########################SDT################################### # TODO: Implement correct internal message service handling here, # instead of using just the dataRecord -@_uds_service(0x84) class UDS_SDT(Packet): name = 'SecuredDataTransmission' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x84, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), BitField('requestMessage', 0, 1), BitField('ISOSAEReservedBackwardsCompatibility', 0, 2), BitField('preEstablishedKeyUsed', 0, 1), @@ -516,10 +596,16 @@ class UDS_SDT(Packet): ] -@_uds_service(0xC4) +bind_layers(UDS, UDS_SDT, service=0x84) +UDS._service_cls[0x84] = UDS_SDT + + class UDS_SDTPR(Packet): name = 'SecuredDataTransmissionPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xc4, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), BitField('requestMessage', 0, 1), BitField('ISOSAEReservedBackwardsCompatibility', 0, 2), BitField('preEstablishedKeyUsed', 0, 1), @@ -538,8 +624,11 @@ def answers(self, other): return isinstance(other, UDS_SDT) +bind_layers(UDS, UDS_SDTPR, service=0xc4) +UDS._service_cls[0xc4] = UDS_SDTPR + + # #########################CDTCS################################### -@_uds_service(0x85) class UDS_CDTCS(Packet): DTCSettingTypes = { 0: 'ISOSAEReserved', @@ -548,15 +637,24 @@ class UDS_CDTCS(Packet): } name = 'ControlDTCSetting' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x85, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('DTCSettingType', 0, DTCSettingTypes), StrField('DTCSettingControlOptionRecord', b"") ] -@_uds_service(0xC5) +bind_layers(UDS, UDS_CDTCS, service=0x85) +UDS._service_cls[0x85] = UDS_CDTCS + + class UDS_CDTCSPR(Packet): name = 'ControlDTCSettingPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xc5, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('DTCSettingType', 0, UDS_CDTCS.DTCSettingTypes) ] @@ -564,9 +662,12 @@ def answers(self, other): return isinstance(other, UDS_CDTCS) +bind_layers(UDS, UDS_CDTCSPR, service=0xc5) +UDS._service_cls[0xc5] = UDS_CDTCSPR + + # #########################ROE################################### # TODO: improve this protocol implementation -@_uds_service(0x86) class UDS_ROE(Packet): eventTypes = { 0: 'doNotStoreEvent', @@ -574,16 +675,25 @@ class UDS_ROE(Packet): } name = 'ResponseOnEvent' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x86, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('eventType', 0, eventTypes), ByteField('eventWindowTime', 0), StrField('eventTypeRecord', b"") ] -@_uds_service(0xC6) +bind_layers(UDS, UDS_ROE, service=0x86) +UDS._service_cls[0x86] = UDS_ROE + + class UDS_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xc6, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('eventType', 0, UDS_ROE.eventTypes), ByteField('numberOfIdentifiedEvents', 0), ByteField('eventWindowTime', 0), @@ -595,8 +705,11 @@ def answers(self, other): and other.eventType == self.eventType +bind_layers(UDS, UDS_ROEPR, service=0xc6) +UDS._service_cls[0xc6] = UDS_ROEPR + + # #########################LC################################### -@_uds_service(0x87) class UDS_LC(Packet): linkControlTypes = { 0: 'ISOSAEReserved', @@ -606,6 +719,9 @@ class UDS_LC(Packet): } name = 'LinkControl' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x87, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('linkControlType', 0, linkControlTypes), ConditionalField(ByteField('baudrateIdentifier', 0), lambda pkt: pkt.linkControlType == 0x1), @@ -618,10 +734,16 @@ class UDS_LC(Packet): ] -@_uds_service(0xC7) +bind_layers(UDS, UDS_LC, service=0x87) +UDS._service_cls[0x87] = UDS_LC + + class UDS_LCPR(Packet): name = 'LinkControlPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0xc7, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('linkControlType', 0, UDS_LC.linkControlTypes) ] @@ -630,22 +752,34 @@ def answers(self, other): and other.linkControlType == self.linkControlType +bind_layers(UDS, UDS_LCPR, service=0xc7) +UDS._service_cls[0xc7] = UDS_LCPR + + # #########################RDBI################################### -@_uds_service(0x22) class UDS_RDBI(Packet): dataIdentifiers = ObservableDict() name = 'ReadDataByIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x22, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), FieldListField("identifiers", None, XShortEnumField('dataIdentifier', 0, dataIdentifiers)) ] -@_uds_service(0x62) +bind_layers(UDS, UDS_RDBI, service=0x22) +UDS._service_cls[0x22] = UDS_RDBI + + class UDS_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x62, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -655,11 +789,17 @@ def answers(self, other): and self.dataIdentifier in other.identifiers +bind_layers(UDS, UDS_RDBIPR, service=0x62) +UDS._service_cls[0x62] = UDS_RDBIPR + + # #########################RMBA################################### -@_uds_service(0x23) class UDS_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x23, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -681,10 +821,16 @@ class UDS_RMBA(Packet): ] -@_uds_service(0x63) +bind_layers(UDS, UDS_RMBA, service=0x23) +UDS._service_cls[0x23] = UDS_RMBA + + class UDS_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x63, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), StrField('dataRecord', b"", fmt="B") ] @@ -692,21 +838,33 @@ def answers(self, other): return isinstance(other, UDS_RMBA) +bind_layers(UDS, UDS_RMBAPR, service=0x63) +UDS._service_cls[0x63] = UDS_RMBAPR + + # #########################RSDBI################################### -@_uds_service(0x24) class UDS_RSDBI(Packet): name = 'ReadScalingDataByIdentifier' dataIdentifiers = ObservableDict() fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x24, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), XShortEnumField('dataIdentifier', 0, dataIdentifiers) ] +bind_layers(UDS, UDS_RSDBI, service=0x24) +UDS._service_cls[0x24] = UDS_RSDBI + + # TODO: Implement correct scaling here, instead of using just the dataRecord -@_uds_service(0x64) class UDS_RSDBIPR(Packet): name = 'ReadScalingDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x64, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), XShortEnumField('dataIdentifier', 0, UDS_RSDBI.dataIdentifiers), ByteField('scalingByte', 0), StrField('dataRecord', b"", fmt="B") @@ -717,8 +875,11 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier +bind_layers(UDS, UDS_RSDBIPR, service=0x64) +UDS._service_cls[0x64] = UDS_RSDBIPR + + # #########################RDBPI################################### -@_uds_service(0x2A) class UDS_RDBPI(Packet): periodicDataIdentifiers = ObservableDict() transmissionModes = { @@ -730,17 +891,26 @@ class UDS_RDBPI(Packet): } name = 'ReadDataByPeriodicIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x2a, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('transmissionMode', 0, transmissionModes), ByteEnumField('periodicDataIdentifier', 0, periodicDataIdentifiers), StrField('furtherPeriodicDataIdentifier', b"", fmt="B") ] +bind_layers(UDS, UDS_RDBPI, service=0x2a) +UDS._service_cls[0x2a] = UDS_RDBPI + + # TODO: Implement correct scaling here, instead of using just the dataRecord -@_uds_service(0x6A) class UDS_RDBPIPR(Packet): name = 'ReadDataByPeriodicIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x6a, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteField('periodicDataIdentifier', 0), StrField('dataRecord', b"", fmt="B") ] @@ -750,25 +920,37 @@ def answers(self, other): and other.periodicDataIdentifier == self.periodicDataIdentifier +bind_layers(UDS, UDS_RDBPIPR, service=0x6a) +UDS._service_cls[0x6a] = UDS_RDBPIPR + + # #########################DDDI################################### # TODO: Implement correct interpretation here, # instead of using just the dataRecord -@_uds_service(0x2C) class UDS_DDDI(Packet): name = 'DynamicallyDefineDataIdentifier' subFunctions = {0x1: "defineByIdentifier", 0x2: "defineByMemoryAddress", 0x3: "clearDynamicallyDefinedDataIdentifier"} fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x2c, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('subFunction', 0, subFunctions), StrField('dataRecord', b"", fmt="B") ] -@_uds_service(0x6C) +bind_layers(UDS, UDS_DDDI, service=0x2c) +UDS._service_cls[0x2c] = UDS_DDDI + + class UDS_DDDIPR(Packet): name = 'DynamicallyDefineDataIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x6c, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('subFunction', 0, UDS_DDDI.subFunctions), XShortField('dynamicallyDefinedDataIdentifier', 0) ] @@ -778,20 +960,32 @@ def answers(self, other): and other.subFunction == self.subFunction +bind_layers(UDS, UDS_DDDIPR, service=0x6c) +UDS._service_cls[0x6c] = UDS_DDDIPR + + # #########################WDBI################################### -@_uds_service(0x2E) class UDS_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x2e, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers) ] -@_uds_service(0x6E) +bind_layers(UDS, UDS_WDBI, service=0x2e) +UDS._service_cls[0x2e] = UDS_WDBI + + class UDS_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x6e, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -801,11 +995,17 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier +bind_layers(UDS, UDS_WDBIPR, service=0x6e) +UDS._service_cls[0x6e] = UDS_WDBIPR + + # #########################WMBA################################### -@_uds_service(0x3D) class UDS_WMBA(Packet): name = 'WriteMemoryByAddress' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x3d, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -829,10 +1029,16 @@ class UDS_WMBA(Packet): ] -@_uds_service(0x7D) +bind_layers(UDS, UDS_WMBA, service=0x3d) +UDS._service_cls[0x3d] = UDS_WMBA + + class UDS_WMBAPR(Packet): name = 'WriteMemoryByAddressPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x7d, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -859,6 +1065,10 @@ def answers(self, other): and other.memoryAddressLen == self.memoryAddressLen +bind_layers(UDS, UDS_WMBAPR, service=0x7d) +UDS._service_cls[0x7d] = UDS_WMBAPR + + # ##########################DTC##################################### class DTC(Packet): name = 'Diagnostic Trouble Code' @@ -884,26 +1094,39 @@ def extract_padding(self, s): # #########################CDTCI################################### -@_uds_service(0x14) class UDS_CDTCI(Packet): name = 'ClearDiagnosticInformation' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x14, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteField('groupOfDTCHighByte', 0), ByteField('groupOfDTCMiddleByte', 0), ByteField('groupOfDTCLowByte', 0), ] -@_uds_service(0x54) +bind_layers(UDS, UDS_CDTCI, service=0x14) +UDS._service_cls[0x14] = UDS_CDTCI + + class UDS_CDTCIPR(Packet): + fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x54, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ] name = 'ClearDiagnosticInformationPositiveResponse' def answers(self, other): return isinstance(other, UDS_CDTCI) +bind_layers(UDS, UDS_CDTCIPR, service=0x54) +UDS._service_cls[0x54] = UDS_CDTCIPR + + # #########################RDTCI################################### -@_uds_service(0x19) class UDS_RDTCI(Packet): reportTypes = { 0: 'ISOSAEReserved', @@ -958,6 +1181,9 @@ class UDS_RDTCI(Packet): } name = 'ReadDTCInformation' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x19, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('reportType', 0, reportTypes), ConditionalField(FlagsField('DTCSeverityMask', 0, 8, dtcSeverityMask), lambda pkt: pkt.reportType in [0x07, 0x08]), @@ -974,6 +1200,10 @@ class UDS_RDTCI(Packet): ] +bind_layers(UDS, UDS_RDTCI, service=0x19) +UDS._service_cls[0x19] = UDS_RDTCI + + class DTCAndStatusRecord(Packet): name = 'DTC and status record' fields_desc = [ @@ -1031,10 +1261,12 @@ class DTCSnapshotRecord(Packet): ] -@_uds_service(0x59) class UDS_RDTCIPR(Packet): name = 'ReadDTCInformationPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x59, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('reportType', 0, UDS_RDTCI.reportTypes), ConditionalField( FlagsField('DTCStatusAvailabilityMask', 0, 8, UDS_RDTCI.dtcStatus), @@ -1083,8 +1315,11 @@ def answers(self, other): return True +bind_layers(UDS, UDS_RDTCIPR, service=0x59) +UDS._service_cls[0x59] = UDS_RDTCIPR + + # #########################RC################################### -@_uds_service(0x31) class UDS_RC(Packet): routineControlTypes = { 0: 'ISOSAEReserved', @@ -1095,15 +1330,24 @@ class UDS_RC(Packet): routineControlIdentifiers = ObservableDict() name = 'RoutineControl' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x31, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('routineControlType', 0, routineControlTypes), XShortEnumField('routineIdentifier', 0, routineControlIdentifiers) ] -@_uds_service(0x71) +bind_layers(UDS, UDS_RC, service=0x31) +UDS._service_cls[0x31] = UDS_RC + + class UDS_RCPR(Packet): name = 'RoutineControlPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x71, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('routineControlType', 0, UDS_RC.routineControlTypes), XShortEnumField('routineIdentifier', 0, UDS_RC.routineControlIdentifiers), @@ -1120,14 +1364,20 @@ def answers(self, other): return False +bind_layers(UDS, UDS_RCPR, service=0x71) +UDS._service_cls[0x71] = UDS_RCPR + + # #########################RD################################### -@_uds_service(0x34) class UDS_RD(Packet): dataFormatIdentifiers = ObservableDict({ 0: 'noCompressionNoEncryption' }) name = 'RequestDownload' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x34, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('dataFormatIdentifier', 0, dataFormatIdentifiers), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), @@ -1150,10 +1400,16 @@ class UDS_RD(Packet): ] -@_uds_service(0x74) +bind_layers(UDS, UDS_RD, service=0x34) +UDS._service_cls[0x34] = UDS_RD + + class UDS_RDPR(Packet): name = 'RequestDownloadPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x74, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), BitField('memorySizeLen', 0, 4), BitField('reserved', 0, 4), StrField('maxNumberOfBlockLength', b"", fmt="B"), @@ -1163,11 +1419,17 @@ def answers(self, other): return isinstance(other, UDS_RD) +bind_layers(UDS, UDS_RDPR, service=0x74) +UDS._service_cls[0x74] = UDS_RDPR + + # #########################RU################################### -@_uds_service(0x35) class UDS_RU(Packet): name = 'RequestUpload' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x35, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteEnumField('dataFormatIdentifier', 0, UDS_RD.dataFormatIdentifiers), BitField('memorySizeLen', 0, 4), @@ -1191,10 +1453,16 @@ class UDS_RU(Packet): ] -@_uds_service(0x75) +bind_layers(UDS, UDS_RU, service=0x35) +UDS._service_cls[0x35] = UDS_RU + + class UDS_RUPR(Packet): name = 'RequestUploadPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x75, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), BitField('memorySizeLen', 0, 4), BitField('reserved', 0, 4), StrField('maxNumberOfBlockLength', b"", fmt="B"), @@ -1204,20 +1472,32 @@ def answers(self, other): return isinstance(other, UDS_RU) +bind_layers(UDS, UDS_RUPR, service=0x75) +UDS._service_cls[0x75] = UDS_RUPR + + # #########################TD################################### -@_uds_service(0x36) class UDS_TD(Packet): name = 'TransferData' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x36, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteField('blockSequenceCounter', 0), StrField('transferRequestParameterRecord', b"", fmt="B") ] -@_uds_service(0x76) +bind_layers(UDS, UDS_TD, service=0x36) +UDS._service_cls[0x36] = UDS_TD + + class UDS_TDPR(Packet): name = 'TransferDataPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x76, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), ByteField('blockSequenceCounter', 0), StrField('transferResponseParameterRecord', b"", fmt="B") ] @@ -1227,19 +1507,31 @@ def answers(self, other): and other.blockSequenceCounter == self.blockSequenceCounter +bind_layers(UDS, UDS_TDPR, service=0x76) +UDS._service_cls[0x76] = UDS_TDPR + + # #########################RTE################################### -@_uds_service(0x37) class UDS_RTE(Packet): name = 'RequestTransferExit' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x37, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), StrField('transferRequestParameterRecord', b"", fmt="B") ] -@_uds_service(0x77) +bind_layers(UDS, UDS_RTE, service=0x37) +UDS._service_cls[0x37] = UDS_RTE + + class UDS_RTEPR(Packet): name = 'RequestTransferExitPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x77, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), StrField('transferResponseParameterRecord', b"", fmt="B") ] @@ -1247,8 +1539,11 @@ def answers(self, other): return isinstance(other, UDS_RTE) +bind_layers(UDS, UDS_RTEPR, service=0x77) +UDS._service_cls[0x77] = UDS_RTEPR + + # #########################RFT################################### -@_uds_service(0x38) class UDS_RFT(Packet): name = 'RequestFileTransfer' @@ -1266,6 +1561,9 @@ def _contains_file_size(packet): return packet.modeOfOperation not in [2, 4, 5] fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x38, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), XByteEnumField('modeOfOperation', 0, modeOfOperations), FieldLenField('filePathAndNameLength', None, length_of='filePathAndName', fmt='H'), @@ -1290,7 +1588,10 @@ def _contains_file_size(packet): ] -@_uds_service(0x78) +bind_layers(UDS, UDS_RFT, service=0x38) +UDS._service_cls[0x38] = UDS_RFT + + class UDS_RFTPR(Packet): name = 'RequestFileTransferPositiveResponse' @@ -1299,6 +1600,9 @@ def _contains_data_format_identifier(packet): return packet.modeOfOperation != 0x02 fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x78, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), XByteEnumField('modeOfOperation', 0, UDS_RFT.modeOfOperations), ConditionalField(FieldLenField('lengthFormatIdentifier', None, length_of='maxNumberOfBlockLength', @@ -1330,19 +1634,31 @@ def answers(self, other): return isinstance(other, UDS_RFT) +bind_layers(UDS, UDS_RFTPR, service=0x78) +UDS._service_cls[0x78] = UDS_RFTPR + + # #########################IOCBI################################### -@_uds_service(0x2F) class UDS_IOCBI(Packet): name = 'InputOutputControlByIdentifier' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x2f, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] -@_uds_service(0x6F) +bind_layers(UDS, UDS_IOCBI, service=0x2f) +UDS._service_cls[0x2f] = UDS_IOCBI + + class UDS_IOCBIPR(Packet): name = 'InputOutputControlByIdentifierPositiveResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x6f, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -1351,8 +1667,11 @@ def answers(self, other): and other.dataIdentifier == self.dataIdentifier +bind_layers(UDS, UDS_IOCBIPR, service=0x6f) +UDS._service_cls[0x6f] = UDS_IOCBIPR + + # #########################NR################################### -@_uds_service(0x7f) class UDS_NR(Packet): negativeResponseCodes = { 0x00: 'positiveResponse', @@ -1419,6 +1738,9 @@ class UDS_NR(Packet): } name = 'NegativeResponse' fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x7f, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), XByteEnumField('requestServiceId', 0, UDS.services), ByteEnumField('negativeResponseCode', 0, negativeResponseCodes) ] @@ -1429,6 +1751,10 @@ def answers(self, other): conf.contribs['UDS']['treat-response-pending-as-answer']) +bind_layers(UDS, UDS_NR, service=0x7f) +UDS._service_cls[0x7f] = UDS_NR + + # ################################################################## # ######################## UTILS ################################### # ################################################################## diff --git a/scapy/contrib/automotive/utils.py b/scapy/contrib/automotive/utils.py index 89ef3e5b715..d619d0b2a16 100644 --- a/scapy/contrib/automotive/utils.py +++ b/scapy/contrib/automotive/utils.py @@ -12,115 +12,24 @@ returned directly by the base-class dispatcher instead of being nested inside the parent layer. -To add single layer support to a protocol: +Single layer mode is controlled via the ``single_layer_mode`` flag in the +protocol's :attr:`~scapy.config.conf.contribs` entry:: -1. Add a ``_service_cls = {}`` class attribute and a ``dispatch_hook`` - classmethod to the base class:: + conf.contribs['UDS']['single_layer_mode'] = True # enable + conf.contribs['UDS']['single_layer_mode'] = False # disable (default) - class KWP(ISOTP): - _service_cls = {} - ... - @classmethod - def dispatch_hook(cls, _pkt=b"", *args, **kwargs): - if conf.contribs['KWP'].get('single_layer_mode', False) and \ - len(_pkt) >= 1: - service = orb(_pkt[0]) - return cls._service_cls.get(service, cls) - return cls +The same key is used for all supported protocols (``'UDS'``, ``'KWP'``, +``'OBD'``, ``'GMLAN'``). -2. Create the per-protocol service decorator:: +Each service packet class carries a conditional ``service`` field as its first +field. The field is visible (included in build / dissection) only when single +layer mode is active:: - _kwp_service = _make_service_decorator(KWP, 'KWP') + ConditionalField( + XByteEnumField('service', 0x10, UDS.services), + lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)) -3. Decorate every service subpacket instead of calling ``bind_layers``:: - - @_kwp_service(0x10) - class KWP_SDS(Packet): - ... - - For classes defined in a separate module (e.g. OBD), apply the decorator - post-definition using the functional form:: - - _obd_service(0x01)(OBD_S01) - -4. To enable or disable single layer mode at runtime, set the config flag - directly:: - - conf.contribs['KWP']['single_layer_mode'] = True # enable - conf.contribs['KWP']['single_layer_mode'] = False # disable +In single layer mode the base class ``dispatch_hook`` reads the first byte of +raw data and routes it directly to the appropriate service class via the +``_service_cls`` dispatch table that is populated at module load time. """ - -import struct -from typing import Any, Callable - -from scapy.config import conf -from scapy.fields import ConditionalField, XByteEnumField -from scapy.packet import Packet, bind_layers - - -def _make_service_decorator(base_cls, conf_contrib_key): - # type: (Any, str) -> Callable[[int], Any] - """Return a class-decorator factory for an automotive protocol service. - - The returned decorator factory accepts a *service_id* integer and returns - a class decorator that: - - 1. Prepends a conditional ``service`` field (visible only in single layer - mode) to the subpacket's ``fields_desc``. - 2. Registers the class in ``base_cls._service_cls`` so the - ``dispatch_hook`` can route raw bytes to the correct type. - 3. Calls :func:`~scapy.packet.bind_layers` to link the subpacket to - *base_cls* (multi-layer mode). - 4. Injects a ``hashret`` method that returns the correct value for - request/response matching in single layer mode (unless the class - already defines its own ``hashret``). - - The single layer mode is controlled at runtime via - ``conf.contribs[conf_contrib_key]['single_layer_mode']``. - - Args: - base_cls: The base protocol class (e.g. ``UDS``, ``KWP``). - conf_contrib_key: Key used in :attr:`~scapy.config.conf.contribs` - (e.g. ``'UDS'``, ``'KWP'``). - - Returns: - A ``service_decorator(service_id)`` factory function. - - Example:: - - _kwp_service = _make_service_decorator(KWP, 'KWP') - - @_kwp_service(0x10) - class KWP_SDS(Packet): - ... - """ - _base = base_cls - _key = conf_contrib_key - _flag = 'single_layer_mode' - - def service_decorator(service_id: int) -> Callable[[Any], Any]: - def decorator(cls: Any) -> Any: - # Prepend a conditional service field so that in single layer mode - # the service byte is part of the subpacket itself. - svc_field = ConditionalField( - XByteEnumField('service', service_id, _base.services), - lambda pkt: conf.contribs[_key].get(_flag, False) - ) - cls.fields_desc = [svc_field] + list(cls.fields_desc) - # Register in base class dispatch table for single layer mode. - _base._service_cls[service_id] = cls - # Bind to base class for multi-layer mode payload routing. - bind_layers(_base, cls, service=service_id) - # Capture service_id by value to avoid late-binding closure issues. - _sid = service_id - - def _hashret(self: Any) -> bytes: - if conf.contribs[_key].get(_flag, False): - return struct.pack('B', _sid & ~0x40) - return Packet.hashret(self) - - if 'hashret' not in cls.__dict__: - cls.hashret = _hashret - return cls - return decorator - return service_decorator From a8f05e9dbb87a4eef06d8bf0ed73c209cb62af39 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:36:20 +0000 Subject: [PATCH 11/16] Make OBD service ConditionalField modifications explicit per-class instead of loop Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/c30eedc4-0783-44f0-86b8-2a0dcdf0fb75 Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- scapy/contrib/automotive/obd/obd.py | 183 +++++++++++++++++++++++----- 1 file changed, 151 insertions(+), 32 deletions(-) diff --git a/scapy/contrib/automotive/obd/obd.py b/scapy/contrib/automotive/obd/obd.py index a59211cc116..bb871ebd76a 100644 --- a/scapy/contrib/automotive/obd/obd.py +++ b/scapy/contrib/automotive/obd/obd.py @@ -93,35 +93,154 @@ def dispatch_hook(cls, _pkt=b"", *args, **kwargs): return cls -_OBD_SERVICES = [ - (0x01, OBD_S01), - (0x02, OBD_S02), - (0x03, OBD_S03), - (0x04, OBD_S04), - (0x06, OBD_S06), - (0x07, OBD_S07), - (0x08, OBD_S08), - (0x09, OBD_S09), - (0x0A, OBD_S0A), - (0x41, OBD_S01_PR), - (0x42, OBD_S02_PR), - (0x43, OBD_S03_PR), - (0x44, OBD_S04_PR), - (0x46, OBD_S06_PR), - (0x47, OBD_S07_PR), - (0x48, OBD_S08_PR), - (0x49, OBD_S09_PR), - (0x4A, OBD_S0A_PR), - (0x7F, OBD_NR), -] - -for _sid, _cls in _OBD_SERVICES: - _cls.fields_desc = [ - ConditionalField( - XByteEnumField('service', _sid, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) - ] + list(_cls.fields_desc) - bind_layers(OBD, _cls, service=_sid) - OBD._service_cls[_sid] = _cls - -del _sid, _cls +OBD_S01.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x01, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S01.fields_desc) +bind_layers(OBD, OBD_S01, service=0x01) +OBD._service_cls[0x01] = OBD_S01 + +OBD_S02.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x02, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S02.fields_desc) +bind_layers(OBD, OBD_S02, service=0x02) +OBD._service_cls[0x02] = OBD_S02 + +OBD_S03.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x03, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S03.fields_desc) +bind_layers(OBD, OBD_S03, service=0x03) +OBD._service_cls[0x03] = OBD_S03 + +OBD_S04.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x04, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S04.fields_desc) +bind_layers(OBD, OBD_S04, service=0x04) +OBD._service_cls[0x04] = OBD_S04 + +OBD_S06.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x06, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S06.fields_desc) +bind_layers(OBD, OBD_S06, service=0x06) +OBD._service_cls[0x06] = OBD_S06 + +OBD_S07.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x07, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S07.fields_desc) +bind_layers(OBD, OBD_S07, service=0x07) +OBD._service_cls[0x07] = OBD_S07 + +OBD_S08.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x08, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S08.fields_desc) +bind_layers(OBD, OBD_S08, service=0x08) +OBD._service_cls[0x08] = OBD_S08 + +OBD_S09.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x09, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S09.fields_desc) +bind_layers(OBD, OBD_S09, service=0x09) +OBD._service_cls[0x09] = OBD_S09 + +OBD_S0A.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x0A, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S0A.fields_desc) +bind_layers(OBD, OBD_S0A, service=0x0A) +OBD._service_cls[0x0A] = OBD_S0A + +OBD_S01_PR.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x41, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S01_PR.fields_desc) +bind_layers(OBD, OBD_S01_PR, service=0x41) +OBD._service_cls[0x41] = OBD_S01_PR + +OBD_S02_PR.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x42, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S02_PR.fields_desc) +bind_layers(OBD, OBD_S02_PR, service=0x42) +OBD._service_cls[0x42] = OBD_S02_PR + +OBD_S03_PR.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x43, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S03_PR.fields_desc) +bind_layers(OBD, OBD_S03_PR, service=0x43) +OBD._service_cls[0x43] = OBD_S03_PR + +OBD_S04_PR.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x44, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S04_PR.fields_desc) +bind_layers(OBD, OBD_S04_PR, service=0x44) +OBD._service_cls[0x44] = OBD_S04_PR + +OBD_S06_PR.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x46, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S06_PR.fields_desc) +bind_layers(OBD, OBD_S06_PR, service=0x46) +OBD._service_cls[0x46] = OBD_S06_PR + +OBD_S07_PR.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x47, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S07_PR.fields_desc) +bind_layers(OBD, OBD_S07_PR, service=0x47) +OBD._service_cls[0x47] = OBD_S07_PR + +OBD_S08_PR.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x48, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S08_PR.fields_desc) +bind_layers(OBD, OBD_S08_PR, service=0x48) +OBD._service_cls[0x48] = OBD_S08_PR + +OBD_S09_PR.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x49, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S09_PR.fields_desc) +bind_layers(OBD, OBD_S09_PR, service=0x49) +OBD._service_cls[0x49] = OBD_S09_PR + +OBD_S0A_PR.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x4A, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_S0A_PR.fields_desc) +bind_layers(OBD, OBD_S0A_PR, service=0x4A) +OBD._service_cls[0x4A] = OBD_S0A_PR + +OBD_NR.fields_desc = [ + ConditionalField( + XByteEnumField('service', 0x7F, OBD.services), + lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) +] + list(OBD_NR.fields_desc) +bind_layers(OBD, OBD_NR, service=0x7F) +OBD._service_cls[0x7F] = OBD_NR From c137becef8acf3b120c808678cb3ebcbd911a8a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:02:52 +0000 Subject: [PATCH 12/16] Address PR review: delete utils.py, fix type annotations, named slm functions, OBD ConditionalField in class bodies, simplify doc Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/ce2f23f8-7fa9-462b-b281-7165aad3cb78 Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- doc/scapy/layers/automotive.rst | 29 +-- scapy/contrib/automotive/gm/gmlan.py | 114 ++++-------- scapy/contrib/automotive/kwp.py | 218 ++++++---------------- scapy/contrib/automotive/obd/iid/iids.py | 7 +- scapy/contrib/automotive/obd/mid/mids.py | 5 +- scapy/contrib/automotive/obd/obd.py | 130 +------------ scapy/contrib/automotive/obd/pid/pids.py | 6 +- scapy/contrib/automotive/obd/services.py | 55 +++++- scapy/contrib/automotive/obd/tid/tids.py | 5 +- scapy/contrib/automotive/uds.py | 226 ++++++----------------- scapy/contrib/automotive/utils.py | 35 ---- 11 files changed, 234 insertions(+), 596 deletions(-) delete mode 100644 scapy/contrib/automotive/utils.py diff --git a/doc/scapy/layers/automotive.rst b/doc/scapy/layers/automotive.rst index 75c2b69abc0..a5b9faee57a 100644 --- a/doc/scapy/layers/automotive.rst +++ b/doc/scapy/layers/automotive.rst @@ -1086,9 +1086,7 @@ Single Layer Mode ----------------- UDS, KWP, OBD, and GMLAN all support a *single layer mode* that makes each -service packet a standalone ``Packet`` rather than a nested sublayer. The -feature is backed by the generic helpers in -:mod:`scapy.contrib.automotive.utils` and works identically for every protocol. +service packet a standalone ``Packet`` rather than a nested sublayer. **Default (multi-layer) mode** @@ -1115,29 +1113,8 @@ To toggle at runtime after loading:: b'\x10\x01' >>> conf.contribs['UDS']['single_layer_mode'] = False # revert to multi-layer mode -The same API is available for the other protocols — use the ``single_layer_mode`` -config key in the corresponding ``conf.contribs`` entry: - -+----------+-----------------------------------------------+ -| Protocol | Config entry | -+==========+===============================================+ -| UDS | ``conf.contribs['UDS']['single_layer_mode']`` | -+----------+-----------------------------------------------+ -| KWP | ``conf.contribs['KWP']['single_layer_mode']`` | -+----------+-----------------------------------------------+ -| OBD | ``conf.contribs['OBD']['single_layer_mode']`` | -+----------+-----------------------------------------------+ -| GMLAN | ``conf.contribs['GMLAN']['single_layer_mode']`` | -+----------+-----------------------------------------------+ - -In single layer mode: - -- The base class (e.g. ``KWP``) acts as a dispatcher via ``dispatch_hook``: - it reads the first byte and returns the correct service class directly. -- Each service packet has a conditional ``service`` field that is present - (for building and dissection) only when single layer mode is active. -- Service packets' ``answers()`` and ``hashret()`` methods work correctly in - both modes. +The same ``single_layer_mode`` key works for all protocols: replace ``'UDS'`` +with ``'KWP'``, ``'OBD'``, or ``'GMLAN'`` as appropriate. GMLAN ===== diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index 9be1fbd4010..34636ea89fb 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -34,6 +34,11 @@ from scapy.contrib.isotp import ISOTP from scapy.compat import orb +from typing import ( # noqa: F401 + Dict, + Type, +) + """ GMLAN """ @@ -53,6 +58,9 @@ conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = None +def _gmlan_slm(pkt): + return conf.contribs['GMLAN'].get('single_layer_mode', False) + class GMLAN(ISOTP): @staticmethod def determine_len(x): @@ -132,7 +140,7 @@ def hashret(self): return struct.pack('B', self.requestServiceId & ~0x40) return struct.pack('B', self.service & ~0x40) - _service_cls = {} # type: ignore + _service_cls = {} # type: Dict[int, Type[Packet]] @classmethod def dispatch_hook(cls, _pkt=b"", *args, **kwargs): @@ -152,9 +160,7 @@ class GMLAN_IDO(Packet): 0x04: 'wakeUpLinks'} name = 'InitiateDiagnosticOperation' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x10, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x10, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions) ] @@ -183,9 +189,7 @@ class GMLAN_RFRD(Packet): 0x02: 'readFailureRecordParameters'} name = 'ReadFailureRecordData' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x12, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x12, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), ConditionalField(PacketField("dtc", None, GMLAN_DTC), lambda pkt: pkt.subfunction == 0x02) @@ -199,9 +203,7 @@ class GMLAN_RFRD(Packet): class GMLAN_RFRDPR(Packet): name = 'ReadFailureRecordDataPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x52, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x52, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, GMLAN_RFRD.subfunctions) ] @@ -329,9 +331,7 @@ class GMLAN_RDBI(Packet): name = 'ReadDataByIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x1a, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x1a, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, dataIdentifiers) ] @@ -343,9 +343,7 @@ class GMLAN_RDBI(Packet): class GMLAN_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x5a, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x5a, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers), ] @@ -367,9 +365,7 @@ class GMLAN_RDBPI(Packet): }) name = 'ReadDataByParameterIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x22, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x22, GMLAN.services), _gmlan_slm), FieldListField("identifiers", [], XShortEnumField('parameterIdentifier', 0, dataIdentifiers)) @@ -383,9 +379,7 @@ class GMLAN_RDBPI(Packet): class GMLAN_RDBPIPR(Packet): name = 'ReadDataByParameterIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x62, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x62, GMLAN.services), _gmlan_slm), XShortEnumField('parameterIdentifier', 0, GMLAN_RDBPI.dataIdentifiers), ] @@ -410,9 +404,7 @@ class GMLAN_RDBPKTI(Packet): } fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xaa, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xaa, GMLAN.services), _gmlan_slm), XByteEnumField('subfunction', 0, subfunctions), ConditionalField(FieldListField('request_DPIDs', [], XByteField("", 0)), @@ -428,9 +420,7 @@ class GMLAN_RDBPKTI(Packet): class GMLAN_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x23, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x23, GMLAN.services), _gmlan_slm), MultipleTypeField( [ (XShortField('memoryAddress', 0), @@ -452,9 +442,7 @@ class GMLAN_RMBA(Packet): class GMLAN_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x63, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x63, GMLAN.services), _gmlan_slm), MultipleTypeField( [ (XShortField('memoryAddress', 0), @@ -496,9 +484,7 @@ class GMLAN_SA(Packet): name = 'SecurityAccess' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x27, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x27, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), ConditionalField(XShortField('securityKey', 0), lambda pkt: pkt.subfunction % 2 == 0) @@ -512,9 +498,7 @@ class GMLAN_SA(Packet): class GMLAN_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x67, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x67, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, GMLAN_SA.subfunctions), ConditionalField(XShortField('securitySeed', 0), lambda pkt: pkt.subfunction % 2 == 1), @@ -533,9 +517,7 @@ def answers(self, other): class GMLAN_DDM(Packet): name = 'DynamicallyDefineMessage' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x2c, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x2c, GMLAN.services), _gmlan_slm), XByteField('DPIDIdentifier', 0), StrField('PIDData', b'\x00\x00') ] @@ -548,9 +530,7 @@ class GMLAN_DDM(Packet): class GMLAN_DDMPR(Packet): name = 'DynamicallyDefineMessagePositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x6c, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x6c, GMLAN.services), _gmlan_slm), XByteField('DPIDIdentifier', 0) ] @@ -567,9 +547,7 @@ def answers(self, other): class GMLAN_DPBA(Packet): name = 'DefinePIDByAddress' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x2d, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x2d, GMLAN.services), _gmlan_slm), XShortField('parameterIdentifier', 0), MultipleTypeField( [ @@ -592,9 +570,7 @@ class GMLAN_DPBA(Packet): class GMLAN_DPBAPR(Packet): name = 'DefinePIDByAddressPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x6d, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x6d, GMLAN.services), _gmlan_slm), XShortField('parameterIdentifier', 0), ] @@ -611,9 +587,7 @@ def answers(self, other): class GMLAN_RD(Packet): name = 'RequestDownload' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x34, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x34, GMLAN.services), _gmlan_slm), XByteField('dataFormatIdentifier', 0), MultipleTypeField( [ @@ -640,9 +614,7 @@ class GMLAN_TD(Packet): } name = 'TransferData' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x36, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x36, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), MultipleTypeField( [ @@ -666,9 +638,7 @@ class GMLAN_TD(Packet): class GMLAN_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x3b, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x3b, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers), StrField("dataRecord", b'') ] @@ -681,9 +651,7 @@ class GMLAN_WDBI(Packet): class GMLAN_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x7b, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x7b, GMLAN.services), _gmlan_slm), XByteEnumField('dataIdentifier', 0, GMLAN_RDBI.dataIdentifiers) ] @@ -712,9 +680,7 @@ class GMLAN_RPSPR(Packet): } name = 'ReportProgrammedStatePositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xe2, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xe2, GMLAN.services), _gmlan_slm), ByteEnumField('programmedState', 0, programmedStates), ] @@ -732,9 +698,7 @@ class GMLAN_PM(Packet): } name = 'ProgrammingMode' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xa5, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xa5, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions), ] @@ -752,9 +716,7 @@ class GMLAN_RDI(Packet): } name = 'ReadDiagnosticInformation' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xa9, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xa9, GMLAN.services), _gmlan_slm), ByteEnumField('subfunction', 0, subfunctions) ] @@ -802,9 +764,7 @@ class GMLAN_RDI_BC(Packet): class GMLAN_DC(Packet): name = 'DeviceControl' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xae, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xae, GMLAN.services), _gmlan_slm), XByteField('CPIDNumber', 0), StrFixedLenField('CPIDControlBytes', b"", 5) ] @@ -817,9 +777,7 @@ class GMLAN_DC(Packet): class GMLAN_DCPR(Packet): name = 'DeviceControlPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xee, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xee, GMLAN.services), _gmlan_slm), XByteField('CPIDNumber', 0) ] @@ -852,9 +810,7 @@ class GMLAN_NR(Packet): } name = 'NegativeResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x7f, GMLAN.services), - lambda pkt: conf.contribs['GMLAN'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x7f, GMLAN.services), _gmlan_slm), XByteEnumField('requestServiceId', 0, GMLAN.services), MayEnd(ByteEnumField('returnCode', 0, negativeResponseCodes)), # XXX Is this MayEnd correct? Why is the field below also 0xe3 ? diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index e97abdb6850..c41e620e60a 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -32,6 +32,7 @@ from typing import ( # noqa: F401 Any, Dict, + Type, ) @@ -48,6 +49,9 @@ 'single_layer_mode': False} +def _kwp_slm(pkt): + return conf.contribs['KWP'].get('single_layer_mode', False) + class KWP(ISOTP): services = ObservableDict( {0x10: 'StartDiagnosticSession', @@ -131,7 +135,7 @@ def hashret(self): else: return struct.pack('B', self.service & ~0x40) - _service_cls = {} # type: Dict[int, type] + _service_cls = {} # type: Dict[int, Type[Packet]] @classmethod def dispatch_hook(cls, _pkt=b"", *args, **kwargs): @@ -153,9 +157,7 @@ class KWP_SDS(Packet): 0x92: 'extendedDiagnosticSession'}) name = 'StartDiagnosticSession' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x10, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x10, KWP.services), _kwp_slm), ByteEnumField('diagnosticSession', 0, diagnosticSessionTypes) ] @@ -167,9 +169,7 @@ class KWP_SDS(Packet): class KWP_SDSPR(Packet): name = 'StartDiagnosticSessionPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x50, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x50, KWP.services), _kwp_slm), ByteEnumField('diagnosticSession', 0, KWP_SDS.diagnosticSessionTypes), ] @@ -192,9 +192,7 @@ class KWP_ER(Packet): 0x82: 'nonvolatileMemoryReset'} name = 'ECUReset' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x11, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x11, KWP.services), _kwp_slm), ByteEnumField('resetMode', 0, resetModes) ] @@ -205,9 +203,7 @@ class KWP_ER(Packet): class KWP_ERPR(Packet): fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x51, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x51, KWP.services), _kwp_slm), ] name = 'ECUResetPositiveResponse' @@ -224,9 +220,7 @@ def answers(self, other): class KWP_SA(Packet): name = 'SecurityAccess' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x27, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x27, KWP.services), _kwp_slm), ByteField('accessMode', 0), ConditionalField(StrField('key', b""), lambda pkt: pkt.accessMode % 2 == 0) @@ -240,9 +234,7 @@ class KWP_SA(Packet): class KWP_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x67, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x67, KWP.services), _kwp_slm), ByteField('accessMode', 0), ConditionalField(StrField('seed', b""), lambda pkt: pkt.accessMode % 2 == 1), @@ -270,9 +262,7 @@ class KWP_IOCBLI(Packet): 0x08: "Long Term Adjustment" } fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x30, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x30, KWP.services), _kwp_slm), XByteField('localIdentifier', 0), XByteEnumField('inputOutputControlParameter', 0, inputOutputControlParameters), @@ -287,9 +277,7 @@ class KWP_IOCBLI(Packet): class KWP_IOCBLIPR(Packet): name = 'InputOutputControlByLocalIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x70, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x70, KWP.services), _kwp_slm), XByteField('localIdentifier', 0), XByteEnumField('inputOutputControlParameter', 0, KWP_IOCBLI.inputOutputControlParameters), @@ -314,9 +302,7 @@ class KWP_DNMT(Packet): } name = 'DisableNormalMessageTransmission' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x28, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x28, KWP.services), _kwp_slm), ByteEnumField('responseRequired', 0, responseTypes) ] @@ -327,9 +313,7 @@ class KWP_DNMT(Packet): class KWP_DNMTPR(Packet): fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x68, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x68, KWP.services), _kwp_slm), ] name = 'DisableNormalMessageTransmissionPositiveResponse' @@ -350,9 +334,7 @@ class KWP_ENMT(Packet): } name = 'EnableNormalMessageTransmission' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x29, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x29, KWP.services), _kwp_slm), ByteEnumField('responseRequired', 1, responseTypes) ] @@ -363,9 +345,7 @@ class KWP_ENMT(Packet): class KWP_ENMTPR(Packet): fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x69, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x69, KWP.services), _kwp_slm), ] name = 'EnableNormalMessageTransmissionPositiveResponse' @@ -386,9 +366,7 @@ class KWP_TP(Packet): } name = 'TesterPresent' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x3e, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x3e, KWP.services), _kwp_slm), ByteEnumField('responseRequired', 1, responseTypes) ] @@ -399,9 +377,7 @@ class KWP_TP(Packet): class KWP_TPPR(Packet): fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x7e, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x7e, KWP.services), _kwp_slm), ] name = 'TesterPresentPositiveResponse' @@ -434,9 +410,7 @@ class KWP_CDTCS(Packet): } name = 'ControlDTCSetting' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x85, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x85, KWP.services), _kwp_slm), ByteEnumField('responseRequired', 1, responseTypes), XShortEnumField('groupOfDTC', 0, DTCGroups), ByteEnumField('DTCSettingMode', 0, DTCSettingModes), @@ -449,9 +423,7 @@ class KWP_CDTCS(Packet): class KWP_CDTCSPR(Packet): fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xc5, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xc5, KWP.services), _kwp_slm), ] name = 'ControlDTCSettingPositiveResponse' @@ -486,9 +458,7 @@ class KWP_ROE(Packet): } name = 'ResponseOnEvent' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x86, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x86, KWP.services), _kwp_slm), ByteEnumField('responseRequired', 1, responseTypes), ByteEnumField('eventWindowTime', 0, eventWindowTimes), MayEnd(ByteEnumField('eventType', 0, eventTypes)), @@ -506,9 +476,7 @@ class KWP_ROE(Packet): class KWP_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xc6, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xc6, KWP.services), _kwp_slm), ByteField("numberOfActivatedEvents", 0), MayEnd(ByteEnumField('eventWindowTime', 0, KWP_ROE.eventWindowTimes)), # XXX Is this MayEnd correct? @@ -543,9 +511,7 @@ class KWP_RDBLI(Packet): }) name = 'ReadDataByLocalIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x21, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x21, KWP.services), _kwp_slm), XByteEnumField('recordLocalIdentifier', 0, localIdentifiers) ] @@ -557,9 +523,7 @@ class KWP_RDBLI(Packet): class KWP_RDBLIPR(Packet): name = 'ReadDataByLocalIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x61, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x61, KWP.services), _kwp_slm), XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) ] @@ -577,9 +541,7 @@ def answers(self, other): class KWP_WDBLI(Packet): name = 'WriteDataByLocalIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x3b, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x3b, KWP.services), _kwp_slm), XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) ] @@ -591,9 +553,7 @@ class KWP_WDBLI(Packet): class KWP_WDBLIPR(Packet): name = 'WriteDataByLocalIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x7b, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x7b, KWP.services), _kwp_slm), XByteEnumField('recordLocalIdentifier', 0, KWP_RDBLI.localIdentifiers) ] @@ -612,9 +572,7 @@ class KWP_RDBI(Packet): dataIdentifiers = ObservableDict() name = 'ReadDataByIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x22, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x22, KWP.services), _kwp_slm), XShortEnumField('identifier', 0, dataIdentifiers) ] @@ -626,9 +584,7 @@ class KWP_RDBI(Packet): class KWP_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x62, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x62, KWP.services), _kwp_slm), XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers), ] @@ -646,9 +602,7 @@ def answers(self, other): class KWP_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x23, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x23, KWP.services), _kwp_slm), X3BytesField('memoryAddress', 0), ByteField('memorySize', 0) ] @@ -661,9 +615,7 @@ class KWP_RMBA(Packet): class KWP_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x63, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x63, KWP.services), _kwp_slm), StrField('dataRecord', b"", fmt="B") ] @@ -686,9 +638,7 @@ class KWP_DDLI(Packet): 0x3: "defineByIdentifier", 0x4: "clearDynamicallyDefinedLocalIdentifier"} fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x2c, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x2c, KWP.services), _kwp_slm), XByteField('dynamicallyDefineLocalIdentifier', 0), ByteEnumField('definitionMode', 0, definitionModes), StrField('dataRecord', b"", fmt="B") @@ -702,9 +652,7 @@ class KWP_DDLI(Packet): class KWP_DDLIPR(Packet): name = 'DynamicallyDefineLocalIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x6c, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x6c, KWP.services), _kwp_slm), XByteField('dynamicallyDefineLocalIdentifier', 0) ] @@ -722,9 +670,7 @@ def answers(self, other): class KWP_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x2e, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x2e, KWP.services), _kwp_slm), XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers) ] @@ -736,9 +682,7 @@ class KWP_WDBI(Packet): class KWP_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x6e, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x6e, KWP.services), _kwp_slm), XShortEnumField('identifier', 0, KWP_RDBI.dataIdentifiers), ] @@ -756,9 +700,7 @@ def answers(self, other): class KWP_WMBA(Packet): name = 'WriteMemoryByAddress' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x3d, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x3d, KWP.services), _kwp_slm), X3BytesField('memoryAddress', 0), ByteField('memorySize', 0), StrField('dataRecord', b'', fmt="B") @@ -772,9 +714,7 @@ class KWP_WMBA(Packet): class KWP_WMBAPR(Packet): name = 'WriteMemoryByAddressPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x7d, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x7d, KWP.services), _kwp_slm), X3BytesField('memoryAddress', 0) ] @@ -799,9 +739,7 @@ class KWP_CDI(Packet): } name = 'ClearDiagnosticInformation' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x14, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x14, KWP.services), _kwp_slm), XShortEnumField('groupOfDTC', 0, DTCGroups) ] @@ -814,9 +752,7 @@ class KWP_CDIPR(Packet): name = 'ClearDiagnosticInformationPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x54, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x54, KWP.services), _kwp_slm), XShortEnumField('groupOfDTC', 0, KWP_CDI.DTCGroups) ] @@ -834,9 +770,7 @@ def answers(self, other): class KWP_RSODTC(Packet): name = 'ReadStatusOfDiagnosticTroubleCodes' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x17, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x17, KWP.services), _kwp_slm), XShortEnumField('groupOfDTC', 0, KWP_CDI.DTCGroups) ] @@ -849,9 +783,7 @@ class KWP_RSODTCPR(Packet): name = 'ReadStatusOfDiagnosticTroubleCodesPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x57, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x57, KWP.services), _kwp_slm), ByteField('numberOfDTC', 0), ] @@ -883,9 +815,7 @@ class KWP_RECUI(Packet): 0x9F: "ECU Boot Fingerprint" }) fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x1a, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x1a, KWP.services), _kwp_slm), XByteEnumField('localIdentifier', 0, localIdentifiers) ] @@ -898,9 +828,7 @@ class KWP_RECUIPR(Packet): name = 'ReadECUIdentificationPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x5a, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x5a, KWP.services), _kwp_slm), XByteEnumField('localIdentifier', 0, KWP_RECUI.localIdentifiers) ] @@ -930,9 +858,7 @@ class KWP_SRBLI(Packet): }) name = 'StartRoutineByLocalIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x31, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x31, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, routineLocalIdentifiers) ] @@ -944,9 +870,7 @@ class KWP_SRBLI(Packet): class KWP_SRBLIPR(Packet): name = 'StartRoutineByLocalIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x71, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x71, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] @@ -965,9 +889,7 @@ def answers(self, other): class KWP_STRBLI(Packet): name = 'StopRoutineByLocalIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x32, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x32, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] @@ -980,9 +902,7 @@ class KWP_STRBLI(Packet): class KWP_STRBLIPR(Packet): name = 'StopRoutineByLocalIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x72, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x72, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] @@ -1001,9 +921,7 @@ def answers(self, other): class KWP_RRRBLI(Packet): name = 'RequestRoutineResultsByLocalIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x33, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x33, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] @@ -1016,9 +934,7 @@ class KWP_RRRBLI(Packet): class KWP_RRRBLIPR(Packet): name = 'RequestRoutineResultsByLocalIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x73, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x73, KWP.services), _kwp_slm), XByteEnumField('routineLocalIdentifier', 0, KWP_SRBLI.routineLocalIdentifiers) ] @@ -1037,9 +953,7 @@ def answers(self, other): class KWP_RD(Packet): name = 'RequestDownload' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x34, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x34, KWP.services), _kwp_slm), X3BytesField('memoryAddress', 0), BitField('compression', 0, 4), BitField('encryption', 0, 4), @@ -1054,9 +968,7 @@ class KWP_RD(Packet): class KWP_RDPR(Packet): name = 'RequestDownloadPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x74, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x74, KWP.services), _kwp_slm), StrField('maxNumberOfBlockLength', b"", fmt="B"), ] @@ -1073,9 +985,7 @@ def answers(self, other): class KWP_RU(Packet): name = 'RequestUpload' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x35, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x35, KWP.services), _kwp_slm), X3BytesField('memoryAddress', 0), BitField('compression', 0, 4), BitField('encryption', 0, 4), @@ -1090,9 +1000,7 @@ class KWP_RU(Packet): class KWP_RUPR(Packet): name = 'RequestUploadPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x75, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x75, KWP.services), _kwp_slm), StrField('maxNumberOfBlockLength', b"", fmt="B"), ] @@ -1109,9 +1017,7 @@ def answers(self, other): class KWP_TD(Packet): name = 'TransferData' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x36, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x36, KWP.services), _kwp_slm), ByteField('blockSequenceCounter', 0), StrField('transferDataRequestParameter', b"", fmt="B") ] @@ -1124,9 +1030,7 @@ class KWP_TD(Packet): class KWP_TDPR(Packet): name = 'TransferDataPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x76, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x76, KWP.services), _kwp_slm), ByteField('blockSequenceCounter', 0), StrField('transferDataRequestParameter', b"", fmt="B") ] @@ -1145,9 +1049,7 @@ def answers(self, other): class KWP_RTE(Packet): name = 'RequestTransferExit' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x37, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x37, KWP.services), _kwp_slm), StrField('transferDataRequestParameter', b"", fmt="B") ] @@ -1159,9 +1061,7 @@ class KWP_RTE(Packet): class KWP_RTEPR(Packet): name = 'RequestTransferExitPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x77, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x77, KWP.services), _kwp_slm), StrField('transferDataRequestParameter', b"", fmt="B") ] @@ -1201,9 +1101,7 @@ class KWP_NR(Packet): } name = 'NegativeResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x7f, KWP.services), - lambda pkt: conf.contribs['KWP'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x7f, KWP.services), _kwp_slm), MayEnd(XByteEnumField('requestServiceId', 0, KWP.services)), # XXX Is this MayEnd correct? ByteEnumField('negativeResponseCode', 0, negativeResponseCodes) diff --git a/scapy/contrib/automotive/obd/iid/iids.py b/scapy/contrib/automotive/obd/iid/iids.py index 908f5b66d42..8ec2c9475b3 100644 --- a/scapy/contrib/automotive/obd/iid/iids.py +++ b/scapy/contrib/automotive/obd/iid/iids.py @@ -6,11 +6,11 @@ # scapy.contrib.status = skip -from scapy.fields import FieldLenField, FieldListField, StrFixedLenField, \ - ByteField, ShortField, FlagsField, XByteField, PacketListField +from scapy.fields import ConditionalField, FieldLenField, FieldListField, StrFixedLenField, \ + ByteField, ShortField, FlagsField, XByteEnumField, XByteField, PacketListField from scapy.packet import Packet, bind_layers from scapy.contrib.automotive.obd.packet import OBD_Packet -from scapy.contrib.automotive.obd.services import OBD_S09 +from scapy.contrib.automotive.obd.services import OBD_S09, _OBD_SERVICES, _obd_slm # See https://en.wikipedia.org/wiki/OBD-II_PIDs#Service_09 @@ -26,6 +26,7 @@ class OBD_S09_PR_Record(Packet): class OBD_S09_PR(Packet): name = "Infotype IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x49, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S09_PR_Record) ] diff --git a/scapy/contrib/automotive/obd/mid/mids.py b/scapy/contrib/automotive/obd/mid/mids.py index 8aa6b7b5624..ee50ca6f287 100644 --- a/scapy/contrib/automotive/obd/mid/mids.py +++ b/scapy/contrib/automotive/obd/mid/mids.py @@ -6,11 +6,11 @@ # scapy.contrib.status = skip -from scapy.fields import FlagsField, ScalingField, ByteEnumField, \ +from scapy.fields import ConditionalField, FlagsField, ScalingField, ByteEnumField, XByteEnumField, \ MultipleTypeField, ShortField, ShortEnumField, PacketListField from scapy.packet import Packet, bind_layers from scapy.contrib.automotive.obd.packet import OBD_Packet -from scapy.contrib.automotive.obd.services import OBD_S06 +from scapy.contrib.automotive.obd.services import OBD_S06, _OBD_SERVICES, _obd_slm def _unit_and_scaling_fields(name): @@ -457,6 +457,7 @@ class OBD_S06_PR_Record(Packet): class OBD_S06_PR(Packet): name = "On-Board monitoring IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x46, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S06_PR_Record) ] diff --git a/scapy/contrib/automotive/obd/obd.py b/scapy/contrib/automotive/obd/obd.py index bb871ebd76a..c77c6ff6e00 100644 --- a/scapy/contrib/automotive/obd/obd.py +++ b/scapy/contrib/automotive/obd/obd.py @@ -14,12 +14,18 @@ from scapy.contrib.automotive.obd.pid.pids import * from scapy.contrib.automotive.obd.tid.tids import * from scapy.contrib.automotive.obd.services import * -from scapy.packet import NoPayload, bind_layers +from scapy.contrib.automotive.obd.services import _OBD_SERVICES +from scapy.packet import NoPayload, Packet, bind_layers from scapy.config import conf -from scapy.fields import ConditionalField, XByteEnumField +from scapy.fields import XByteEnumField from scapy.contrib.isotp import ISOTP from scapy.compat import orb +from typing import ( # noqa: F401 + Dict, + Type, +) + try: if conf.contribs['OBD']['treat-response-pending-as-answer']: pass @@ -34,28 +40,7 @@ class OBD(ISOTP): - services = { - 0x01: 'CurrentPowertrainDiagnosticDataRequest', - 0x02: 'PowertrainFreezeFrameDataRequest', - 0x03: 'EmissionRelatedDiagnosticTroubleCodesRequest', - 0x04: 'ClearResetDiagnosticTroubleCodesRequest', - 0x05: 'OxygenSensorMonitoringTestResultsRequest', - 0x06: 'OnBoardMonitoringTestResultsRequest', - 0x07: 'PendingEmissionRelatedDiagnosticTroubleCodesRequest', - 0x08: 'ControlOperationRequest', - 0x09: 'VehicleInformationRequest', - 0x0A: 'PermanentDiagnosticTroubleCodesRequest', - 0x41: 'CurrentPowertrainDiagnosticDataResponse', - 0x42: 'PowertrainFreezeFrameDataResponse', - 0x43: 'EmissionRelatedDiagnosticTroubleCodesResponse', - 0x44: 'ClearResetDiagnosticTroubleCodesResponse', - 0x45: 'OxygenSensorMonitoringTestResultsResponse', - 0x46: 'OnBoardMonitoringTestResultsResponse', - 0x47: 'PendingEmissionRelatedDiagnosticTroubleCodesResponse', - 0x48: 'ControlOperationResponse', - 0x49: 'VehicleInformationResponse', - 0x4A: 'PermanentDiagnosticTroubleCodesResponse', - 0x7f: 'NegativeResponse'} + services = _OBD_SERVICES name = "On-board diagnostics" @@ -81,7 +66,7 @@ def answers(self, other): return self.payload.answers(other.payload) return False - _service_cls = {} # type: ignore + _service_cls = {} # type: Dict[int, Type[Packet]] @classmethod def dispatch_hook(cls, _pkt=b"", *args, **kwargs): @@ -93,154 +78,59 @@ def dispatch_hook(cls, _pkt=b"", *args, **kwargs): return cls -OBD_S01.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x01, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S01.fields_desc) bind_layers(OBD, OBD_S01, service=0x01) OBD._service_cls[0x01] = OBD_S01 -OBD_S02.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x02, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S02.fields_desc) bind_layers(OBD, OBD_S02, service=0x02) OBD._service_cls[0x02] = OBD_S02 -OBD_S03.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x03, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S03.fields_desc) bind_layers(OBD, OBD_S03, service=0x03) OBD._service_cls[0x03] = OBD_S03 -OBD_S04.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x04, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S04.fields_desc) bind_layers(OBD, OBD_S04, service=0x04) OBD._service_cls[0x04] = OBD_S04 -OBD_S06.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x06, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S06.fields_desc) bind_layers(OBD, OBD_S06, service=0x06) OBD._service_cls[0x06] = OBD_S06 -OBD_S07.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x07, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S07.fields_desc) bind_layers(OBD, OBD_S07, service=0x07) OBD._service_cls[0x07] = OBD_S07 -OBD_S08.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x08, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S08.fields_desc) bind_layers(OBD, OBD_S08, service=0x08) OBD._service_cls[0x08] = OBD_S08 -OBD_S09.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x09, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S09.fields_desc) bind_layers(OBD, OBD_S09, service=0x09) OBD._service_cls[0x09] = OBD_S09 -OBD_S0A.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x0A, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S0A.fields_desc) bind_layers(OBD, OBD_S0A, service=0x0A) OBD._service_cls[0x0A] = OBD_S0A -OBD_S01_PR.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x41, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S01_PR.fields_desc) bind_layers(OBD, OBD_S01_PR, service=0x41) OBD._service_cls[0x41] = OBD_S01_PR -OBD_S02_PR.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x42, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S02_PR.fields_desc) bind_layers(OBD, OBD_S02_PR, service=0x42) OBD._service_cls[0x42] = OBD_S02_PR -OBD_S03_PR.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x43, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S03_PR.fields_desc) bind_layers(OBD, OBD_S03_PR, service=0x43) OBD._service_cls[0x43] = OBD_S03_PR -OBD_S04_PR.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x44, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S04_PR.fields_desc) bind_layers(OBD, OBD_S04_PR, service=0x44) OBD._service_cls[0x44] = OBD_S04_PR -OBD_S06_PR.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x46, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S06_PR.fields_desc) bind_layers(OBD, OBD_S06_PR, service=0x46) OBD._service_cls[0x46] = OBD_S06_PR -OBD_S07_PR.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x47, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S07_PR.fields_desc) bind_layers(OBD, OBD_S07_PR, service=0x47) OBD._service_cls[0x47] = OBD_S07_PR -OBD_S08_PR.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x48, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S08_PR.fields_desc) bind_layers(OBD, OBD_S08_PR, service=0x48) OBD._service_cls[0x48] = OBD_S08_PR -OBD_S09_PR.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x49, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S09_PR.fields_desc) bind_layers(OBD, OBD_S09_PR, service=0x49) OBD._service_cls[0x49] = OBD_S09_PR -OBD_S0A_PR.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x4A, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_S0A_PR.fields_desc) bind_layers(OBD, OBD_S0A_PR, service=0x4A) OBD._service_cls[0x4A] = OBD_S0A_PR -OBD_NR.fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x7F, OBD.services), - lambda pkt: conf.contribs['OBD'].get('single_layer_mode', False)) -] + list(OBD_NR.fields_desc) bind_layers(OBD, OBD_NR, service=0x7F) OBD._service_cls[0x7F] = OBD_NR diff --git a/scapy/contrib/automotive/obd/pid/pids.py b/scapy/contrib/automotive/obd/pid/pids.py index 6367ef1caa5..8bd79ac6f01 100644 --- a/scapy/contrib/automotive/obd/pid/pids.py +++ b/scapy/contrib/automotive/obd/pid/pids.py @@ -7,9 +7,9 @@ # scapy.contrib.status = skip from scapy.packet import Packet, bind_layers -from scapy.fields import PacketListField +from scapy.fields import ConditionalField, PacketListField, XByteEnumField -from scapy.contrib.automotive.obd.services import OBD_S01, OBD_S02 +from scapy.contrib.automotive.obd.services import OBD_S01, OBD_S02, _OBD_SERVICES, _obd_slm from scapy.contrib.automotive.obd.pid.pids_00_1F import * from scapy.contrib.automotive.obd.pid.pids_20_3F import * from scapy.contrib.automotive.obd.pid.pids_40_5F import * @@ -27,6 +27,7 @@ class OBD_S01_PR_Record(Packet): class OBD_S01_PR(Packet): name = "Parameter IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x41, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S01_PR_Record) ] @@ -45,6 +46,7 @@ class OBD_S02_PR_Record(Packet): class OBD_S02_PR(Packet): name = "Parameter IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x42, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S02_PR_Record) ] diff --git a/scapy/contrib/automotive/obd/services.py b/scapy/contrib/automotive/obd/services.py index f00cf0dd519..1ad6f234daf 100644 --- a/scapy/contrib/automotive/obd/services.py +++ b/scapy/contrib/automotive/obd/services.py @@ -7,11 +7,40 @@ # scapy.contrib.status = skip from scapy.fields import ByteField, XByteField, BitEnumField, \ - PacketListField, XBitField, XByteEnumField, FieldListField, FieldLenField + PacketListField, XBitField, XByteEnumField, FieldListField, \ + FieldLenField, ConditionalField from scapy.packet import Packet from scapy.contrib.automotive.obd.packet import OBD_Packet from scapy.config import conf +_OBD_SERVICES = { + 0x01: 'CurrentPowertrainDiagnosticDataRequest', + 0x02: 'PowertrainFreezeFrameDataRequest', + 0x03: 'EmissionRelatedDiagnosticTroubleCodesRequest', + 0x04: 'ClearResetDiagnosticTroubleCodesRequest', + 0x05: 'OxygenSensorMonitoringTestResultsRequest', + 0x06: 'OnBoardMonitoringTestResultsRequest', + 0x07: 'PendingEmissionRelatedDiagnosticTroubleCodesRequest', + 0x08: 'ControlOperationRequest', + 0x09: 'VehicleInformationRequest', + 0x0A: 'PermanentDiagnosticTroubleCodesRequest', + 0x41: 'CurrentPowertrainDiagnosticDataResponse', + 0x42: 'PowertrainFreezeFrameDataResponse', + 0x43: 'EmissionRelatedDiagnosticTroubleCodesResponse', + 0x44: 'ClearResetDiagnosticTroubleCodesResponse', + 0x45: 'OxygenSensorMonitoringTestResultsResponse', + 0x46: 'OnBoardMonitoringTestResultsResponse', + 0x47: 'PendingEmissionRelatedDiagnosticTroubleCodesResponse', + 0x48: 'ControlOperationResponse', + 0x49: 'VehicleInformationResponse', + 0x4A: 'PermanentDiagnosticTroubleCodesResponse', + 0x7f: 'NegativeResponse', +} + + +def _obd_slm(pkt): + return conf.contribs['OBD'].get('single_layer_mode', False) + class OBD_DTC(OBD_Packet): name = "DiagnosticTroubleCode" @@ -45,6 +74,7 @@ class OBD_NR(Packet): } fields_desc = [ + ConditionalField(XByteEnumField('service', 0x7F, _OBD_SERVICES), _obd_slm), XByteField('request_service_id', 0), XByteEnumField('response_code', 0, responses) ] @@ -58,6 +88,7 @@ def answers(self, other): class OBD_S01(Packet): name = "S1_CurrentData" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x01, _OBD_SERVICES), _obd_slm), FieldListField("pid", [], XByteField('', 0)) ] @@ -72,17 +103,22 @@ class OBD_S02_Record(OBD_Packet): class OBD_S02(Packet): name = "S2_FreezeFrameData" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x02, _OBD_SERVICES), _obd_slm), PacketListField("requests", [], OBD_S02_Record) ] class OBD_S03(Packet): name = "S3_RequestDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x03, _OBD_SERVICES), _obd_slm), + ] class OBD_S03_PR(Packet): name = "S3_ResponseDTCs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x43, _OBD_SERVICES), _obd_slm), FieldLenField('count', None, count_of='dtcs', fmt='B'), PacketListField('dtcs', [], OBD_DTC, count_from=lambda pkt: pkt.count) ] @@ -93,10 +129,16 @@ def answers(self, other): class OBD_S04(Packet): name = "S4_ClearDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x04, _OBD_SERVICES), _obd_slm), + ] class OBD_S04_PR(Packet): name = "S4_ClearDTCsPositiveResponse" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x44, _OBD_SERVICES), _obd_slm), + ] def answers(self, other): return isinstance(other, OBD_S04) @@ -105,17 +147,22 @@ def answers(self, other): class OBD_S06(Packet): name = "S6_OnBoardDiagnosticMonitoring" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x06, _OBD_SERVICES), _obd_slm), FieldListField("mid", [], XByteField('', 0)) ] class OBD_S07(Packet): name = "S7_RequestPendingDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x07, _OBD_SERVICES), _obd_slm), + ] class OBD_S07_PR(Packet): name = "S7_ResponsePendingDTCs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x47, _OBD_SERVICES), _obd_slm), FieldLenField('count', None, count_of='dtcs', fmt='B'), PacketListField('dtcs', [], OBD_DTC, count_from=lambda pkt: pkt.count) ] @@ -127,6 +174,7 @@ def answers(self, other): class OBD_S08(Packet): name = "S8_RequestControlOfSystem" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x08, _OBD_SERVICES), _obd_slm), FieldListField("tid", [], XByteField('', 0)) ] @@ -134,17 +182,22 @@ class OBD_S08(Packet): class OBD_S09(Packet): name = "S9_VehicleInformation" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x09, _OBD_SERVICES), _obd_slm), FieldListField("iid", [], XByteField('', 0)) ] class OBD_S0A(Packet): name = "S0A_RequestPermanentDTCs" + fields_desc = [ + ConditionalField(XByteEnumField('service', 0x0A, _OBD_SERVICES), _obd_slm), + ] class OBD_S0A_PR(Packet): name = "S0A_ResponsePermanentDTCs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x4A, _OBD_SERVICES), _obd_slm), FieldLenField('count', None, count_of='dtcs', fmt='B'), PacketListField('dtcs', [], OBD_DTC, count_from=lambda pkt: pkt.count) ] diff --git a/scapy/contrib/automotive/obd/tid/tids.py b/scapy/contrib/automotive/obd/tid/tids.py index 27bda0df58a..f19fc01bb75 100644 --- a/scapy/contrib/automotive/obd/tid/tids.py +++ b/scapy/contrib/automotive/obd/tid/tids.py @@ -6,10 +6,10 @@ # scapy.contrib.status = skip -from scapy.fields import FlagsField, ByteField, ScalingField, PacketListField +from scapy.fields import ConditionalField, FlagsField, ByteField, ScalingField, PacketListField, XByteEnumField from scapy.packet import bind_layers, Packet from scapy.contrib.automotive.obd.packet import OBD_Packet -from scapy.contrib.automotive.obd.services import OBD_S08 +from scapy.contrib.automotive.obd.services import OBD_S08, _OBD_SERVICES, _obd_slm class _OBD_TID_Voltage(OBD_Packet): @@ -132,6 +132,7 @@ class OBD_S08_PR_Record(Packet): class OBD_S08_PR(Packet): name = "Control Operation IDs" fields_desc = [ + ConditionalField(XByteEnumField('service', 0x48, _OBD_SERVICES), _obd_slm), PacketListField("data_records", [], OBD_S08_PR_Record) ] diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 76b204f3007..40b0f4b2aea 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -28,6 +28,7 @@ # Typing imports from typing import ( # noqa: F401 Dict, + Type, Union, ) @@ -45,6 +46,9 @@ conf.debug_dissector = True +def _uds_slm(pkt): + return conf.contribs['UDS'].get('single_layer_mode', False) + class UDS(ISOTP): services = ObservableDict( @@ -128,7 +132,7 @@ def hashret(self): return struct.pack('B', bytes(self)[1] & ~0x40) return struct.pack('B', self.service & ~0x40) - _service_cls = {} # type: Dict[int, type] + _service_cls = {} # type: Dict[int, Type[Packet]] @classmethod def dispatch_hook(cls, _pkt=b"", *args, **kwargs): @@ -151,9 +155,7 @@ class UDS_DSC(Packet): 0x7F: 'ISOSAEReserved'}) name = 'DiagnosticSessionControl' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x10, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x10, UDS.services), _uds_slm), ByteEnumField('diagnosticSessionType', 0, diagnosticSessionTypes) ] @@ -165,9 +167,7 @@ class UDS_DSC(Packet): class UDS_DSCPR(Packet): name = 'DiagnosticSessionControlPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x50, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x50, UDS.services), _uds_slm), ByteEnumField('diagnosticSessionType', 0, UDS_DSC.diagnosticSessionTypes), StrField('sessionParameterRecord', b"") @@ -195,9 +195,7 @@ class UDS_ER(Packet): 0x7F: 'ISOSAEReserved'} name = 'ECUReset' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x11, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x11, UDS.services), _uds_slm), ByteEnumField('resetType', 0, resetTypes) ] @@ -209,9 +207,7 @@ class UDS_ER(Packet): class UDS_ERPR(Packet): name = 'ECUResetPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x51, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x51, UDS.services), _uds_slm), ByteEnumField('resetType', 0, UDS_ER.resetTypes), ConditionalField(ByteField('powerDownTime', 0), lambda pkt: pkt.resetType == 0x04) @@ -229,9 +225,7 @@ def answers(self, other): class UDS_SA(Packet): name = 'SecurityAccess' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x27, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x27, UDS.services), _uds_slm), ByteField('securityAccessType', 0), ConditionalField(StrField('securityAccessDataRecord', b""), lambda pkt: pkt.securityAccessType % 2 == 1), @@ -247,9 +241,7 @@ class UDS_SA(Packet): class UDS_SAPR(Packet): name = 'SecurityAccessPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x67, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x67, UDS.services), _uds_slm), ByteField('securityAccessType', 0), ConditionalField(StrField('securitySeed', b""), lambda pkt: pkt.securityAccessType % 2 == 1), @@ -274,9 +266,7 @@ class UDS_CC(Packet): } name = 'CommunicationControl' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x28, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x28, UDS.services), _uds_slm), ByteEnumField('controlType', 0, controlTypes), BitEnumField('communicationType0', 0, 2, {0: 'ISOSAEReserved', @@ -312,9 +302,7 @@ class UDS_CC(Packet): class UDS_CCPR(Packet): name = 'CommunicationControlPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x68, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x68, UDS.services), _uds_slm), ByteEnumField('controlType', 0, UDS_CC.controlTypes) ] @@ -343,9 +331,7 @@ class UDS_AUTH(Packet): } name = "Authentication" fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x29, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x29, UDS.services), _uds_slm), ByteEnumField('subFunction', 0, subFunctions), ConditionalField(XByteField('communicationConfiguration', 0), lambda pkt: pkt.subFunction in [0x01, 0x02, 0x5]), @@ -429,9 +415,7 @@ class UDS_AUTHPR(Packet): } name = 'AuthenticationPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x69, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x69, UDS.services), _uds_slm), ByteEnumField('subFunction', 0, UDS_AUTH.subFunctions), ByteEnumField('returnValue', 0, authenticationReturnParameterTypes), ConditionalField( @@ -497,9 +481,7 @@ def answers(self, other): class UDS_TP(Packet): name = 'TesterPresent' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x3e, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x3e, UDS.services), _uds_slm), ByteField('subFunction', 0) ] @@ -511,9 +493,7 @@ class UDS_TP(Packet): class UDS_TPPR(Packet): name = 'TesterPresentPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x7e, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x7e, UDS.services), _uds_slm), ByteField('zeroSubFunction', 0) ] @@ -536,9 +516,7 @@ class UDS_ATP(Packet): } name = 'AccessTimingParameter' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x83, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x83, UDS.services), _uds_slm), ByteEnumField('timingParameterAccessType', 0, timingParameterAccessTypes), ConditionalField(StrField('timingParameterRequestRecord', b""), @@ -553,9 +531,7 @@ class UDS_ATP(Packet): class UDS_ATPPR(Packet): name = 'AccessTimingParameterPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xc3, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xc3, UDS.services), _uds_slm), ByteEnumField('timingParameterAccessType', 0, UDS_ATP.timingParameterAccessTypes), ConditionalField(StrField('timingParameterResponseRecord', b""), @@ -578,9 +554,7 @@ def answers(self, other): class UDS_SDT(Packet): name = 'SecuredDataTransmission' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x84, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x84, UDS.services), _uds_slm), BitField('requestMessage', 0, 1), BitField('ISOSAEReservedBackwardsCompatibility', 0, 2), BitField('preEstablishedKeyUsed', 0, 1), @@ -603,9 +577,7 @@ class UDS_SDT(Packet): class UDS_SDTPR(Packet): name = 'SecuredDataTransmissionPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xc4, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xc4, UDS.services), _uds_slm), BitField('requestMessage', 0, 1), BitField('ISOSAEReservedBackwardsCompatibility', 0, 2), BitField('preEstablishedKeyUsed', 0, 1), @@ -637,9 +609,7 @@ class UDS_CDTCS(Packet): } name = 'ControlDTCSetting' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x85, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x85, UDS.services), _uds_slm), ByteEnumField('DTCSettingType', 0, DTCSettingTypes), StrField('DTCSettingControlOptionRecord', b"") ] @@ -652,9 +622,7 @@ class UDS_CDTCS(Packet): class UDS_CDTCSPR(Packet): name = 'ControlDTCSettingPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xc5, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xc5, UDS.services), _uds_slm), ByteEnumField('DTCSettingType', 0, UDS_CDTCS.DTCSettingTypes) ] @@ -675,9 +643,7 @@ class UDS_ROE(Packet): } name = 'ResponseOnEvent' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x86, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x86, UDS.services), _uds_slm), ByteEnumField('eventType', 0, eventTypes), ByteField('eventWindowTime', 0), StrField('eventTypeRecord', b"") @@ -691,9 +657,7 @@ class UDS_ROE(Packet): class UDS_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xc6, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xc6, UDS.services), _uds_slm), ByteEnumField('eventType', 0, UDS_ROE.eventTypes), ByteField('numberOfIdentifiedEvents', 0), ByteField('eventWindowTime', 0), @@ -719,9 +683,7 @@ class UDS_LC(Packet): } name = 'LinkControl' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x87, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x87, UDS.services), _uds_slm), ByteEnumField('linkControlType', 0, linkControlTypes), ConditionalField(ByteField('baudrateIdentifier', 0), lambda pkt: pkt.linkControlType == 0x1), @@ -741,9 +703,7 @@ class UDS_LC(Packet): class UDS_LCPR(Packet): name = 'LinkControlPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0xc7, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0xc7, UDS.services), _uds_slm), ByteEnumField('linkControlType', 0, UDS_LC.linkControlTypes) ] @@ -761,9 +721,7 @@ class UDS_RDBI(Packet): dataIdentifiers = ObservableDict() name = 'ReadDataByIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x22, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x22, UDS.services), _uds_slm), FieldListField("identifiers", None, XShortEnumField('dataIdentifier', 0, dataIdentifiers)) @@ -777,9 +735,7 @@ class UDS_RDBI(Packet): class UDS_RDBIPR(Packet): name = 'ReadDataByIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x62, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x62, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -797,9 +753,7 @@ def answers(self, other): class UDS_RMBA(Packet): name = 'ReadMemoryByAddress' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x23, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x23, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -828,9 +782,7 @@ class UDS_RMBA(Packet): class UDS_RMBAPR(Packet): name = 'ReadMemoryByAddressPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x63, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x63, UDS.services), _uds_slm), StrField('dataRecord', b"", fmt="B") ] @@ -847,9 +799,7 @@ class UDS_RSDBI(Packet): name = 'ReadScalingDataByIdentifier' dataIdentifiers = ObservableDict() fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x24, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x24, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, dataIdentifiers) ] @@ -862,9 +812,7 @@ class UDS_RSDBI(Packet): class UDS_RSDBIPR(Packet): name = 'ReadScalingDataByIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x64, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x64, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RSDBI.dataIdentifiers), ByteField('scalingByte', 0), StrField('dataRecord', b"", fmt="B") @@ -891,9 +839,7 @@ class UDS_RDBPI(Packet): } name = 'ReadDataByPeriodicIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x2a, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x2a, UDS.services), _uds_slm), ByteEnumField('transmissionMode', 0, transmissionModes), ByteEnumField('periodicDataIdentifier', 0, periodicDataIdentifiers), StrField('furtherPeriodicDataIdentifier', b"", fmt="B") @@ -908,9 +854,7 @@ class UDS_RDBPI(Packet): class UDS_RDBPIPR(Packet): name = 'ReadDataByPeriodicIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x6a, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x6a, UDS.services), _uds_slm), ByteField('periodicDataIdentifier', 0), StrField('dataRecord', b"", fmt="B") ] @@ -933,9 +877,7 @@ class UDS_DDDI(Packet): 0x2: "defineByMemoryAddress", 0x3: "clearDynamicallyDefinedDataIdentifier"} fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x2c, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x2c, UDS.services), _uds_slm), ByteEnumField('subFunction', 0, subFunctions), StrField('dataRecord', b"", fmt="B") ] @@ -948,9 +890,7 @@ class UDS_DDDI(Packet): class UDS_DDDIPR(Packet): name = 'DynamicallyDefineDataIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x6c, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x6c, UDS.services), _uds_slm), ByteEnumField('subFunction', 0, UDS_DDDI.subFunctions), XShortField('dynamicallyDefinedDataIdentifier', 0) ] @@ -968,9 +908,7 @@ def answers(self, other): class UDS_WDBI(Packet): name = 'WriteDataByIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x2e, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x2e, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers) ] @@ -983,9 +921,7 @@ class UDS_WDBI(Packet): class UDS_WDBIPR(Packet): name = 'WriteDataByIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x6e, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x6e, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -1003,9 +939,7 @@ def answers(self, other): class UDS_WMBA(Packet): name = 'WriteMemoryByAddress' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x3d, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x3d, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -1036,9 +970,7 @@ class UDS_WMBA(Packet): class UDS_WMBAPR(Packet): name = 'WriteMemoryByAddressPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x7d, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x7d, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), ConditionalField(XByteField('memoryAddress1', 0), @@ -1097,9 +1029,7 @@ def extract_padding(self, s): class UDS_CDTCI(Packet): name = 'ClearDiagnosticInformation' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x14, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x14, UDS.services), _uds_slm), ByteField('groupOfDTCHighByte', 0), ByteField('groupOfDTCMiddleByte', 0), ByteField('groupOfDTCLowByte', 0), @@ -1112,9 +1042,7 @@ class UDS_CDTCI(Packet): class UDS_CDTCIPR(Packet): fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x54, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x54, UDS.services), _uds_slm), ] name = 'ClearDiagnosticInformationPositiveResponse' @@ -1181,9 +1109,7 @@ class UDS_RDTCI(Packet): } name = 'ReadDTCInformation' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x19, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x19, UDS.services), _uds_slm), ByteEnumField('reportType', 0, reportTypes), ConditionalField(FlagsField('DTCSeverityMask', 0, 8, dtcSeverityMask), lambda pkt: pkt.reportType in [0x07, 0x08]), @@ -1264,9 +1190,7 @@ class DTCSnapshotRecord(Packet): class UDS_RDTCIPR(Packet): name = 'ReadDTCInformationPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x59, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x59, UDS.services), _uds_slm), ByteEnumField('reportType', 0, UDS_RDTCI.reportTypes), ConditionalField( FlagsField('DTCStatusAvailabilityMask', 0, 8, UDS_RDTCI.dtcStatus), @@ -1330,9 +1254,7 @@ class UDS_RC(Packet): routineControlIdentifiers = ObservableDict() name = 'RoutineControl' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x31, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x31, UDS.services), _uds_slm), ByteEnumField('routineControlType', 0, routineControlTypes), XShortEnumField('routineIdentifier', 0, routineControlIdentifiers) ] @@ -1345,9 +1267,7 @@ class UDS_RC(Packet): class UDS_RCPR(Packet): name = 'RoutineControlPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x71, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x71, UDS.services), _uds_slm), ByteEnumField('routineControlType', 0, UDS_RC.routineControlTypes), XShortEnumField('routineIdentifier', 0, UDS_RC.routineControlIdentifiers), @@ -1375,9 +1295,7 @@ class UDS_RD(Packet): }) name = 'RequestDownload' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x34, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x34, UDS.services), _uds_slm), ByteEnumField('dataFormatIdentifier', 0, dataFormatIdentifiers), BitField('memorySizeLen', 0, 4), BitField('memoryAddressLen', 0, 4), @@ -1407,9 +1325,7 @@ class UDS_RD(Packet): class UDS_RDPR(Packet): name = 'RequestDownloadPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x74, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x74, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('reserved', 0, 4), StrField('maxNumberOfBlockLength', b"", fmt="B"), @@ -1427,9 +1343,7 @@ def answers(self, other): class UDS_RU(Packet): name = 'RequestUpload' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x35, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x35, UDS.services), _uds_slm), ByteEnumField('dataFormatIdentifier', 0, UDS_RD.dataFormatIdentifiers), BitField('memorySizeLen', 0, 4), @@ -1460,9 +1374,7 @@ class UDS_RU(Packet): class UDS_RUPR(Packet): name = 'RequestUploadPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x75, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x75, UDS.services), _uds_slm), BitField('memorySizeLen', 0, 4), BitField('reserved', 0, 4), StrField('maxNumberOfBlockLength', b"", fmt="B"), @@ -1480,9 +1392,7 @@ def answers(self, other): class UDS_TD(Packet): name = 'TransferData' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x36, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x36, UDS.services), _uds_slm), ByteField('blockSequenceCounter', 0), StrField('transferRequestParameterRecord', b"", fmt="B") ] @@ -1495,9 +1405,7 @@ class UDS_TD(Packet): class UDS_TDPR(Packet): name = 'TransferDataPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x76, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x76, UDS.services), _uds_slm), ByteField('blockSequenceCounter', 0), StrField('transferResponseParameterRecord', b"", fmt="B") ] @@ -1515,9 +1423,7 @@ def answers(self, other): class UDS_RTE(Packet): name = 'RequestTransferExit' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x37, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x37, UDS.services), _uds_slm), StrField('transferRequestParameterRecord', b"", fmt="B") ] @@ -1529,9 +1435,7 @@ class UDS_RTE(Packet): class UDS_RTEPR(Packet): name = 'RequestTransferExitPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x77, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x77, UDS.services), _uds_slm), StrField('transferResponseParameterRecord', b"", fmt="B") ] @@ -1561,9 +1465,7 @@ def _contains_file_size(packet): return packet.modeOfOperation not in [2, 4, 5] fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x38, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x38, UDS.services), _uds_slm), XByteEnumField('modeOfOperation', 0, modeOfOperations), FieldLenField('filePathAndNameLength', None, length_of='filePathAndName', fmt='H'), @@ -1600,9 +1502,7 @@ def _contains_data_format_identifier(packet): return packet.modeOfOperation != 0x02 fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x78, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x78, UDS.services), _uds_slm), XByteEnumField('modeOfOperation', 0, UDS_RFT.modeOfOperations), ConditionalField(FieldLenField('lengthFormatIdentifier', None, length_of='maxNumberOfBlockLength', @@ -1642,9 +1542,7 @@ def answers(self, other): class UDS_IOCBI(Packet): name = 'InputOutputControlByIdentifier' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x2f, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x2f, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -1656,9 +1554,7 @@ class UDS_IOCBI(Packet): class UDS_IOCBIPR(Packet): name = 'InputOutputControlByIdentifierPositiveResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x6f, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x6f, UDS.services), _uds_slm), XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -1738,9 +1634,7 @@ class UDS_NR(Packet): } name = 'NegativeResponse' fields_desc = [ - ConditionalField( - XByteEnumField('service', 0x7f, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)), + ConditionalField(XByteEnumField('service', 0x7f, UDS.services), _uds_slm), XByteEnumField('requestServiceId', 0, UDS.services), ByteEnumField('negativeResponseCode', 0, negativeResponseCodes) ] diff --git a/scapy/contrib/automotive/utils.py b/scapy/contrib/automotive/utils.py deleted file mode 100644 index d619d0b2a16..00000000000 --- a/scapy/contrib/automotive/utils.py +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# This file is part of Scapy -# See https://scapy.net/ for more information - -# scapy.contrib.status = skip - -""" -Generic utilities for automotive protocol single layer mode support. - -These helpers allow automotive protocol base classes (UDS, KWP, OBD, GMLAN …) -to offer a *single layer mode* where every service packet (e.g. ``UDS_DSC``) is -returned directly by the base-class dispatcher instead of being nested inside -the parent layer. - -Single layer mode is controlled via the ``single_layer_mode`` flag in the -protocol's :attr:`~scapy.config.conf.contribs` entry:: - - conf.contribs['UDS']['single_layer_mode'] = True # enable - conf.contribs['UDS']['single_layer_mode'] = False # disable (default) - -The same key is used for all supported protocols (``'UDS'``, ``'KWP'``, -``'OBD'``, ``'GMLAN'``). - -Each service packet class carries a conditional ``service`` field as its first -field. The field is visible (included in build / dissection) only when single -layer mode is active:: - - ConditionalField( - XByteEnumField('service', 0x10, UDS.services), - lambda pkt: conf.contribs['UDS'].get('single_layer_mode', False)) - -In single layer mode the base class ``dispatch_hook`` reads the first byte of -raw data and routes it directly to the appropriate service class via the -``_service_cls`` dispatch table that is populated at module load time. -""" From 45e66533383608553aff3f39b56510762b8911ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 06:14:15 +0000 Subject: [PATCH 13/16] feat: add compatibility_mode flag to UDS, KWP, GMLAN, OBD single-layer mode Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/d5e15641-5fee-4efa-8436-1e172bb245e4 Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- doc/scapy/layers/automotive.rst | 57 ++++++++++++++++++++ scapy/contrib/automotive/gm/gmlan.py | 26 ++++++++- scapy/contrib/automotive/kwp.py | 26 ++++++++- scapy/contrib/automotive/obd/obd.py | 3 +- scapy/contrib/automotive/obd/services.py | 30 ++++++++++- scapy/contrib/automotive/uds.py | 30 ++++++++++- test/contrib/automotive/gm/gmlan.uts | 57 ++++++++++++++++++++ test/contrib/automotive/kwp.uts | 57 ++++++++++++++++++++ test/contrib/automotive/obd/obd.uts | 63 ++++++++++++++++++++++ test/contrib/automotive/uds.uts | 69 ++++++++++++++++++++++++ 10 files changed, 410 insertions(+), 8 deletions(-) diff --git a/doc/scapy/layers/automotive.rst b/doc/scapy/layers/automotive.rst index a5b9faee57a..a9fa9d3e107 100644 --- a/doc/scapy/layers/automotive.rst +++ b/doc/scapy/layers/automotive.rst @@ -1116,6 +1116,63 @@ To toggle at runtime after loading:: The same ``single_layer_mode`` key works for all protocols: replace ``'UDS'`` with ``'KWP'``, ``'OBD'``, or ``'GMLAN'`` as appropriate. +Compatibility Mode +------------------ + +Scapy allows crafting packets freely, including stacking a service sub-packet +on top of the base protocol layer (e.g. ``UDS()/UDS_DSC()``). When both +``single_layer_mode`` *and* stacking are used together, the ``service`` byte +would normally appear twice in the resulting byte stream – once from the base +layer and once from the sub-packet's own ``service`` ConditionalField. + +The **compatibility mode** flag (``compatibility_mode``, default ``True``) +addresses this: when it is enabled and ``single_layer_mode`` is active, the +sub-packet's ``service`` field is automatically **suppressed** whenever the +immediate underlayer is already the matching base-protocol packet. + +.. list-table:: Behaviour matrix + :header-rows: 1 + :widths: 25 25 50 + + * - ``single_layer_mode`` + - ``compatibility_mode`` + - ``UDS()/UDS_DSC()`` byte layout + * - ``False`` + - any + - ``service`` (UDS) + ``diagnosticSessionType`` (UDS_DSC) + * - ``True`` + - ``True`` *(default)* + - ``service`` (UDS) + ``diagnosticSessionType`` (UDS_DSC) — duplicate suppressed + * - ``True`` + - ``False`` + - ``service`` (UDS) + ``service`` (UDS_DSC) + ``diagnosticSessionType`` (UDS_DSC) + +Example with compatibility mode on (default):: + + >>> conf.contribs['UDS']['single_layer_mode'] = True + >>> conf.contribs['UDS']['compatibility_mode'] = True # already the default + + >>> # Standalone sub-packet: service field IS present (no UDS underlayer) + >>> bytes(UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x01' + + >>> # Stacked: service field in UDS_DSC is suppressed (UDS is the underlayer) + >>> bytes(UDS() / UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x01' + +Example with compatibility mode off:: + + >>> conf.contribs['UDS']['compatibility_mode'] = False + + >>> # Stacked: both UDS and UDS_DSC emit a service byte + >>> bytes(UDS() / UDS_DSC(diagnosticSessionType=0x01)) + b'\x10\x10\x01' + + >>> conf.contribs['UDS']['compatibility_mode'] = True # restore default + +The same ``compatibility_mode`` key works for all protocols: replace ``'UDS'`` +with ``'KWP'``, ``'OBD'``, or ``'GMLAN'`` as appropriate. + GMLAN ===== diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index 34636ea89fb..213729ace3e 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -53,13 +53,35 @@ # "ResponsePending' as answer of a request. \n" # "The default value is False.") conf.contribs['GMLAN'] = {'treat-response-pending-as-answer': False, - 'single_layer_mode': False} + 'single_layer_mode': False, + 'compatibility_mode': True} conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = None def _gmlan_slm(pkt): - return conf.contribs['GMLAN'].get('single_layer_mode', False) + # type: (Packet) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['GMLAN']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`GMLAN` acts as a dispatch layer and returns the + matching service sub-packet directly. Each sub-packet gains its own + ``service`` field so that it can be built and dissected stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already a :class:`GMLAN` packet, preventing a duplicate + service byte when sub-packets are stacked (``GMLAN()/GMLAN_IDO()``). + Set to *False* to always emit the ``service`` byte from the sub-packet. + """ + if not conf.contribs['GMLAN'].get('single_layer_mode', False): + return False + if conf.contribs['GMLAN'].get('compatibility_mode', True): + return pkt.underlayer is None or not isinstance(pkt.underlayer, GMLAN) + return True class GMLAN(ISOTP): @staticmethod diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index c41e620e60a..6918611a3fb 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -46,11 +46,33 @@ "ResponsePending' as answer of a request. \n" "The default value is False.") conf.contribs['KWP'] = {'treat-response-pending-as-answer': False, - 'single_layer_mode': False} + 'single_layer_mode': False, + 'compatibility_mode': True} def _kwp_slm(pkt): - return conf.contribs['KWP'].get('single_layer_mode', False) + # type: (Any) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['KWP']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`KWP` acts as a dispatch layer and returns the + matching service sub-packet directly. Each sub-packet gains its own + ``service`` field so that it can be built and dissected stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already a :class:`KWP` packet, preventing a duplicate + service byte when sub-packets are stacked (``KWP()/KWP_SDS()``). + Set to *False* to always emit the ``service`` byte from the sub-packet. + """ + if not conf.contribs['KWP'].get('single_layer_mode', False): + return False + if conf.contribs['KWP'].get('compatibility_mode', True): + return pkt.underlayer is None or not isinstance(pkt.underlayer, KWP) + return True class KWP(ISOTP): services = ObservableDict( diff --git a/scapy/contrib/automotive/obd/obd.py b/scapy/contrib/automotive/obd/obd.py index c77c6ff6e00..ce942d66c97 100644 --- a/scapy/contrib/automotive/obd/obd.py +++ b/scapy/contrib/automotive/obd/obd.py @@ -36,7 +36,8 @@ # "ResponsePending' as answer of a request. \n" # "The default value is False.") conf.contribs['OBD'] = {'treat-response-pending-as-answer': False, - 'single_layer_mode': False} + 'single_layer_mode': False, + 'compatibility_mode': True} class OBD(ISOTP): diff --git a/scapy/contrib/automotive/obd/services.py b/scapy/contrib/automotive/obd/services.py index 1ad6f234daf..57303bc13fb 100644 --- a/scapy/contrib/automotive/obd/services.py +++ b/scapy/contrib/automotive/obd/services.py @@ -39,7 +39,35 @@ def _obd_slm(pkt): - return conf.contribs['OBD'].get('single_layer_mode', False) + # type: (Packet) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['OBD']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`OBD` acts as a dispatch layer and returns the + matching service sub-packet directly. Each sub-packet gains its own + ``service`` field so that it can be built and dissected stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already an :class:`OBD` packet, preventing a duplicate + service byte when sub-packets are stacked (``OBD()/OBD_S01()``). + Set to *False* to always emit the ``service`` byte from the sub-packet. + + .. note:: + OBD service classes live in ``services.py`` which is imported by + ``obd.py``. To avoid a circular import the underlayer class is + identified by its class name (``'OBD'``) rather than by an + ``isinstance`` check. + """ + if not conf.contribs['OBD'].get('single_layer_mode', False): + return False + if conf.contribs['OBD'].get('compatibility_mode', True): + ul = pkt.underlayer + return ul is None or type(ul).__name__ != 'OBD' + return True class OBD_DTC(OBD_Packet): diff --git a/scapy/contrib/automotive/uds.py b/scapy/contrib/automotive/uds.py index 40b0f4b2aea..aedd4f5652b 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -42,12 +42,38 @@ # "ResponsePending' as answer of a request. \n" # "The default value is False.") conf.contribs['UDS'] = {'treat-response-pending-as-answer': False, - 'single_layer_mode': False} + 'single_layer_mode': False, + 'compatibility_mode': True} conf.debug_dissector = True + def _uds_slm(pkt): - return conf.contribs['UDS'].get('single_layer_mode', False) + # type: (Packet) -> bool + """Return True when the service ConditionalField should be present. + + Two configuration keys in ``conf.contribs['UDS']`` control the behaviour: + + ``single_layer_mode`` (bool, default ``False``): + When *True*, :class:`UDS` acts as a dispatch layer and returns the + matching service sub-packet directly (e.g. + ``UDS(b'\\x10\\x01')`` → ``UDS_DSC``). Each sub-packet gains its + own ``service`` field so that it can be built and dissected + stand-alone. + + ``compatibility_mode`` (bool, default ``True``): + Only relevant when ``single_layer_mode`` is *True*. When *True* the + ``service`` field is **suppressed** in a sub-packet whose immediate + underlayer is already a :class:`UDS` packet. This prevents a + duplicate service byte when a sub-packet is stacked on top of a UDS + base layer (``UDS()/UDS_DSC()``). Set to *False* to always emit the + ``service`` byte from the sub-packet regardless of stacking. + """ + if not conf.contribs['UDS'].get('single_layer_mode', False): + return False + if conf.contribs['UDS'].get('compatibility_mode', True): + return pkt.underlayer is None or not isinstance(pkt.underlayer, UDS) + return True class UDS(ISOTP): diff --git a/test/contrib/automotive/gm/gmlan.uts b/test/contrib/automotive/gm/gmlan.uts index 822ce5c3448..73a4198e182 100644 --- a/test/contrib/automotive/gm/gmlan.uts +++ b/test/contrib/automotive/gm/gmlan.uts @@ -680,3 +680,60 @@ assert ido4[GMLAN_IDO].subfunction == 0x02 conf.contribs['GMLAN']['single_layer_mode'] = False assert not conf.contribs['GMLAN']['single_layer_mode'] + ++ Compatibility mode GMLAN + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['GMLAN']['single_layer_mode'] = True +conf.contribs['GMLAN']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +ido_sa = GMLAN_IDO(subfunction=0x02) +assert bytes(ido_sa) == b'\x10\x02', \ + "Standalone GMLAN_IDO should include service byte, got %s" % bytes(ido_sa).hex() +assert ido_sa.service == 0x10 + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = GMLAN() / GMLAN_IDO(subfunction=0x02) +assert bytes(stacked) == b'\x10\x02', \ + "Stacked GMLAN/GMLAN_IDO should produce 2 bytes (no duplicate service), got %s" % bytes(stacked).hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +ido_dis = GMLAN(b'\x10\x02') +assert isinstance(ido_dis, GMLAN_IDO) +assert ido_dis.service == 0x10 +assert ido_dis.subfunction == 0x02 + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['GMLAN']['compatibility_mode'] = False + +stacked_nc = GMLAN() / GMLAN_IDO(subfunction=0x02) +assert bytes(stacked_nc) == b'\x10\x10\x02', \ + "With compat OFF, stacked GMLAN/GMLAN_IDO should produce 3 bytes, got %s" % bytes(stacked_nc).hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +ido_nc = GMLAN_IDO(subfunction=0x02) +assert bytes(ido_nc) == b'\x10\x02', \ + "Standalone GMLAN_IDO should include service byte even with compat OFF, got %s" % bytes(ido_nc).hex() + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['GMLAN']['single_layer_mode'] = False +conf.contribs['GMLAN']['compatibility_mode'] = False + +stacked_slm_off = GMLAN() / GMLAN_IDO(subfunction=0x02) +assert bytes(stacked_slm_off) == b'\x10\x02', \ + "With SLM OFF, no service field in GMLAN_IDO regardless of compat mode, got %s" % bytes(stacked_slm_off).hex() + += Compatibility mode: cleanup + +conf.contribs['GMLAN']['single_layer_mode'] = False +conf.contribs['GMLAN']['compatibility_mode'] = True +assert not conf.contribs['GMLAN']['single_layer_mode'] +assert conf.contribs['GMLAN']['compatibility_mode'] diff --git a/test/contrib/automotive/kwp.uts b/test/contrib/automotive/kwp.uts index ddc83373d28..73c99dff1a2 100644 --- a/test/contrib/automotive/kwp.uts +++ b/test/contrib/automotive/kwp.uts @@ -591,3 +591,60 @@ assert count == 1, "Expected 1 binding for KWP_SDS, got %d" % count conf.contribs['KWP']['single_layer_mode'] = False assert not conf.contribs['KWP']['single_layer_mode'] + ++ Compatibility mode KWP + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['KWP']['single_layer_mode'] = True +conf.contribs['KWP']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +sds_sa = KWP_SDS(diagnosticSession=0x01) +assert bytes(sds_sa) == b'\x10\x01', \ + "Standalone KWP_SDS should include service byte, got %s" % bytes(sds_sa).hex() +assert sds_sa.service == 0x10 + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = KWP() / KWP_SDS(diagnosticSession=0x01) +assert bytes(stacked) == b'\x10\x01', \ + "Stacked KWP/KWP_SDS should produce 2 bytes (no duplicate service), got %s" % bytes(stacked).hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +sds_dis = KWP(b'\x10\x01') +assert isinstance(sds_dis, KWP_SDS) +assert sds_dis.service == 0x10 +assert sds_dis.diagnosticSession == 0x01 + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['KWP']['compatibility_mode'] = False + +stacked_nc = KWP() / KWP_SDS(diagnosticSession=0x01) +assert bytes(stacked_nc) == b'\x10\x10\x01', \ + "With compat OFF, stacked KWP/KWP_SDS should produce 3 bytes, got %s" % bytes(stacked_nc).hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +sds_nc = KWP_SDS(diagnosticSession=0x01) +assert bytes(sds_nc) == b'\x10\x01', \ + "Standalone KWP_SDS should include service byte even with compat OFF, got %s" % bytes(sds_nc).hex() + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['KWP']['single_layer_mode'] = False +conf.contribs['KWP']['compatibility_mode'] = False + +stacked_slm_off = KWP() / KWP_SDS(diagnosticSession=0x01) +assert bytes(stacked_slm_off) == b'\x10\x01', \ + "With SLM OFF, no service field in KWP_SDS regardless of compat mode, got %s" % bytes(stacked_slm_off).hex() + += Compatibility mode: cleanup + +conf.contribs['KWP']['single_layer_mode'] = False +conf.contribs['KWP']['compatibility_mode'] = True +assert not conf.contribs['KWP']['single_layer_mode'] +assert conf.contribs['KWP']['compatibility_mode'] diff --git a/test/contrib/automotive/obd/obd.uts b/test/contrib/automotive/obd/obd.uts index f9e577c4200..90bef535dc5 100644 --- a/test/contrib/automotive/obd/obd.uts +++ b/test/contrib/automotive/obd/obd.uts @@ -1092,3 +1092,66 @@ assert isinstance(s01_3[OBD_S01], OBD_S01) conf.contribs['OBD']['single_layer_mode'] = False assert not conf.contribs['OBD']['single_layer_mode'] + ++ Compatibility mode OBD + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['OBD']['single_layer_mode'] = True +conf.contribs['OBD']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +s01_sa = OBD_S01(pid=[0x0c]) +assert bytes(s01_sa)[0:1] == b'\x01', \ + "Standalone OBD_S01 should include service byte 0x01, got %s" % bytes(s01_sa).hex() + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = OBD() / OBD_S01(pid=[0x0c]) +stacked_bytes = bytes(stacked) +assert stacked_bytes[0:1] == b'\x01', \ + "Stacked OBD/OBD_S01 first byte should be 0x01 (OBD service), got %s" % stacked_bytes.hex() +assert stacked_bytes.count(b'\x01') == 1 or stacked_bytes[1:2] != b'\x01', \ + "No duplicate service byte expected, got %s" % stacked_bytes.hex() +assert len(stacked_bytes) == 2, \ + "Stacked OBD/OBD_S01(pid=[0x0c]) should be 2 bytes, got %s" % stacked_bytes.hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +s01_dis = OBD(b'\x01\x0c') +assert isinstance(s01_dis, OBD_S01) + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['OBD']['compatibility_mode'] = False + +stacked_nc = OBD() / OBD_S01(pid=[0x0c]) +stacked_nc_bytes = bytes(stacked_nc) +assert len(stacked_nc_bytes) == 3, \ + "With compat OFF, stacked OBD/OBD_S01 should produce 3 bytes (duplicate service), got %s" % stacked_nc_bytes.hex() +assert stacked_nc_bytes[0:1] == b'\x01' and stacked_nc_bytes[1:2] == b'\x01', \ + "With compat OFF, first two bytes should both be service 0x01, got %s" % stacked_nc_bytes.hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +s01_nc = OBD_S01(pid=[0x0c]) +assert bytes(s01_nc)[0:1] == b'\x01', \ + "Standalone OBD_S01 should include service byte even with compat OFF" + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['OBD']['single_layer_mode'] = False +conf.contribs['OBD']['compatibility_mode'] = False + +stacked_slm_off = OBD() / OBD_S01(pid=[0x0c]) +slm_off_bytes = bytes(stacked_slm_off) +assert len(slm_off_bytes) == 2, \ + "With SLM OFF, no service field in OBD_S01 regardless of compat mode, got %s" % slm_off_bytes.hex() + += Compatibility mode: cleanup + +conf.contribs['OBD']['single_layer_mode'] = False +conf.contribs['OBD']['compatibility_mode'] = True +assert not conf.contribs['OBD']['single_layer_mode'] +assert conf.contribs['OBD']['compatibility_mode'] diff --git a/test/contrib/automotive/uds.uts b/test/contrib/automotive/uds.uts index 90a90962d64..e4a6bf07ba8 100644 --- a/test/contrib/automotive/uds.uts +++ b/test/contrib/automotive/uds.uts @@ -1545,3 +1545,72 @@ assert er6.resetType == 0x01 conf.contribs['UDS']['single_layer_mode'] = False assert not conf.contribs['UDS']['single_layer_mode'] + ++ Compatibility mode UDS + += Compatibility mode: setup - enable both SLM and compat mode (default) + +conf.contribs['UDS']['single_layer_mode'] = True +conf.contribs['UDS']['compatibility_mode'] = True + += Compatibility mode ON + SLM ON: standalone sub-packet has service field + +dsc_sa = UDS_DSC(diagnosticSessionType=0x01) +assert bytes(dsc_sa) == b'\x10\x01', \ + "Standalone UDS_DSC should include service byte, got %s" % bytes(dsc_sa).hex() +assert dsc_sa.service == 0x10 + += Compatibility mode ON + SLM ON: stacked sub-packet suppresses its service field + +stacked = UDS() / UDS_DSC(diagnosticSessionType=0x01) +assert bytes(stacked) == b'\x10\x01', \ + "Stacked UDS/UDS_DSC should produce 2 bytes (no duplicate service), got %s" % bytes(stacked).hex() + += Compatibility mode ON + SLM ON: positive response stacked suppresses service + +stacked_pr = UDS() / UDS_DSCPR(diagnosticSessionType=0x01, sessionParameterRecord=b"") +assert bytes(stacked_pr) == b'\x50\x01', \ + "Stacked UDS/UDS_DSCPR should produce 2 bytes, got %s" % bytes(stacked_pr).hex() + += Compatibility mode ON + SLM ON: dissect standalone still works + +dsc_dis = UDS(b'\x10\x01') +assert isinstance(dsc_dis, UDS_DSC) +assert dsc_dis.service == 0x10 +assert dsc_dis.diagnosticSessionType == 0x01 + += Compatibility mode OFF + SLM ON: stacked sub-packet always emits service field + +conf.contribs['UDS']['compatibility_mode'] = False + +stacked_nc = UDS() / UDS_DSC(diagnosticSessionType=0x01) +assert bytes(stacked_nc) == b'\x10\x10\x01', \ + "With compat OFF, stacked UDS/UDS_DSC should produce 3 bytes (duplicate service), got %s" % bytes(stacked_nc).hex() + += Compatibility mode OFF + SLM ON: standalone sub-packet also has service field + +dsc_nc = UDS_DSC(diagnosticSessionType=0x01) +assert bytes(dsc_nc) == b'\x10\x01', \ + "Standalone UDS_DSC should include service byte even with compat OFF, got %s" % bytes(dsc_nc).hex() + += Compatibility mode OFF + SLM ON: dissect standalone still works + +dsc_dis2 = UDS(b'\x10\x01') +assert isinstance(dsc_dis2, UDS_DSC) +assert dsc_dis2.service == 0x10 + += Compatibility mode: SLM OFF overrides compat mode - no service field in sub-packet + +conf.contribs['UDS']['single_layer_mode'] = False +conf.contribs['UDS']['compatibility_mode'] = False + +stacked_slm_off = UDS() / UDS_DSC(diagnosticSessionType=0x01) +assert bytes(stacked_slm_off) == b'\x10\x01', \ + "With SLM OFF, no service field in UDS_DSC regardless of compat mode, got %s" % bytes(stacked_slm_off).hex() + += Compatibility mode: cleanup + +conf.contribs['UDS']['single_layer_mode'] = False +conf.contribs['UDS']['compatibility_mode'] = True +assert not conf.contribs['UDS']['single_layer_mode'] +assert conf.contribs['UDS']['compatibility_mode'] From 242ab0d5aa004081bdeeaf047cb02db3ef09d3d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 06:17:18 +0000 Subject: [PATCH 14/16] fix: use Packet type annotation in _kwp_slm; simplify OBD assertion in tests Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/d5e15641-5fee-4efa-8436-1e172bb245e4 Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- scapy/contrib/automotive/kwp.py | 2 +- test/contrib/automotive/obd/obd.uts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index 6918611a3fb..c95a9e1dd5d 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -51,7 +51,7 @@ def _kwp_slm(pkt): - # type: (Any) -> bool + # type: (Packet) -> bool """Return True when the service ConditionalField should be present. Two configuration keys in ``conf.contribs['KWP']`` control the behaviour: diff --git a/test/contrib/automotive/obd/obd.uts b/test/contrib/automotive/obd/obd.uts index 90bef535dc5..2c5e8e71e11 100644 --- a/test/contrib/automotive/obd/obd.uts +++ b/test/contrib/automotive/obd/obd.uts @@ -1112,7 +1112,7 @@ stacked = OBD() / OBD_S01(pid=[0x0c]) stacked_bytes = bytes(stacked) assert stacked_bytes[0:1] == b'\x01', \ "Stacked OBD/OBD_S01 first byte should be 0x01 (OBD service), got %s" % stacked_bytes.hex() -assert stacked_bytes.count(b'\x01') == 1 or stacked_bytes[1:2] != b'\x01', \ +assert stacked_bytes[1:2] != b'\x01', \ "No duplicate service byte expected, got %s" % stacked_bytes.hex() assert len(stacked_bytes) == 2, \ "Stacked OBD/OBD_S01(pid=[0x0c]) should be 2 bytes, got %s" % stacked_bytes.hex() From ea02c8f349a6190a4280a9c1d75e4a8fbc12a5f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:08:04 +0000 Subject: [PATCH 15/16] fix: use valid 7-byte test data for GMLAN_RFRDPR sub-subpacket test in SLM mode Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/987a3bb3-9f72-440b-8b2d-7e36a911bb39 Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- test/contrib/automotive/gm/gmlan.uts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/contrib/automotive/gm/gmlan.uts b/test/contrib/automotive/gm/gmlan.uts index 73a4198e182..9b8d0a175aa 100644 --- a/test/contrib/automotive/gm/gmlan.uts +++ b/test/contrib/automotive/gm/gmlan.uts @@ -659,7 +659,7 @@ assert sa3.hashret() == sapr3.hashret(), \ = Single layer mode: sub-subpacket bindings are unaffected -rfrdpr = GMLAN(b'\x52\x01\x00\x01\x02\x03') +rfrdpr = GMLAN(b'\x52\x01\x00\x01\x02\x03\x04') assert isinstance(rfrdpr, GMLAN_RFRDPR), "Expected GMLAN_RFRDPR, got %s" % type(rfrdpr) = Single layer mode: unknown service falls back to GMLAN From b8ed5981989ac9e4146daeb47d4c4da465d6dd2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:20:37 +0000 Subject: [PATCH 16/16] fix: resolve flake8 E302/E501/F401 errors in changed automotive files Agent-Logs-Url: https://github.com/polybassa/scapy-1/sessions/7880b535-39ac-4795-83a2-06408efc1460 Co-authored-by: polybassa <1676055+polybassa@users.noreply.github.com> --- scapy/contrib/automotive/gm/gmlan.py | 1 + scapy/contrib/automotive/kwp.py | 1 + scapy/contrib/automotive/obd/iid/iids.py | 7 +++++-- scapy/contrib/automotive/obd/mid/mids.py | 7 +++++-- scapy/contrib/automotive/obd/obd.py | 2 +- scapy/contrib/automotive/obd/pid/pids.py | 4 +++- scapy/contrib/automotive/obd/tid/tids.py | 5 ++++- 7 files changed, 20 insertions(+), 7 deletions(-) diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index 213729ace3e..62c88b208d7 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -83,6 +83,7 @@ def _gmlan_slm(pkt): return pkt.underlayer is None or not isinstance(pkt.underlayer, GMLAN) return True + class GMLAN(ISOTP): @staticmethod def determine_len(x): diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index c95a9e1dd5d..4d6582756ee 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -74,6 +74,7 @@ def _kwp_slm(pkt): return pkt.underlayer is None or not isinstance(pkt.underlayer, KWP) return True + class KWP(ISOTP): services = ObservableDict( {0x10: 'StartDiagnosticSession', diff --git a/scapy/contrib/automotive/obd/iid/iids.py b/scapy/contrib/automotive/obd/iid/iids.py index 8ec2c9475b3..5c85c2c4834 100644 --- a/scapy/contrib/automotive/obd/iid/iids.py +++ b/scapy/contrib/automotive/obd/iid/iids.py @@ -6,8 +6,11 @@ # scapy.contrib.status = skip -from scapy.fields import ConditionalField, FieldLenField, FieldListField, StrFixedLenField, \ - ByteField, ShortField, FlagsField, XByteEnumField, XByteField, PacketListField +from scapy.fields import ( + ConditionalField, FieldLenField, FieldListField, StrFixedLenField, + ByteField, ShortField, FlagsField, XByteEnumField, XByteField, + PacketListField +) from scapy.packet import Packet, bind_layers from scapy.contrib.automotive.obd.packet import OBD_Packet from scapy.contrib.automotive.obd.services import OBD_S09, _OBD_SERVICES, _obd_slm diff --git a/scapy/contrib/automotive/obd/mid/mids.py b/scapy/contrib/automotive/obd/mid/mids.py index ee50ca6f287..0acd4a4df1c 100644 --- a/scapy/contrib/automotive/obd/mid/mids.py +++ b/scapy/contrib/automotive/obd/mid/mids.py @@ -6,8 +6,11 @@ # scapy.contrib.status = skip -from scapy.fields import ConditionalField, FlagsField, ScalingField, ByteEnumField, XByteEnumField, \ - MultipleTypeField, ShortField, ShortEnumField, PacketListField +from scapy.fields import ( + ConditionalField, FlagsField, ScalingField, ByteEnumField, + XByteEnumField, MultipleTypeField, ShortField, ShortEnumField, + PacketListField +) from scapy.packet import Packet, bind_layers from scapy.contrib.automotive.obd.packet import OBD_Packet from scapy.contrib.automotive.obd.services import OBD_S06, _OBD_SERVICES, _obd_slm diff --git a/scapy/contrib/automotive/obd/obd.py b/scapy/contrib/automotive/obd/obd.py index ce942d66c97..005264f3559 100644 --- a/scapy/contrib/automotive/obd/obd.py +++ b/scapy/contrib/automotive/obd/obd.py @@ -15,7 +15,7 @@ from scapy.contrib.automotive.obd.tid.tids import * from scapy.contrib.automotive.obd.services import * from scapy.contrib.automotive.obd.services import _OBD_SERVICES -from scapy.packet import NoPayload, Packet, bind_layers +from scapy.packet import NoPayload, bind_layers from scapy.config import conf from scapy.fields import XByteEnumField from scapy.contrib.isotp import ISOTP diff --git a/scapy/contrib/automotive/obd/pid/pids.py b/scapy/contrib/automotive/obd/pid/pids.py index 8bd79ac6f01..9ae039a52e1 100644 --- a/scapy/contrib/automotive/obd/pid/pids.py +++ b/scapy/contrib/automotive/obd/pid/pids.py @@ -9,7 +9,9 @@ from scapy.packet import Packet, bind_layers from scapy.fields import ConditionalField, PacketListField, XByteEnumField -from scapy.contrib.automotive.obd.services import OBD_S01, OBD_S02, _OBD_SERVICES, _obd_slm +from scapy.contrib.automotive.obd.services import ( + OBD_S01, OBD_S02, _OBD_SERVICES, _obd_slm +) from scapy.contrib.automotive.obd.pid.pids_00_1F import * from scapy.contrib.automotive.obd.pid.pids_20_3F import * from scapy.contrib.automotive.obd.pid.pids_40_5F import * diff --git a/scapy/contrib/automotive/obd/tid/tids.py b/scapy/contrib/automotive/obd/tid/tids.py index f19fc01bb75..cf92b7acd01 100644 --- a/scapy/contrib/automotive/obd/tid/tids.py +++ b/scapy/contrib/automotive/obd/tid/tids.py @@ -6,7 +6,10 @@ # scapy.contrib.status = skip -from scapy.fields import ConditionalField, FlagsField, ByteField, ScalingField, PacketListField, XByteEnumField +from scapy.fields import ( + ConditionalField, FlagsField, ByteField, ScalingField, PacketListField, + XByteEnumField +) from scapy.packet import bind_layers, Packet from scapy.contrib.automotive.obd.packet import OBD_Packet from scapy.contrib.automotive.obd.services import OBD_S08, _OBD_SERVICES, _obd_slm