Why industrial protocols are different

Office protocols such as HTTP were designed for many users, changing content and the open internet. Industrial protocols were designed for something else: a controller that asks the same few devices for the same few values, over and over, for twenty years. That shows in four ways:

  • Cyclic and predictable. A master or client typically asks each device for its values on a fixed schedule (polling), so the load on the network is known in advance.
  • Deterministic where it matters. Protection and motion control need messages to arrive within a guaranteed time. Some protocols bypass IP altogether and send Ethernet frames directly, as GOOSE and PROFINET's real-time traffic do.
  • Long-lived. A protocol in a substation or a factory has to stay compatible with devices installed decades ago. Changing it is a multi-year project.
  • No security by design. They were made for isolated serial links and closed networks where only trusted equipment was connected. A message that arrives is assumed to be legitimate: there is no login, no signature and no encryption. The security extensions that exist today were added much later, and many installed devices do not support them.

The last point is the root of much of OT security: on a network where these protocols run, being able to send packets is often the same as being allowed to give commands. That is why the network itself, with zones and conduits, is the first line of defense.

Modbus

Modbus is a simple request-and-response protocol for reading and writing values in a controller. It was created by Modicon, an early PLC maker, and its own specification calls it "the industry's serial de facto standard since 1979". It is open, free and supported by almost every industrial device, which is why it is still everywhere: in pumping stations, energy meters, solar inverters and building systems. One device (the client, called the master in older serial documents) sends a request; the addressed device (the server or slave) answers.

Modbus sees a device as four tables of numbered values:

  • Coils: single bits that can be read and written (a pump on or off).
  • Discrete inputs: single bits that can only be read (a limit switch).
  • Input registers: 16-bit numbers that can only be read (a measurement).
  • Holding registers: 16-bit numbers that can be read and written (a setpoint, a configuration value).

A register holds only a number. Whether 235 means 23.5 °C, 235 liters or an alarm code is not in the protocol; it is in the device's register map, a table in the vendor's manual.

Function codes

Every request starts with a function code that says what to do. The common public ones:

Code Name Effect
01 Read Coils Read
02 Read Discrete Inputs Read
03 Read Holding Registers Read
04 Read Input Registers Read
05 Write Single Coil Write
06 Write Single Register Write
15 (0x0F) Write Multiple Coils Write
16 (0x10) Write Multiple Registers Write
23 (0x17) Read/Write Multiple Registers Read and write
43 (0x2B) Encapsulated Interface Transport (for example Read Device Identification) Read

When a device cannot carry out a request, it answers with the function code plus 0x80 (so 0x83 for a failed read of holding registers) and an exception code, such as 02 for "illegal data address". Vendors may also use their own function codes, for example for programming a controller. For a defender, the split between read and write codes is the useful part: monitoring tools and industrial firewalls can tell a harmless read from a write that changes the process.

Modbus RTU and Modbus TCP

Modbus runs over two kinds of transport. On serial lines, usually RS-485 two-wire, Modbus RTU is the mandatory and default mode (an ASCII mode exists but is optional). Each frame starts with the device address (1 to 247; 0 is broadcast) and ends with a 16-bit cyclic redundancy check (CRC), so the receiver can detect bits damaged by electrical noise. The CRC algorithm is fixed by the specification: start at 0xFFFF and, bit by bit, shift right and, when the bit shifted out is 1, XOR with 0xA001.

Modbus TCP carries the same function codes inside TCP, on the reserved port 502. The serial address and CRC are replaced by a 7-byte header, the Modbus Application Protocol (MBAP) header: a transaction identifier to match answers to requests, a protocol identifier (always 0), a length field and a unit identifier. Error detection is left to TCP/IP. Gateways translate between the two, so a SCADA server on Ethernet can poll old serial meters behind a gateway.

The script below builds a Modbus RTU "read holding registers" request for device 17, starting at register 0x006B, three registers, and checks CRCs. It only builds and checks bytes; nothing is sent anywhere.

def crc16_modbus(data: bytes) -> int:
    """CRC-16/MODBUS: start at 0xFFFF; shift right, XOR 0xA001 if a 1 fell out."""
    crc = 0xFFFF
    for byte in data:
        crc ^= byte
        for _ in range(8):
            if crc & 1:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return crc


def read_holding_registers(unit: int, start: int, count: int) -> bytes:
    """Build (not send) a Modbus RTU request with function code 03."""
    # address, function code 03, first register, number of registers
    body = bytes([unit, 0x03]) + start.to_bytes(2, "big") + count.to_bytes(2, "big")
    return body + crc16_modbus(body).to_bytes(2, "little")   # CRC goes low byte first


def crc_ok(frame: bytes) -> bool:
    return crc16_modbus(frame[:-2]) == int.from_bytes(frame[-2:], "little")


request = read_holding_registers(unit=17, start=0x006B, count=3)
print("request:", request.hex(" ").upper())
print("CRC ok: ", crc_ok(request))

# A reply as it might arrive on the RS-485 line, and the same reply with one bit flipped
reply = bytes.fromhex("11 03 06 AE 41 56 52 43 40 49 AD")
damaged = bytearray(reply)
damaged[3] ^= 0x01
for name, frame in [("reply", reply), ("damaged", bytes(damaged))]:
    print(f"{name:8} {frame.hex(' ').upper()}  CRC ok: {crc_ok(frame)}")
request: 11 03 00 6B 00 03 76 87
CRC ok:  True
reply    11 03 06 AE 41 56 52 43 40 49 AD  CRC ok: True
damaged  11 03 06 AF 41 56 52 43 40 49 AD  CRC ok: False

The CRC is sent low byte first, which is why the value 0x8776 appears as 76 87 at the end of the frame. The damaged reply differs in a single bit, and the CRC check catches it. A CRC protects against noise, not against attackers: anyone who builds a frame can compute a valid CRC, exactly as this script does.

Decoding a captured response

On an Ethernet network you see Modbus TCP in a packet capture or in the logs of a monitoring tool. A short exchange between an HMI and a PLC looks like this:

09:14:02.101  192.0.2.50:49731 > 192.0.2.70:502   Modbus/TCP  query     trans 7  unit 1  func 3  start 0  count 3
09:14:02.104  192.0.2.70:502 > 192.0.2.50:49731   Modbus/TCP  response  trans 7  unit 1  func 3  bytes 6
09:14:03.101  192.0.2.50:49731 > 192.0.2.70:502   Modbus/TCP  query     trans 8  unit 1  func 3  start 900  count 2
09:14:03.103  192.0.2.70:502 > 192.0.2.50:49731   Modbus/TCP  response  trans 8  unit 1  exception 0x83 code 2

The script below decodes the raw bytes of both responses and applies the register map to turn numbers into values.

import struct

EXCEPTIONS = {1: "illegal function", 2: "illegal data address", 3: "illegal data value"}

# Modbus only carries 16-bit numbers; the device's register map gives them meaning.
REGISTER_MAP = [
    ("outlet temperature", 0.1, "°C"),     # raw 235  -> 23.5 °C
    ("discharge pressure", 0.001, "bar"),  # raw 4000 -> 4.000 bar
    ("status bits", None, ""),             # bit 0 = running, bit 1 = remote mode
]


def decode_read_response(adu: bytes) -> None:
    # MBAP header: transaction id, protocol id (0 = Modbus), length, unit id
    tid, proto, length, unit = struct.unpack(">HHHB", adu[:7])
    function = adu[7]
    print(f"transaction {tid}, unit {unit}, function 0x{function:02X}")
    if proto != 0 or length != len(adu) - 6:
        print("  malformed: protocol id or length does not match")
        return
    if function & 0x80:                       # high bit set = exception response
        code = adu[8]
        print(f"  exception {code}: {EXCEPTIONS.get(code, 'other')}")
        return
    byte_count = adu[8]
    registers = struct.unpack(f">{byte_count // 2}H", adu[9:9 + byte_count])
    for raw, (name, scale, unit_name) in zip(registers, REGISTER_MAP):
        if scale is None:
            print(f"  {name:19} raw {raw:5}  running={bool(raw & 1)} remote={bool(raw & 2)}")
        else:
            print(f"  {name:19} raw {raw:5}  {raw * scale:g} {unit_name}")


# Two responses captured from an HMI polling a pump-station PLC (bytes as hex)
decode_read_response(bytes.fromhex("0007 0000 0009 01 03 06 00EB 0FA0 0003"))
decode_read_response(bytes.fromhex("0008 0000 0003 01 83 02"))
transaction 7, unit 1, function 0x03
  outlet temperature  raw   235  23.5 °C
  discharge pressure  raw  4000  4 bar
  status bits         raw     3  running=True remote=True
transaction 8, unit 1, function 0x83
  exception 2: illegal data address

The second response is an exception: the HMI asked for registers the PLC does not have. One such answer is usually a configuration mistake. Many of them, from a device that does not normally poll this PLC, look like someone mapping the device, which is the kind of pattern OT monitoring looks for (see Asset Inventory and Monitoring).

Modbus security

Classic Modbus has no authentication, no authorization and no encryption: the request contains no field for a user, a password or a signature. Any device that can reach port 502 can read values and, unless the device itself refuses, write coils and registers. Two measures address this:

  • Modbus/TCP Security, a specification from the Modbus Organization, wraps Modbus in TLS with mutual X.509v3 certificate authentication and carries role-based authorization in a certificate extension. It uses port 802. It was first published around 2018; the current version dates from 2021. Support in installed devices is still limited.
  • Protocol-aware firewalls between zones that inspect Modbus and allow only specific function codes from specific hosts. For example, a monitoring system may only read:
# Conduit: supervisory zone -> pump-station PLC
rule 10  allow  modbus  192.0.2.60 -> 192.0.2.70:502  unit 1  function 1,2,3,4   # monitoring server: reads only
rule 90  deny   modbus  any        -> 192.0.2.70:502  function any       log alert
rule 99  deny   ip      any        -> any                                log

An HMI usually has to write a few registers as well (start and stop commands, setpoints). Then the rule allows those write codes for those register addresses only, and alerts on anything else.

Grid protocols

Electric power has its own family of protocols, because utilities need time-stamped events, reliable delivery over long and sometimes slow links, and very fast messages inside substations. The US Cybersecurity and Infrastructure Security Agency (CISA) describes the IEC protocols 101, 104 and 61850 as "more commonly used outside the United States in electric power control systems"; North American utilities lean on DNP3.

DNP3

DNP3 (Distributed Network Protocol 3) is a SCADA protocol between a control center and RTUs or IEDs, used mainly by electricity and water utilities in North America. It was standardized as IEEE 1815 and runs over serial links or TCP/UDP port 20000. Compared with Modbus it is far richer: devices keep a buffer of time-stamped events, sort data into priority classes, and can send changes without being asked (unsolicited responses), so nothing is lost when a link drops for a while. Security came later: Secure Authentication version 5, included in IEEE 1815-2012 and based on IEC/TS 62351-5, adds challenge-response authentication of critical commands; its successor, version 6 with encryption, was still in standardization in 2026. Many deployed devices use neither.

IEC 60870-5-101 and -104

The IEC 60870-5 series is the European counterpart of DNP3 for telecontrol, the communication between a grid control center and its substations. Part 101 runs over serial links; IEC 60870-5-104 carries the same application messages over TCP/IP on port 2404 and is common in European grids. Like DNP3 it supports time-stamped events and spontaneous transmission of changes, plus periodic general interrogations that refresh the full picture. The original protocol has no security; IEC 62351, the security series for power-system protocols, defines TLS profiles for 104 (part 3) and authentication for the 60870-5 family (part 5). Their adoption takes time, because every device on both ends must support it.

IEC 61850

IEC 61850 is the international standard for communication inside substations, and increasingly between substations and control centers. Instead of registers, it describes the substation itself in an object model (a circuit breaker, a measurement unit, a protection function), and it defines a configuration language so that devices from different vendors can be engineered together. It uses three main services:

  • MMS (Manufacturing Message Specification, IEC 61850-8-1): client and server over TCP/IP on the station bus, between IEDs and the station computer or SCADA, for reading values, reports and commands. It uses the ISO transport port 102.
  • GOOSE (Generic Object Oriented Substation Event): fast multicast Ethernet messages between IEDs, for example "breaker failure, trip the neighbor". GOOSE skips TCP/IP completely and is sent directly in Ethernet frames (EtherType 0x88B8), repeated quickly after every change so that a lost frame does not matter. That makes it fast enough for protection, with transfer times of a few milliseconds.
  • Sampled Values (IEC 61850-9-2): a continuous stream of digitized current and voltage measurements from the instrument transformers to the protection relays, on the process bus.

Because GOOSE and Sampled Values are plain layer 2 multicast without authentication in their basic form, anyone on the substation network can send them; IEC 62351-6 defines how to authenticate them. In practice this is one more reason why substation networks are strictly separated zones.

ICCP between control centers

ICCP (Inter-Control Center Communications Protocol), standardized as TASE.2 in IEC 60870-6, exchanges real-time data between the control centers of different grid companies: line flows, generator outputs, schedules. It runs on top of MMS. European transmission system operators exchange such data over dedicated private networks rather than the internet; ENTSO-E has long operated one, known as the Electronic Highway. IEC 62351-3 and -4 define TLS and MMS security for ICCP. The role of these exchanges in grid operation is described on The Power Grid.

OPC UA

OPC UA (OPC Unified Architecture), published as IEC 62541, is a platform-independent protocol and information model for exchanging data between industrial systems: from controllers to SCADA, historians, manufacturing systems and cloud platforms. It is the successor to the older "OPC Classic" specifications, which are still widely installed and were built on Microsoft's DCOM, notoriously hard to pass through a firewall. OPC UA uses TCP port 4840 for its binary protocol and has a registered TLS port, 4843.

Unlike the older protocols, OPC UA was designed with security in its core specification:

  • Applications authenticate each other with X.509 certificates.
  • Users authenticate with a certificate, Kerberos or a user name and password.
  • Each connection has a security mode: None, Sign or SignAndEncrypt.

PROFINET, EtherNet/IP and BACnet

In factories, controllers talk to their remote I/O, drives and robots over industrial Ethernet protocols, successors of the older serial fieldbuses.

  • PROFINET, maintained by PROFIBUS & PROFINET International (PI) and widespread in the Siemens ecosystem, is the Ethernet successor of PROFIBUS. Connection setup and acyclic data use DCE/RPC over UDP (IANA ports 34962–34964); device discovery and naming (DCP) and the cyclic real-time I/O data are sent directly in Ethernet frames for speed. Security extensions were in progress in 2026, with new port registrations for secure connections.
  • EtherNet/IP (the "IP" stands for Industrial Protocol), maintained by ODVA and common with Rockwell Automation controllers, carries the Common Industrial Protocol (CIP) over standard Ethernet: explicit messages such as configuration and reads on TCP port 44818, cyclic I/O on UDP port 2222, and a TLS/DTLS variant on port 2221.
  • BACnet (Building Automation and Control Networks) runs heating, ventilation, lighting and access systems in buildings; BACnet/IP uses UDP port 47808. Building systems are often managed by facility contractors and overlooked by security teams, while they also run the cooling of data rooms and control centers.

Polling and report by exception

Polling means the master asks every device for its values on a fixed schedule, for example every two seconds, whether anything changed or not. It is simple and predictable, and a missing answer immediately shows that a device or link is down. But on a SCADA system with hundreds of remote sites on slow links, it wastes bandwidth on unchanged values, and a change that happens just after a poll is seen only at the next one.

Report by exception turns this around: the device sends data only when a value changes by more than a configured deadband, or when a status changes, often with a timestamp from its own clock. DNP3's unsolicited responses and IEC 104's spontaneous transmission work this way. Systems that use it still run a periodic full poll (an integrity poll or general interrogation) to make sure no change was missed and that silent devices are noticed. Modbus has no report by exception: it is polling only.

For security monitoring, both patterns are a gift: industrial traffic is so regular that a new device, a new function code or a change in timing stands out against the baseline.

The protocols compared

Protocol Main use Transport Port (IANA) Security
Modbus RTU / TCP Generic device data, all sectors RS-485 serial; TCP 502 (TLS variant: 802) None in classic Modbus; Modbus/TCP Security adds TLS
DNP3 (IEEE 1815) SCADA telecontrol, North American utilities Serial; TCP/UDP 20000 Secure Authentication v5 optional; often not used
IEC 60870-5-101/104 Grid telecontrol, common in Europe Serial (101); TCP (104) 2404 None by itself; IEC 62351-3/-5
IEC 61850 (MMS, GOOSE, SV) Inside substations TCP/IP (MMS); Ethernet multicast (GOOSE, SV) 102 (MMS) IEC 62351-4/-6; often not deployed
ICCP / TASE.2 Between control centers MMS over TCP/IP 102 IEC 62351-3/-4 (TLS)
OPC UA (IEC 62541) Data exchange controller to SCADA, MES, cloud TCP 4840 (TLS: 4843) Built in: certificates, signing, encryption; mode None possible
PROFINET Factory I/O, Siemens ecosystem Ethernet (real time); UDP 34962–34964 Security extensions emerging
EtherNet/IP (CIP) Factory I/O, Rockwell ecosystem TCP and UDP 44818, 2222 (TLS: 2221) CIP Security optional
BACnet/IP Building automation UDP 47808 Mostly none in installed systems

Port numbers are useful for firewall rules and for spotting protocols in traffic, but they are conventions: a protocol can run on another port, and something else can run on port 502. Monitoring tools that parse the protocols themselves, such as the open-source Malcolm suite from CISA and Idaho National Laboratory, which understands Modbus, DNP3, IEC 104, EtherNet/IP, PROFINET, BACnet and OPC UA, tell you what is really on the wire. The general port concepts are on TCP, UDP and Ports.