Repository navigation
DYODE_python3 1.5.0 Interface Control Document
DYODE 1.5.0 · nc-intezra/dyode_python3
Prepared against the CMS Target Life Cycle (TLC) Interface Control Document template.
Version 0.1 — Draft for review
Date 10/23/2026
System under control DYODE 1.5.0 (tag v1.5.0, commit ab214c9, released 21 October 2026)
Source system DYODE Input box (sending-side network)
Target system DYODE Output box (receiving-side network)
Basis and open items
This draft was built from DYODE's public README, the v1.5.0 release notes and the project wiki (which documents release 1.5.0), plus the original Wavestone install notes. It is not a line-by-line review of the source code. Process diagrams are reproduced from the wiki. Anything marked TBD has to be confirmed in the repository (config schema, wire layout, defaults) before approval. Template sections that assume two-way exchange are kept and marked as not applicable, with the reason given.
-
- Introduction
-
- Assumptions / Constraints / Risks
-
- Interface Overview
-
- Detailed Interface Requirements
-
- Quality Assurance
- Appendix A: Record of Changes
- Appendix B: Glossary
- Appendix C: Referenced Documents
- Appendix D: Approvals
- Appendix E: Open Items
- Figure 1 — Boxes and networks
- Figure 2 — Processes on each box
- Figure 3 — The two transports
- Figure 4 — The journey of a file
- Figure 5 — One batch, over time
- Figure 6 — The receiver's view of a batch
- Figure 7 — How lost packets are handled (FEC)
- Figure 8 — Log streaming path
This ICD describes the relationship between the DYODE Input box (the source system) and the DYODE Output box (the target system), and the external systems attached to each. DYODE (“Do Your Own Diode”) is a low-cost, open-source data diode for industrial control systems. It moves data in one direction only, from a sending-side network to a receiving-side network, over a light-based link with no return path. It is often placed between operational technology (OT) and information technology (IT) networks, but this ICD treats the two sides as generic networks.
The ICD covers:
- the common one-way transport between the two boxes;
- folder transfer across it, together with the local file-share endpoints the installer can set up on either box;
- log streaming, Modbus mirroring and screen sharing, also carried over the transport.
Security and privacy. The interface carries operational data: process values, logs, files and screen images. DYODE is not designed to carry PII or PHI, but logs and transferred files may contain them. The data owner must classify the content before go-live. TBD
Hardware. Each link has two Linux computers and a one-way light path between them, either enforced by a third-party certified data diode or the DIY diode solution created with media converters within the https://github.com/wavestone-cdt/dyode project that inspired this fork. Wavestone's full version is a 19-inch rack unit that uses copper-to-optical media converters; the original install notes list three converters and three NICs per computer. The light version (DYODE v2) is a compact, low-cost Raspberry Pi pair that replaces the Ethernet link with a serial line through an optocoupler; it carries Modbus only. Folder transfer, log streaming and screen sharing need the full version (DYODE v1).
Software. DYODE is written in Python and needs Python 3.11 or later. It uses udpcast for file transfer, pymodbus (pinned to ≥3.12,<3.13 in 1.5.0) for Modbus, systemd to run the service, and nftables for the diode-port firewall. Optional services are NFS, Samba and OpenSSH. Supported systems are Ubuntu 24.04 and 26.04 LTS (amd64, arm64) and 64-bit Raspberry Pi OS 12 and 13. Ubuntu 22.04 is not supported.
Processes. The input box runs dyode_in.py and the output box runs dyode_out.py. Each starts a supervisor that runs every configured module in its own process and restarts a crashed module after a 5-second guard; systemd restarts the supervisor itself (Restart=always). See Figure 2.
Configuration. Both boxes carry the same config.yaml and, from 1.5.0, the same dyode.key. The setup wizard (dyode_setup.py) detects interfaces, records the diode NIC's MAC address, configures modules, and writes the config and a systemd unit.
- The deployment is DYODE full (v1) with both boxes on release 1.5.0.
- Data flows from the sending side (input box) to the receiving side (output box). Each deployment direction is documented by its own ICD.
-
config.yamlanddyode.keyare identical on both boxes, andauth.requiredis left at its default (authentication on). - Both boxes are installed with
install.sh, including receive-side tuning and the diode-port firewall (no--no-tuningor--no-firewall). - Each box has a dedicated diode NIC that carries only diode traffic.
- No return channel. The receiver cannot acknowledge, request retransmission or apply flow control. Reliability depends on redundancy, sequencing and checks at the receiver.
- Throughput. The link bitrate, the receiving NIC's buffers and the receiving disk's sustained write speed all limit throughput. No kernel setting removes the disk limit.
- Reassembly bounds. The receiver drops oversized pieces, messages announcing more pieces than a module ever sends, partial messages older than 10 s, and anything over 32 MB held per listener.
- Light version. Carries Modbus only, over a serial line.
-
Viewers. At most
max_streams(default 8) screen viewers. - Platform. Tested on Ubuntu 24.04 LTS, Ubuntu 26.04 LTS and 64-bit Raspberry Pi OS (Raspbian).
| Risk | Impact | Mitigation |
|---|---|---|
| Disk slower than link | Writes stall and the transfer aborts. | Measure sustained disk speed and set the bitrate from it. |
| Key exposure or mismatch | A mismatch stops the service. An exposed key lets receiving-side hosts forge signed messages. | Protect dyode.key as a credential; define rotation. TBD |
| Authentication disabled | Spoofed file announcements, log lines or Modbus values are accepted. | Keep auth.required: true; alert on the start-up warning. |
| Host compromise | Signing protects the messages, not the boxes. | Harden the hosts, patch them and monitor them under the site baseline. |
| Silent data loss | Consumers act on incomplete data. | Log gaps are reported by sequence range (logs_gap); stall errors report UDP RcvbufErrors. |
flowchart TB
subgraph SNET["Sending-side network"]
direction LR
PLC["PLC"]
SRC["File sources,<br/>syslog senders"]
end
subgraph INBOX["Input box"]
direction LR
NIC_S["Sending-side NIC"]
DIN["dyode_in.py"]
NIC_D_IN["Diode-facing NIC<br/>dyode_in.ip / interface"]
end
DIODE{{"Data diode<br/>(fiber or optocoupler)"}}
subgraph OUTBOX["Output box"]
direction LR
NIC_D_OUT["Diode-facing NIC<br/>dyode_out.ip / interface"]
DOUT["dyode_out.py"]
NIC_R["Receiving-side NIC"]
end
subgraph RNET["Receiving-side network"]
direction LR
CLIENTS["SCADA, file shares,<br/>SIEM, browsers"]
end
PLC <--> NIC_S
SRC --> NIC_S
NIC_S --- DIN --- NIC_D_IN
NIC_D_IN ==>|"UDP only"| DIODE ==> NIC_D_OUT
NIC_D_OUT --- DOUT --- NIC_R
NIC_R <--> CLIENTS
Figure 1 — Boxes and networks. Source: DYODE wiki, Architecture (labels adapted to sending / receiving side).
| ID | Interface | From → To | Module type | Version |
|---|---|---|---|---|
| I-1 | Diode transport | Input box → Output box | All modules | Full, light |
| I-2 | Folder transfer | Input folder → Output folder | folder |
Full |
| I-3 | Local file shares | Users ↔ Input drop folder; Output received folder → users | Installer-managed | Either box |
| I-4 | Log streaming | Syslog sources / log files → Output files or SIEM | logs |
Full |
| I-5 | Modbus mirror | PLC → Input → Output → receiving-side Modbus clients | modbus |
Full (UDP), light (serial) |
| I-6 | Screen sharing | Screen source → receiving-side viewers | screen |
Full |
The template's seven OSI-layer tables (Tables 1–7) are combined below, one row per layer.
| OSI layer | Interface type | From | To | Description / other information |
|---|---|---|---|---|
| 7 Application | File folders; NFS / SMB / SFTP; syslog; Modbus TCP; screen frames | Sending-side sources, Input box | Output box, receiving-side consumers | Module services I-2 to I-6. The Modbus mirror on the output side is read-only. |
| 6 Presentation | DYO1 framing carrying DYA1 authentication | Input box | Output box | Every message is HMAC-SHA256 signed. Log batches are compressed. File manifests carry SHA-256 hashes. Field layout TBD. |
| 5 Session | None across the diode | — | — | No sessions are possible one way. TCP sessions exist only on each side's local endpoints. |
| 4 Transport | UDP across the diode; TCP/UDP locally | Input box | Output box | Each module's base UDP port is set in config.yaml. Files use udpcast with forward error correction; messages use DYO1 datagrams of at most 1400 bytes, so no IP fragmentation (Figure 3). Light version: serial line carrying newline-delimited JSON. |
| 3 Network | IPv4 | dyode_in.ip |
dyode_out.ip |
Diode-port datagrams are accepted only from the input box on the diode NIC. IPv6 is dropped. rp_filter=1 on the diode NIC. |
| 2 Data link | Ethernet | Input diode NIC | Output diode NIC | ARP replies cannot cross the diode, so the input box pins dyode_out.mac as a permanent neighbor entry. The arp-keeper re-asserts it every arp_interval seconds (default 60). If the entry cannot be set, dyode_in.py refuses to start. |
| 1 Physical | Optical, transmit-only | Input box | Output box | Either a third-party certified data diode, or the Wavestone DIY build — full: copper–optical media converters and fiber; light: optocoupler board (open-source Gerber and case files). |
Input box: watches input folders; receives syslog and follows log files; polls the configured Modbus registers and coils; captures screen frames; signs, frames and sends each message over the diode NIC. Output box: filters diode traffic (nftables); reassembles within bounds; verifies signatures and counters; writes received files; writes log files and forwards logs to the SIEM; serves the read-only Modbus mirror; serves screen viewers.
End users don't use the diode directly. Data moves on these triggers: a file dropped in an input folder, a log line arriving, the Modbus polling interval TBD (the original code polled every 1 s), or a screen capture. Operators work through install.sh, dyode_setup.py, systemd and dyode_logs.py --stats.
flowchart TB
subgraph IN["Input box"]
SDI["systemd<br/>dyode-in.service"] --> SUPI["dyode_in.py<br/>supervisor"]
SUPI --> FI["folder module(s)<br/>udp-sender"]
SUPI --> LI["logs module(s)<br/>syslog + tail"]
SUPI --> MI["modbus module(s)<br/>PLC poller"]
SUPI --> SI["screen module(s)"]
SUPI --> ARP["arp-keeper"]
end
subgraph OUT["Output box"]
SDO["systemd<br/>dyode-out.service"] --> SUPO["dyode_out.py<br/>supervisor"]
SUPO --> FO["folder module(s)<br/>udp-receiver + verify thread"]
SUPO --> LO["logs module(s)<br/>writer / forwarder"]
SUPO --> MO["modbus module(s)<br/>Modbus TCP server"]
SUPO --> SO["screen module(s)<br/>HTTP server"]
end
Figure 2 — Processes on each box. Source: DYODE wiki, Architecture (labels adapted to sending / receiving side).
Each module builds a message and wraps it in DYO1 framing. The message carries a DYA1 authentication block: an HMAC-SHA256 tag made with dyode.key, a random sender id and a counter. The input box splits the message into DYO1 datagrams of at most 1400 bytes and sends them to the module's port on the output box. The output box drops any datagram that does not arrive from the input box on the diode NIC. It then reassembles the message within the bounds in 2.2, verifies the tag, rejects replays and passes the message to the module. Folder payloads go over udpcast and are accepted only when their SHA-256 matches a signed manifest. Log batches are numbered, compressed and sent twice, about half a second apart. Any message with a missing piece is dropped whole, never delivered half-corrupted.
flowchart LR
subgraph T1["Bulk files: folder modules"]
direction LR
A1["File"] --> B1["udp-sender<br/>--async + FEC"] --> C1["udp-receiver"] --> D1["SHA-256 check"]
end
subgraph T2["Messages: logs, modbus, screen"]
direction LR
A2["Message<br/>(JSON / JPEG / batch)"] --> B2["Split into 1400-byte<br/>DYO1 datagrams"] --> C2["Reassembler<br/>(CRC32 check)"] --> D2["Whole message<br/>or nothing"]
end
Figure 3 — The two transports. Source: DYODE wiki, Architecture (labels adapted to sending / receiving side).
T1 ~~~ T2
The template expects request/response transactions. That is not applicable across the diode: every exchange is a one-way message with no response. The output box logs its validation results locally.
| Message type | Module | Content | Response |
|---|---|---|---|
| Folder manifest | I-2 | Files announced for transfer, with SHA-256 hashes | None |
| File payload | I-2 | File content over udpcast | None |
| Log batch | I-4 | Numbered, compressed batch of log lines (sent twice) | None |
| Heartbeat | I-4 | Liveness signal for stale-link detection | None |
| Modbus update | I-5 | Values for the configured register and coil ranges | None |
| Screen frame | I-6 | Captured screen image | None |
- Physical one-way path: no data can travel from the receiving side to the sending side.
-
Message authentication (1.5.0): HMAC-SHA256 with a shared key; replay protection with sender id and counter. DYODE refuses to start if the key is missing, unless
auth.required: falseis set, in which case it runs unauthenticated and logs a warning. - Receiver firewall (D-02): nftables rules on receiving boxes; listeners bind to the diode NIC.
- Bounded reassembly (D-05): a datagram flood cannot exhaust memory.
- Read-only Modbus (D-03): writes and out-of-range reads are refused and logged.
-
Smaller items (D-10 to D-12): NFS
subtree_check; screen viewer listens on the receiving-side address and caps viewers; the wizard warns when the syslogallow_fromlist is empty. - Install integrity: the offline bundle is checked against SHA256SUMS before anything changes.
Requirement IDs (R-n.m) are assigned here for traceability to Section 5.
4.1.1 Assumptions Dedicated diode NICs; IPv4; shared key and config on both boxes.
4.1.2 Technical requirements
R-1.1 Every message is HMAC-SHA256 signed (DYA1 inside DYO1). R-1.2 The receiver rejects replayed messages, using the sender id and counter. R-1.3 The receiver drops diode-port datagrams unless they come from the input box on the diode NIC; IPv6 is never accepted. R-1.4 Reassembly enforces the 10 s and 32 MB-per-listener bounds and the piece-count and size limits. R-1.5 The service does not start without dyode.key while auth.required is true.
4.1.3 Processing steps Build → sign → frame → fragment → send (UDP) → firewall filter → reassemble → verify → hand to module.
4.1.4 Processing time Partial messages expire after 10 s. End-to-end latency targets TBD.
4.1.5 Message format
Each DYO1 datagram carries a header: magic DYO1 (4 B, ignores stray traffic), message id (4 B), index / total (2 + 2 B, reassembly in any order) and a CRC32 of the whole message (4 B). Payload ≤ 1400 B. The DYA1 authentication block (tag, sender id, counter) sits inside this framing; its byte layout TBD from source.
4.1.6 Communication
UDP over the dedicated diode NIC; one base port per module in config.yaml.
4.1.7 Security
Section 3.5. The systemd unit is bound to the diode NIC (BindsTo=), waits for its address, stops when the NIC disappears and starts again when it returns.
4.2.1 Assumptions Full version only. The input folder is fed by users or systems on the sending side (directly or through the I-3 drop share). Files of any size; tested to 10 GB.
4.2.2 Technical requirements
R-2.1 Files are accepted only when their SHA-256 matches a signed manifest. R-2.2 The receiving box keeps /etc/sysctl.d/90-dyode-receive.conf: a larger UDP receive buffer, continuous writeback, and vm.dirty_bytes at 20% of RAM (minimum 128 MB, maximum 2 GB). R-2.3 A stalled transfer reports whether packets were dropped on the box (RcvbufErrors) or lost before reaching it. R-2.4 Manifest paths must stay inside out: no absolute paths, no .. or .dyode* parts. Symbolic links in in are never sent. R-2.5 Received files land in a staging folder outside out and are moved in atomically, so shares never expose half-received files. R-2.6 When a batch closes with files missing, each missing file is named in the log.
4.2.3 Processing steps
Pick up (inotify, with a 30 s rescan) → wait for settle → hash and send a manifest → send each file with FEC → retire it to .dyode_sent/ → receive into staging → verify SHA-256 against an open batch → atomic move into out/<path>. See Figures 4–6. Up to four batches stay open at once.
4.2.4 Processing time
Defaults: settle 2 s, autostart 5 hellos, file_gap 0.5 s, start_timeout 300 s, batch_timeout 300 s, keep_sent_hours 24. Throughput is capped by bitrate (8 Mbit/s shared across folder modules when unset), the single-threaded FEC encoder (about 350–400 Mbit/s with the default 8x16/64) and the receiving disk. Verified on v1.5.0 builds: 1.3 GB and 5 GB at 900 Mbit/s arrived with identical SHA-256. Site bitrate TBD.
4.2.5 Message format
JSON manifest carrying the dyode-manifest-v1 magic, a batch id, and each file's relative path, size and SHA-256; followed by each file as a udpcast payload.
4.2.6 Communication
UDP from the module base port, using udp-sender --async / udp-receiver with FEC 8x16/64 by default: up to 16 lost blocks per stripe are rebuilt; more aborts the file (Figure 7).
4.2.7 Security The staging folder cannot be shared (see I-3). External programs are run with argument lists, never a shell. A damaged or substituted file is rejected by SHA-256.
flowchart TB
subgraph SEND["Input box"]
direction TB
DROP["File written to<br/>the 'in' folder"] --> SETTLE{"Unchanged for<br/>'settle' seconds?"}
SETTLE -- "no, still being written" --> DROP
SETTLE -- yes --> MAN["Build a manifest:<br/>path, size, SHA-256<br/>of every ready file"]
MAN --> SENDM["udp-sender: manifest"]
SENDM --> SENDF["udp-sender: each file,<br/>with FEC repair data"]
SENDF --> RETIRE["Move file to .dyode_sent<br/>(kept 24 h, then deleted)"]
end
DIODE{{"Data diode"}}
subgraph RECV["Output box"]
direction TB
RX["udp-receiver writes<br/>into the staging folder"] --> Q["Queue for verification"]
Q --> ISMAN{"Is it a<br/>manifest?"}
ISMAN -- yes --> OPEN["Open a batch:<br/>expect these files"]
ISMAN -- no --> HASH["SHA-256 of the<br/>received file"]
HASH --> MATCH{"Matches a file<br/>an open batch expects?"}
MATCH -- yes --> PLACE["Atomic move into<br/>'out/path'"]
MATCH -- no --> REJECT["Discard and log<br/>'matching no expected file'"]
end
SENDM ==> DIODE
SENDF ==> DIODE
DIODE ==> RX
Figure 4 — The journey of a file. Source: DYODE wiki, Folder Transfers (labels adapted to sending / receiving side).
sequenceDiagram
autonumber
participant W as in/ folder
participant S as Input module
participant D as Diode
participant R as Output module
participant V as Verify thread
participant O as out/ folder
W->>S: a.csv, sub/b.bin settle
S->>S: hash both, write manifest
S->>D: udp-sender manifest.json
D->>R: udp-receiver to staging
R->>V: queue manifest
V->>V: open batch with 2 files expected
S->>D: udp-sender a.csv
D->>R: staging
R->>V: queue
S->>W: move a.csv to .dyode_sent/
V->>V: SHA-256 matches a.csv
V->>O: move to out/a.csv
S->>D: udp-sender sub/b.bin
D->>R: staging
R->>V: queue
S->>W: move sub/b.bin to .dyode_sent/
V->>O: move to out/sub/b.bin
V->>V: batch complete
Figure 5 — One batch, over time. Source: DYODE wiki, Folder Transfers (labels adapted to sending / receiving side).
stateDiagram-v2
[*] --> Open: manifest received
Open --> Open: file verified and stored (clock reset)
Open --> Open: data still arriving (clock reset)
Open --> Complete: last expected file stored
Open --> TimedOut: batch_timeout with nothing arriving
Open --> Superseded: a newer batch re-sends the missing files
Open --> Evicted: more than 4 batches open
Complete --> [*]
TimedOut --> [*]: missing files named in the log
Superseded --> [*]
Evicted --> [*]: missing files named in the log
Figure 6 — The receiver's view of a batch. Source: DYODE wiki, Folder Transfers (labels adapted to sending / receiving side).
flowchart LR
F["File"] --> SL["Slice: 512 data blocks<br/>in 8 stripes of 64"]
SL --> FEC["Plus 16 repair blocks<br/>per stripe"]
FEC --> LINK["Sent at 'bitrate'"]
LINK --> CHK{"Any stripe lost<br/>more than 16 blocks?"}
CHK -- no --> OK["Slice rebuilt"]
CHK -- yes --> FAIL["udp-receiver aborts:<br/>'Missed blocks: ...'"]
Figure 7 — How lost packets are handled (FEC). Source: DYODE wiki, Folder Transfers (labels adapted to sending / receiving side).
4.3.1 Assumptions
Optional. Set up by install.sh after the wizard, or unattended with flags. These shares stay on one side and never cross the diode.
4.3.2 Technical requirements
R-3.1 The output box shares received files read-only by default; the input box's drop folder is read-write. R-3.2 Share settings live in one managed block in /etc/exports and smb.conf; smb.conf is checked with testparm; a rejected export is rolled back; every changed file keeps a backup. R-3.3 The installer refuses: sharing the staging folder or a folder containing it, NFS to everyone, SMB guest access, SMB as root, and paths or names that could inject settings. R-3.4 NFS exports of a folder inside a larger filesystem use subtree_check.
4.3.3 Processing steps Users mount the share or connect with SFTP; re-running the same command changes nothing.
4.3.4 Processing time Not applicable.
4.3.5 Message format NFS, SMB, SSH/SFTP (standard protocols).
4.3.6 Communication
--share-nfs PATH=CLIENTS[:rw], --share-smb PATH=USER[:rw], --smb-password-file, --shares-only; client-only options through --with.
4.3.7 Security Client lists and SMB users defined per site TBD.
4.4.1 Assumptions Full version only. Sending-side devices can send syslog to the input box, or write log files on it.
4.4.2 Technical requirements
R-4.1 Accept syslog over UDP and TCP, with LF and RFC 6587 octet-counted framing; follow log files through rotation. R-4.2 Accept only sources in allow_from. R-4.3 Number and compress batches and send each twice, spaced in time. R-4.4 Report lost batches by sequence range (logs_gap), never drop them silently. R-4.5 Heartbeat with stale-link detection; bandwidth cap; control-character escaping.
4.4.3 Processing steps
Collect → bounded buffer → batch (boot id + sequence number) → zlib compress → bandwidth cap → send two copies about 0.5 s apart → drop duplicates, reorder, detect gaps → write to received.log and/or forward to the SIEM (Figure 8). A sender restart appears as a new boot id (logs_sender_restart), not a gap.
4.4.4 Processing time
Heartbeat every heartbeat s; the output box warns after stale_after s of silence. Default values and batch interval TBD; see the wiki Configuration Reference.
4.4.5 Message format
Output written to received.log (or one file per sending host with split_by_source: true), logrotate-friendly, in prefixed (default), raw or json format. Batch byte layout TBD.
4.4.6 Communication
Input box: listen_udp (e.g. 514), listen_tcp (e.g. 601), tail globs with tail_state offsets. Output box: forward to udp:// or tcp:// SIEM targets.
4.4.7 Security Source allow-list; signed batches (R-1.1).
flowchart TB
subgraph SRC["Input box: sources"]
direction LR
U["syslog over UDP<br/>listen_udp"]
T["syslog over TCP<br/>listen_tcp"]
F["files, like tail -F<br/>tail"]
end
SRC --> BUF["Bounded buffer<br/>(counts overflow)"]
BUF --> BATCH["Batch:<br/>boot id + sequence no.<br/>+ lines"]
BATCH --> Z["zlib compress"]
Z --> CAP["Bandwidth cap<br/>max_kbps"]
CAP --> C1["Copy 1"]
CAP --> C2["Copy 2, ~0.5 s later"]
C1 ==> D{{"Diode"}}
C2 ==> D
D ==> RX["Output box:<br/>drop duplicates,<br/>reorder, detect gaps"]
RX --> FILE["received.log<br/>(or one file per source)"]
RX --> FWD["Forward to SIEM<br/>udp:// or tcp://"]
Figure 8 — Log streaming path. Source: DYODE wiki, Log Streaming (labels adapted to sending / receiving side).
4.5.1 Assumptions The PLC exposes Modbus TCP and is reachable from the input box. Available in both versions: framed UDP on the full version, serial on the light version.
4.5.2 Technical requirements
R-5.1 Only the configured registers and coils ranges are mirrored. R-5.2 Client writes get exception 01 (Illegal Function) and are logged. R-5.3 Reads outside the configured ranges get exception 02 (Illegal Data Address); unmapped: zero keeps the old answer of 0. R-5.4 Updates crossing the diode are signed (R-1.1); unsigned values are not applied. R-5.5 Stale Modbus data is reported in the log and as a JSON event.
4.5.3 Processing steps The input box polls the PLC and sends an update; the output box verifies it and refreshes the mirror; receiving-side clients read the mirror.
4.5.4 Processing time Polling interval and staleness threshold TBD.
4.5.5 Message format
Modbus TCP (standard) on both local sides. Config fields, per the original example: type, port, ip, port_out, registers, coils. Current schema Verify.
4.5.6 Communication
PLC on TCP 502 → input box. Output box serves TCP port_out to receiving-side clients.
4.5.7 Security Mirror is read-only; receiving-side client allow-list TBD.
4.6.1 Assumptions Full version only. The input box watches a folder of JPEG screenshots produced on the sending side.
4.6.2 Technical requirements
R-6.1 The viewer listens on the receiving-side address by default, not on all addresses. R-6.2 At most max_streams (8) viewers at once. R-6.3 Frames are signed (R-1.1).
4.6.3 Processing steps New screenshot → sign → send as DYO1 datagrams → reassemble and verify → serve as a live MJPEG stream over HTTP.
4.6.4 Processing time Frame rate and latency TBD.
4.6.5 Message format JPEG frames across the diode; MJPEG over HTTP to browsers on the receiving side.
4.6.6 Communication UDP across the diode; HTTP viewer port on the output box TBD.
4.6.7 Security View-only; no input path to the source.
| Req. | Method | Verification |
|---|---|---|
| R-1.1, R-1.2 | Test | Capture a valid message on the diode segment, replay it, and confirm the receiver rejects it. Alter one byte and confirm the signature check fails. |
| R-1.3 | Test | Send a datagram to a diode port from a receiving-side host and over IPv6; confirm both are dropped. |
| R-1.4 | Test | Flood partial messages past 32 MB and 10 s; confirm memory stays bounded. |
| R-1.5 | Demonstration | Remove dyode.key; confirm the service refuses to start. |
| R-2.1–R-2.6 | Test | Transfer 1 GB and 5 GB sets at the site bitrate; compare SHA-256; withhold one file and confirm it is named as missing; attempt a .. path and a symlink; read the stall diagnostics. |
| R-3.1–R-3.4 | Inspection / Test | Review the managed blocks; attempt each refused share configuration. |
| R-4.1–R-4.5 | Test / Analysis | Send syslog in both framings; block delivery for a period; confirm a logs_gap entry and the dyode_logs.py --stats totals. |
| R-5.2, R-5.3 | Test | Issue a write and an out-of-range read from a receiving-side client; expect exceptions 01 and 02 and log entries. |
| R-6.1, R-6.2 | Inspection / Test | Check the listen address; open a ninth viewer and confirm it is refused. |
| Version | Date | Author / Owner | Description of change |
|---|---|---|---|
| 0.1 | 10/23/2026 | TBD | Initial draft mapping DYODE 1.5.0 to the CMS TLC ICD template. |
| Term | Definition |
|---|---|
| Data diode | A device that lets data travel in one direction only. |
| OT / ICS | Operational technology / industrial control systems: plant-floor controllers, sensors and networks. |
| DYO1 / DYA1 | DYODE's message framing and the authentication block carried inside it (1.5.0). |
| HMAC-SHA256 | A keyed message fingerprint; without the key, a matching tag cannot be produced. |
| Modbus TCP | An industrial protocol exposing numbered registers and coils over TCP. |
| udpcast | An open-source tool for sending files over UDP without acknowledgements. |
| nftables | The Linux packet-filtering framework used for the diode-port firewall. |
| SIEM | Security information and event management platform. |
| RcvbufErrors | Kernel counter of UDP packets dropped because a receive buffer was full. |
| Document | Location |
|---|---|
| CMS TLC Interface Control Document template | cms.gov — TLC downloads, Interface-Control-Document.docx |
| DYODE README and PYTHON3_MIGRATION.md | github.com/nc-intezra/dyode_python3 |
| Release notes v1.5.0 | github.com/nc-intezra/dyode_python3/releases |
| DYODE wiki: Home, Architecture, Folder Transfers, Log Streaming (source of Figures 1–8) | github.com/nc-intezra/dyode_python3/wiki (pages kept in docs/wiki/) |
| CAVEATS.md; DYODE_v1_full/LOG_STREAMING.md; wiki Configuration Reference | Repository (not reviewed for this draft) |
| Original DYODE install notes | github.com/wavestone-cdt/dyode |
The undersigned acknowledge that they have reviewed this ICD and agree with the information presented in it.
| Role | Name | Signature | Date |
|---|---|---|---|
| Business Owner | |||
| Sending-Side System Owner (Input box) | |||
| Receiving-Side System Owner (Output box) | |||
| Information System Security Officer |
The template's Appendix E (Notes to the Author) is removed in a finished ICD. This appendix replaces it with the items that must be closed before approval.
- DYA1 authentication block layout and log-batch layout (4.1.5, 4.4.5).
- Default module ports and the screen viewer HTTP port (4.6.6).
- Log heartbeat,
stale_afterand batch interval defaults; Modbus polling interval; screen frame rate (4.4.4, 4.5.4, 4.6.4). - Current
config.yamlschema against the wiki Configuration Reference (4.5.5). - Key generation, distribution and rotation procedure (2.3).
- Data classification for logs and files (Section 1).
- Site values: bitrate, receiving-side client allow-lists, NFS clients and SMB users (4.2.4, 4.5.7, 4.3.7).
DYODE Python 3 edition · documents release 1.5.0 · source pages are kept in docs/wiki/ of the main repository · GPLv3
DYODE (Python 3)
Modules
Running it
Repository