# Tutorial: OT / ICS decoys for beginners

**Audience:** Students and newcomers who already completed the
[first honeypot lab](beginner-student-lab.md) (or equivalent) and want to see
**fake industrial / ICS devices** that speak real OT protocols on the wire.

**Time:** about 30-40 minutes  
**Outcome:** You run Contained Modbus, SNMP, S7comm, and EtherNet/IP decoys on
loopback, probe them with simple clients, and understand that **no real PLC /
building controller / BMC** is running on your laptop.

!!! warning "Research only: loopback"
    Keep every `--listen-*` on `127.0.0.1`. Do not publish OT ports to the
    Internet for this tutorial. See [safety model](../explanation/safety-model.md).

## What you will learn

1. Why CyberHalluciNet speaks Modbus / SNMP / S7 / ENIP (and friends) as **protocol emulators**
2. How to start Contained-tier OT listeners with the sensor binary
3. How to confirm health and which protocols are active
4. How to read a Modbus register and an SNMP `sysDescr`
5. Where shared OT identity lives (one persona across protocols)

## Prerequisites

- Repository checked out
- Go toolchain able to build the sensor (`cd engine && go build ./cmd/sensor`)
- Optional clients (install what you need; Python fallbacks are included):
  - `snmpget` / `snmpwalk` (net-snmp) **or** skip SNMP client steps
  - `curl` (status JSON)
  - Python 3 (stdlib only) for Modbus / ENIP smoke probes

You do **not** need: Ollama, Live ack, MicroVM, real PLCs, or cloud accounts.

## Background (3 minutes)

An OT honeypot is not a Siemens PLC, Rockwell PAC, or BACnet controller. It is a
**Contained protocol emulator** that:

- answers recon with plausible identity (vendor, product, firmware, serial),
- shares one **process image** / device persona across colocated OT listeners,
- refuses or demotes abuse (writes, vault uploads, UDP amplification) under budgets,
- never executes attacker PLC logic (no MC7 run, no inbound Dial).

| Research port | Decoy | Transport | Real device on sensor? |
|---------------|-------|-----------|------------------------|
| 1502 | Modbus TCP | TCP | No |
| 1161 or 161 | SNMP | UDP | No |
| 1102 | S7comm | TCP | No |
| 44818 | EtherNet/IP | TCP + UDP | No |
| 47808 | BACnet/IP | UDP | No |
| 6623 → 623 | IPMI/RMCP | UDP | No |
| 2404 | IEC-104 smoke | TCP | No |
| 6969 | TFTP smoke | UDP | No |

Full flag table: [OT decoys reference](../reference/ot-decoys.md).

## Step 1: Build the sensor

From the repository root:

```bash
cd engine
go build -o ../bin/sensor ./cmd/sensor
mkdir -p /tmp/chn-intel-ot
```

## Step 2: Start Contained OT decoys

In a dedicated terminal (paths assume repository root):

```bash
./bin/sensor \
  --profile=research \
  --interaction=contained \
  --mode=static \
  --listen-status=127.0.0.1:8080 \
  --listen-ssh=127.0.0.1:12222 \
  --listen-http=127.0.0.1:18081 \
  --listen-modbus=127.0.0.1:1502 \
  --listen-snmp=127.0.0.1:1161 \
  --listen-s7=127.0.0.1:1102 \
  --listen-enip=127.0.0.1:44818 \
  --ot-disable-mutation \
  --ot-vendor-family=siemens \
  --containment-config=config/containment/research.json \
  --intel-dir=/tmp/chn-intel-ot \
  --telemetry-spool=/tmp/chn-spool-ot \
  --sensor-id=student-ot-01
```

Leave it running. SSH/HTTP use high loopback ports so they do not collide with
other labs; the OT listeners above are the focus.

!!! tip "Shared OT plane"
    As soon as any OT listen address is set, the sensor builds one shared
    `otwire` context (persona + memory + UDP guard + Tier2 budget). Modbus and
    SNMP then answer with the **same** vendor family facts.

## Step 3: Confirm status

```bash
curl -s http://127.0.0.1:8080/ | python3 -m json.tool | head -40
```

Look for `"protocols"` containing at least `modbus`, `snmp`, `s7comm`, and `enip`,
and `"status": "ok"`. Sensor logs should also show:

```text
sensor: OT shared context ready profile=research vendor=siemens ...
sensor: modbus decoy on 127.0.0.1:1502
sensor: s7comm decoy on 127.0.0.1:1102
...
```

## Step 4: Modbus TCP (easiest OT win)

Read three holding registers from unit id `1` (function code 0x03) with Python:

```bash
python3 - <<'PY'
import socket, struct
def mb_read_holding(host, port, unit=1, addr=0, qty=3):
    # MBAP + PDU: FC03
    pdu = bytes([0x03, addr >> 8, addr & 0xFF, qty >> 8, qty & 0xFF])
    mbap = struct.pack(">HHHB", 1, 0, len(pdu) + 1, unit) + pdu
    s = socket.create_connection((host, port), 2)
    s.sendall(mbap)
    resp = s.recv(256)
    s.close()
    assert resp[7] == 0x03, resp.hex()
    n = resp[8]
    regs = struct.unpack(">" + "H" * (n // 2), resp[9:9 + n])
    return regs
print(mb_read_holding("127.0.0.1", 1502))
PY
```

You should see a short tuple of register values (persona / profile seed), not a
connection refusal.

Optional with `mbpoll` / `pymodbus` if installed: same host/port.

## Step 5: SNMP UDP

If you have net-snmp tools:

```bash
snmpget -v2c -c public 127.0.0.1:1161 1.3.6.1.2.1.1.1.0
# sysDescr: expect Linux / vendor-ish text after OT seed rewrite
snmpget -v2c -c public 127.0.0.1:1161 1.3.6.1.2.1.1.5.0
# sysName
```

Community `public` / `private` are accepted on the research sample. Wrong
community → no useful GET response (drop / empty), not a crash.

Python fallback (GET `sysDescr`):

```bash
python3 - <<'PY'
# Minimal SNMPv2c GET for sysDescr: educational only
import socket
# Prefer snmpget when available; this is a tiny smoke that the UDP listener answers.
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(2)
# Prebuilt GET for 1.3.6.1.2.1.1.1.0 community public (from unit tests / labs)
pkt = bytes.fromhex(
  "302602010104067075626c6963a01902040a0b0c0d020100020100300b300906082b06010201010100"
)
s.sendto(pkt, ("127.0.0.1", 1161))
data, _ = s.recvfrom(2048)
print("snmp_udp_bytes", len(data), "hex_prefix", data[:16].hex())
PY
```

## Step 6: Touch S7 and ENIP (optional, still beginner)

Prove the TCP listeners accept connections:

```bash
nc -vz 127.0.0.1 1102
nc -vz 127.0.0.1 44818
```

ENIP also listens on UDP `44818` for ListIdentity. A zero-length UDP probe may
be rate-limited / ignored by the egress guard, that is expected Contained
behavior, not a broken bind.

## Step 7: Stop and inspect intel (optional)

Stop the sensor with Ctrl-C. If session logging was enabled under
`/tmp/chn-intel-ot`, list transcripts:

```bash
ls -la /tmp/chn-intel-ot/transcripts 2>/dev/null || true
```

## What you did *not* run

- Real Siemens TIA / Rockwell Studio 5000 / BACnet building controllers
- Vault MC7 execution or outbound CTI dials from the sensor
- Internet-facing OT ports

## Next tutorials

- [OT decoys advanced](ot-decoys-advanced.md): shared persona, UDP anti-amp,
  Tier2 / vault, canaries, BACnet / IPMI / IEC-104 / TFTP, compose
- Reference: [OT decoys](../reference/ot-decoys.md)
- Safety: [safety model](../explanation/safety-model.md)
