iec104ctl — IEC 60870-5-104 Command-Line Tool: Interrogate, Monitor, Command, Transfer Files & Simulate an RTU
iec104ctl is a command-line toolkit for IEC 60870-5-104 (IEC 104), the telecontrol protocol of power system SCADA, in both roles. As a controlling station it interrogates an RTU, gateway or IED, watches the events it sends, issues commands and set points (direct or select-before-operate), synchronizes its clock and transfers files. As a controlled station it simulates an RTU from a model of points, commands and files, so that a SCADA front end or a protocol gateway has something to talk to. Both roles speak plain TCP, TLS (IEC 62351-3) and the application-layer security of IEC 62351-5.
It is built for commissioning and site acceptance tests, point-to-point checks, troubleshooting a link between a control centre and a substation, and hardware-free test setups. Output can be plain text, a table or JSON with stable exit codes, so it works in scripts and CI. It is written in Go on top of go-iec104.
Quick links: Quick start · Features · Installation · IEC 104 in two minutes · Connecting · Commands · Simulated station · FAQ
Responsible use: iec104ctl sends real telecontrol requests. Only use it on stations and networks you own or are authorized to test.
client commandoperates switchgear and changes set points,client clockandclient resetchange the station itself, andclient file putcan deliver a configuration: know what an address controls before you send to it. A station often accepts few connections and sends its events on only one: connecting to a station that is in service can take its events away from the control centre.
- See the whole process image —
client interrogateruns a general or group interrogation and lists every value with its quality and time tag;client countersreads the integrated totals. - Watch a station live —
client monitorstays connected and prints every spontaneous event and periodic value as it arrives, as text, a table or JSON Lines. - Operate, carefully —
client commandsends single, double, step, set point and bitstring commands, direct or select-before-operate, with qualifier and optional time tag. Everything is validated first, and a command is sent exactly once. - Find what is there —
discoversweeps subnets for IEC 104 stations with a single test frame;client scanfinds the stations (common addresses) behind one connection. - Test the link —
client pingmeasures the round trip of test frames or test commands;--debugtraces every frame, decoded. - Files — list the directory of a station, fetch disturbance records, deliver a file.
- Simulate an RTU —
server startserves a station from a YAML model: values that change by themselves, commands with select-before-operate, events that are queued and delivered, redundant connections. - Secure — IEC 62351-3 TLS with mutual authentication, and IEC 62351-5 application-layer security (
--secure: certificates by authority or pinned, HMAC or AES-256-GCM, roles at the station), for client and server, separately or together. - Script-friendly —
--format text|json|table, a single JSON error object on failure, stable exit codes andIEC104CTL_*environment variables. - Pre-built binaries — Linux, macOS and Windows (x86-64 and ARM) from GitHub Releases, with SHA-256 checksums; no runtime to install.
Download a binary from GitHub Releases (see Installation for copy-paste commands for Linux, macOS and Windows, or build from source). No station at hand? Start the built-in demo station in one terminal:
iec104ctl server startand talk to it from another (replace 127.0.0.1 with your station):
# What does this station hold? (general interrogation)
iec104ctl client interrogate --ip 127.0.0.1 --ca 1
# Watch events and measured values as they arrive, until Ctrl-C
iec104ctl client monitor --ip 127.0.0.1 --interrogate
# Open a breaker: single command, direct execute
iec104ctl client command --ip 127.0.0.1 --type single --ioa 5000 --value off
# Operate a disconnector with select-before-operate
iec104ctl client command --ip 127.0.0.1 --type double --ioa 5001 --value off --mode sbo
# Is the link alive, and how fast?
iec104ctl client ping --ip 127.0.0.1
# Which stations are on this subnet?
iec104ctl discover --subnets 192.168.1.0/24
# The same interrogation as JSON, for a script
iec104ctl client interrogate --ip 127.0.0.1 --format jsonAddresses are plain numbers: the common address (--ca) names the station, the information object address (--ioa) the point. See IEC 104 in two minutes.
IEC 104 test tools are mostly graphical: protocol test harnesses and simulators with a window per connection, many of them commercial and Windows-only. Protocol libraries are the other end of the range: they do everything, once you have written a program. iec104ctl sits in between.
| iec104ctl | GUI test tools and simulators | Protocol libraries | Wireshark | |
|---|---|---|---|---|
| Interrogate, monitor, command without writing code | ✅ | ✅ | — | — |
| Works over SSH, on a headless gateway or a jump host | ✅ | — | ✅ | partial (tshark) |
| Scriptable: JSON output, exit codes, environment variables | ✅ | rarely | ✅ (it is code) | partial |
| Simulate a controlled station from a text file | ✅ | ✅ | with code | — |
| Runs in CI, in a container, on a Raspberry Pi | ✅ | — | ✅ | ✅ |
| Decodes traffic between two other parties | — | some | — | ✅ |
| Single static binary, Linux / macOS / Windows | ✅ | — | — | — |
They combine well: use iec104ctl to drive and check a station from the command line, a GUI tool when you want to click through a point list, and Wireshark when you need to see what two other devices say to each other.
- Commissioning and site acceptance tests — Interrogate the station, compare with the signal list, operate each command with select-before-operate and watch the return information arrive.
- Point-to-point tests — Leave
client monitorrunning while someone operates the primary equipment; every change is printed with its time tag. - Troubleshooting a link —
client pingand--debugshow whether the station answers, how fast, and what exactly is on the wire. - Front-end and gateway development — Point your SCADA front end or protocol gateway at
server startand get a station that answers interrogations, executes commands and produces events. - Network inventory — Sweep subnets for IEC 104 stations and list the common addresses behind each.
- Automation and CI — Use
--format json, exit codes and environment variables in scripts and pipelines; run the simulated station as a test fixture. - Collecting records — Fetch disturbance records and logs from a station's file directory.
- Why iec104ctl?
- Quick start
- How it compares
- Use cases
- Features
- Installation
- IEC 104 in two minutes
- Usage
- Environment variable configuration
- FAQ
- Project structure
- Development
- Version history
- Contributing
- License
| Feature | Description |
|---|---|
| Interrogate | General (station) and group interrogation (C_IC_NA_1): every value with quality and time tag, optionally filtered by type and address. |
| Counters | Counter interrogation (C_CI_NA_1), general or per counter group, with read, freeze, freeze-with-reset and reset. |
| Read | Read command (C_RD_NA_1) for a list or range of information object addresses. |
| Monitor | Stay connected and stream every value the station sends: spontaneous, periodic, background, return information. Reconnects, optional general interrogation, filters, --count and --duration. |
| Command | Single, double, regulating step, set point (normalized, scaled, short floating point) and bitstring commands; direct execute, select, select-before-operate and cancel; qualifier of command; optional CP56Time2a time tag; optional wait for the activation termination. |
| Clock | Clock synchronization (C_CS_NA_1) with the current or a given time, in the time zone of the station. |
| Reset | Reset process command (C_RP_NA_1): general reset of the process, or reset of pending events. |
| Ping | Link test frames (TESTFR act/con) or test commands (C_TS_TA_1) at an interval, with round-trip times. |
| Scan | Find the common addresses behind one connection: one interrogation to the broadcast address, or a sweep of a range. |
| Files | File transfer of IEC 60870-5-101/104: list the directory, fetch a file, deliver a file, with checksums verified. |
| Types | List the type identifications iec104ctl knows, with direction and description. |
| Discover | Find IEC 104 stations on subnets with a non-intrusive probe (one test frame; data transfer is not started). |
| Server | A simulated controlled station from a YAML or JSON model: interrogations, read, counters, commands with select-before-operate, clock, reset, files and uploads, simulated values, queued events, redundant connections, TLS, IEC 62351-5 security with roles, allow-list. |
| Transports | TCP, and TLS (IEC 62351-3) with server verification and client certificates, selected with --url. See Connecting to a station. |
| Security | The application-layer security of IEC 62351-5 (A-Profile of IEC TS 60870-5-7) on every client command and on the simulated station: --secure, certificates trusted by authority or pinned, HMAC-SHA-256 or AES-256-GCM, role-based access control at the station. See Connecting to a station. |
| Protocol parameters | k, w, t0-t3, originator address, and the ASDU field sizes of IEC 60870-5-101 gateways (cause, common address, object address). |
| Type coverage | Every monitoring type of IEC 60870-5-104 including the CP24Time2a and CP56Time2a variants, protection events, packed single points, parameters and file transfer, and the security types of IEC 62351-5. |
--format |
Every command that prints a result accepts --format text|json|table (or IEC104CTL_FORMAT); streams write JSON Lines. |
--debug |
Trace every frame sent and received, decoded, to stderr. |
| Completion | Shell completion for bash, zsh, fish and PowerShell; flags with a fixed set of values (--format, --type, --mode, --method, …) complete to those values. |
The following tree view gives the overview of available commands.
iec104ctl
├── client
│ ├── interrogate General or group interrogation (C_IC_NA_1)
│ ├── counters Counter interrogation (C_CI_NA_1)
│ ├── read Read information objects (C_RD_NA_1)
│ ├── monitor Print every value the station sends, until interrupted
│ ├── command Process command: single, double, step, set point, bitstring (execute | select | sbo | cancel)
│ ├── clock Clock synchronization (C_CS_NA_1)
│ ├── reset Reset process command (C_RP_NA_1)
│ ├── ping Test frames (TESTFR) or test commands (C_TS_TA_1) with round-trip time
│ ├── scan Find the stations (common addresses) behind a connection
│ ├── file
│ │ ├── list List the file directory
│ │ ├── get Fetch a file
│ │ └── put Deliver a file
│ └── types List the ASDU type identifications
├── server
│ ├── start Serve a simulated station from a model (or the built-in demo)
│ └── generate-config Write an example station model
├── discover Find IEC 60870-5-104 stations on subnets
├── completion Generate shell completion script (bash|zsh|fish|powershell)
└── version Print version
The easiest way to install iec104ctl is to download a pre-built binary from the GitHub Releases page. No Go toolchain is required.
| Platform | CPU | Download |
|---|---|---|
| Linux | x86-64 | iec104ctl-<version>-linux-amd64.tar.gz |
| Linux | 64-bit ARM (Raspberry Pi 4/5 with a 64-bit OS, AWS Graviton) | iec104ctl-<version>-linux-arm64.tar.gz |
| Linux | 32-bit ARM (Raspberry Pi with a 32-bit OS) | iec104ctl-<version>-linux-armv7.tar.gz |
| macOS | Apple Silicon (M1 and later) | iec104ctl-<version>-darwin-arm64.tar.gz |
| macOS | Intel | iec104ctl-<version>-darwin-amd64.tar.gz |
| Windows | x86-64 | iec104ctl-<version>-windows-amd64.zip |
| Windows | ARM64 | iec104ctl-<version>-windows-arm64.zip |
Each release also contains iec104ctl-<version>-checksums.txt (SHA-256) so you can verify your download. The commands below fetch the latest release, verify its checksum and install it. Afterwards, check that it works:
iec104ctl version# 1. Find the latest version and your CPU architecture
VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/otfabric/iec104ctl/releases/latest | sed 's|.*/v||')
case "$(uname -m)" in
x86_64) ARCH=amd64 ;;
aarch64|arm64) ARCH=arm64 ;;
armv7l) ARCH=armv7 ;;
*) echo "Unsupported architecture: $(uname -m)" ;;
esac
# 2. Download the archive and the checksum file
FILE="iec104ctl-${VERSION}-linux-${ARCH}.tar.gz"
BASE="https://github.com/otfabric/iec104ctl/releases/download/v${VERSION}"
curl -fsSLO "${BASE}/${FILE}"
curl -fsSLO "${BASE}/iec104ctl-${VERSION}-checksums.txt"
# 3. Verify the download (should print "OK")
grep "${FILE}" "iec104ctl-${VERSION}-checksums.txt" | sha256sum -c -
# 4. Extract and install (the binary in the archive carries the version and platform in its name)
mkdir -p iec104ctl-dl && tar -xzf "${FILE}" -C iec104ctl-dl
sudo install -m 0755 "$(find iec104ctl-dl -type f -name 'iec104ctl*' | head -n1)" /usr/local/bin/iec104ctlNo sudo? Install into your home directory instead (and make sure ~/.local/bin is in your PATH):
mkdir -p ~/.local/bin
install -m 0755 "$(find iec104ctl-dl -type f -name 'iec104ctl*' | head -n1)" ~/.local/bin/iec104ctl# 1. Find the latest version and your CPU architecture (arm64 = Apple Silicon, x86_64 = Intel)
VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/otfabric/iec104ctl/releases/latest | sed 's|.*/v||')
case "$(uname -m)" in
arm64) ARCH=arm64 ;;
x86_64) ARCH=amd64 ;;
esac
# 2. Download the archive and the checksum file
FILE="iec104ctl-${VERSION}-darwin-${ARCH}.tar.gz"
BASE="https://github.com/otfabric/iec104ctl/releases/download/v${VERSION}"
curl -fsSLO "${BASE}/${FILE}"
curl -fsSLO "${BASE}/iec104ctl-${VERSION}-checksums.txt"
# 3. Verify the download (should print "OK")
grep "${FILE}" "iec104ctl-${VERSION}-checksums.txt" | shasum -a 256 -c -
# 4. Extract and install (the binary in the archive carries the version and platform in its name)
mkdir -p iec104ctl-dl && tar -xzf "${FILE}" -C iec104ctl-dl
sudo install -m 0755 "$(find iec104ctl-dl -type f -name 'iec104ctl*' | head -n1)" /usr/local/bin/iec104ctlIf you downloaded the archive with a web browser instead of curl and macOS refuses to run the binary ("cannot be verified"), remove the quarantine flag: xattr -d com.apple.quarantine /usr/local/bin/iec104ctl.
# 1. Find the latest version and your CPU architecture
$version = (Invoke-RestMethod https://api.github.com/repos/otfabric/iec104ctl/releases/latest).tag_name.TrimStart('v')
$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' }
# 2. Download the archive and the checksum file
$file = "iec104ctl-$version-windows-$arch.zip"
$base = "https://github.com/otfabric/iec104ctl/releases/download/v$version"
Invoke-WebRequest "$base/$file" -OutFile "$env:TEMP\$file"
Invoke-WebRequest "$base/iec104ctl-$version-checksums.txt" -OutFile "$env:TEMP\iec104ctl-checksums.txt"
# 3. Verify the download (throws an error on mismatch)
$expected = ((Select-String -Path "$env:TEMP\iec104ctl-checksums.txt" -Pattern $file).Line -split '\s+')[0]
$actual = (Get-FileHash "$env:TEMP\$file" -Algorithm SHA256).Hash
if ($expected -ne $actual) { throw "Checksum mismatch for $file" }
# 4. Extract to a per-user folder and name the program iec104ctl.exe
# (the .exe in the archive carries the version and platform in its name)
$dir = "$env:LOCALAPPDATA\Programs\iec104ctl"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Expand-Archive "$env:TEMP\$file" -DestinationPath "$env:TEMP\iec104ctl-dl" -Force
$exe = Get-ChildItem "$env:TEMP\iec104ctl-dl" -Recurse -Filter 'iec104ctl*.exe' | Select-Object -First 1
Copy-Item $exe.FullName "$dir\iec104ctl.exe" -Force
# 5. Add iec104ctl to your user PATH (then open a new terminal)
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if ($userPath -notlike "*$dir*") {
[Environment]::SetEnvironmentVariable('Path', "$userPath;$dir", 'User')
}If Windows SmartScreen warns about an unrecognised app, choose More info → Run anyway.
- Open the latest release.
- Download the archive for your platform from the table above.
- Extract it, rename the binary to
iec104ctl(iec104ctl.exeon Windows) and move it to a folder on yourPATH.
Run the same commands again. They always fetch the latest release and overwrite the previous binary.
Requires Go 1.25 or later:
git clone https://github.com/otfabric/iec104ctl.git
cd iec104ctl
make buildmake build runs the checks and writes the binary to bin/iec104ctl (see Building). With a Go toolchain you can also install straight from the module:
go install github.com/otfabric/iec104ctl@latestIEC 60870-5-104 carries the telecontrol messages of IEC 60870-5-101 over TCP, by default on port 2404. One side is the controlling station (the control centre, a SCADA front end: the client), the other the controlled station (an RTU, a substation gateway, an IED: the server).
| Term | Meaning | In iec104ctl |
|---|---|---|
| Common address (CA) | The address of a station. One connection can lead to several: a gateway answers for every station behind it. 65535 is the broadcast address | --ca |
| Information object address (IOA) | The address of one point within a station: a breaker position, a voltage, a command | --ioa |
| Type identification | What kind of object an ASDU carries. M_… types travel in monitor direction (station → centre), C_… in control direction, F_… are file transfer. M_SP_NA_1 is a single point, M_ME_NC_1 a floating point measured value, C_SC_NA_1 a single command. A suffix such as _TB_1 marks the variant with a time tag |
--type, client types |
| Cause of transmission (COT) | Why an ASDU was sent: spontaneous (a change), periodic, interrogated-station (answer to a general interrogation), activation / activation-con / activation-term (a command and its answers) |
shown with every value |
| Quality | Flags that qualify a value: IV invalid, NT not topical, SB substituted, BL blocked, OV overflow |
shown with every value |
| General interrogation | The request that makes a station send its whole process image. Every station supports it | client interrogate |
| STARTDT / STOPDT | A connection carries data only after the controlling station has started data transfer. A station with several connections sends its events on one of them | done by every client command |
| Select before operate | A command in two steps: the station first confirms that the object is free and the command acceptable (select), then carries it out (execute) | client command --mode sbo |
k, w, t0-t3 |
The window sizes and timers of the transport: how many frames may be unacknowledged, and when a silent link is given up | --k, --w, --t0 … --t3 |
There are no names on the wire: a station does not tell you that IOA 1000 is "Voltage L1". The signal list of the station is where addresses get their meaning.
iec104ctl [command] [flags]
iec104ctl client interrogate
iec104ctl client counters
iec104ctl client read
iec104ctl client monitor
iec104ctl client command
iec104ctl client clock
iec104ctl client reset
iec104ctl client ping
iec104ctl client scan
iec104ctl client file list
iec104ctl client file get
iec104ctl client file put
iec104ctl client types
iec104ctl server start
iec104ctl server generate-config
iec104ctl discover
iec104ctl versionEvery command has --help, with a description and examples.
Global flag: --debug — trace every frame sent and received to stderr, with the ASDU decoded and in hex, and print the chain of causes after an error.
Output format (--format): every command that prints a result accepts --format text (default), json or table, or set IEC104CTL_FORMAT. It controls only the result on stdout. Progress, connection state, summaries and the debug trace go to stderr, so stdout can be piped or parsed as it is. The JSON shapes are a stable contract: see docs/json-output.md.
Streams: client monitor, client ping and server start write one record as it happens: one line (text), one JSON object per line (json, JSON Lines) or one table row (table). They run until interrupted with Ctrl-C, which ends them normally.
Fatal errors: with --format json a command writes a single JSON error object to stdout on failure (fields kind, code, message); with text or table it writes a single error: line to stderr and leaves stdout empty. Exit codes are stable:
| Code | Meaning |
|---|---|
| 0 | Success |
| 2 | Usage: unknown command or flag, missing required flag |
| 3 | Invalid input: nothing was sent |
| 4 | Transport: cannot connect, connection lost, TLS failure |
| 5 | Timeout: no answer |
| 6 | The station refused the request, or a protocol error |
| 7 | Partial success: a valid result that holds failures |
| 10 | Output or internal error |
| 130 | Interrupted before a result |
See docs/exitcodes.md for the details and the code values.
Shell completion: iec104ctl completion bash (or zsh, fish, powershell) prints a completion script; for example source <(iec104ctl completion bash). Flags with a fixed set of values complete to those values, --type to the type names.
Every iec104ctl client command takes the station as a URL, or as an address and a port. The two styles are mutually exclusive, and one of them is required.
| Flags | Connects to |
|---|---|
--url tcp://host[:port] |
IEC 104 over TCP; port 2404 when omitted |
--url tls://host[:port] |
IEC 104 over TLS (IEC 62351-3); port 19998 when omitted |
--ip host [--port n] |
TCP on port 2404 or n; TLS when a --tls… flag is given |
--ca is the common address of the station (default 1). --timeout bounds connecting and each request in milliseconds (default 10000); a large station may need more for its general interrogation. --originator sets the originator address sent with every request.
TLS
| Flag | Meaning |
|---|---|
--tls |
Use TLS with --ip (a tls:// URL implies it) |
--tls-ca |
PEM file with the CA certificates to verify the station with; the system roots by default |
--tls-cert, --tls-key |
PEM files with the client certificate and its key, for a station that authenticates its clients |
--tls-server-name |
The name to verify the station's certificate against, when it is not the host you connect to |
--tls-insecure |
Do not verify the station's certificate. For test setups only |
iec104ctl client interrogate --url tls://rtu.example.net --tls-ca ca.pem --tls-cert scada.pem --tls-key scada.keyApplication-layer security (IEC 62351-5)
--secure runs the A-Profile of IEC 62351-5, as IEC TS 60870-5-7 maps it onto IEC 104. After STARTDT the client and the station first agree on keys in a Station Association authenticated with their certificates, then on session keys; from then on every ASDU travels as Secure Data (S_SD_NA_1), authenticated with a truncated HMAC, or encrypted and authenticated with AES-256-GCM, and numbered against replay. The command starts only once the session is secure. It works over TCP and over TLS.
| Flag | Meaning |
|---|---|
--secure |
Run the A-Profile. Every other flag of this table is invalid without it, and --secure needs the next three |
--security-cert |
PEM file with the own station certificate: P-256, or X25519 signed by an authority |
--security-key |
PEM file with its private key (PKCS #8, or SEC 1 for P-256) |
--security-trust |
PEM file with one or more certificates of the stations to trust. A CA certificate (basic constraints CA:TRUE) is an authority: every station certificate it signed is accepted. Any other certificate is pinned: that station's self-signed certificate is accepted, by its fingerprint |
--association-id |
The AIM, this controlling station's half of the association ID, 1-65535 (default 1) |
--mal |
MAC algorithm, by code or name: 4 / hmac-sha-256 (default), 3 / hmac-sha-256/8, 8 / hmac-sha3-256, 7 / hmac-sha3-256/8 |
--dpa |
Data protection algorithm, by code or name: a MAC algorithm as for --mal, or 11 / aes-256-gcm to encrypt as well (default: --mal) |
Pinned self-signed certificates, one per station (only P-256 keys can sign their own certificate):
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 365 \
-keyout station.key -out station.pem -subj "/CN=station 1" -addext basicConstraints=critical,CA:FALSE
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 365 \
-keyout scada.key -out scada.pem -subj "/CN=scada" -addext basicConstraints=critical,CA:FALSE
# Each side trusts the other's certificate
iec104ctl server start --secure --security-cert station.pem --security-key station.key --security-trust scada.pem
iec104ctl client interrogate --ip 127.0.0.1 --secure --security-cert scada.pem --security-key scada.key --security-trust station.pemA plant authority that signs the station certificates, so that every side trusts the authority alone:
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 3650 \
-keyout ca.key -out ca.pem -subj "/CN=Plant authority" \
-addext basicConstraints=critical,CA:TRUE -addext keyUsage=critical,keyCertSign
printf 'basicConstraints=critical,CA:FALSE\n' > station.ext
for n in station scada; do
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout $n.key -out $n.csr -subj "/CN=$n"
openssl x509 -req -in $n.csr -CA ca.pem -CAkey ca.key -CAcreateserial -days 365 -out $n.pem -extfile station.ext
done
iec104ctl server start --secure --security-cert station.pem --security-key station.key --security-trust ca.pem --roles viewer
iec104ctl client interrogate --ip 127.0.0.1 --secure --security-cert scada.pem --security-key scada.key --security-trust ca.pem --dpa aes-256-gcmA refused certificate, an algorithm the station does not accept, or no answer to the key management ends the command with exit code 4 and the error code security_failed, before anything else is sent. A request the station's roles do not permit is refused with cause 17 (not-authorized), exit code 6. A client without --secure gets no answer from a secure station (exit code 5). With --debug the security events (STAS_PROC_SUCC, NODE_NOT_AUTR, …) and the algorithms of the session are traced with the frames.
Protocol parameters
The parameters of IEC 60870-5-104 clause 9 default to the standard values. Both ends must agree on them; change them only to match a station that deviates.
| Flag | Parameter | Default |
|---|---|---|
--k |
Maximum number of unacknowledged I frames sent | 12 |
--w |
Acknowledge after at most this many received I frames | 8 |
--t0 |
Connection establishment timeout, seconds | 30 |
--t1 |
Timeout for the acknowledgement of a sent frame, seconds | 15 |
--t2 |
Acknowledge after this many seconds without sending (must be below t1) | 10 |
--t3 |
Send a test frame after this many seconds of inactivity | 20 |
A station behind an IEC 60870-5-101 gateway may use shorter address fields. --cot-size (1 or 2 octets), --ca-size (1 or 2) and --ioa-size (1 to 3) select them; the defaults are the IEC 104 values 2, 2 and 3. If every value looks scrambled, or the station closes the connection on the first request, these are the first thing to check.
Time zone. A time tag on the wire carries no zone. --timezone says which zone the station keeps its clock in: utc (default), local, or an IANA name such as Europe/Brussels. It applies to the time tags iec104ctl prints and to the time client clock sends.
# General interrogation of station 1
iec104ctl client interrogate --ip 192.168.1.10 --ca 1
# Interrogation group 3 only
iec104ctl client interrogate --ip 192.168.1.10 --group 3
# Show only single points and floating point values of addresses 100-1999
iec104ctl client interrogate --ip 192.168.1.10 --type M_SP_NA_1,M_ME_NC_1 --ioa 100-1999
# As a table, or as JSON
iec104ctl client interrogate --ip 192.168.1.10 --format table
iec104ctl client interrogate --ip 192.168.1.10 --format jsonExample result:
General interrogation of station 1 at tcp://192.168.1.10:2404: 10 objects in 1 ms
M_SP_NA_1 ca=1 ioa=100 on [good] interrogated-station
M_SP_NA_1 ca=1 ioa=101 off [good] interrogated-station
M_DP_NA_1 ca=1 ioa=200 on [good] interrogated-station
M_ST_NA_1 ca=1 ioa=300 3 [good] interrogated-station
M_BO_NA_1 ca=1 ioa=400 15 [good] interrogated-station
M_ME_NC_1 ca=1 ioa=1000 230.4 [good] interrogated-station
M_ME_NC_1 ca=1 ioa=1001 50 [good] interrogated-station
M_ME_NC_1 ca=1 ioa=1010 230 [good] interrogated-station
M_ME_NB_1 ca=1 ioa=1002 1250 [good] interrogated-station
M_ME_NA_1 ca=1 ioa=1003 0.25 (raw=8192) [good] interrogated-stationEach line is one information object: type, common address, object address, value, quality, cause of transmission and, for types that carry one, the time tag after @. --type and --ioa filter what is shown; the station is always asked for everything.
Values are printed as the type holds them: on / off for single points, off / on / intermediate / indeterminate for double points, the number for everything else. A normalized value is shown as a fraction of -1 … 1 with the raw 16-bit number beside it; scaling to engineering units is up to the signal list.
Integrated totals are not part of an interrogation:
# All counters, then counter group 1
iec104ctl client counters --ip 192.168.1.10
iec104ctl client counters --ip 192.168.1.10 --group 1
# Freeze the counters and report the frozen values
iec104ctl client counters --ip 192.168.1.10 --freeze freeze--freeze is read (default), freeze, freeze-reset or reset. Anything but read changes the counters in the station.
iec104ctl client read --ip 192.168.1.10 --ioa 1000
iec104ctl client read --ip 192.168.1.10 --ioa 100,1000-1003 --format tableOne read command (C_RD_NA_1) is sent per address. An address the station refuses or does not answer is reported in its row and the others are still read; the exit code is then 7. Not every station implements the read command: an interrogation always works.
# Everything the station sends, starting with a general interrogation, until Ctrl-C
iec104ctl client monitor --ip 192.168.1.10 --interrogate
# Two objects of station 1, the next 10 changes
iec104ctl client monitor --ip 192.168.1.10 --ca 1 --ioa 100,200 --count 10
# One minute of floating point values as JSON Lines, into a file
iec104ctl client monitor --ip 192.168.1.10 --type M_ME_NC_1,M_ME_TF_1 --duration 60 --format json > values.jsonlExample result (stdout, with the connection state and the summary on stderr):
monitor: data transfer started
2026-10-10T09:15:17.676Z M_DP_TB_1 ca=1 ioa=200 off [good] return-remote @ 2026-10-10T09:15:17.666Z
2026-10-10T09:15:19.286Z M_ME_TF_1 ca=1 ioa=1000 230.20906 [good] spontaneous @ 2026-10-10T09:15:19.285Z
2026-10-10T09:15:22.285Z M_ME_TE_1 ca=1 ioa=1002 1275 [good] spontaneous @ 2026-10-10T09:15:22.285Z
--- tcp://192.168.1.10:2404: 3 values in 5.001s (M_DP_TB_1×1 M_ME_TE_1×1 M_ME_TF_1×1)The first column is when the value arrived; the time after @ is the time tag the station gave it. An event that was queued in the station shows the difference.
| Flag | Effect |
|---|---|
--interrogate |
Run a general interrogation after connecting, and after every reconnect, so the stream starts with all current values |
--ca |
Only this station; also the station --interrogate addresses (default: all stations, interrogation by broadcast) |
--type, --ioa |
Only these types and addresses. Note that an event has the time-tagged type (M_SP_TB_1), an interrogated value the plain one (M_SP_NA_1) |
--count, --duration |
Stop after that many values, or seconds |
--all |
Also show confirmations and terminations of commands, and file transfer |
--no-reconnect |
Exit with code 4 when the connection is lost, instead of reconnecting |
A station sends its events on one connection, usually the one that started data transfer last. While you monitor, the regular controlling station may not receive them. Stations with several redundancy groups keep a queue per group; ask before you monitor a station in service.
# Single command, direct execute
iec104ctl client command --ip 192.168.1.10 --type single --ioa 5000 --value on
# Double command with select-before-operate and a short pulse
iec104ctl client command --ip 192.168.1.10 --type double --ioa 5001 --value off --mode sbo --qualifier short
# Regulating step command: tap changer one step higher
iec104ctl client command --ip 192.168.1.10 --type step --ioa 5002 --value higher
# Set points
iec104ctl client command --ip 192.168.1.10 --type setpoint-float --ioa 5003 --value 231.5
iec104ctl client command --ip 192.168.1.10 --type setpoint-scaled --ioa 5004 --value 1200
iec104ctl client command --ip 192.168.1.10 --type setpoint-normalized --ioa 5005 --value 0.5
# With a time tag (C_SC_TA_1), waiting up to 5 s for the activation termination
iec104ctl client command --ip 192.168.1.10 --type single --ioa 5000 --value on --time-tag --wait-termination 5000--type |
Type identification | --value |
|---|---|---|
single |
C_SC_NA_1 |
on, off |
double |
C_DC_NA_1 |
on, off |
step |
C_RC_NA_1 |
higher, lower |
setpoint-normalized |
C_SE_NA_1 |
-1 … < 1 |
setpoint-scaled |
C_SE_NB_1 |
-32768 … 32767 |
setpoint-float |
C_SE_NC_1 |
any number |
bitstring |
C_BO_NA_1 |
0 … 4294967295, or 0x hex |
--type also takes the type name (C_SC_NA_1, or C_SC_TA_1 for the time-tagged variant).
--mode |
Procedure |
|---|---|
execute |
Direct execute (default) |
sbo |
Select before operate: select, and when the station has confirmed it, execute |
select |
Select only, to see whether the station accepts the selection |
cancel |
Select, then deactivate the selection |
A selection belongs to the connection that made it and ends with it, so select and cancel operate nothing: they show how the station handles the two steps.
--qualifier is the qualifier of command for switching commands (none, short, long, persistent) or the QL number 0-127 for set points.
Example result of a select-before-operate:
Command C_DC_NA_1 to station 1 at tcp://192.168.1.10:2404, ioa=5001, value off (sbo)
select confirmed 0.3 ms
execute confirmed 0.2 ms
termination received 0.0 msThis changes the process. Everything is validated before anything is sent; an invalid invocation exits 3 and has sent nothing. A command is sent exactly once: it is never repeated, also not when its confirmation does not arrive (exit 5). A command the station refuses — negative confirmation, or "unknown information object address" — exits 6, and the message says which step was refused.
The effect of a command arrives as an event in monitor direction, usually with cause return-remote. Run client monitor in a second terminal to see it.
# Set the clock of station 1 to the current time
iec104ctl client clock --ip 192.168.1.10 --ca 1
# A station that keeps local time; and a given time for every station behind the link
iec104ctl client clock --ip 192.168.1.10 --timezone Europe/Brussels
iec104ctl client clock --ip 192.168.1.10 --ca 65535 --time 2026-01-31T12:00:00Z
# Clear the event buffer, or restart the process
iec104ctl client reset --ip 192.168.1.10 --qualifier events
iec104ctl client reset --ip 192.168.1.10 --qualifier processBoth change the station and are sent once. client reset has no default for --qualifier: a process reset restarts the station's application, and it may drop its connections.
# Four link tests, one per second
iec104ctl client ping --ip 192.168.1.10
# The application of station 1, every 500 ms until Ctrl-C
iec104ctl client ping --ip 192.168.1.10 --method command --ca 1 --interval 500 --count 02026-10-10T09:15:23.307Z seq=1 testfr rtt=0.19 ms
2026-10-10T09:15:23.508Z seq=2 testfr rtt=0.52 ms
2026-10-10T09:15:23.710Z seq=3 testfr rtt=0.48 ms
--- tcp://192.168.1.10:2404 testfr: 3 tests, 0 failed, rtt min/avg/max = 0.19/0.40/0.52 ms--method testfr (default) sends the link test frame, which the protocol machine of the station answers; it changes nothing. --method command sends the test command C_TS_TA_1 through the station's application; not every station implements it. A failed test is reported and testing continues; the exit code is 7 when any test failed.
To see exactly what is exchanged, add --debug to any command:
$ iec104ctl --debug client read --ip 192.168.1.10 --ioa 100
debug: 09:04:48.676 -- 192.168.1.10:2404 connected
debug: 09:04:48.676 -> 192.168.1.10:2404 U STARTDT act
debug: 09:04:48.677 <- 192.168.1.10:2404 U STARTDT con
debug: 09:04:48.677 -> 192.168.1.10:2404 I N(S)=0 N(R)=0 len=9 C_RD_NA_1 request ca=1 n=1 [66 01 05 00 01 00 64 00 00]
debug: 09:04:48.677 <- 192.168.1.10:2404 I N(S)=0 N(R)=1 len=10 M_SP_NA_1 request ca=1 n=1 [01 01 05 00 01 00 64 00 00 00]
debug: 09:04:48.677 -- 192.168.1.10:2404 disconnectedOn a network:
iec104ctl discover --subnets 192.168.1.0/24
iec104ctl discover --subnets 10.0.1.0/24,10.0.2.10-50 --ports 2404,19998 --parallel 64 --format jsonDiscovery: 1 IEC 60870-5-104 stations among 254 hosts in 1031 ms
192.168.1.10 port 2404 iec104 0.5 msThe probe opens a TCP connection and sends one link test frame. A station answers it in any state: data transfer is not started and no data is asked for. A port that accepts the connection without answering as a station is listed as open. More than 4096 hosts need --force-large-scan. A station in service may turn the probe away, or log it.
Behind one connection:
# Which common addresses answer? (one interrogation to the broadcast address)
iec104ctl client scan --ip 192.168.1.10
# The same by trying addresses 1-50, for stations that ignore the broadcast address
iec104ctl client scan --ip 192.168.1.10 --method sweep --start 1 --end 50 --timeout 1000Scan of tcp://192.168.1.10:2404 (broadcast): 1 stations found, 1 common addresses probed, 1528 ms
ca=1 found 10 objects M_SP_NA_1×2 M_DP_NA_1×1 M_ST_NA_1×1 M_BO_NA_1×1 M_ME_NA_1×1 M_ME_NB_1×1 M_ME_NC_1×3Both methods make every station found send its whole process image.
# What does the station offer?
iec104ctl client file list --ip 192.168.1.10
# Fetch a file, or pipe it
iec104ctl client file get --ip 192.168.1.10 --ioa 30000 --name 1 --output record.bin
iec104ctl client file get --ip 192.168.1.10 --ioa 30001 --name 1 --output - | less
# Deliver a file, in sections of 4096 octets
iec104ctl client file put --ip 192.168.1.10 --ioa 40000 --name 1 --input settings.bin --section-size 4096Directory of station 1 at tcp://192.168.1.10:2404: 2 entries
ioa=30000 name=1 4096 octets 2026-10-10T09:15:17.283Z file
ioa=30001 name=1 84 octets 2026-10-10T09:15:17.283Z fileA file is identified by its information object address and its name of file, a number (--name, default 1). The checksum of every section and of the file is verified in both directions, and the result carries the SHA-256 of the content. A file holds at most 16 777 215 octets. file put changes the station and is sent once.
# The built-in demo station on port 2404
iec104ctl server start
# Write its model, edit it, serve it
iec104ctl server generate-config --output station.yaml
iec104ctl server start --config station.yaml
# On another port, on localhost only, events as JSON Lines
iec104ctl server start --config station.yaml --listen 127.0.0.1 --port 12404 --format json
# With TLS, and only for two front ends
iec104ctl server start --config station.yaml --tls-cert server.pem --tls-key server.key --tls-client-ca ca.pem --allow 10.0.0.5,10.0.0.6
# With the application-layer security of IEC 62351-5; controlling stations may look, not operate
iec104ctl server start --config station.yaml --secure --security-cert station.pem --security-key station.key --security-trust ca.pem --roles viewerThe station is described by a model: its common address, its points with their initial values, its commands and its files.
station:
name: Demo bay
common_address: 1
cyclic_interval: 30s
points:
- {ioa: 100, type: M_SP_NA_1, name: Circuit breaker closed, value: true, group: 1}
- ioa: 1000
type: M_ME_NC_1
name: Voltage L1 (kV)
value: 230.4
simulate: {mode: sine, interval: 2s, min: 228, max: 232}
- {ioa: 2000, type: M_IT_NA_1, name: Energy counter (kWh), value: 123456}
commands:
- {ioa: 5000, type: C_SC_NA_1, name: Circuit breaker, target: 100}
- {ioa: 5001, type: C_DC_NA_1, name: Disconnector, target: 200, select_required: true}
files:
- {ioa: 30000, name: 1, size: 4096}
uploads:
accept: trueWhat it does:
- Answers general, group and counter interrogations, read commands, clock synchronization, the test command and the reset process command.
- Executes commands on the points named as their
target, direct or select-before-operate, and reports the change as an event with cause "return information caused by a remote command".select_requiredrefuses an execute without a select. - Changes values by itself where the model says
simulate(toggle,ramp,sine,random,count), and reports the changes as spontaneous events with a time tag.cyclic_intervalsends the measured values periodically. - Queues events of state changes and commands while no controlling station is connected and delivers them to the next one, as an RTU does.
- Serves any number of connections, of which one receives the events (redundancy).
- Serves files and, with
uploads.accept, takes them.
Every event is written to stdout as it happens:
2026-10-10T09:15:17.284Z listening 127.0.0.1:2404, station 1 (Demo bay), 11 points, 7 commands, 2 files
2026-10-10T09:15:17.624Z connected 127.0.0.1:52488 data transfer stopped
2026-10-10T09:15:17.624Z started 127.0.0.1:52488 data transfer started
2026-10-10T09:15:17.625Z interrogation 127.0.0.1:52488 ca=1 station, 10 objects
2026-10-10T09:15:17.666Z command 127.0.0.1:52491 ca=1 ioa=5001 C_DC_NA_1 off selected
2026-10-10T09:15:17.666Z command 127.0.0.1:52491 ca=1 ioa=5001 C_DC_NA_1 off executed, point 200 set
2026-10-10T09:15:17.666Z disconnected 127.0.0.1:52491 connection lost: closed by peerWith --secure the station also logs secured when a controlling station has associated, security-refused when an association is refused (an untrusted certificate, an algorithm it does not accept), and refused for a request its roles do not permit.
| Flag | Meaning |
|---|---|
--config |
The model file (YAML or JSON). Without it the built-in demo station is served |
--listen, --port |
Address and port to listen on. All interfaces and 2404 by default (19998 with TLS) |
--tls-cert, --tls-key |
Serve TLS with this certificate |
--tls-client-ca |
Require a client certificate of this CA (mutual TLS) |
--secure, --security-cert, --security-key, --security-trust |
The application-layer security of IEC 62351-5, as for the client: every controlling station must associate with a certificate --security-trust accepts, and every ASDU travels as Secure Data |
--association-id |
The AIS, the station's half of the association ID (default 1). Every controlling station (AIM) gets its own association |
--roles |
The roles every controlling station has: viewer, operator, engineer, installer, secadm, secaud, rbacmnt. A request no role permits is refused with cause 17. Without it there is no access control |
--allow |
Only accept clients from these addresses or CIDR prefixes |
--max-sessions |
Maximum number of simultaneous connections |
--duration |
Stop after this many seconds, for use in test pipelines |
--k, --w, --t0 … --t3, --cot-size, --ca-size, --ioa-size, --timezone |
As for the client |
The full reference of the model is in docs/server-config.md. The server listens on all interfaces and accepts any client unless told otherwise: see SECURITY.md.
Every flag can also be set through an environment variable, which is convenient for automation and for flags you would otherwise repeat on every command (IEC104CTL_URL, IEC104CTL_CA, IEC104CTL_FORMAT). A flag on the command line wins over the environment. A value that cannot be parsed (a port that is not a number) stops iec104ctl at startup. A flag that is required (--ioa of client read, --type and --value of client command, --qualifier of client reset) must still be given on the command line. The global flag --debug has no environment variable.
export IEC104CTL_URL=tcp://192.168.1.10:2404
export IEC104CTL_CA=1
iec104ctl client interrogate
iec104ctl client read --ioa 1000All IEC104CTL_* environment variables
| Variable | Type | Description | Flag | Default |
|---|---|---|---|---|
| IEC104CTL_ALL | bool | client monitor: also show confirmations and terminations of commands, and file transfer |
--all |
false |
| IEC104CTL_ALLOW | list | server start: only accept clients from these addresses or CIDR prefixes |
--allow |
any |
| IEC104CTL_ASSOCIATION_ID | uint16 | Association ID of IEC 62351-5, 1-65535: the AIM of a client, the AIS of server start (needs --secure) |
--association-id |
1 |
| IEC104CTL_CA | uint16 | Common address of the station (client monitor: only show this station) |
--ca |
1 (monitor: all) |
| IEC104CTL_CA_SIZE | uint8 | Octets of the common address, 1 or 2 (0 = 2) | --ca-size |
0 |
| IEC104CTL_COMMAND_MODE | string | client command: execute, select, sbo or cancel |
--mode |
execute |
| IEC104CTL_COMMAND_TYPE | string | client command: single, double, step, setpoint-normalized, setpoint-scaled, setpoint-float, bitstring, or a type name |
--type |
|
| IEC104CTL_CONFIG | string | server start: station model file (YAML or JSON) |
--config |
built-in demo |
| IEC104CTL_CONFIG_FORMAT | string | server generate-config: yaml or json |
--format |
yaml |
| IEC104CTL_COT_SIZE | uint8 | Octets of the cause of transmission, 1 or 2 (0 = 2) | --cot-size |
0 |
| IEC104CTL_COUNT | uint32 | client monitor: stop after this many values; client ping: number of tests (0 = until interrupted) |
--count |
0 (ping: 4) |
| IEC104CTL_DELAY | uint32 | client scan: delay in ms between the requests of a sweep |
--delay |
0 |
| IEC104CTL_DIRECTION | string | client types: monitor or control |
--direction |
|
| IEC104CTL_DISCOVER_FORCE_LARGE | bool | discover: allow more hosts than the built-in safety cap |
--force-large-scan |
false |
| IEC104CTL_DPA | string | Data protection algorithm of IEC 62351-5, by code or name (needs --secure) |
--dpa |
--mal |
| IEC104CTL_DURATION | uint32 | client monitor, server start: stop after this many seconds (0 = until interrupted) |
--duration |
0 |
| IEC104CTL_END | uint16 | client scan: last common address of a sweep |
--end |
255 |
| IEC104CTL_FILE_NAME | uint16 | client file get, put: name of file (NOF) |
--name |
1 |
| IEC104CTL_FORCE | bool | server generate-config: overwrite an existing file |
--force |
false |
| IEC104CTL_FREEZE | string | client counters: read, freeze, freeze-reset or reset |
--freeze |
read |
| IEC104CTL_GROUP | uint8 | Interrogation group 1-16 (client interrogate) or counter group 1-4 (client counters); 0 = general |
--group |
0 |
| IEC104CTL_INPUT | string | client file put: file whose content is sent |
--input |
|
| IEC104CTL_INTERROGATE | bool | client monitor: general interrogation after connecting and after every reconnect |
--interrogate |
false |
| IEC104CTL_INTERVAL | uint32 | client ping: interval between tests in ms |
--interval |
1000 |
| IEC104CTL_IOA | uint32 / list | Information object address: of the command or file; a list with ranges for client read, interrogate and monitor |
--ioa |
|
| IEC104CTL_IOA_SIZE | uint8 | Octets of the information object address, 1 to 3 (0 = 3) | --ioa-size |
0 |
| IEC104CTL_IP | string | Station IP address or host name | --ip |
|
| IEC104CTL_K | uint16 | k: maximum number of unacknowledged I frames sent (0 = 12) | --k |
0 |
| IEC104CTL_LISTEN | string | server start: address to listen on |
--listen |
all interfaces |
| IEC104CTL_MAL | string | MAC algorithm of IEC 62351-5, by code or name (needs --secure) |
--mal |
hmac-sha-256 |
| IEC104CTL_MAX_SESSIONS | uint16 | server start: maximum number of connections (0 = no limit) |
--max-sessions |
0 |
| IEC104CTL_NO_RECONNECT | bool | client monitor: exit when the connection is lost |
--no-reconnect |
false |
| IEC104CTL_ORIGINATOR | uint8 | Originator address sent with every request | --originator |
0 |
| IEC104CTL_OUTPUT | string | client file get: file to write (- = stdout); server generate-config: file to write |
--output |
(generate-config: -) |
| IEC104CTL_FORMAT | string | (IEC104CTL_OUTPUT_FORMAT stays accepted) Output format: text, json or table |
--format |
text |
| IEC104CTL_PARALLEL | uint16 | discover: concurrent probes (1-256) |
--parallel |
32 |
| IEC104CTL_PING_METHOD | string | client ping: testfr or command |
--method |
testfr |
| IEC104CTL_PORT | uint16 | Station TCP port; server start: port to listen on (0 = 2404, or 19998 with TLS) |
--port |
2404 |
| IEC104CTL_PORTS | list | discover: TCP ports to probe |
--ports |
2404 |
| IEC104CTL_QUALIFIER | string | client command: none, short, long, persistent, or a number |
--qualifier |
none |
| IEC104CTL_RESET_QUALIFIER | string | client reset: process or events (the flag is required) |
--qualifier |
|
| IEC104CTL_ROLES | list | server start: roles of every controlling station (needs --secure) |
--roles |
no access control |
| IEC104CTL_SCAN_METHOD | string | client scan: broadcast or sweep |
--method |
broadcast |
| IEC104CTL_SECTION_SIZE | uint32 | client file put: octets per section (0 = one section) |
--section-size |
0 |
| IEC104CTL_SECURE | bool | Run the application-layer security of IEC 62351-5 | --secure |
false |
| IEC104CTL_SECURITY_CERT | string | PEM file with the own station certificate of IEC 62351-5 | --security-cert |
|
| IEC104CTL_SECURITY_KEY | string | PEM file with the private key of --security-cert |
--security-key |
|
| IEC104CTL_SECURITY_TRUST | string | PEM file with the trusted authorities and pinned station certificates | --security-trust |
|
| IEC104CTL_START | uint16 | client scan: first common address of a sweep |
--start |
1 |
| IEC104CTL_SUBNETS | list | discover: subnets, ranges or addresses to probe |
--subnets |
|
| IEC104CTL_T0 | uint16 | t0: connection establishment timeout in seconds (0 = 30) | --t0 |
0 |
| IEC104CTL_T1 | uint16 | t1: acknowledgement timeout in seconds (0 = 15) | --t1 |
0 |
| IEC104CTL_T2 | uint16 | t2: acknowledge after this many seconds without sending (0 = 10) | --t2 |
0 |
| IEC104CTL_T3 | uint16 | t3: test frame after this many seconds of inactivity (0 = 20) | --t3 |
0 |
| IEC104CTL_TERMINATION | uint32 | client command: wait this many ms for the activation termination (0 = do not wait) |
--wait-termination |
0 |
| IEC104CTL_TIME | string | client clock: now, or an RFC 3339 time |
--time |
now |
| IEC104CTL_TIMEOUT | uint32 | Timeout in ms for connecting and for each request (0 = 10000); discover: per probe (0 = 1000) |
--timeout |
0 |
| IEC104CTL_TIMEZONE | string | Time zone of the time tags on the wire: utc, local or an IANA name | --timezone |
utc |
| IEC104CTL_TIME_TAG | bool | client command: send the CP56Time2a variant of the type |
--time-tag |
false |
| IEC104CTL_TLS | bool | Use TLS with --ip |
--tls |
false |
| IEC104CTL_TLS_CA | string | PEM file with the CA certificates to verify the station with | --tls-ca |
system roots |
| IEC104CTL_TLS_CERT | string | PEM file with the own certificate (client: authentication; server: the server certificate) | --tls-cert |
|
| IEC104CTL_TLS_CLIENT_CA | string | server start: CA whose certificates clients must present |
--tls-client-ca |
|
| IEC104CTL_TLS_INSECURE | bool | Do not verify the station certificate (testing only) | --tls-insecure |
false |
| IEC104CTL_TLS_KEY | string | PEM file with the private key of --tls-cert |
--tls-key |
|
| IEC104CTL_TLS_SERVER_NAME | string | Name to verify the station certificate against | --tls-server-name |
the host |
| IEC104CTL_TYPES | string | client interrogate, monitor: only show these types |
--type |
|
| IEC104CTL_URL | string | Station URL: tcp://host[:port] or tls://host[:port] |
--url |
|
| IEC104CTL_VALUE | string | client command: the value |
--value |
|
| IEC104CTL_W | uint16 | w: acknowledge after at most this many received I frames (0 = 8) | --w |
0 |
iec104ctl is a command-line tool for IEC 60870-5-104, the telecontrol protocol used between control centres and substations. As a client it interrogates and monitors a station, sends commands and set points, synchronizes the clock and transfers files; as a server it simulates an RTU from a model file. It also finds stations on a network.
Run a general interrogation: iec104ctl client interrogate --ip 192.168.1.10 --ca 1. Add --format table or --format json for other output. Counters need iec104ctl client counters. See Interrogate a station.
iec104ctl client monitor --ip 192.168.1.10 --interrogate prints the current values and then every change, with its time tag, until you press Ctrl-C. --format json gives one JSON object per line. See Monitor events and measured values.
iec104ctl client command --ip 192.168.1.10 --type double --ioa 5001 --value off --mode sbo selects, waits for the confirmation and executes. See Send a command.
iec104ctl server start serves a built-in demo station on port 2404. iec104ctl server generate-config --output station.yaml writes its model for you to edit, and iec104ctl server start --config station.yaml serves yours. See Simulate a station and docs/server-config.md.
- Is it an IEC 104 station at all?
iec104ctl discover --subnets <address>tells in a second, without starting data transfer. - Does it accept your address? Many stations only accept connections from the controlling stations they are configured for, and only a few at a time.
- The common address. "unknown common address" (exit code 6) means the station is there and
--cais wrong;iec104ctl client scanfinds the right one. - The field sizes. A station behind an IEC 101 gateway may use shorter addresses: try
--ioa-size 2,--ca-size 1,--cot-size 1. - What is on the wire.
--debugshows every frame in both directions.
A time tag carries no time zone. iec104ctl reads it as UTC unless you say which zone the station keeps: --timezone Europe/Brussels, or --timezone local.
Every client command was exercised against the servers of three independent protocol stacks: lib60870-C, j60870 (OpenMUC) and wendy512/iec104. The simulated station was driven by the clients of the same three: interrogation, counter interrogation, read, clock synchronization, test command, commands (direct, select-before-operate, cancel, refused) and, with lib60870-C and j60870, file download. The underlying library, go-iec104, runs an interoperability suite against these stacks on every change. If you use iec104ctl with a device it has trouble with, please open an issue with the --debug trace.
Yes: --url tls://host (port 19998 by default) with --tls-ca, and --tls-cert / --tls-key for a station that wants a client certificate. The simulated station serves TLS with --tls-cert and --tls-key. This is the transport security of IEC 62351-3.
Yes, the A-Profile of IEC TS 60870-5-7: --secure with --security-cert, --security-key and --security-trust on every client command and on server start, with or without TLS. See Connecting to a station. The implementation is the one of go-iec104, tested for interoperability with an independent implementation; the standards are not public, and conformance is not established independently.
Not the serial link layer. It does speak IEC 104 with the shorter address fields of 101, which gateways between the two often pass on: --cot-size, --ca-size and --ioa-size.
It can. A station sends its events on one connection, and yours may become that one; and a station may accept few connections. A general interrogation makes it send its whole process image. On a station in service, agree on a time first, or use a station that keeps a separate event queue for you.
Pass --format json (or set IEC104CTL_FORMAT=json). Progress goes to stderr, the result goes to stdout, and a failure returns one JSON error object with a stable exit code. iec104ctl server start --config model.yaml --duration 60 gives a pipeline a station to test against. See General CLI usage, docs/json-output.md and docs/exitcodes.md.
Repository layout
iec104ctl/
├── cmd/ (root command, version, completion)
│ ├── client (interrogate, counters, read, monitor, command, clock, reset, ping, scan, file, types)
│ ├── discover (subnet discovery)
│ └── server (start, generate-config)
├── internal/
│ ├── config (CLI & env: configuration types, flag and environment binding, station URLs)
│ ├── cli (shared Cobra helpers: enum completion, required flags, debug)
│ ├── format (stdout **text/json/table** via `Write`, streaming output via `StreamWriter`, table rendering)
│ ├── runner (command wiring, formatted and streaming runs, fatal JSON/text rendering)
│ ├── errs (typed CLI errors, stable codes, exit-code mapping)
│ ├── iec (what the commands do: connections, TLS, IEC 62351-5 security, requests, monitor, scan, file transfer, discovery, error classification)
│ ├── station (the simulated station: model, validation, behaviour, simulation)
│ ├── stationtest (the simulated station for tests, with throw-away TLS certificates)
│ └── types (DTOs for JSON/text/table output)
├── docs/ (exit codes, JSON output contract, station model reference)
├── Makefile (Build & test commands)
└── go.mod (Dependencies)A toplevel Makefile is provided for convenience. Run make (or make help) for the full list. Common targets:
| Target | Purpose |
|---|---|
make test |
Unit and end-to-end tests with race detector (go test -race ./...) |
make coverage |
Tests + coverage.out across the whole module; fails below 85% total statement coverage (COVERAGE_MIN) |
make compile |
Full-module typecheck (go build ./...) |
make lint |
compile then staticcheck |
make lint-ci |
compile, go vet, golangci-lint (uses .golangci.yml) |
make fmt / fmt-check |
Format with the gofmt of the Go version in go.mod (the one CI uses); fmt-check fails if that gofmt or the locally installed one would change any file |
make check |
fmt, fmt-check, lint, lint-ci, vet, test, coverage |
make build |
Runs check then writes bin/iec104ctl |
make build-all / release-all |
Cross-compile and package archives |
make build writes the binary to bin/iec104ctl (not the repo root).
make build
make build-allmake test
make coverageTests need no hardware and no network beyond the loopback interface: internal/stationtest serves the simulated station of iec104ctl server on a free port, and the tests of every command run the real command tree against it, over TCP and TLS.
Release notes for each version are maintained in RELEASE.md.
Bug reports, station-compatibility notes and pull requests are welcome. Please open an issue to discuss larger changes first, and run make check before submitting. See CONTRIBUTING.md for the development setup and guidelines.
Please report security vulnerabilities privately, as described in SECURITY.md, not in a public issue.
If iec104ctl is useful to you, a star on GitHub helps other engineers find it.
This project is licensed under the MIT License. See LICENSE.