Skip to content

Complete DNP3 SBO Policy

This page embeds the executable, tested multi-object DNP3 Select-Before-Operate policy from the repository. The displayed YAML is not a shortened marketing illustration.

DNP3 SBO sequence used by the complete policy example

Figure — Complete DNP3 SBO policy example mapped to the protected select/operate sequence.

What to customize

  • Select and Operate TTLs;
  • Direct Operate policy;
  • customer severity and incident metadata;
  • approved master/outstation scope, if a narrower allow-list is required;
  • final default behavior for the deployment boundary.

Operational behavior

  • malformed control identity fails closed;
  • Select request creates only a pending state;
  • only a successful matching response confirms the Select;
  • a matching Operate consumes the selected state atomically;
  • mismatched, expired, concurrent, or replayed Operate is dropped;
  • multi-point and multi-object target order is canonicalized.

Canonical YAML

# Production-style DNP3 SBO policy for complete multi-point CROB/G41 commands.
# Customize TTL and the final default policy for the deployment. The rule is
# fail-closed for malformed control requests and consumes a Select exactly once.
version: 1

rules:
  - name: dnp3-sbo-drop-incomplete-control
    order: 5
    condition: >
      dnp3.IsDNP3() && dnp3.IsRequest() && dnp3.IsControl() &&
      (!dnp3.HasCanonicalEndpoints() || !dnp3.HasControlFingerprint())
    action: drop
    log: true
    meta:
      policy: '"sbo_required"'
      violation: '"incomplete_control_identity"'
      function: 'dnp3.meta.FunctionCodeText()'

  - name: dnp3-sbo-drop-direct-operate
    order: 10
    condition: >
      dnp3.IsDNP3() && dnp3.IsRequest() &&
      dnp3.IsDirectOperate() && dnp3.HasControlFingerprint()
    action: drop
    log: true
    meta:
      policy: '"sbo_required"'
      violation: '"direct_operate"'
      master: 'dnp3.Master()'
      outstation: 'dnp3.Outstation()'
      targets: 'dnp3.ControlTargetCount()'
      fingerprint: 'dnp3.ControlFingerprint()'

  - name: dnp3-sbo-record-select-request
    order: 20
    condition: >
      dnp3.IsDNP3() && dnp3.IsRequest() && dnp3.IsSelect() &&
      dnp3.HasCanonicalEndpoints() && dnp3.HasApplicationSequence() &&
      dnp3.HasControlFingerprint()
    action: set_state
    state_key_expr: '"dnp3:sbo:" + dnp3.ControlSessionKey()'
    state_value_expr: >
      "pending|" + string(dnp3.ApplicationSequence()) + "|" +
      dnp3.ControlFingerprint()
    state_ttl: 5
    log: true
    meta:
      phase: '"select_request"'
      master: 'dnp3.Master()'
      outstation: 'dnp3.Outstation()'
      sequence: 'dnp3.ApplicationSequence()'
      targets: 'dnp3.ControlTargetCount()'
      fingerprint: 'dnp3.ControlFingerprint()'

  - name: dnp3-sbo-confirm-select-response
    order: 30
    condition: >
      dnp3.IsDNP3() && dnp3.IsResponse() &&
      dnp3.HasCanonicalEndpoints() && dnp3.HasApplicationSequence() &&
      dnp3.ControlResponseOK()
    action: transition_state
    state_key_expr: '"dnp3:sbo:" + dnp3.ControlSessionKey()'
    state_from_expr: >
      "pending|" + string(dnp3.ApplicationSequence()) + "|" +
      dnp3.ControlFingerprint()
    state_value_expr: '"selected|" + dnp3.ControlFingerprint()'
    state_ttl: 5
    log: true
    meta:
      phase: '"select_response"'
      result: '"success"'
      master: 'dnp3.Master()'
      outstation: 'dnp3.Outstation()'
      sequence: 'dnp3.ApplicationSequence()'
      targets: 'dnp3.ControlTargetCount()'
      fingerprint: 'dnp3.ControlFingerprint()'

  - name: dnp3-sbo-clear-mismatched-control-response
    order: 34
    description: Invalidate pending state when a successful response cannot confirm it.
    condition: >
      dnp3.IsDNP3() && dnp3.IsResponse() && dnp3.ControlResponseOK() &&
      state_action_rule == "dnp3-sbo-confirm-select-response" &&
      !state_action_applied
    action: delete_state
    state_key_expr: '"dnp3:sbo:" + dnp3.ControlSessionKey()'
    log: true
    meta:
      phase: '"select_response"'
      violation: '"select_response_mismatch"'
      sequence: 'dnp3.ApplicationSequence()'
      targets: 'dnp3.ControlTargetCount()'
      fingerprint: 'dnp3.ControlFingerprint()'

  - name: dnp3-sbo-clear-failed-control-response
    order: 35
    condition: >
      dnp3.IsDNP3() && dnp3.IsResponse() &&
      dnp3.HasCanonicalEndpoints() && dnp3.HasControlFingerprint() &&
      !dnp3.ControlResponseOK()
    action: delete_state
    state_key_expr: '"dnp3:sbo:" + dnp3.ControlSessionKey()'
    log: true
    meta:
      phase: '"control_response"'
      result: '"failed"'
      master: 'dnp3.Master()'
      outstation: 'dnp3.Outstation()'
      targets: 'dnp3.ControlTargetCount()'
      fingerprint: 'dnp3.ControlFingerprint()'
      iin: 'dnp3.meta.IIN()'

  - name: dnp3-sbo-consume-operate
    order: 40
    condition: >
      dnp3.IsDNP3() && dnp3.IsRequest() && dnp3.IsOperate() &&
      dnp3.HasCanonicalEndpoints() && dnp3.HasControlFingerprint()
    action: transition_state
    state_key_expr: '"dnp3:sbo:" + dnp3.ControlSessionKey()'
    state_from_expr: '"selected|" + dnp3.ControlFingerprint()'
    state_value: consumed
    state_ttl: 1
    log: true
    meta:
      phase: '"operate"'
      master: 'dnp3.Master()'
      outstation: 'dnp3.Outstation()'
      targets: 'dnp3.ControlTargetCount()'
      fingerprint: 'dnp3.ControlFingerprint()'

  - name: dnp3-sbo-accept-valid-operate
    order: 50
    condition: >
      dnp3.IsDNP3() && dnp3.IsRequest() && dnp3.IsOperate() &&
      state_action_rule == "dnp3-sbo-consume-operate" &&
      state_action_applied
    action: accept
    log: true
    meta:
      policy: '"sbo_required"'
      result: '"valid_operate"'
      master: 'dnp3.Master()'
      outstation: 'dnp3.Outstation()'
      targets: 'dnp3.ControlTargetCount()'
      fingerprint: 'dnp3.ControlFingerprint()'

  - name: dnp3-sbo-drop-invalid-operate
    order: 60
    condition: 'dnp3.IsDNP3() && dnp3.IsRequest() && dnp3.IsOperate()'
    action: drop
    log: true
    meta:
      policy: '"sbo_required"'
      violation: '"operate_without_matching_select"'
      master: 'dnp3.Master()'
      outstation: 'dnp3.Outstation()'
      targets: 'dnp3.ControlTargetCount()'
      fingerprint: 'dnp3.ControlFingerprint()'

  - name: dnp3-sbo-default-accept
    order: 1000
    condition: 'true'
    action: accept

Source path: rules/muli_state_rules/dnp3_sbo_multi_control.yaml

Test evidence

The Rule Engine test suite loads every YAML in rules/muli_state_rules. Dedicated end-to-end tests exercise Select request, successful response, Operate, mismatch, failed response, expiration, replay, multi-object ordering, and concurrent consumption.

GOCACHE=/tmp/l2proxy-go-cache go test ./internal/rule_engine/...
GOCACHE=/tmp/l2proxy-go-cache go test -race ./internal/rule_engine