Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/scapy/contrib/ethercat.py: 66%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

219 statements  

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)