1# SPDX-License-Identifier: GPL-2.0-only
2# This file is part of Scapy
3# See https://scapy.net/ for more information
4
5# scapy.contrib.description = EtherCat
6# scapy.contrib.status = loads
7
8"""
9 EtherCat automation protocol
10 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11
12 :author: Thomas Tannhaeuser, hecke@naberius.de
13
14 :description:
15
16 This module provides Scapy layers for the EtherCat protocol.
17
18 normative references:
19 - IEC 61158-3-12 - data link service and topology description
20 - IEC 61158-4-12 - protocol specification
21
22 Currently only read/write services as defined in IEC 61158-4-12,
23 sec. 5.4 are supported.
24
25 :TODO:
26
27 - Mailbox service (sec. 5.5)
28 - Network variable service (sec. 5.6)
29
30 :NOTES:
31
32 - EtherCat frame type defaults to TYPE-12-PDU (0x01) using xxx bytes
33 of padding
34 - padding for minimum frame size is added automatically
35
36"""
37
38
39import struct
40
41
42from scapy.compat import raw
43from scapy.error import log_runtime, Scapy_Exception
44from scapy.fields import BitField, ByteField, LEShortField, FieldListField, \
45 LEIntField, FieldLenField, _EnumField, EnumField
46from scapy.layers.l2 import Ether, Dot1Q
47from scapy.packet import bind_layers, Packet, Padding
48
49'''
50EtherCat uses some little endian bitfields without alignment to any common boundaries. # noqa: E501
51See https://github.com/secdev/scapy/pull/569#issuecomment-295419176 for a short explanation # noqa: E501
52why the following field definitions are necessary.
53'''
54
55
56class LEBitFieldSequenceException(Scapy_Exception):
57 """
58 thrown by EtherCat structure tests
59 """
60 pass
61
62
63class LEBitField(BitField):
64 """
65 a little endian version of the BitField
66 """
67
68 def _check_field_type(self, pkt, index):
69 """
70 check if the field addressed by given index relative to this field
71 shares type of this field so we can catch a mix of LEBitField
72 and BitField/other types
73 """
74 my_idx = pkt.fields_desc.index(self)
75 try:
76 next_field = pkt.fields_desc[my_idx + index]
77 if type(next_field) is not LEBitField and \
78 next_field.__class__.__base__ is not LEBitField:
79 raise LEBitFieldSequenceException('field after field {} must '
80 'be of type LEBitField or '
81 'derived classes'.format(self.name)) # noqa: E501
82 except IndexError:
83 # no more fields -> error
84 raise LEBitFieldSequenceException('Missing further LEBitField '
85 'based fields after field '
86 '{} '.format(self.name))
87
88 def addfield(self, pkt, s, val):
89 """
90
91 :param pkt: packet instance the raw string s and field belongs to
92 :param s: raw string representing the frame
93 :param val: value
94 :return: final raw string, tuple (s, bitsdone, data) if in between bit field # noqa: E501
95
96 as we don't know the final size of the full bitfield we need to accumulate the data. # noqa: E501
97 if we reach a field that ends at a octet boundary, we build the whole string # noqa: E501
98
99 """
100 if type(s) is tuple and len(s) == 4:
101 s, bitsdone, data, _ = s
102 self._check_field_type(pkt, -1)
103 else:
104 # this is the first bit field in the set
105 bitsdone = 0
106 data = []
107
108 bitsdone += self.size
109 data.append((self.size, self.i2m(pkt, val)))
110
111 if bitsdone % 8:
112 # somewhere in between bit 0 .. 7 - next field should add more bits... # noqa: E501
113 self._check_field_type(pkt, 1)
114 return s, bitsdone, data, type(LEBitField)
115 else:
116 data.reverse()
117 octet = 0
118 remaining_len = 8
119 octets = bytearray()
120 for size, val in data:
121
122 while True:
123 if size < remaining_len:
124 remaining_len = remaining_len - size
125 octet |= val << remaining_len
126 break
127
128 elif size > remaining_len:
129 # take the leading bits and add them to octet
130 size -= remaining_len
131 octet |= val >> size
132 octets = struct.pack('!B', octet) + octets
133
134 octet = 0
135 remaining_len = 8
136 # delete all consumed bits
137 # TODO: do we need to add a check for bitfields > 64 bits to catch overruns here? # noqa: E501
138 val &= ((2 ** size) - 1)
139 continue
140 else:
141 # size == remaining len
142 octet |= val
143 octets = struct.pack('!B', octet) + octets
144 octet = 0
145 remaining_len = 8
146 break
147
148 return s + octets
149
150 def getfield(self, pkt, s):
151
152 """
153 extract data from raw str
154
155 collect all instances belonging to the bit field set.
156 if we reach a field that ends at a octet boundary, dissect the whole bit field at once # noqa: E501
157
158 :param pkt: packet instance the field belongs to
159 :param s: raw string representing the frame -or- tuple containing raw str, number of bits and array of fields # noqa: E501
160 :return: tuple containing raw str, number of bits and array of fields -or- remaining raw str and value of this # noqa: E501
161 """
162
163 if type(s) is tuple and len(s) == 3:
164 s, bits_in_set, fields = s
165 else:
166 bits_in_set = 0
167 fields = []
168
169 bits_in_set += self.size
170
171 fields.append(self)
172
173 if bits_in_set % 8:
174 # we are in between the bitfield
175 return (s, bits_in_set, fields), None
176
177 else:
178 cur_val = 0
179 cur_val_bit_idx = 0
180 this_val = 0
181
182 field_idx = 0
183 field = fields[field_idx]
184 field_required_bits = field.size
185 idx = 0
186
187 s = bytearray(s)
188 bf_total_byte_length = bits_in_set // 8
189
190 for octet in s[0:bf_total_byte_length]:
191 idx += 1
192
193 octet_bits_left = 8
194
195 while octet_bits_left:
196
197 if field_required_bits == octet_bits_left:
198 # whole field fits into remaining bits
199 # as this also signals byte-alignment this should exit the inner and outer loop # noqa: E501
200 cur_val |= octet << cur_val_bit_idx
201 pkt.fields[field.name] = cur_val
202
203 '''
204 TODO: check if do_dessect() needs a non-None check for assignment to raw_packet_cache_fields # noqa: E501
205
206 setfieldval() is evil as it sets raw_packet_cache_fields to None - but this attribute # noqa: E501
207 is accessed in do_dissect() without checking for None... exception is caught and the # noqa: E501
208 user ends up with a layer decoded as raw...
209
210 pkt.setfieldval(field.name, int(bit_str[:field.size], 2)) # noqa: E501
211 '''
212
213 octet_bits_left = 0
214
215 this_val = cur_val
216
217 elif field_required_bits < octet_bits_left:
218 # pick required bits
219 cur_val |= (octet & ((2 ** field_required_bits) - 1)) << cur_val_bit_idx # noqa: E501
220 pkt.fields[field.name] = cur_val
221
222 # remove consumed bits
223 octet >>= field_required_bits
224 octet_bits_left -= field_required_bits
225
226 # and move to the next field
227 field_idx += 1
228 field = fields[field_idx]
229 field_required_bits = field.size
230 cur_val_bit_idx = 0
231 cur_val = 0
232
233 elif field_required_bits > octet_bits_left:
234 # take remaining bits
235 cur_val |= octet << cur_val_bit_idx
236
237 cur_val_bit_idx += octet_bits_left
238 field_required_bits -= octet_bits_left
239 octet_bits_left = 0
240
241 return s[bf_total_byte_length:], this_val
242
243
244class LEBitFieldLenField(LEBitField):
245 __slots__ = ["length_of", "count_of", "adjust"]
246
247 def __init__(self, name, default, size, length_of=None, count_of=None, adjust=lambda pkt, x: x): # noqa: E501
248 LEBitField.__init__(self, name, default, size)
249 self.length_of = length_of
250 self.count_of = count_of
251 self.adjust = adjust
252
253 def i2m(self, pkt, x):
254 return FieldLenField.i2m(self, pkt, x)
255
256
257class LEBitEnumField(LEBitField, _EnumField):
258 __slots__ = EnumField.__slots__
259
260 def __init__(self, name, default, size, enum):
261 _EnumField.__init__(self, name, default, enum)
262 self.rev = size < 0
263 self.size = abs(size)
264
265
266################################################
267# DLPDU structure definitions (read/write PDUs)
268################################################
269
270ETHERCAT_TYPE_12_CIRCULATING_FRAME = {
271 0x00: 'FRAME-NOT-CIRCULATING',
272 0x01: 'FRAME-CIRCULATED-ONCE'
273}
274
275ETHERCAT_TYPE_12_NEXT_FRAME = {
276 0x00: 'LAST-TYPE12-PDU',
277 0x01: 'TYPE12-PDU-FOLLOWS'
278}
279
280
281class EtherCatType12DLPDU(Packet):
282 """
283 Type12 message base class
284 """
285 def post_build(self, pkt, pay):
286 """
287
288 set next attr automatically if not set explicitly by user
289
290 :param pkt: raw string containing the current layer
291 :param pay: raw string containing the payload
292 :return: <new current layer> + payload
293 """
294
295 data_len = len(self.data)
296 if data_len > 2047:
297 raise ValueError('payload size {} exceeds maximum length {} '
298 'of data size.'.format(data_len, 2047))
299
300 if self.next is not None:
301 has_next = True if self.next else False
302 else:
303 if pay:
304 has_next = True
305 else:
306 has_next = False
307
308 if has_next:
309 next_flag = bytearray([pkt[7] | 0b10000000])
310 else:
311 next_flag = bytearray([pkt[7] & 0b01111111])
312
313 return pkt[:7] + next_flag + pkt[8:] + pay
314
315 def guess_payload_class(self, payload):
316
317 try:
318 dlpdu_type = payload[0]
319 return EtherCat.ETHERCAT_TYPE12_DLPDU_TYPES[dlpdu_type]
320
321 except KeyError:
322 log_runtime.error(
323 '{}.guess_payload_class() - unknown or invalid '
324 'DLPDU type'.format(self.__class__.__name__))
325 return Packet.guess_payload_class(self, payload)
326
327 # structure templates lacking leading cmd-attribute
328 PHYSICAL_ADDRESSING_DESC = [
329 ByteField('idx', 0),
330 LEShortField('adp', 0),
331 LEShortField('ado', 0),
332 LEBitFieldLenField('len', None, 11, count_of='data'),
333 LEBitField('_reserved', 0, 3),
334 LEBitEnumField('c', 0, 1, ETHERCAT_TYPE_12_CIRCULATING_FRAME),
335 LEBitEnumField('next', None, 1, ETHERCAT_TYPE_12_NEXT_FRAME),
336 LEShortField('irq', 0),
337 FieldListField('data', [], ByteField('', 0x00),
338 count_from=lambda pkt: pkt.len),
339 LEShortField('wkc', 0)
340 ]
341
342 BROADCAST_ADDRESSING_DESC = PHYSICAL_ADDRESSING_DESC
343
344 LOGICAL_ADDRESSING_DESC = [
345 ByteField('idx', 0),
346 LEIntField('adr', 0),
347 LEBitFieldLenField('len', None, 11, count_of='data'),
348 LEBitField('_reserved', 0, 3),
349 LEBitEnumField('c', 0, 1, ETHERCAT_TYPE_12_CIRCULATING_FRAME),
350 LEBitEnumField('next', None, 1, ETHERCAT_TYPE_12_NEXT_FRAME),
351 LEShortField('irq', 0),
352 FieldListField('data', [], ByteField('', 0x00),
353 count_from=lambda pkt: pkt.len),
354 LEShortField('wkc', 0)
355 ]
356
357
358################
359# read messages
360################
361
362class EtherCatAPRD(EtherCatType12DLPDU):
363 """
364 APRD - Auto Increment Physical Read
365 (IEC 61158-5-12, sec. 5.4.1.2 tab. 14 / p. 32)
366 """
367
368 fields_desc = [ByteField('_cmd', 0x01)] + \
369 EtherCatType12DLPDU.PHYSICAL_ADDRESSING_DESC
370
371
372class EtherCatFPRD(EtherCatType12DLPDU):
373 """
374 FPRD - Configured address physical read
375 (IEC 61158-5-12, sec. 5.4.1.3 tab. 15 / p. 33)
376 """
377
378 fields_desc = [ByteField('_cmd', 0x04)] + \
379 EtherCatType12DLPDU.PHYSICAL_ADDRESSING_DESC
380
381
382class EtherCatBRD(EtherCatType12DLPDU):
383 """
384 BRD - Broadcast read
385 (IEC 61158-5-12, sec. 5.4.1.4 tab. 16 / p. 34)
386 """
387
388 fields_desc = [ByteField('_cmd', 0x07)] + \
389 EtherCatType12DLPDU.BROADCAST_ADDRESSING_DESC
390
391
392class EtherCatLRD(EtherCatType12DLPDU):
393 """
394 LRD - Logical read
395 (IEC 61158-5-12, sec. 5.4.1.5 tab. 17 / p. 36)
396 """
397
398 fields_desc = [ByteField('_cmd', 0x0a)] + \
399 EtherCatType12DLPDU.LOGICAL_ADDRESSING_DESC
400
401
402#################
403# write messages
404#################
405
406
407class EtherCatAPWR(EtherCatType12DLPDU):
408 """
409 APWR - Auto Increment Physical Write
410 (IEC 61158-5-12, sec. 5.4.2.2 tab. 18 / p. 37)
411 """
412
413 fields_desc = [ByteField('_cmd', 0x02)] + \
414 EtherCatType12DLPDU.PHYSICAL_ADDRESSING_DESC
415
416
417class EtherCatFPWR(EtherCatType12DLPDU):
418 """
419 FPWR - Configured address physical write
420 (IEC 61158-5-12, sec. 5.4.2.3 tab. 19 / p. 38)
421 """
422
423 fields_desc = [ByteField('_cmd', 0x05)] + \
424 EtherCatType12DLPDU.PHYSICAL_ADDRESSING_DESC
425
426
427class EtherCatBWR(EtherCatType12DLPDU):
428 """
429 BWR - Broadcast read (IEC 61158-5-12, sec. 5.4.2.4 tab. 20 / p. 39)
430 """
431
432 fields_desc = [ByteField('_cmd', 0x08)] + \
433 EtherCatType12DLPDU.BROADCAST_ADDRESSING_DESC
434
435
436class EtherCatLWR(EtherCatType12DLPDU):
437 """
438 LWR - Logical write
439 (IEC 61158-5-12, sec. 5.4.2.5 tab. 21 / p. 40)
440 """
441
442 fields_desc = [ByteField('_cmd', 0x0b)] + \
443 EtherCatType12DLPDU.LOGICAL_ADDRESSING_DESC
444
445
446######################
447# read/write messages
448######################
449
450
451class EtherCatAPRW(EtherCatType12DLPDU):
452 """
453 APRW - Auto Increment Physical Read Write
454 (IEC 61158-5-12, sec. 5.4.3.1 tab. 22 / p. 41)
455 """
456
457 fields_desc = [ByteField('_cmd', 0x03)] + \
458 EtherCatType12DLPDU.PHYSICAL_ADDRESSING_DESC
459
460
461class EtherCatFPRW(EtherCatType12DLPDU):
462 """
463 FPRW - Configured address physical read write
464 (IEC 61158-5-12, sec. 5.4.3.2 tab. 23 / p. 43)
465 """
466
467 fields_desc = [ByteField('_cmd', 0x06)] + \
468 EtherCatType12DLPDU.PHYSICAL_ADDRESSING_DESC
469
470
471class EtherCatBRW(EtherCatType12DLPDU):
472 """
473 BRW - Broadcast read write
474 (IEC 61158-5-12, sec. 5.4.3.3 tab. 24 / p. 39)
475 """
476
477 fields_desc = [ByteField('_cmd', 0x09)] + \
478 EtherCatType12DLPDU.BROADCAST_ADDRESSING_DESC
479
480
481class EtherCatLRW(EtherCatType12DLPDU):
482 """
483 LRW - Logical read write
484 (IEC 61158-5-12, sec. 5.4.3.4 tab. 25 / p. 45)
485 """
486
487 fields_desc = [ByteField('_cmd', 0x0c)] + \
488 EtherCatType12DLPDU.LOGICAL_ADDRESSING_DESC
489
490
491class EtherCatARMW(EtherCatType12DLPDU):
492 """
493 ARMW - Auto increment physical read multiple write
494 (IEC 61158-5-12, sec. 5.4.3.5 tab. 26 / p. 46)
495 """
496
497 fields_desc = [ByteField('_cmd', 0x0d)] + \
498 EtherCatType12DLPDU.PHYSICAL_ADDRESSING_DESC
499
500
501class EtherCatFRMW(EtherCatType12DLPDU):
502 """
503 FRMW - Configured address physical read multiple write
504 (IEC 61158-5-12, sec. 5.4.3.6 tab. 27 / p. 47)
505 """
506
507 fields_desc = [ByteField('_cmd', 0x0e)] + \
508 EtherCatType12DLPDU.PHYSICAL_ADDRESSING_DESC
509
510
511class EtherCat(Packet):
512 """
513 Common EtherCat header layer
514 """
515 ETHER_HEADER_LEN = 14
516 ETHER_FSC_LEN = 4
517 ETHER_FRAME_MIN_LEN = 64
518 ETHERCAT_HEADER_LEN = 2
519
520 FRAME_TYPES = {
521 0x01: 'TYPE-12-PDU',
522 0x04: 'NETWORK-VARIABLES',
523 0x05: 'MAILBOX'
524 }
525
526 fields_desc = [
527 LEBitField('length', 0, 11),
528 LEBitField('_reserved', 0, 1),
529 LEBitField('type', 0, 4),
530 ]
531
532 ETHERCAT_TYPE12_DLPDU_TYPES = {
533 0x01: EtherCatAPRD,
534 0x04: EtherCatFPRD,
535 0x07: EtherCatBRD,
536 0x0a: EtherCatLRD,
537 0x02: EtherCatAPWR,
538 0x05: EtherCatFPWR,
539 0x08: EtherCatBWR,
540 0x0b: EtherCatLWR,
541 0x03: EtherCatAPRW,
542 0x06: EtherCatFPRW,
543 0x09: EtherCatBRW,
544 0x0c: EtherCatLRW,
545 0x0d: EtherCatARMW,
546 0x0e: EtherCatFRMW
547 }
548
549 def post_build(self, pkt, pay):
550 """
551 need to set the length of the whole PDU manually
552 to avoid any bit fiddling use a dummy class to build the layer content
553
554 also add padding if frame is < 64 bytes
555
556 Note: padding only handles Ether/n*Dot1Q/EtherCat
557 (no special mumbo jumbo)
558
559 :param pkt: raw string containing the current layer
560 :param pay: raw string containing the payload
561 :return: <new current layer> + payload
562 """
563
564 class _EtherCatLengthCalc(Packet):
565 """
566 dummy class used to generate str representation easily
567 """
568 fields_desc = [
569 LEBitField('length', None, 11),
570 LEBitField('_reserved', 0, 1),
571 LEBitField('type', 0, 4),
572 ]
573
574 payload_len = len(pay)
575
576 # length field is 11 bit
577 if payload_len > 2047:
578 raise ValueError('payload size {} exceeds maximum length {} '
579 'of EtherCat message.'.format(payload_len, 2047))
580
581 self.length = payload_len
582
583 vlan_headers_total_size = 0
584 upper_layer = self.underlayer
585
586 # add size occupied by VLAN tags
587 while upper_layer and isinstance(upper_layer, Dot1Q):
588 vlan_headers_total_size += 4
589 upper_layer = upper_layer.underlayer
590
591 if not isinstance(upper_layer, Ether):
592 raise Exception('missing Ether layer')
593
594 pad_len = EtherCat.ETHER_FRAME_MIN_LEN - (EtherCat.ETHER_HEADER_LEN +
595 vlan_headers_total_size +
596 EtherCat.ETHERCAT_HEADER_LEN + # noqa: E501
597 payload_len +
598 EtherCat.ETHER_FSC_LEN)
599
600 if pad_len > 0:
601 pad = Padding()
602 pad.load = b'\x00' * pad_len
603
604 return raw(_EtherCatLengthCalc(length=self.length,
605 type=self.type)) + pay + raw(pad)
606 return raw(_EtherCatLengthCalc(length=self.length,
607 type=self.type)) + pay
608
609 def guess_payload_class(self, payload):
610 try:
611 dlpdu_type = payload[0]
612 return EtherCat.ETHERCAT_TYPE12_DLPDU_TYPES[dlpdu_type]
613 except KeyError:
614 log_runtime.error(
615 '{}.guess_payload_class() - unknown or invalid '
616 'DLPDU type'.format(self.__class__.__name__))
617 return Packet.guess_payload_class(self, payload)
618
619
620bind_layers(Ether, EtherCat, type=0x88a4)
621bind_layers(Dot1Q, EtherCat, type=0x88a4)
622
623# bindings for DLPDUs
624
625bind_layers(EtherCat, EtherCatAPRD, type=0x01)
626bind_layers(EtherCat, EtherCatFPRD, type=0x01)
627bind_layers(EtherCat, EtherCatBRD, type=0x01)
628bind_layers(EtherCat, EtherCatLRD, type=0x01)
629bind_layers(EtherCat, EtherCatAPWR, type=0x01)
630bind_layers(EtherCat, EtherCatFPWR, type=0x01)
631bind_layers(EtherCat, EtherCatBWR, type=0x01)
632bind_layers(EtherCat, EtherCatLWR, type=0x01)
633bind_layers(EtherCat, EtherCatAPRW, type=0x01)
634bind_layers(EtherCat, EtherCatFPRW, type=0x01)
635bind_layers(EtherCat, EtherCatBRW, type=0x01)
636bind_layers(EtherCat, EtherCatLRW, type=0x01)
637bind_layers(EtherCat, EtherCatARMW, type=0x01)
638bind_layers(EtherCat, EtherCatFRMW, type=0x01)