def do_disconnect(self, arg: str): 'Disconnect from an RTU' if len(self.__rtu_comms) > 0: rtuaddr = runprompt( listq(message='Disconnect from which RTU?', choices=self.__rtu_comms.keys())) self.__keepalive_kill[rtuaddr] = True # Stop keepalive t = self.__keepalive[rtuaddr] t.join() self.__rtu_u_state[rtuaddr] = 0x08 # Expect STOPDT con pkt = APDU() / APCI(ApduLen=4, Type=0x03, UType=0x04) # STOPDT act self.__rtu_comms[rtuaddr].send(pkt.build()) while self.__rtu_u_state[ rtuaddr] is not None and self.__rtu_u_state[rtuaddr] > 0: print(f'\rTerminating connection with {rtuaddr:s} ... ', end='') sleep(0.33) print('') self.__killsignals[rtuaddr] = True t = self.__threads.pop(rtuaddr) t.join() s = self.__rtu_comms.pop(rtuaddr) d = self.__rtu_data.pop(rtuaddr) k = self.__killsignals.pop(rtuaddr) self.__rtu_asdu.pop(rtuaddr) s.close() else: print('Not connected to any RTUs')
def do_connect(self, arg: str): 'Connect to a new RTU' try: arg = arg.split(';') self.__rtu_asdu[arg[0]] = arg[1] arg = arg[0] assert IPv4_REGEX.match(arg) is not None if '/' in arg: prefix = int(arg.split('/')[1]) assert prefix > 0 and prefix <= 32 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) s.settimeout(2) s.connect((arg, IEC104_PORT)) self.__rtu_comms[arg] = s self.__killsignals[arg] = False self.__rtu_u_state[arg] = 0x02 # Expect a STARTDT con U-frame self.__rtu_i_state[arg] = None # Don't expect any I-frames t = Thread(target=self.__handle_rtu, kwargs={ 's': s, 'k': arg }) # Create a receiving thread for this RTU. t.start() self.__threads[arg] = t pkt = APDU() / APCI(ApduLen=4, Type=0x03, UType=0x01) # STARTDT act s.send(pkt.build()) while self.__rtu_u_state[ arg] is not None and self.__rtu_u_state[arg] > 0: print(f'\rInitiating connection with peer {arg:s} ... ', end='') sleep(0.33) print('') if self.__rtu_u_state[arg] is None: print(f'Unable to connect to {arg:s}') self.__killsignals[arg] = True t = self.__threads.pop(arg) t.join() s.close() self.__rtu_asdu.pop(arg) self.__rtu_comms.pop(arg) self.__killsignals.pop(arg) self.__rtu_i_state.pop(arg) self.__rtu_u_state.pop(arg) if arg in self.__rtu_data.keys(): self.__rtu_data.pop(arg) else: self.__keepalive_kill[arg] = False t = Thread(target=self.__keepalive_handler, kwargs={ 's': s, 'k': arg }) # Create a keepalive thread for this RTU. t.start() self.__keepalive[arg] = t except AssertionError: print('Invalid IPv4 address: %s' % arg) except socket.timeout: print('Unable to connect to %s' % arg) return False
def __keepalive_handler(self, s: socket.socket, k:str): while not self.__done and not self.__keepalive_kill[k]: sleep(10) # Send a keepalive every 10 seconds self.__rtu_u_state[k] = 0x20 # Expect a TESTFR con pkt = APDU()/APCI(ApduLen=4, Type=0x03, UType=0x10) # 'TESTFR act' as a keepalive self.__rtu_comms[k].send(pkt.build()) while self.__rtu_u_state[k] is not None and self.__rtu_u_state[k] > 0: sleep(0.33)
def build_104_asdu_packet(typeASDU: int, asdu: int, ioa: int, tx: int, rx: int, causeTx: int = 1, **kwargs) -> bytes: pkt = APDU() pkt /= APCI(ApduLen=APDULEN[typeASDU], Type=0x00, Tx=tx, Rx=rx) if typeASDU == 3: pkt /= ASDU(TypeId=typeASDU, SQ=0, NumIx=1, CauseTx=causeTx, Test=0, OA=0, Addr=asdu, IOA=[IOA3(IOA=ioa, DIQ=DIQ(DPI=kwargs['value'], flags=0))]) elif typeASDU == 36: ct = cp56time() pkt /= ASDU( TypeId=typeASDU, SQ=0, NumIx=1, CauseTx=causeTx, Test=0, OA=0, Addr=asdu, IOA=[IOA36(IOA=ioa, Value=kwargs['value'], QDS=0x00, CP56Time=ct)]) elif typeASDU == 45: pkt /= ASDU(TypeId=typeASDU, SQ=0, NumIx=1, CauseTx=causeTx, Test=0, OA=0, Addr=asdu, IOA=[ IOA45(IOA=ioa, SCO=SCO(SE=kwargs['SE'], QU=kwargs['QU'], SCS=kwargs['SCS'])) ]) elif typeASDU == 50: pkt /= ASDU( TypeId=typeASDU, SQ=0, NumIx=1, CauseTx=causeTx, Test=0, OA=0, Addr=asdu, IOA=[IOA50(IOA=ioa, Value=kwargs['value'], QOS=QOS(QL=0, SE=0))]) else: raise AttributeError if __name__ == '__main__': pkt.show() return pkt.build()
def extract_104_value(pkt: APDU) -> dict: if pkt.haslayer('IOA3'): ioa = pkt['IOA3'].IOA value = pkt['DIQ'].DPI elif pkt.haslayer('IOA36'): ioa = pkt['IOA36'].IOA value = pkt['IOA36'].Value elif pkt.haslayer('IOA45'): ioa = pkt['IOA45'].IOA value = pkt['IOA45'].SCO.SE | pkt['IOA45'].SCO.SCS elif pkt.haslayer('IOA50'): ioa = pkt['IOA50'].IOA value = pkt['IOA50'].Value else: ioa = 0 value = -1 if pkt.haslayer('APCI') and pkt['APCI'].Type == 0x00: tx = pkt['APCI'].Tx rx = pkt['APCI'].Rx else: tx = 0 rx = 0 return {'ioa': ioa, 'tx': tx, 'rx': rx, 'value': value}
def __handle_rtu(self, s: socket.socket, k: str): if k not in self.__rtu_data.keys(): self.__rtu_data[k] = {'ioas': {}} while not self.__done and not self.__killsignals[k]: try: data = s.recv(BUFFER_SIZE) data = APDU(data) if data['APCI'].Type == 0x03: # U-frame if data['APCI'].UType in [1, 4, 16]: # All the 'act' variants => Shouldn't happen. Do nothing pass elif data['APCI'].UType == self.__rtu_u_state[k]: # Correct expected U-frame response self.__rtu_u_state[k] = 0 else: # Unexpected U-frame response => Shouldn't happen. Alert user. print(f'**** WARNING: Received an unexpected U-frame from {str(self.__rtu_comms[k].getpeername()):s} ****') self.__rtu_u_state[k] = None elif data['APCI'].Type == 0x01: # S-frame => Shouldn't happen. Alert user. print(f'**** WARNING: Received an S-frame from {str(self.__rtu_comms[k].getpeername()):s} ****') elif data['APCI'].Type == 0x00: # I-frame asdu = data['ASDU'] data = extract_104_value(data) self.__rtu_data[k]['tx'] = data['rx'] self.__rtu_data[k]['rx'] = data['tx'] if asdu.TypeId in [3, 36]: # Measurement value value = data['value'] if isinstance(value, str): if value == 'determined state OFF': value = 0 else: value = 1 self.__rtu_data[k]['ioas'][data['ioa']] = value elif asdu.TypeId == 45: # Single command print(f'''Received: {((asdu.CauseTx << 8) | (asdu['IOA45'].SCO.SE << 7) | asdu['IOA45'].SCO.SCS):04x} Expected: {self.__rtu_i_state[k]:04x}''') if self.__rtu_i_state[k] is not None and self.__rtu_i_state[k] == ((asdu.CauseTx << 8) | (asdu['IOA45'].SCO.SE << 7) | asdu['IOA45'].SCO.SCS): # Expected single command response self.__rtu_i_state[k] = 0x0000 else: # Unexpected single command response => Alert user. self.__rtu_i_state[k] = None print(f'**** WARNING: Received an unexpected I-frame from {str(self.__rtu_comms[k].getpeername()):s} ****') else: # Received an I-frame that has not been implemented => Alert user. print(f'**** WARNING: Received an unknown I-frame from {str(self.__rtu_comms[k].getpeername()):s} ****') else: # Received a malformed packet => Alert user. print(f'**** WARNING: Received a malformed packet from {str(self.__rtu_comms[k].getpeername()):s} ****') except (socket.timeout, KeyError, IndexError): self.__rtu_i_state[k] = None self.__rtu_u_state[k] = None except ConnectionResetError: self.__rtu_i_state[k] = None self.__rtu_u_state[k] = None
def __subloop(self, wsock: socket.socket): 'This method handles the state transitions for the Start/Stop procedures' connid = randint(0, 65535) while connid in self.__startdt.keys(): connid = randint(0, 65535) msr = None self.__startdt[connid] = False wsock.settimeout(RTU_TIMEOUT) self.log(f'Initiating state handler with ID {connid:d}') while not self.terminate: try: data = wsock.recv(BUFFER_SIZE) data = APDU(data) atype = data['APCI'].Type if msr is None: # STOPPED connection as shown in figure 17 from 60870-5-104 IEC:2006 if atype in [0x00, 0x01]: # I-frame (0x00) or S-frame (0x01) self.log(f'Received an unexpected frame ({TYPE_APCI[atype]:s}) in "STOPPED connection" state. Terminating thread ...') self.__terminate = True elif atype == 0x03: # U-frame (0x03) ut = data['APCI'].UType if ut == 0x01: # STARTDT act self.log('Received a "STARTDT act" U-frame') data = startdt(True) # STARTDT actcon elif ut == 0x04: # STOPDT act self.log('Received a "STOPDT act" U-frame') data = stopdt(True) # STOPDT actcon else: # TESTFR act self.log('Received a "TESTFR act" U-frame') data = testfr(True) # TESTFR actcon # NOTE: If more than one bit is activated, it will be registered as a 'TESTFR act' wsock.send(data) if ut == 0x01: # Start the connection self._RTU__startdt[connid] = True # Track the state of the current connection msr = Thread(target=self._RTU__measure, kwargs={'wsock': wsock, 'connid': connid}) self.log('Start measuring data ...') msr.start() # Start measuring else: # STARTED connection as shown in figure 17 from 60870-5-104 IEC:2006 if atype == 0x03: # U-frame (0x03) ut = data['APCI'].UType if ut == 0x01: # STARTDT act self.log('Received a "STARTDT act" U-frame') data = startdt(True) # STARTDT actcon elif ut == 0x04: # STOPDT act self.log('Received a "STOPDT act" U-frame') data = stopdt(True) # STOPDT actcon self._RTU__startdt[connid] = False # Change measurement state self.log('Stop measusing data ...') msr.join() # Stop measuring msr = None else: # TESTFR act self.log('Received a "TESTFR act" U-frame') data = testfr(True) # TESTFR actcon wsock.send(data) elif atype == 0x01: # S-frame (0x01) self.log('Received an S-frame') self.__tx = data['APCI'].Rx else: # I-frame (0x00) self.log('Received an I-frame. Initiating handler ...') self.__handle_iframe(wsock, data) # NOTE: In this particular simulation, we are not considering the 'Pending UNCONFIRMED STOPPED connection' state, as our responses are faster except socket.timeout: self.log('ERROR: T1 timeout') self.__terminate = True # RTU T1 timeout => terminate connection except BrokenPipeError: self.log('ERROR: Connection ended unexpectedly') self.__terminate = True # Connection ended unexpectedly. except socket.error as e: if e.errno != errno.ECONNRESET: self.log(f'ERROR: Unknown socket error: {e.errno:d}') raise # Other unknown error self.__terminate = True except IndexError: self.log('ERROR: Index error') if msr is not None: # The connection was still measuring self.__startdt[connid] = False # Change measurement state msr.join() # Stop measuring msr = None self.__startdt.pop(connid) # Remove the connection tracking wsock.close()
def testfr(actcon: bool = False) -> bytes: pkt = APDU() / APCI(ApduLen=4, Type=0x03, UType=0x10 << int(actcon)) return pkt.build()