示例#1
0
 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')
示例#2
0
 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
示例#3
0
 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)
示例#4
0
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()
示例#5
0
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}
示例#6
0
 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
示例#7
0
 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()
示例#8
0
def testfr(actcon: bool = False) -> bytes:
    pkt = APDU() / APCI(ApduLen=4, Type=0x03, UType=0x10 << int(actcon))
    return pkt.build()