Industrial Protocols
The languages of industrial devices: Modbus, DNP3, IEC 60870-5-104, IEC 61850, OPC UA, PROFINET and EtherNet/IP — what each is for and why most were designed without security.
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,SignorSignAndEncrypt.
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.