DEV Community

Cover image for Hands-On DNP3 Protocol Analysis: State Machines, Transport Mechanics, and Secure Authentication (SA v5)
404Saint
404Saint

Posted on

Hands-On DNP3 Protocol Analysis: State Machines, Transport Mechanics, and Secure Authentication (SA v5)

By RUGERO Tesla (@404saint)

In my previous article, Beyond Cyclic Polling: Objects, Classes, and Outstation State in DNP3, we explored how DNP3 (IEEE 1815) organizes information through object groups, event classes, and Internal Indications (IIN). That discussion focused on the protocol's architecture and how an outstation exposes operational state to a SCADA master.

Understanding the specification, however, is only half of the story.

The more interesting question is what happens inside an implementation when frames begin arriving on the wire. How does an outstation process malformed transport fragments? What state transitions occur when control commands arrive out of sequence? How are administrative function codes handled internally? What actually changes when Secure Authentication (SA v5) is introduced?

To answer those questions, I built an isolated DNP3 research laboratory entirely in Python. Rather than relying on vendor implementations or third-party protocol libraries, I implemented custom outstation targets, protocol parsers, and master-side test harnesses to observe protocol behavior from the raw socket layer upward. This made it possible to inspect state transitions, reproduce protocol edge cases, and verify implementation behavior under controlled conditions.

The repository contains:

  • A custom DNP3 outstation implementation
  • A Secure Authentication (SA v5) enabled outstation
  • Individual master-side test suites for each research phase
  • Packet captures for every experiment
  • Detailed technical notes documenting protocol behavior and implementation observations

You can explore the complete project here:

GitHub Repository

https://github.com/404saint/industrial-protocol-labs/tree/main/dnp3-research

This article summarizes five phases of that research:

  1. Reconnaissance and protocol fingerprinting
  2. Control execution and Select-Before-Operate validation
  3. Administrative state transitions
  4. Transport-layer behavior and unsolicited messaging
  5. Secure Authentication (SA v5)

Phase 1 — Reconnaissance & Internal Indication Analysis

Before attempting any control operations, it is useful to understand what information a DNP3 endpoint exposes during normal communication.

Unlike many industrial protocols that require several exchanges before meaningful information becomes available, a standard Class 0 Integrity Read (FC 0x01) immediately returns an Application Layer response containing the Internal Indications (IIN) field.

Those two bytes provide a surprisingly useful snapshot of the outstation's operational state.

Depending on the implementation, the response may reveal whether:

  • the device has recently restarted,
  • time synchronization is required,
  • event buffers contain pending data,
  • hardware faults have been detected,
  • unsupported function codes were received,
  • parameter validation has failed.

Because the IIN field is part of normal protocol behavior, it represents one of the earliest opportunities to understand how an outstation is currently operating.

The first phase of the lab simply performs a baseline integrity poll and parses those response flags.

Running the reconnaissance suite against the simulated outstation.

Figure 1. recon_enum.py issuing a Class 0 Integrity Read and decoding the returned Internal Indications.

On the opposite side of the connection, the custom outstation processes the incoming FT3 frame, builds the Application Layer response, and returns the corresponding IIN values.

Outstation processing an incoming reconnaissance request.

Figure 2. The custom outstation parsing the request and constructing the response APDU.

One observation from this phase is the persistence of the Device Restart indication (IIN1.7). Following initialization, this flag remains asserted until the master acknowledges the restart condition. Although simple, this mechanism allows supervisory systems to distinguish between continuous operation and a recently restarted device without relying on external monitoring.

This phase also establishes an important baseline for later experiments. Once administrative commands, transport anomalies, and authentication mechanisms are introduced, changes in the IIN field become an effective way to observe how internal state evolves throughout the protocol.


Phase 2 — Control Execution & Select-Before-Operate Validation

One of DNP3's defining characteristics is its emphasis on deterministic control execution.

Rather than immediately actuating field devices whenever a command arrives, IEEE 1815 defines a Select-Before-Operate (SBO) workflow for Control Relay Output Blocks (CROB). The design intentionally separates validation from execution, reducing the likelihood of accidental operations caused by communication errors or malformed traffic.

Under the SBO model, a control operation normally follows two stages:

  1. Select (FC 0x03) : The master requests control of a specific output point. The outstation validates the request and temporarily reserves that point.

  2. Operate (FC 0x04) : If the reservation remains valid, the outstation executes the requested operation and clears the reservation state.

The protocol also defines Direct Operate (FC 0x05), which bypasses the reservation phase entirely and requests immediate execution.

To understand how the implementation enforced these different execution paths, I evaluated two scenarios:

  • issuing a Direct Operate request without a preceding Select, and
  • transmitting an Operate request against an unarmed point.

The master-side test suite executed both cases sequentially.

Control execution test suite issuing Direct Operate and Operate commands.

Figure 3. control_attacks.py exercising multiple CROB execution paths.

The first experiment demonstrated that Direct Operate followed its intended execution path. Because FC 0x05 explicitly bypasses the reservation stage, the simulated outstation accepted the request and executed the control action immediately.

The second experiment intentionally violated the SBO state machine.

An Operate request (FC 0x04) was transmitted without a matching active Select. Rather than executing the command, the outstation rejected the request because no reservation existed for the specified control point.

The internal logs clearly illustrate that transition.

Outstation validating Select-Before-Operate state transitions.

Figure 4. The outstation detecting an invalid Operate sequence and rejecting the request.

Although these experiments were performed against a custom implementation, they demonstrate why protocol state machines matter just as much as packet syntax. A correctly implemented DNP3 endpoint does more than decode bytes... it continuously tracks execution context, validates command ordering, and determines whether a requested action is currently valid.

That distinction becomes even more important when administrative function codes begin modifying the outstation's runtime state.


Phase 3 — Administrative State Transitions

Control commands are only one part of the DNP3 application layer.

IEEE 1815 also defines a collection of administrative function codes that influence the lifecycle of an outstation itself. Rather than manipulating individual field points, these commands affect the runtime environment responsible for processing telemetry, maintaining event history, and coordinating system time.

Among the most significant administrative functions are:

Function Code Purpose
0x0D Cold Restart
0x0E Warm Restart
0x12 Stop Application
0x18 Write Time

Unlike CROB operations, these requests alter the state of the outstation rather than the state of an individual control point.

To observe these transitions, I executed each function sequentially against the simulated outstation while monitoring both protocol responses and internal application state.

Administrative function test suite executing restart and lifecycle commands.

Figure 5. system_attacks.py exercising administrative function codes against the baseline outstation.

Several interesting behaviors emerged during testing.

Time Synchronization

The Write Time function (FC 0x18) updates the outstation's internal clock. In the lab, submitting a new timestamp immediately modified the runtime clock and cleared the Need Time indication (IIN1.4), demonstrating how time synchronization directly affects protocol state.

Although expected behavior, it reinforces how closely event sequencing depends on accurate clocks in industrial systems.

Warm Restart

Issuing a Warm Restart (FC 0x0E) temporarily interrupted application processing while preserving much of the runtime context.

The response included a restart delay object, allowing the master to estimate when normal communications could safely resume.

Stop Application

The most interesting behavior occurred after issuing Stop Application (FC 0x12).

Rather than terminating the network connection, the transport and link layers continued accepting frames while the application layer stopped processing operational requests.

Subsequent read operations were therefore rejected, not because the TCP session had failed, but because the application itself had transitioned into a halted state.

That distinction is subtle but important.

From a network perspective, the device still appeared reachable.

From an application perspective, however, it was no longer servicing requests.

The outstation logs clearly illustrate these transitions.

Outstation processing administrative state transitions.

Figure 6. Runtime state changes following administrative commands, including clock updates, application halt, and restart processing.

This phase reinforced an important implementation detail: protocol availability is not determined solely by socket state. Internal application state can significantly alter how an endpoint responds, even while lower protocol layers continue operating normally.


Phase 4 — Transport Reassembly & Unsolicited Messaging

The DNP3 Transport Pseudo-Layer is often overlooked because it consists of only a single control byte.

Despite its simplicity, that byte is responsible for coordinating fragmentation and reassembly across every multi-frame Application Protocol Data Unit (APDU).

Bit Position

7      6      5 4 3 2 1 0
+------+------+
| FIR  | FIN  | Sequence |
+------+------+
Enter fullscreen mode Exit fullscreen mode

The transport header contains three pieces of information:

  • FIR (First Fragment) : Indicates the beginning of an APDU stream.
  • FIN (Final Fragment) : Marks the final fragment in the stream.
  • Sequence Number : A six-bit counter used to order fragments.

Most DNP3 communications fit within a single frame, making fragmentation relatively uncommon during normal operation.

Nevertheless, any implementation supporting larger payloads must correctly maintain fragment state, sequence tracking, and timeout behavior.

To evaluate that logic, I intentionally exercised several edge cases.


Reassembly State Validation

The first experiment transmitted a frame marked as the final fragment without ever establishing an active fragment stream.

From the parser's perspective, this meant receiving the end of a conversation that had never started.

The master-side test suite generated the malformed transport header and transmitted it directly to the outstation.

Transport-layer edge case testing.

Figure 7. transport_attacks.py generating malformed transport sequences and unsolicited response traffic.

Rather than attempting to process the payload, the parser detected the invalid state transition and rejected the frame.

The corresponding runtime logs clearly show the parser identifying the missing fragment context before terminating reassembly.

Transport parser rejecting malformed fragment state.

Figure 8. The transport engine detecting an invalid fragment sequence during APDU reassembly.

The second experiment intentionally opened a fragmented stream without completing it.

Instead of immediately raising an error, the parser allocated reassembly state and advanced the expected sequence counter.

This illustrates another important implementation consideration.

Transport parsers do more than decode individual packets. They maintain state across multiple frames. Any robust implementation must therefore handle incomplete streams, unexpected sequence numbers, retransmissions, and timeout cleanup without exhausting internal resources.


Unsolicited Response Processing

The final experiment in this phase focused on Unsolicited Responses (FC 0x82).

Unlike conventional request-response communication, unsolicited messages allow an outstation to report significant events without waiting for the master to poll.

Typical uses include:

  • binary input changes,
  • analog threshold crossings,
  • event notifications,
  • alarm conditions.

Within the laboratory, I generated a synthetic unsolicited response carrying an Analog Input object (Group 30 Variation 1) to observe how the implementation processed an event arriving outside the normal polling cycle.

Because the frame matched the expected protocol format, the simulated receiver accepted the message and updated its internal state accordingly.

The purpose of this experiment was not to demonstrate a universal weakness in DNP3 deployments, but to study how unsolicited messaging is integrated into protocol processing and why implementations must validate message origin, sequencing, and where supported, authentication before committing unsolicited event data.


5. DNP3 Secure Authentication (SA v5) Verification

One of the most significant limitations of legacy DNP3 is the absence of built-in authentication for critical control operations. Any device capable of reaching an outstation can potentially issue administrative or control function codes unless additional security controls are deployed.

To address this, IEEE 1815-2012 introduced DNP3 Secure Authentication Version 5 (SA v5), later aligned with IEC 62351-5. Rather than encrypting protocol traffic, SA v5 protects sensitive operations through a challenge-response mechanism that verifies message authenticity and integrity before control commands are executed.

To better understand this workflow, I implemented a dedicated Secure Authentication outstation (outstation-sa.py) alongside a companion master test harness (master_test_runner.py). Together they reproduce the complete authentication state machine, allowing each stage of the protocol exchange to be observed in isolation.

Master test runner executing Secure Authentication validation

The master-side test harness executing the complete Secure Authentication validation suite, including challenge negotiation, HMAC verification, session establishment, and authenticated control execution.

Walking Through the Authentication Workflow

The authentication sequence follows a well-defined state machine.

  1. Baseline Read (FC 0x01)

Standard telemetry reads continue to operate normally. Since these requests are considered non-critical, they do not require cryptographic validation and are processed without additional authentication overhead.

  1. Challenge Negotiation (FC 0x20, Object 120 Variation 1)

Before executing a protected operation, the master initiates the authentication process. The outstation responds with a challenge containing a nonce, sequence information, and additional parameters required to construct a valid authentication response.

  1. Challenge Verification (FC 0x20, Object 120 Variation 2)

Using the pre-shared session key, the master computes an HMAC over the challenge data and submits the resulting authentication payload. The outstation independently performs the same calculation and compares the received digest against its own result.

  1. Authenticated Session Establishment

If verification succeeds, the outstation marks the communication session as authenticated and returns its current Secure Authentication status (Object Group 120). Subsequent protected operations are now permitted within the authenticated session.

  1. Protected Control Execution

With authentication complete, privileged operations, such as Select-Before-Operate (FC 0x03) are processed normally. Requests that would otherwise require authorization can now proceed under the active authenticated session.

Dedicated Secure Authentication outstation processing Group 120 objects

The Secure Authentication outstation validating Group 120 authentication objects, updating session state, and authorizing protected control operations after successful HMAC verification.


Engineering Observations

Building this Secure Authentication implementation reinforced an important distinction that is often overlooked: Secure Authentication is not encryption.

Application payloads remain visible on the wire, making DNP3 traffic fully inspectable by monitoring platforms and industrial intrusion detection systems. Instead, SA v5 protects the integrity and authenticity of critical operations by ensuring that protected commands originate from an authenticated master and have not been modified or replayed in transit.

Across the five phases of this research, several observations consistently emerged:

  • DNP3 is fundamentally a stateful protocol. Correct behavior depends not only on packet contents, but also on transport state, control sequencing, and session context.
  • Administrative function codes deserve the same operational scrutiny as relay control commands, as they directly influence device availability and operational visibility.
  • Transport-layer correctness is just as important as application-layer validation. Robust fragment handling and sequence tracking are essential for reliable implementations.
  • Secure Authentication significantly strengthens DNP3 by introducing integrity verification and replay protection for sensitive operations, while remaining compatible with existing protocol deployments.

Perhaps the biggest lesson from this project was methodological.

Reading the specification explains what the protocol is designed to do. Packet captures show what happened on the network. Building an implementation exposes the internal decisions that connect those two perspectives.

That shift,, from interacting with industrial protocols to implementing them, has fundamentally changed how I approach protocol research. Understanding parser behavior, state machines, and implementation details provides insights that are difficult to obtain through passive observation alone.

The complete research laboratory, including the custom outstation implementations, master-side tooling, packet captures, screenshots, and detailed technical notes, is available in the accompanying GitHub repository.

As with every project in this series, all experiments were performed in an isolated laboratory environment for protocol research, defensive engineering, and implementation analysis. No testing was conducted against production industrial systems.

Top comments (0)