text: #
  # The development of Bro's Modbus analyzer has been made possible thanks to
  # the support of the Ministry of Security and Justice of the Kingdom of the
  # Netherlands within the projects of Hermes, Castor and Midas.
  #
  # Useful references: http://www.modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf
  #                    http://www.simplymodbus.ca/faq.htm
  
  enum function_codes {
  	# Standard functions
  	READ_COILS                    = 0x01,
  	READ_DISCRETE_INPUTS          = 0x02,
  	READ_HOLDING_REGISTERS        = 0x03,
  	READ_INPUT_REGISTERS          = 0x04,
  	WRITE_SINGLE_COIL             = 0x05,
  	WRITE_SINGLE_REGISTER         = 0x06,
  	# READ_EXCEPTION_STATUS         = 0x07,
  	# DIAGNOSTICS                   = 0x08,
  	# GET_COMM_EVENT_COUNTER        = 0x0B,
  	# GET_COMM_EVENT_LOG            = 0x0C,
  	WRITE_MULTIPLE_COILS          = 0x0F,
  	WRITE_MULTIPLE_REGISTERS      = 0x10,
  	# REPORT_SLAVE_ID               = 0x11,
  	READ_FILE_RECORD              = 0x14,
  	WRITE_FILE_RECORD             = 0x15,
  	MASK_WRITE_REGISTER           = 0x16,
  	READ_WRITE_MULTIPLE_REGISTERS = 0x17,
  	READ_FIFO_QUEUE               = 0x18,
  	ENCAP_INTERFACE_TRANSPORT     = 0x2B,
  
  	# Machine/vendor/network specific functions
  	PROGRAM_484                   = 0x09,
  	POLL_484                      = 0x0A,
  	PROGRAM_584_984               = 0x0D,
  	POLL_584_984                  = 0x0E,
  	PROGRAM_884_U84               = 0x12,
  	RESET_COMM_LINK_884_U84       = 0x13,
  	PROGRAM_CONCEPT               = 0x28,
  	FIRMWARE_REPLACEMENT          = 0x7D,
  	PROGRAM_584_984_2             = 0x7E,
  	REPORT_LOCAL_ADDRESS          = 0x7F,
  
  	# Exceptions (not really function codes but they are used similarly)
  	READ_COILS_EXCEPTION                    = 0x81,
  	READ_DISCRETE_INPUTS_EXCEPTION          = 0x82,
  	READ_HOLDING_REGISTERS_EXCEPTION        = 0x83,
  	READ_INPUT_REGISTERS_EXCEPTION          = 0x84,
  	WRITE_SINGLE_COIL_EXCEPTION             = 0x85,
  	WRITE_SINGLE_REGISTER_EXCEPTION         = 0x86,
  	READ_EXCEPTION_STATUS_EXCEPTION         = 0x87,
  	WRITE_MULTIPLE_COILS_EXCEPTION          = 0x8F,
  	WRITE_MULTIPLE_REGISTERS_EXCEPTION      = 0x90,
  	READ_FILE_RECORD_EXCEPTION              = 0x94,
  	WRITE_FILE_RECORD_EXCEPTION             = 0x95,
  	MASK_WRITE_REGISTER_EXCEPTION           = 0x96,
  	READ_WRITE_MULTIPLE_REGISTERS_EXCEPTION = 0x97,
  	READ_FIFO_QUEUE_EXCEPTION               = 0x98,
  };
  
  # Main Modbus/TCP PDU
  type ModbusTCP_PDU(is_orig: bool) = record {
  	header: ModbusTCP_TransportHeader;
  	body: case is_orig of {
  		true  -> request:  ModbusTCP_Request(header);
  		false -> response: ModbusTCP_Response(header);
  	};
  } &let {
  	deliver: bool = $context.flow.deliver_ModbusTCP_PDU(this);
  } &length=header.len+6, &byteorder=bigendian;
  
  type ModbusTCP_TransportHeader = record {
  	tid: uint16; # Transaction identifier
  	pid: uint16; # Protocol identifier
  	len: uint16; # Length of everyting after this field
  	uid: uint8;  # Unit identifier (previously 'slave address')
  	fc:  uint8;  # MODBUS function code (see function_codes enum)
  } &byteorder=bigendian, &let {
  	deliver: bool = $context.flow.deliver_message(this);
  };
  
  type ModbusTCP_Request(header: ModbusTCP_TransportHeader) = case header.fc of {
  	READ_COILS                    -> readCoils:                  ReadCoilsRequest(header);
  	READ_DISCRETE_INPUTS          -> readDiscreteInputs:         ReadDiscreteInputsRequest(header);
  	READ_HOLDING_REGISTERS        -> readHoldingRegisters:       ReadHoldingRegistersRequest(header);
  	READ_INPUT_REGISTERS          -> readInputRegisters:         ReadInputRegistersRequest(header);
  	WRITE_SINGLE_COIL             -> writeSingleCoil:            WriteSingleCoilRequest(header);
  	WRITE_SINGLE_REGISTER         -> writeSingleRegister:        WriteSingleRegisterRequest(header);
  	#READ_EXCEPTION_STATUS         -> readExceptionStatus:        ReadExceptionStatusRequest(header);
  	#DIAGNOSTICS                   -> diagnostics:                DiagnosticsRequest(header);
  	#GET_COMM_EVENT_COUNTER        -> getCommEventCounter:        GetCommEventCounterRequest(header);
  	#GET_COMM_EVENT_LOG            -> getCommEventLog:            GetCommEventLogRequest(header);
  	WRITE_MULTIPLE_COILS          -> writeMultipleCoils:         WriteMultipleCoilsRequest(header);
  	WRITE_MULTIPLE_REGISTERS      -> writeMultRegisters:         WriteMultipleRegistersRequest(header);
  	#REPORT_SLAVE_ID
  	READ_FILE_RECORD              -> readFileRecord:             ReadFileRecordRequest(header);
  	WRITE_FILE_RECORD             -> writeFileRecord:            WriteFileRecordRequest(header);
  	MASK_WRITE_REGISTER           -> maskWriteRegister:          MaskWriteRegisterRequest(header);
  	READ_WRITE_MULTIPLE_REGISTERS -> readWriteMultipleRegisters: ReadWriteMultipleRegistersRequest(header);
  	READ_FIFO_QUEUE               -> readFIFOQueue:              ReadFIFOQueueRequest(header);
  	#ENCAP_INTERFACE_TRANSPORT
  
  	# All the rest
  	default                       -> unknown:                    bytestring &restofdata;
  };
  
  # Responses
  #
  type ModbusTCP_Response(header: ModbusTCP_TransportHeader) = case header.fc of {
  	READ_COILS                          -> readCoils:                       ReadCoilsResponse(header);
  	READ_DISCRETE_INPUTS                -> readDiscreteInputs:              ReadDiscreteInputsResponse(header);
  	READ_HOLDING_REGISTERS              -> readHoldingRegisters:            ReadHoldingRegistersResponse(header);
  	READ_INPUT_REGISTERS                -> readInputRegisters:              ReadInputRegistersResponse(header);
  	WRITE_SINGLE_COIL                   -> writeSingleCoil:                 WriteSingleCoilResponse(header);
  	WRITE_SINGLE_REGISTER               -> writeSingleRegister:             WriteSingleRegisterResponse(header);
  	#READ_EXCEPTION_STATUS               -> readExceptionStatus:             ReadExceptionStatusResponse(header);
  	#DIAGNOSTICS                         -> diagnostics:                     DiagnosticsResponse(header);
  	#GET_COMM_EVENT_COUNTER              -> getCommEventCounter:             GetCommEventCounterResponse(header);
  	#GET_COMM_EVENT_LOG                  -> getCommEventLog:                 GetCommEventLogResponse(header);
  	WRITE_MULTIPLE_COILS                -> writeMultipleCoils:              WriteMultipleCoilsResponse(header);
  	WRITE_MULTIPLE_REGISTERS            -> writeMultRegisters:              WriteMultipleRegistersResponse(header);
  	#REPORT_SLAVE_ID
  	READ_FILE_RECORD                    -> readFileRecord:                  ReadFileRecordResponse(header);
  	WRITE_FILE_RECORD                   -> writeFileRecord:                 WriteFileRecordResponse(header);
  	MASK_WRITE_REGISTER                 -> maskWriteRegister:               MaskWriteRegisterResponse(header);
  	READ_WRITE_MULTIPLE_REGISTERS       -> readWriteMultipleRegisters:      ReadWriteMultipleRegistersResponse(header);
  	READ_FIFO_QUEUE                     -> readFIFOQueue:                   ReadFIFOQueueResponse(header);
  
  	# Exceptions
  	READ_HOLDING_REGISTERS_EXCEPTION        -> readHoldingRegistersException:   Exception(header);
  	WRITE_MULTIPLE_REGISTERS_EXCEPTION      -> writeMultRegistersException:     Exception(header);
  	READ_COILS_EXCEPTION                    -> readCoilsException:              Exception(header);
  	READ_DISCRETE_INPUTS_EXCEPTION          -> readDiscreteInputsException:     Exception(header);
  	READ_INPUT_REGISTERS_EXCEPTION          -> readInputRegistersException:     Exception(header);
  	WRITE_SINGLE_COIL_EXCEPTION             -> writeCoilException:              Exception(header);
  	WRITE_SINGLE_REGISTER_EXCEPTION         -> writeSingleRegisterException:    Exception(header);
  	READ_EXCEPTION_STATUS_EXCEPTION         -> readExceptionStatusException:    Exception(header);
  	WRITE_MULTIPLE_COILS_EXCEPTION          -> forceMultipleCoilsException:     Exception(header);
  	READ_FILE_RECORD_EXCEPTION              -> readGeneralReferenceException:   Exception(header);
  	WRITE_FILE_RECORD_EXCEPTION             -> writeGeneralReferenceException:  Exception(header);
  	MASK_WRITE_REGISTER_EXCEPTION           -> maskWriteRegisterException:      Exception(header);
  	READ_WRITE_MULTIPLE_REGISTERS_EXCEPTION -> readWriteRegistersException:     Exception(header);
  	READ_FIFO_QUEUE_EXCEPTION               -> readFIFOQueueException:          Exception(header);
  
  	# All the rest
  	default                                 -> unknown:                         bytestring &restofdata;
  };
  
  type Exception(header: ModbusTCP_TransportHeader) = record {
  	code: uint8;
  } &let {
  	deliver: bool = $context.flow.deliver_Exception(header, this);
  };
  
  # REQUEST FC=1
  type ReadCoilsRequest(header: ModbusTCP_TransportHeader) = record {
  	start_address:  uint16;
  	quantity:       uint16; # &check(quantity <= 2000);
  } &let {
  	deliver: bool = $context.flow.deliver_ReadCoilsRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=1
  type ReadCoilsResponse(header: ModbusTCP_TransportHeader) = record {
  	byte_count: uint8;
  	bits:       bytestring &length=byte_count;
  } &let {
  	deliver: bool = $context.flow.deliver_ReadCoilsResponse(header, this);
  } &byteorder=bigendian;
  
  # REQUEST FC=2
  type ReadDiscreteInputsRequest(header: ModbusTCP_TransportHeader) = record {
  	start_address: uint16;
  	quantity:      uint16; # &check(quantity <= 2000);
  } &let {
  	deliver: bool = $context.flow.deliver_ReadDiscreteInputsRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=2
  type ReadDiscreteInputsResponse(header: ModbusTCP_TransportHeader) = record {
  	byte_count: uint8;
  	bits:       bytestring &length=byte_count;
  } &let {
  	deliver: bool = $context.flow.deliver_ReadDiscreteInputsResponse(header, this);
  } &byteorder=bigendian;
  
  # REQUEST FC=3
  type ReadHoldingRegistersRequest(header: ModbusTCP_TransportHeader) = record {
  	start_address: uint16;
  	quantity:      uint16; # &check(quantity <= 125);
  } &let {
  	deliver: bool = $context.flow.deliver_ReadHoldingRegistersRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=3
  type ReadHoldingRegistersResponse(header: ModbusTCP_TransportHeader) = record {
  	byte_count: uint8;
  	registers:  uint16[byte_count/2] &length=byte_count;
  } &let {
  	deliver: bool = $context.flow.deliver_ReadHoldingRegistersResponse(header, this);
  } &byteorder=bigendian;
  
  # REQUEST FC=4
  type ReadInputRegistersRequest(header: ModbusTCP_TransportHeader) = record {
  	start_address: uint16;
  	quantity:      uint16; # &check(quantity <= 125);
  } &let {
  	deliver: bool = $context.flow.deliver_ReadInputRegistersRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=4
  type ReadInputRegistersResponse(header: ModbusTCP_TransportHeader) = record {
  	byte_count: uint8;
  	registers:  uint16[byte_count/2] &length=byte_count;
  } &let {
  	deliver: bool = $context.flow.deliver_ReadInputRegistersResponse(header, this);
  } &byteorder=bigendian;
  
  # REQUEST FC=5
  type WriteSingleCoilRequest(header: ModbusTCP_TransportHeader) = record {
  	address: uint16;
  	value:   uint16; # &check(value == 0x0000 || value == 0xFF00);
  } &let {
  	deliver: bool = $context.flow.deliver_WriteSingleCoilRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=5
  type WriteSingleCoilResponse(header: ModbusTCP_TransportHeader) = record {
  	address: uint16;
  	value:   uint16; # &check(value == 0x0000 || value == 0xFF00);
  } &let {
  	deliver: bool = $context.flow.deliver_WriteSingleCoilResponse(header, this);
  } &byteorder=bigendian;
  
  # REQUEST FC=6
  type WriteSingleRegisterRequest(header: ModbusTCP_TransportHeader) = record {
  	address:  uint16;
  	value:    uint16;
  } &let {
  	deliver: bool = $context.flow.deliver_WriteSingleRegisterRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=6
  type WriteSingleRegisterResponse(header: ModbusTCP_TransportHeader) = record {
  	address:   uint16;
  	value:     uint16;
  } &let {
  	deliver: bool = $context.flow.deliver_WriteSingleRegisterResponse(header, this);
  } &byteorder=bigendian;
  
  # REQUEST FC=15
  type WriteMultipleCoilsRequest(header: ModbusTCP_TransportHeader) = record {
  	start_address:    uint16;
  	quantity:         uint16; #     &check(quantity <= 0x07B0);
  	byte_count:       uint8; #      &check(byte_count == (quantity + 7)/8);
  	coils:            bytestring &length=byte_count;
  } &let {
  	deliver: bool = $context.flow.deliver_WriteMultipleCoilsRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=15
  type WriteMultipleCoilsResponse(header: ModbusTCP_TransportHeader) = record {
  	start_address:   uint16;
  	quantity:        uint16; # &check(quantity <= 0x07B0);
  } &let {
  	deliver: bool = $context.flow.deliver_WriteMultipleCoilsResponse(header, this);
  } &byteorder=bigendian;
  
  # REQUEST FC=16
  type WriteMultipleRegistersRequest(header: ModbusTCP_TransportHeader) = record {
  	start_address: uint16;
  	quantity:      uint16;
  	byte_count:    uint8;
  	# We specify registers buffer with quantity and byte_count so that the analyzer 
  	# will choke if something doesn't match right (correct devices should make it right).
  	registers:     uint16[quantity] &length=byte_count;
  } &let {
  	deliver: bool = $context.flow.deliver_WriteMultipleRegistersRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=16
  type WriteMultipleRegistersResponse(header: ModbusTCP_TransportHeader) = record {
  	start_address: uint16;
  	quantity:      uint16;
  } &let {
  	deliver: bool = $context.flow.deliver_WriteMultipleRegistersResponse(header, this);
  } &byteorder=bigendian;
  
  # Support data structure for following message type.
  type FileRecordRequest = record {
  	ref_type:   uint8; #  &check(ref_type == 6);
  	file_num:   uint16; # &check(file_num > 0);
  	record_num: uint16; # &check(record_num <= 0x270F);
  	record_len: uint16;
  } &byteorder=bigendian;
  
  # REQUEST FC=20
  type ReadFileRecordRequest(header: ModbusTCP_TransportHeader) = record {
  	byte_count: uint8; #               &check(byte_count <= 0xF5);
  	references: FileRecordRequest[] &length=byte_count;
  } &let {
  	deliver: bool = $context.flow.deliver_ReadFileRecordRequest(header, this);
  } &byteorder=bigendian;
  
  # Support data structure for the following message type.
  type FileRecordResponse = record {
  	file_len:    uint8; #    &check(file_len >= 0x07 && file_len <= 0xF5);
  	ref_type:    uint8; #    &check(ref_type == 6);
  	record_data: uint16[file_len/2] &length=file_len;
  } &byteorder=bigendian;
  
  # RESPONSE FC=20
  type ReadFileRecordResponse(header: ModbusTCP_TransportHeader) = record {
  	byte_count: uint8; # &check(byte_count >= 0x07 && byte_count <= 0xF5);
  	references: FileRecordResponse[] &length=byte_count;
  } &let {
  	deliver: bool = $context.flow.deliver_ReadFileRecordResponse(header, this);
  } &byteorder=bigendian;
  
  # Support data structure for the two following message types.
  type ReferenceWithData = record {
  	ref_type:       uint8;
  	file_num:       uint16;
  	record_num:     uint16;
  	word_count:     uint16;
  	register_value: uint16[word_count];
  } &byteorder=bigendian;
  
  # REQUEST FC=21
  type WriteFileRecordRequest(header: ModbusTCP_TransportHeader) = record {
  	byte_count: uint8;
  	references: ReferenceWithData[] &length=byte_count;
  } &let {
  	deliver: bool = $context.flow.deliver_WriteFileRecordRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=21
  type WriteFileRecordResponse(header: ModbusTCP_TransportHeader) = record {
  	byte_count: uint8;
  	references: ReferenceWithData[] &length=byte_count;
  } &let {
  	deliver: bool = $context.flow.deliver_WriteFileRecordResponse(header, this);
  } &byteorder=bigendian;
  
  # REQUEST FC=22
  type MaskWriteRegisterRequest(header: ModbusTCP_TransportHeader) = record {
  	address:    uint16;
  	and_mask:   uint16;
  	or_mask:    uint16;
  } &let {
  	deliver: bool = $context.flow.deliver_MaskWriteRegisterRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=22
  type MaskWriteRegisterResponse(header: ModbusTCP_TransportHeader) = record {
  	address:  uint16;
  	and_mask: uint16;
  	or_mask:  uint16;
  } &let {
  	deliver: bool = $context.flow.deliver_MaskWriteRegisterResponse(header, this);
  } &byteorder=bigendian;
  
  # REQUEST FC=23
  type ReadWriteMultipleRegistersRequest(header: ModbusTCP_TransportHeader) = record {
  	read_start_address:    uint16;
  	read_quantity:         uint16; #                 &check(read_quantity <= 125);
  	write_start_address:   uint16;
  	write_quantity:        uint16; #                 &check(write_quantity <= 100);
  	write_byte_count:      uint8;
  	write_register_values: uint16[write_quantity] &length=write_byte_count;
  } &let {
  	 deliver: bool = $context.flow.deliver_ReadWriteMultipleRegistersRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=23
  type ReadWriteMultipleRegistersResponse(header: ModbusTCP_TransportHeader) = record {
  	byte_count:  uint8;
  	registers:   uint16[byte_count/2] &length=byte_count;
  } &let {
  	deliver: bool = $context.flow.deliver_ReadWriteMultipleRegistersResponse(header, this);
  } &byteorder=bigendian;
  
  # REQUEST FC=24
  type ReadFIFOQueueRequest(header: ModbusTCP_TransportHeader) = record {
  	start_address: uint16;
  } &let{
  	deliver: bool = $context.flow.deliver_ReadFIFOQueueRequest(header, this);
  } &byteorder=bigendian;
  
  # RESPONSE FC=24
  type ReadFIFOQueueResponse(header: ModbusTCP_TransportHeader) = record {
  	byte_count:    uint16; #             &check(byte_count <= 62);
  	fifo_count:    uint16; #             &check(fifo_count <= 31);
  	register_data: uint16[fifo_count] &length=fifo_count*2;
  } &let {
  	deliver: bool = $context.flow.deliver_ReadFIFOQueueResponse(header, this);
  } &byteorder=bigendian;
  