EtherNet / IP是为了在以太网中使用CIP协议而进行的封装.EtherNet / IP的CIP帧封装了命令,数据点和消息等信息.CIP帧包括CIP设备配置文件数据包的其余部分是以太网/ IP帧,CIP帧通过它们在以太网上传输。EIP一般使用TCP / UDP的44818端口运行,还有一个2222端口,这两个端口分别实现隐式消息传递和显示消息传递两种方式。客户端/服务器消息,而隐消息为I / O消息。
详细信息可参考下面文章
https://www.cnblogs.com/blacksunny/p/7202815.html
下面是Python 实现的代码,转载自https://github.com/paperwork/pyenip/blob/605ad6d026865e3378542d4428ec975e7c26d2e4/ethernetip.py
""" | |
Copyright (C) 2014 Sebastian Block | |
This program is free software; you can redistribute it and/or modify | |
it under the terms of the GNU General Public License as published by | |
the Free Software Foundation; either version 2 of the License. | |
This program is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License for more details. | |
You should have received a copy of the GNU General Public License along | |
with this program; if not, write to the Free Software Foundation, Inc., | |
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
""" | |
import random | |
import select | |
import socket | |
import struct | |
import threading | |
import time | |
from .dpkt import dpkt | |
ENIP_TCP_PORT = 44818 | |
ENIP_UDP_PORT = 2222 | |
#/* Common Services */ | |
CI_SRV_GET_ALL = 0x01 | |
CI_SRV_SET_ATTR_ALL = 0x02 | |
CI_SRV_GET_ATTR_LIST = 0x03 | |
CI_SRV_SET_ATTR_LIST = 0x04 | |
CI_SRV_RESET = 0x05 | |
CI_SRV_START = 0x06 | |
CI_SRV_STOP = 0x07 | |
CI_SRV_CREATE = 0x08 | |
CI_SRV_DELETE = 0x09 | |
CI_SRV_MULTIPLE_SRV = 0x0A | |
CI_SRV_APPLY_ATTR = 0x0D | |
CI_SRV_GET_ATTR_SINGLE = 0x0E | |
CI_SRV_SET_ATTR_SINGLE = 0x10 | |
CI_SRV_FIND_NEXT_OBJ = 0x11 | |
CI_SRV_RESTORE = 0x15 | |
CI_SRV_SAVE = 0x16 | |
CI_SRV_NOP = 0x17 | |
CI_SRV_GET_MEMBER = 0x18 | |
CI_SRV_SET_MEMBER = 0x19 | |
CI_SRV_INSERT_MEMBER = 0x1A | |
CI_SRV_REMOVE_MEMBER = 0x1B | |
CI_SRV_GROUP_SYNC = 0x1C | |
CI_SRV_FORWARD_CLOSE = 0x4E | |
CI_SRV_UNCONN_SEND = 0x52 | |
CI_SRV_FORWARD_OPEN = 0x54 | |
#/* List of Objects */ | |
CIP_OBJ_IDENTITY = 0x01 | |
CIP_OBJ_MESSAGE_ROUTER = 0x02 | |
CIP_OBJ_ASSEMBLY = 0x04 | |
CIP_OBJ_CONNECTION = 0x05 | |
CIP_OBJ_CONNMANAGER = 0x06 | |
CIP_OBJ_DLR = 0x47 | |
CIP_OBJ_QOS = 0x48 | |
CIP_OBJ_BASE_SWITCH = 0x51 | |
CIP_OBJ_SNMP = 0x52 | |
CIP_OBJ_POWER_MANAGEM = 0x53 | |
CIP_OBJ_RSTP_BRIDGE = 0x54 | |
CIP_OBJ_RSTP_PORT = 0x55 | |
CIP_OBJ_PRP = 0x56 | |
CIP_OBJ_PRP_NODE_TABLE = 0x57 | |
CIP_OBJ_CONN_CONF = 0xF3 | |
CIP_OBJ_PORT = 0xF4 | |
CIP_OBJ_TCPIP = 0xF5 | |
CIP_OBJ_ETHERNET_LINK = 0xF6 | |
#/* The following are CIP (Ethernet/IP) Generic error codes */ | |
CIP_ROUTER_ERROR_SUCCESS = 0x00 # We done good… | |
CIP_ROUTER_ERROR_FAILURE = 0x01 # Connection failure | |
CIP_ROUTER_ERROR_NO_RESOURCE = 0x02 # Resource(s) unavailable | |
CIP_ROUTER_ERROR_INVALID_PARAMETER_VALUE = 0x03 # Obj specific data bad | |
CIP_ROUTER_ERROR_INVALID_SEG_TYPE = 0x04 # Invalid segment type in path | |
CIP_ROUTER_ERROR_INVALID_DESTINATION = 0x05 # Invalid segment value in path | |
CIP_ROUTER_ERROR_PARTIAL_DATA = 0x06 # Not all expected data sent | |
CIP_ROUTER_ERROR_CONN_LOST = 0x07 # Messaging connection lost | |
CIP_ROUTER_ERROR_BAD_SERVICE = 0x08 # Unimplemented service code | |
CIP_ROUTER_ERROR_BAD_ATTR_DATA = 0x09 # Bad attribute data value | |
CIP_ROUTER_ERROR_ATTR_LIST_ERROR = 0x0A # Get/set attr list failed | |
CIP_ROUTER_ERROR_ALREADY_IN_REQUESTED_MODE = 0x0B # Obj already in requested mode | |
CIP_ROUTER_ERROR_OBJECT_STATE_CONFLICT = 0x0C # Obj not in proper mode | |
CIP_ROUTER_ERROR_OBJ_ALREADY_EXISTS = 0x0D # Object already created | |
CIP_ROUTER_ERROR_ATTR_NOT_SETTABLE = 0x0E # Set of get only attr tried | |
CIP_ROUTER_ERROR_PERMISSION_DENIED = 0x0F # Insufficient access permission | |
CIP_ROUTER_ERROR_DEV_IN_WRONG_STATE = 0x10 # Device not in proper mode | |
CIP_ROUTER_ERROR_REPLY_DATA_TOO_LARGE = 0x11 # Response packet too large | |
CIP_ROUTER_ERROR_FRAGMENT_PRIMITIVE = 0x12 # Primitive value will fragment | |
CIP_ROUTER_ERROR_NOT_ENOUGH_DATA = 0x13 # Goldilocks complaint #1 | |
CIP_ROUTER_ERROR_ATTR_NOT_SUPPORTED = 0x14 # Attribute is undefined | |
CIP_ROUTER_ERROR_TOO_MUCH_DATA = 0x15 # Goldilocks complaint #2 | |
CIP_ROUTER_ERROR_OBJ_DOES_NOT_EXIST = 0x16 # Non-existant object specified | |
CIP_ROUTER_ERROR_NO_FRAGMENTATION = 0x17 # Fragmentation not active | |
CIP_ROUTER_ERROR_DATA_NOT_SAVED = 0x18 # Attr data not previously saved | |
CIP_ROUTER_ERROR_DATA_WRITE_FAILURE = 0x19 # Attr data not saved this time | |
CIP_ROUTER_ERROR_REQUEST_TOO_LARGE = 0x1A # Routing failure on request | |
CIP_ROUTER_ERROR_RESPONSE_TOO_LARGE = 0x1B # Routing failure on response | |
CIP_ROUTER_ERROR_MISSING_LIST_DATA = 0x1C # Attr data not found in list | |
CIP_ROUTER_ERROR_INVALID_LIST_STATUS = 0x1D # Returned list of attr w/status | |
CIP_ROUTER_ERROR_SERVICE_ERROR = 0x1E # Embedded service failed | |
CIP_ROUTER_ERROR_VENDOR_SPECIFIC = 0x1F # Vendor specific error | |
CIP_ROUTER_ERROR_INVALID_PARAMETER = 0x20 # Invalid parameter | |
CIP_ROUTER_ERROR_WRITE_ONCE_FAILURE = 0x21 # Write once previously done | |
CIP_ROUTER_ERROR_INVALID_REPLY = 0x22 # Invalid reply received | |
CIP_ROUTER_ERROR_BAD_KEY_IN_PATH = 0x25 # Electronic key in path failed | |
CIP_ROUTER_ERROR_BAD_PATH_SIZE = 0x26 # Invalid path size | |
CIP_ROUTER_ERROR_UNEXPECTED_ATTR = 0x27 # Cannot set attr at this time | |
CIP_ROUTER_ERROR_INVALID_MEMBER = 0x28 # Member ID in list nonexistant | |
CIP_ROUTER_ERROR_MEMBER_NOT_SETTABLE = 0x29 # Cannot set value of member | |
CIP_ROUTER_ERROR_UNKNOWN_MODBUS_ERROR = 0x2B # Unhandled Modbus Error | |
CIP_ROUTER_ERROR_STILL_PROCESSING = 0xFF # Special marker to indicate we haven't finished processing the request yet | |
# Extended status in Forward open response | |
CIP_FWD_OPEN_EXTENDED_STATUS_INVALID_CONFIGURATION_SIZE = 0x126 | |
class EncapsulationPacket(dpkt.Packet): | |
# commands | |
ENCAP_CMD_NOP = 0x0000 | |
ENCAP_CMD_LISTSERVICES = 0x0004 | |
ENCAP_CMD_LISTIDENTITY = 0x0063 | |
ENCAP_CMD_LISTINTERFACES = 0x0064 | |
ENCAP_CMD_REGISTERSESSION = 0x0065 | |
ENCAP_CMD_UNREGISTERSESSION = 0x0066 | |
ENCAP_CMD_SENDRRDATA = 0x006F | |
ENCAP_CMD_SENDUNITDATA = 0x0070 | |
ENCAP_CMD_INDICATESTATUS = 0x0072 | |
ENCAP_CMD_CANCEL = 0x0073 | |
# status | |
ENCAP_STATUS_SUCCESS = 0x0000 | |
ENCAP_STATUS_INVALID_CMD = 0x0001 | |
ENCAP_STATUS_OUT_OF_MEMORY = 0x0002 | |
ENCAP_STATUS_INCORRECT_DATA = 0x0003 | |
ENCAP_STATUS_INVALID_LENGTH = 0x0065 | |
ENCAP_STATUS_UNSUPPORTED_VERSION = 0x0069 | |
__byte_order__ = '<' | |
__hdr__ = (('command', 'H', 0), | |
('length', 'H', 0), | |
('session', 'I', 0), | |
('status', 'I', 0), | |
('sender_context', '8s', bytes([0,0,0,0,0,0,0,0])), | |
('options', 'I', 0)) | |
class CommandSpecificData(dpkt.Packet): | |
# type ID | |
TYPE_ID_NULL = 0x0000 | |
TYPE_ID_LIST_IDENT_RESPONSE = 0x000C | |
TYPE_ID_CONNECTION_BASED = 0x00A1 | |
TYPE_ID_CONNECTED_TRANSPORT_PACKET = 0x00B1 | |
TYPE_ID_UNCONNECTED_MESSAGE = 0x00B2 | |
TYPE_ID_LISTSERVICES_RESPONSE = 0x0100 | |
TYPE_ID_SOCKADDR_INFO_ORIG_TARGET = 0x8000 | |
TYPE_ID_SOCKADDR_INFO_TARGET_ORIG = 0x8001 | |
TYPE_ID_SEQUENCED_ADDRESS = 0x8002 | |
__byte_order__ = '<' | |
__hdr__ = (('item_count', 'H', 0), | |
('type_id', 'H', 0), | |
('length', 'H', 0)) | |
class UnconnectedDataItem(dpkt.Packet): | |
UNCONN_DATA_ITEM_SERVICE_REQUEST = 0x00 | |
UNCONN_DATA_ITEM_SERVICE_RESPONSE = 0x80 | |
__byte_order__ = '<' | |
__hdr__ = (('type_id', 'H', 0), | |
('length', 'H', 0), | |
('service', 'B', 0)) | |
class UnconnectedDataItemHdr(dpkt.Packet): | |
__byte_order__ = '<' | |
__hdr__ = (('type_id', 'H', 0), | |
('length', 'H', 0)) | |
class UnconnectedDataItemResp(dpkt.Packet): | |
__byte_order__ = '<' | |
__hdr__ = (('type_id', 'H', 0), | |
('length', 'H', 0), | |
('service', 'B', 0), | |
('resv', 'B', 0), | |
('status', 'B', 0), | |
('additional_status_size', 'B', 0)) | |
class ForwardOpenReq(dpkt.Packet): | |
# network connection parameter bit offsets | |
FORWARD_OPEN_CONN_PARAM_BIT_CONN_SIZE = 0 | |
FORWARD_OPEN_CONN_PARAM_BIT_FIXED_VAR = 9 | |
FORWARD_OPEN_CONN_PARAM_BIT_PRIORITY = 10 | |
FORWARD_OPEN_CONN_PARAM_BIT_CONN_TYPE = 13 | |
FORWARD_OPEN_CONN_PARAM_BIT_REDAN_OWN = 15 | |
__byte_order__ = '<' | |
__hdr__ = (('mkpath', '5s', b"00000"), # len, 2 path | |
('prio_tick', 'B', 0x0A), # 4 Bit prio, 4 Bit tick time | |
('timeout_ticks', 'B', 0xF0), | |
('otconnid', 'I', 0xdeadbeaf), | |
('toconnid', 'I', 0xaffedead), | |
('conn_serial', 'H', 0x4949), | |
('vendor', 'H', 1), | |
('orig_serial', 'I', 0xbeeff00d), | |
('multiplier', 'B', 1), | |
('reserved', '3s', b"000"), | |
('otrpi', 'I', 0x186a0), # 100 ms | |
('otparams', 'H', 0x480c), | |
('torpi', 'I', 0x186A0), # 100 ms | |
('toparams', 'H', 0x2808), | |
('type_trigger', 'B', 0x01), | |
('plen', 'B', 9)) | |
class ForwardOpenResp(dpkt.Packet): | |
__byte_order__ = '<' | |
__hdr__ = (('reserved', '3s', ''), | |
('otconnid', 'I', 0), | |
('toconnid', 'I', 0), | |
('conn_serial', 'H', 0), | |
('vendor', 'H', 0), | |
('orig_serial', 'I', 0), | |
('otapi', 'I', 0), | |
('toapi', 'I', 0), | |
('appl_reply_size', 'B', 0), | |
('reserved2', 'B', 0) | |
) | |
class ForwardCloseReq(dpkt.Packet): | |
__byte_order__ = '<' | |
__hdr__ = (('mkpath', '5s', b"00000"), # len, 2 path | |
('prio_tick', 'B', 0x0A), # 4 Bit prio, 4 Bit tick time | |
('timeout_ticks', 'B', 0xF0), | |
('conn_serial', 'H', 0x4949), | |
('vendor', 'H', 1), | |
('orig_serial', 'I', 0xbeeff00d), | |
('plen', 'B', 9), | |
('reserved', 'B', 0) | |
) | |
class ForwardCloseResp(dpkt.Packet): | |
__byte_order__ = '<' | |
__hdr__ = (('reserved', '3s', ''), | |
('conn_serial', 'H', 0), | |
('vendor', 'H', 0), | |
('orig_serial', 'I', 0), | |
('appl_reply_size', 'B', 0), | |
('reserved2', 'B', 0) | |
) | |
class RegisterSessionPacket(dpkt.Packet): | |
__byte_order__ = '<' | |
__hdr__ = (('protocol_version', 'H', 1), | |
('option_flags', 'H', 0)) | |
class ListServicesReply(dpkt.Packet): | |
# capcability flags | |
CAP_CIP_ENCAP_VIA_TCP = 0x0020 | |
CAP_CIP_VIA_UDP = 0x0100 | |
__byte_order__ = '<' | |
__hdr__ = (('version', 'H', 0), | |
('capability_flags', 'H', 0), | |
('name_of_service', '16s', '')) | |
class ListIdentifyReply(dpkt.Packet): | |
# status is a bit encoded word | |
LIST_IDENT_STATUS_OWNED = 0x0001 | |
LIST_IDENT_STATUS_CONFIGURED = 0x0004 | |
LIST_IDENT_STATUS_EXTENDED_DEVICE_STATUS = 0x00F0 | |
LIST_IDENT_STATUS_MINOR_RECOVERABLE_FAULT = 0x0100 | |
LIST_IDENT_STATUS_MINOR_UNRECOVERABLE_FAULT = 0x0200 | |
LIST_IDENT_STATUS_MAJOR_RECOVERABLE_FAULT = 0x0400 | |
LIST_IDENT_STATUS_MAJOR_UNRECOVERABLE_FAULT = 0x0800 | |
LIST_IDENT_STATUS_EXTENDED_DEVICE_STATUS2 = 0xF000 | |
# states | |
LIST_IDENT_STATE_NONEXISTENT = 0x00 | |
LIST_IDENT_STATE_SELF_TESTING = 0x01 | |
LIST_IDENT_STATE_STANDBY = 0x02 | |
LIST_IDENT_STATE_OPERATIONAL = 0x03 | |
LIST_IDENT_STATE_RECOVERABLE_FAULT = 0x04 | |
LIST_IDENT_STATE_UNRECOVERABLE_FAULT = 0x05 | |
LIST_IDENT_STATE_DEFAULT = 0xFF | |
__byte_order__ = '<' | |
__hdr__ = (('version', 'H', 1), | |
('socket_addr', '16s', ''), | |
('vendor_id', 'H', 0), | |
('device_type', 'H', 0), | |
('product_code', 'H', 0), | |
('revision_major', 'B', 0), | |
('revision_minor', 'B', 0), | |
('status', 'H', 0), | |
('serial_no', 'I', 0), | |
('product_name_length', 'B', 0), | |
('product_name', '0s', ''), | |
('state', 'B', 0)) | |
def unpack(self, buf): | |
# product name can be a string upto 32 chars, but python does not | |
# support variable string length, so we have to write a little | |
# work-a-round and first unpack it check the length and calculate it | |
# again | |
tmp = dpkt.Packet.unpack(self, buf) | |
self.__hdr_fmt__ = "<H16sHHHBBHIB" + str(self.product_name_length) + "sB" | |
self.__hdr_len__ = struct.calcsize(self.__hdr_fmt__) | |
dpkt.Packet.unpack(self, buf) | |
class SendRRPacket(dpkt.Packet): | |
__byte_order__ = '<' | |
__hdr__ = (('interface_handle', 'I', 0), | |
('timeout', 'H', 10)) | |
class SocketAddressInfo(dpkt.Packet): | |
__byte_order__ = '>' # big endian | |
__hdr__ = (('sin_family', 'H', 0), | |
('sin_port', 'H', 0), | |
('sin_addr', 'I', 0), | |
('sin_zero', '8s', '')) | |
class UdpSendDataPacket(dpkt.Packet): | |
__byte_order__ = '<' | |
__hdr__ = (('count', 'H', 2), | |
('type_id_seq_addr', 'H', 0x8002), | |
('len_seq_addr', 'H', 8), | |
('conn_id', 'I', 0), | |
('seq_num', 'I', 0), | |
('type_id_conn_data', 'H', 0x00b1), | |
('len_conn_data', 'H', 12), | |
('seq_count', 'H', 0)) | |
class UdpRecvDataPacket(dpkt.Packet): | |
__byte_order__ = '<' | |
__hdr__ = (('count', 'H', 2), | |
('type_id_seq_addr', 'H', 0x8002), | |
('len_seq_addr', 'H', 8), | |
('conn_id', 'I', 0), | |
('seq_num', 'I', 0), | |
('type_id_conn_data', 'H', 0x00b1), | |
('length', 'H', 12), | |
('unknown', 'H', 0)) # TODO check for what the first two bytes of data are | |
class EthernetIOThread(threading.Thread): | |
def __init__(self, typ, enip=None, conn=None): | |
self.typ = typ | |
self.enip = enip | |
self.conn = conn | |
threading.Thread.__init__(self) | |
def run(self): | |
if self.typ == 1: | |
self.enip.listenUDP() | |
elif self.typ == 2: | |
self.conn.prodThread() | |
class EtherNetIP(object): | |
ENIP_IO_TYPE_INPUT = 0 | |
ENIP_IO_TYPE_OUTPUT = 1 | |
def __init__(self, ip="127.0.0.1"): | |
self.assembly = {} | |
self.explicit = [] | |
self.udpsock = None | |
self.udpthread = None | |
self.io_state = 0 | |
self.ip = ip | |
def registerAssembly(self, iotype, size, inst, conn): | |
if inst in self.assembly: | |
print("Reg assembly failed for iotype=", iotype) | |
return None | |
bits = [] | |
for i in range(size*8): | |
bits.append(0) | |
self.assembly[inst] = (conn, iotype, bits) | |
if conn != None: | |
if iotype == EtherNetIP.ENIP_IO_TYPE_INPUT: | |
conn.mapIn(bits) | |
elif iotype == EtherNetIP.ENIP_IO_TYPE_OUTPUT: | |
conn.mapOut(bits) | |
return bits | |
def startIO(self): | |
if self.io_state == 0: | |
self.udpsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
self.udpsock.bind(("0.0.0.0", ENIP_UDP_PORT)) | |
self.udpthread = EthernetIOThread(1,self) | |
self.io_state = 1 | |
self.udpthread.start() | |
def stopIO(self): | |
if self.io_state == 1: | |
self.io_state = 0 | |
self.udpsock.close() | |
def listenUDP(self): | |
while 1 == self.io_state: | |
inp, out, err = select.select([self.udpsock], [], [], 2) | |
if len(inp) != 0: | |
try: | |
buf, addr = self.udpsock.recvfrom(1024) | |
except OSError: | |
# If we close the socket asynchronously, the recv will | |
# fail | |
if self.io_state == 0: | |
return | |
raise | |
addr = addr[0] | |
pkt = UdpRecvDataPacket(buf) | |
for inst in self.assembly: | |
conn = self.assembly[inst][0] | |
iotype = self.assembly[inst][1] | |
bits = self.assembly[inst][2] | |
# update i/o | |
if conn.ipaddr == addr and iotype == EtherNetIP.ENIP_IO_TYPE_INPUT and pkt.conn_id == conn.toconnid: | |
i = 0 | |
for byte in pkt.data: | |
for s in range(8): | |
if byte & (1 << s): | |
bits[i] = True | |
else: | |
bits[i] = False | |
i += 1 | |
def explicit_conn(self, ipaddr=None): | |
if ipaddr is None: | |
ipaddr = self.ip | |
exp = EtherNetIPExpConnection(ipaddr) | |
self.explicit.append(exp) | |
return exp | |
def listIDUDP(self, ipaddr=None, timeout=5): | |
udpsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
udpsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
context = random.randint(1, 4026531839) | |
pkt = EncapsulationPacket(command=EncapsulationPacket.ENCAP_CMD_LISTIDENTITY, | |
sender_context=context.to_bytes(8, byteorder='big')) | |
if ipaddr is None: | |
ipaddr = self.ip | |
udpsock.sendto(pkt.pack(),(ipaddr, ENIP_TCP_PORT)) | |
inp, out, err = select.select([udpsock], [], [], timeout) | |
if len(inp) != 0: | |
data = udpsock.recv(1024) | |
pkt = EncapsulationPacket() | |
pkt.unpack(data) | |
if pkt.status == EncapsulationPacket.ENCAP_STATUS_SUCCESS and pkt.command == EncapsulationPacket.ENCAP_CMD_LISTIDENTITY: | |
csd = CommandSpecificData(pkt.data) | |
if csd.type_id == CommandSpecificData.TYPE_ID_LIST_IDENT_RESPONSE: | |
lid = ListIdentifyReply(csd.data) | |
return lid | |
return None | |
class EtherNetIPSocket(object): | |
def __init__(self, ip): | |
self.ipaddr = ip | |
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
self.sock.connect((self.ipaddr, ENIP_TCP_PORT)) | |
self.conn_serial_num = 0 | |
def delete(self): | |
self.sock.close() | |
def mkReqPath(self, clas, inst, attr): | |
if clas > 255: | |
clas_data = struct.pack("BBH", 0x21, 0, 0x300) | |
else: | |
clas_data = struct.pack("BB", 0x20, clas) | |
inst_data = struct.pack("BB", 0x24, inst) | |
attr_data = b'' | |
if attr != None: | |
attr_data = struct.pack("BB", 0x30, attr) | |
data = bytes([(int((len(clas_data) + len(inst_data) + len(attr_data))/2))]) | |
data += clas_data | |
data += inst_data | |
data += attr_data | |
return data | |
def scanNetwork(self, broadcastAddress="255.255.255.0", timeout=10): | |
import time | |
listOfNodes = [] | |
udpsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
#udpsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
udpsock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) | |
context = random.randint(1, 4026531839) | |
pkt = EncapsulationPacket(command=EncapsulationPacket.ENCAP_CMD_LISTIDENTITY, | |
sender_context=context.to_bytes(8, byteorder='big')) | |
udpsock.sendto(pkt.pack(),(broadcastAddress, ENIP_TCP_PORT)) | |
tStart = time.time() | |
while time.time() < (tStart+timeout): | |
timeout = tStart + timeout – time.time() | |
inp, out, err = select.select([udpsock], [], [], timeout) | |
if len(inp) != 0: | |
data = udpsock.recv(1024) | |
pkt = EncapsulationPacket() | |
pkt.unpack(data) | |
if pkt.status == EncapsulationPacket.ENCAP_STATUS_SUCCESS and pkt.command == EncapsulationPacket.ENCAP_CMD_LISTIDENTITY: | |
csd = CommandSpecificData(pkt.data) | |
if csd.type_id == CommandSpecificData.TYPE_ID_LIST_IDENT_RESPONSE: | |
lid = ListIdentifyReply(csd.data) | |
listOfNodes.append(lid) | |
return listOfNodes | |
def listID(self): | |
context = random.randint(1, 4026531839) | |
pkt = EncapsulationPacket(command=EncapsulationPacket.ENCAP_CMD_LISTIDENTITY, | |
sender_context=context.to_bytes(8, byteorder='big')) | |
self.sock.send(pkt.pack()) | |
inp, out, err = select.select([self.sock], [], [], 10) | |
if len(inp) != 0: | |
data = self.sock.recv(1024) | |
pkt = EncapsulationPacket() | |
pkt.unpack(data) | |
if pkt.status == EncapsulationPacket.ENCAP_STATUS_SUCCESS and pkt.command == EncapsulationPacket.ENCAP_CMD_LISTIDENTITY: | |
csd = CommandSpecificData(pkt.data) | |
if csd.type_id == CommandSpecificData.TYPE_ID_LIST_IDENT_RESPONSE: | |
lid = ListIdentifyReply(csd.data) | |
return lid | |
return None | |
def listServices(self): | |
context = random.randint(1,4026531839) | |
pkt = EncapsulationPacket(command=EncapsulationPacket.ENCAP_CMD_LISTSERVICES, | |
sender_context=context.to_bytes(8, byteorder='big')) | |
self.sock.send(pkt.pack()) | |
inp, out, err = select.select([self.sock], [], [], 10) | |
if len(inp) != 0: | |
data = self.sock.recv(1024) | |
pkt = EncapsulationPacket() | |
pkt.unpack(data) | |
if pkt.status == EncapsulationPacket.ENCAP_STATUS_SUCCESS and pkt.command == EncapsulationPacket.ENCAP_CMD_LISTSERVICES: | |
csd = CommandSpecificData(pkt.data) | |
if csd.type_id == CommandSpecificData.TYPE_ID_LISTSERVICES_RESPONSE: | |
lsr = ListServicesReply(csd.data) | |
return lsr | |
return None | |
class EtherNetIPSession(EtherNetIPSocket): | |
def __init__(self, ipaddr): | |
EtherNetIPSocket.__init__(self,ipaddr) | |
self.session = 0 | |
def delete(self): | |
self.session = 0 | |
def registerSession(self): | |
context = random.randint(1,4026531839) | |
csd = RegisterSessionPacket(protocol_version=1, option_flag=0) | |
pkt = EncapsulationPacket(command=EncapsulationPacket.ENCAP_CMD_REGISTERSESSION,\ | |
length=len(csd), sender_context=context.to_bytes(8, byteorder='big'), data=csd) | |
self.sock.send(pkt.pack()) | |
inp, out, err = select.select([self.sock], [], [], 10) | |
if len(inp) != 0: | |
data = self.sock.recv(1024) | |
pkt = EncapsulationPacket() | |
pkt.unpack(data) | |
if pkt.status == EncapsulationPacket.ENCAP_STATUS_SUCCESS and pkt.command == EncapsulationPacket.ENCAP_CMD_REGISTERSESSION: | |
self.session = pkt.session | |
return 0 | |
return None | |
def unregisterSession(self): | |
context = random.randint(1,4026531839) | |
pkt = EncapsulationPacket(command=EncapsulationPacket.ENCAP_CMD_UNREGISTERSESSION,\ | |
length=0, session=self.session, sender_context=context.to_bytes(8, byteorder='big'), data=b'') | |
self.sock.send(pkt.pack()) | |
self.session=0 | |
def sendEncap(self, command, data): | |
context = random.randint(1,4026531839) | |
pkt = EncapsulationPacket(command=command, length=len(data), sender_context=context.to_bytes(8, byteorder='big'), data=data) | |
return self.sock.send(pkt.pack()) | |
def unconnSend(self, service, data, context=0, chk=0, chkdata="\x00"): | |
sz = len(data) + 17 | |
# add service field | |
dsz = len(data) + 1 | |
cpf2 = \ | |
UnconnectedDataItem(type_id=CommandSpecificData.TYPE_ID_UNCONNECTED_MESSAGE, \ | |
length=dsz, data=data, \ | |
service=(service|UnconnectedDataItem.UNCONN_DATA_ITEM_SERVICE_REQUEST)) | |
cpf = CommandSpecificData(type_id=CommandSpecificData.TYPE_ID_NULL, \ | |
item_count=2, length=0, data=cpf2) | |
srr = SendRRPacket(interface_handle=0, timeout=10, data=cpf); | |
pkt = EncapsulationPacket(command=EncapsulationPacket.ENCAP_CMD_SENDRRDATA, \ | |
length=len(srr),session=self.session, sender_context=context.to_bytes(8, byteorder='big'), data=srr) | |
self.sock.send(pkt.pack()) | |
inp, out, err = select.select([self.sock], [], [], 10) | |
if len(inp) != 0: | |
data = self.sock.recv(4096) | |
if len(data) > 0: | |
pkt = EncapsulationPacket() | |
pkt.unpack(data) | |
if pkt.status == EncapsulationPacket.ENCAP_STATUS_SUCCESS and \ | |
pkt.command == EncapsulationPacket.ENCAP_CMD_SENDRRDATA: | |
srr = SendRRPacket(pkt.data) | |
csd = CommandSpecificData(srr.data) | |
cpf = UnconnectedDataItemResp(csd.data) | |
ret = [cpf.status, cpf.data] | |
if chk != 0: | |
rsppkt = self.unconnSendValidRsp(service, chkdata, context) | |
if str(rsppkt) != str(pkt): | |
print("Packets differ") | |
assert(0) | |
return ret | |
return None | |
def unconnSendValidRsp(self, service, data, context=0): | |
sz = len(data) + 17 | |
dsz = len(data) + 1 | |
cpf2 = \ | |
UnconnectedDataItem(type_id=CommandSpecificData.TYPE_ID_UNCONNECTED_MESSAGE, \ | |
length=dsz, data=data, \ | |
service=(service|UnconnectedDataItem.UNCONN_DATA_ITEM_SERVICE_RESPONSE)) | |
cpf = CommandSpecificData(type_id=CommandSpecificData.TYPE_ID_NULL, length=0, data=cpf2) | |
srr = SendRRPacket(interface_handle=0, timeout=10, data=cpf); | |
pkt = EncapsulationPacket(command=EncapsulationPacket.ENCAP_CMD_SENDRRDATA, \ | |
length=len(srr),session=self.session, sender_context=context.to_bytes(8, 'big'), data=srr) | |
return pkt | |
def getAttrSingle(self, clas, inst, attr, data=b'', chk=0, chkdata="\x00", service=CI_SRV_GET_ATTR_SINGLE): | |
path = self.mkReqPath(clas, inst, attr) | |
return self.unconnSend(service, path+data, \ | |
random.randint(1,4026531839), chk, chkdata) | |
def getAttrAll(self, clas, inst, data=b'', chk=0, chkdata="\x00"): | |
path = self.mkReqPath(clas, inst, None) | |
return self.unconnSend(CI_SRV_GET_ALL, path+data, \ | |
random.randint(1,4026531839), chk, chkdata) | |
def setAttrSingle(self, clas, inst, attr, data): | |
if str == type(data): | |
# string values need a special attention (add length before and a padding byte behind) | |
data_len = len(data) | |
data = struct.pack("BB", data_len, 0) + data.encode() | |
if data_len&1: | |
data += b"\x00" | |
path = self.mkReqPath(clas, inst, attr) | |
return self.unconnSend(CI_SRV_SET_ATTR_SINGLE, path+data, \ | |
random.randint(1,4026531839), 0, "") | |
def setAttrAll(self, clas, inst, data): | |
if str == type(data): | |
# string values need a special attention (add length before and a padding byte behind) | |
data_len = len(data) | |
data = struct.pack("BB", data_len, 0) + data.encode() | |
if data_len&1: | |
data += b"\x00" | |
path = self.mkReqPath(clas, inst, None) | |
return self.unconnSend(CI_SRV_SET_ATTR_ALL, path+data, \ | |
random.randint(1,4026531839), 0, "") | |
def resetService(self, inst=1, resetType=0): | |
path = self.mkReqPath(CIP_OBJ_IDENTITY, inst, attr=None) | |
data = struct.pack("B", resetType) | |
return self.unconnSend(CI_SRV_RESET, path+data, random.randint(1,4026531839), 0, "") | |
class EtherNetIPExpConnection(EtherNetIPSession): | |
def __init__(self, ipaddr): | |
EtherNetIPSession.__init__(self, ipaddr) | |
self.inAssem = None | |
self.outAssem = None | |
self.otconnid = 0 | |
self.toconnid = 0 | |
self.otapi = 100 | |
self.toapi = 100 | |
self.prod_state = 0 | |
self.prod_thread = None | |
self.prodsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
self.seqnum = 0 | |
def mapIn(self, inAssem): | |
self.inAssem = inAssem | |
def mapOut(self, outAssem): | |
self.outAssem = outAssem | |
def sendFwdOpenReq(self, inputinst, outputinst, configinst, multiplier=1, \ | |
torpi=1000, otrpi=1000, multicast=False, inputsz=None, \ | |
outputsz=None, fwdo=None, configData=None): | |
rand = random.randint(1, 0xffff) + 0xE4190000 | |
torpi *= 1000 | |
otrpi *= 1000 | |
if None == inputsz: | |
if None != self.inAssem: | |
inputsz = len(self.inAssem) / 8 | |
else: | |
inputsz = 8 | |
if None == outputsz: | |
if None != self.outAssem: | |
outputsz = len(self.outAssem) / 8 | |
else: | |
outputsz = 8 | |
outputsz += 6 # seq num and run/idle header | |
inputsz += 2 # seq num | |
if multicast == False: | |
mcast = 2 # p2p | |
else: | |
mcast = 1 | |
path = struct.pack(">I",0x34040000) + struct.pack("I",0) + \ | |
struct.pack(">I",0x00002004) + struct.pack("B",0x24) + struct.pack("B",configinst) + \ | |
struct.pack("B",0x2c) + struct.pack("B",outputinst) + struct.pack("B",0x2c) + \ | |
struct.pack("B",inputinst) | |
plen = int(len(path) / 2) | |
if configData != None: | |
# 0x80 = simple data segment, with length in words => max 512 bytes of data | |
if len(configData) > 512: | |
return 1 | |
path += struct.pack("BB", 0x80, int(len(configData)/2) ) | |
path += configData | |
plen = int(len(path) / 2) | |
if fwdo == None: | |
self.conn_serial_num += 1 | |
fwdo = ForwardOpenReq( \ | |
otconnid=rand, toconnid=rand-1, \ | |
conn_serial=self.conn_serial_num, \ | |
multiplier=multiplier, \ | |
mkpath=self.mkReqPath(clas=0x06,inst=0x01,attr=None), \ | |
torpi=torpi, \ | |
otrpi=otrpi, \ | |
toparams=(int(inputsz)| \ | |
(0x2<<ForwardOpenReq.FORWARD_OPEN_CONN_PARAM_BIT_PRIORITY) | \ | |
(mcast<<ForwardOpenReq.FORWARD_OPEN_CONN_PARAM_BIT_CONN_TYPE) \ | |
), \ | |
otparams=(int(outputsz) | \ | |
0x2<<ForwardOpenReq.FORWARD_OPEN_CONN_PARAM_BIT_PRIORITY | \ | |
0x2<<ForwardOpenReq.FORWARD_OPEN_CONN_PARAM_BIT_CONN_TYPE \ | |
), \ | |
plen=plen, \ | |
data=path \ | |
) | |
# add service field | |
dsz = len(fwdo) + 1 | |
cpf2 = \ | |
UnconnectedDataItem(type_id=CommandSpecificData.TYPE_ID_UNCONNECTED_MESSAGE, \ | |
length=dsz, data=fwdo, \ | |
service=(CI_SRV_FORWARD_OPEN|UnconnectedDataItem.UNCONN_DATA_ITEM_SERVICE_REQUEST)) | |
cpf = CommandSpecificData(type_id=CommandSpecificData.TYPE_ID_NULL, \ | |
item_count=2, length=0, data=cpf2) | |
srr = SendRRPacket(interface_handle=0, timeout=0, data=cpf); | |
pkt = EncapsulationPacket(command=EncapsulationPacket.ENCAP_CMD_SENDRRDATA, \ | |
length=len(srr),session=self.session, \ | |
sender_context=random.randint(1,4026531839).to_bytes(8, byteorder='big'), \ | |
data=srr | |
) | |
self.sock.send(pkt.pack()) | |
inp, out, err = select.select([self.sock], [], [], 10) | |
if len(inp) != 0: | |
data = self.sock.recv(1024) | |
pkt = EncapsulationPacket() | |
pkt.unpack(data) | |
if pkt.status == EncapsulationPacket.ENCAP_STATUS_SUCCESS and \ | |
pkt.command == EncapsulationPacket.ENCAP_CMD_SENDRRDATA: | |
srr = SendRRPacket(pkt.data) | |
csd = CommandSpecificData(srr.data) | |
udi = UnconnectedDataItem(csd.data) | |
if udi.data[1] == 0: # Forward Open Status | |
fworsp = ForwardOpenResp(udi.data) | |
# socket address info O->T | |
ucdih = UnconnectedDataItemHdr(fworsp.data) | |
otaddrinfo = SocketAddressInfo(ucdih.data) | |
if b'' != otaddrinfo.data: | |
ucdih2 = UnconnectedDataItemHdr(otaddrinfo.data) | |
toaddrinfo = SocketAddressInfo(ucdih2.data) | |
self.otconnid = fworsp.otconnid | |
self.toconnid = fworsp.toconnid | |
self.otapi = fworsp.otapi / 1000 | |
if self.otapi < 8: | |
self.otapi = 8 | |
self.toapi = fworsp.toapi / 1000 | |
if self.toapi < 8: | |
self.toapi = 8 | |
return 0 | |
elif udi.data[1] == 0x01: # Forward open failed with Connection Failure | |
if udi.data[2] > 0: | |
extended_status, = struct.unpack("H", udi.data[3:5]) | |
return extended_status | |
return None | |
def sendFwdCloseReq(self, inputinst, outputinst, configinst): | |
path = struct.pack(">HB",0x2004, 0x24) + struct.pack("B",configinst) + \ | |
struct.pack("B",0x2c) + struct.pack("B",outputinst) + struct.pack("B",0x2c) + \ | |
struct.pack("B",inputinst) | |
fwdc = ForwardCloseReq(conn_serial=self.conn_serial_num, \ | |
mkpath=self.mkReqPath(clas=0x06,inst=0x01,attr=None), \ | |
plen=4, \ | |
data=path \ | |
) | |
# add service field | |
dsz = len(fwdc) + 1 | |
cpf2 = \ | |
UnconnectedDataItem(type_id=CommandSpecificData.TYPE_ID_UNCONNECTED_MESSAGE, \ | |
length=dsz, data=fwdc, \ | |
service=(CI_SRV_FORWARD_CLOSE|UnconnectedDataItem.UNCONN_DATA_ITEM_SERVICE_REQUEST)) | |
cpf = CommandSpecificData(type_id=CommandSpecificData.TYPE_ID_NULL, \ | |
item_count=2, length=0, data=cpf2) | |
srr = SendRRPacket(interface_handle=0, timeout=0, data=cpf); | |
pkt = EncapsulationPacket(command=EncapsulationPacket.ENCAP_CMD_SENDRRDATA, \ | |
length=len(srr),session=self.session, \ | |
sender_context=random.randint(1,4026531839).to_bytes(8, byteorder='big'), \ | |
data=srr | |
) | |
self.sock.send(pkt.pack()) | |
inp, out, err = select.select([self.sock], [], [], 10) | |
if len(inp) != 0: | |
data = self.sock.recv(1024) | |
pkt = EncapsulationPacket() | |
pkt.unpack(data) | |
if pkt.status == EncapsulationPacket.ENCAP_STATUS_SUCCESS and \ | |
pkt.command == EncapsulationPacket.ENCAP_CMD_SENDRRDATA: | |
srr = SendRRPacket(pkt.data) | |
csd = CommandSpecificData(srr.data) | |
udi = UnconnectedDataItem(csd.data) | |
fwcrsp = ForwardCloseResp(udi.data) | |
return 0 | |
return None | |
def sendUdpIO(self, runidle=True): | |
output = b"" | |
if runidle: | |
output += b"\x01\x00\x00\x00" | |
cnt = 0 | |
val = 0 | |
for bit in self.outAssem: | |
if bit == True: | |
val += 1 << cnt | |
cnt += 1 | |
if cnt == 8: | |
cnt = 0 | |
output += struct.pack("B", val) | |
val = 0 | |
pkt = UdpSendDataPacket(seq_num=self.seqnum, seq_count=self.seqnum, \ | |
conn_id=self.otconnid, \ | |
len_conn_data=int((len(self.outAssem)/8)+6), \ | |
data=output \ | |
) | |
self.seqnum += 1 | |
self.prodsock.sendto(pkt.pack(), (self.ipaddr,ENIP_UDP_PORT)) | |
def prodThread(self): | |
import time | |
while self.prod_state == 1: | |
self.sendUdpIO() | |
time.sleep(self.otapi/1000) | |
def produce(self): | |
if self.prod_state == 0: | |
self.prod_thread = EthernetIOThread(2, None, self) | |
self.prod_state = 1 | |
self.prod_thread.start() | |
def stopProduce(self): | |
if self.prod_state == 1: | |
self.prod_state = 0 | |
def testENIP(): | |
hostname = "192.168.1.32" | |
broadcast = "192.168.255.255" | |
inputsize = 1 | |
outputsize = 1 | |
EIP = EtherNetIP(hostname) | |
C1 = EIP.explicit_conn(hostname) | |
""" | |
listOfNodes = C1.scanNetwork(broadcast,5) | |
print("Found ", len(listOfNodes), " nodes") | |
for node in listOfNodes: | |
name = node.product_name.decode() | |
sockinfo = SocketAddressInfo(node.socket_addr) | |
ip = socket.inet_ntoa(struct.pack("!I",sockinfo.sin_addr)) | |
print(ip, " – ", name) | |
""" | |
pkt = C1.listID() | |
if pkt != None: | |
print("Product name: ", pkt.product_name.decode()) | |
pkt = C1.listServices() | |
print("ListServices:", str(pkt)) | |
path = C1.mkReqPath(0x300, 1, None) | |
data = struct.pack("HB", 0x12, 0) | |
r = C1.unconnSend(0x32, path+data, random.randint(1,4026531839)) | |
#r = C1.getAttrSingle(0x300, 1, None, struct.pack("HB", 0x12, 0)) | |
if 0 == r[0]: | |
print("Could read 0x300") | |
else: | |
print("Failed to read 0x300") | |
""" | |
# read input size from global system object (obj 0x84, attr 4) | |
r = C1.getAttrSingle(0x84, 1, 4) | |
if 0 == r[0]: | |
print("Read CPX input size from terminal success (data: "+ str(r[1]) + ")") | |
inputsize = struct.unpack("B", r[1])[0] | |
else: | |
print("Failed to read CPX input size") | |
# read output size from global system object (obj 0x84, attr 5) | |
r = C1.getAttrSingle(0x84, 1, 5) | |
if 0 == r[0]: | |
print("Read CPX output size from terminal sucess (data: " + str(r[1]) + ")") | |
outputsize = struct.unpack("B", r[1])[0] | |
else: | |
print("Failed to read CPX output size") | |
""" | |
""" | |
# configure i/o | |
print("Configure with {0} bytes input and {1} bytes output".format(inputsize, outputsize)) | |
EIP.registerAssembly(EtherNetIP.ENIP_IO_TYPE_INPUT, inputsize, 101, C1) | |
EIP.registerAssembly(EtherNetIP.ENIP_IO_TYPE_OUTPUT, outputsize, 100, C1) | |
EIP.startIO() | |
C1.registerSession() | |
C1.setAttrSingle(CIP_OBJ_TCPIP, 1, 6, "fbxxx") | |
for i in range(1,8): | |
r = C1.getAttrSingle(CIP_OBJ_IDENTITY, 1, i) | |
if 0 == r[0]: | |
print("read ok attr (" + str(i) + ") data: " + str(r[1])) | |
else: | |
print("Err: " + str(r[0])) | |
C1.sendFwdOpenReq(101,100,1) | |
C1.produce() | |
while True: | |
try: | |
time.sleep(0.2) | |
C1.outAssem[random.randint(0, len(C1.outAssem)-1)] = True | |
C1.outAssem[random.randint(0, len(C1.outAssem)-1)] = False | |
except KeyboardInterrupt: | |
break | |
C1.stopProduce() | |
C1.sendFwdCloseReq(101,100,1) | |
EIP.stopIO() | |
""" | |
#testENIP() |