Skip to content

Operator Interface

Authorized Use Only

Espilon is a security research and educational tool. Use it only on devices and networks you own or are explicitly authorized in writing to test.

C3PO is a Qt6-native desktop operator tool (PySide6 + qasync) for managing a fleet of ESP32 agents. Its window is split into a persistent sidebar, a stacked area holding six views, and an always-visible console at the bottom. This page documents each view in detail. For installation and invocation see Installation; for the C2 protocol and crypto see Protocol & Crypto.


Window layout

┌─────────────┬──────────────────────────────────────────────────┐
│  Sidebar    │  Current View                                     │
│  ─────────  │  ──────────────────────────────────────────────── │
│  [Fleet]    │  (FleetView / DeviceView / ModulesView / ...)     │
│  [Device]   │                                                   │
│  [Modules]  │                                                   │
│  [Data]     │                                                   │
│  [Keystore] │                                                   │
│  [Provision]│                                                   │
│             ├────────────────────────────────────────────────── │
│  ● :2626    │  Console (log + command bar)                      │
│  3 agents   │                                                   │
└─────────────┴──────────────────────────────────────────────────┘
Component Source Role
Sidebar app/widgets/sidebar.py Navigation buttons, server status badge (:2626), live agent count, logo
View stack app/mainwindow.py (QStackedWidget) The six views, switched via sidebar buttons
Console strip app/widgets/log_pane.py, command_bar.py Global log pane, device selector, and command bar, visible on every view
flowchart LR
    SB[Sidebar nav] --> VS[View stack]
    VS --> FL[Fleet]
    VS --> DV[Device]
    VS --> MD[Modules]
    VS --> DA[Data]
    VS --> KS[Keystore]
    VS --> PR[Provision]
    C2[C2Events signals] --> VS
    C2 --> CON[Console strip]

All views receive their data through C2Events Qt signals. The C2 layer never imports UI code, so the interface updates reactively as devices connect, respond, or drop.


1. Fleet view

File: app/views/fleet.py.

The Fleet view is the landing screen. It renders every connected device as a card in a responsive grid. Each card shows:

  • Device ID (for example esp32-home-01)
  • Status dot: green = Connected, yellow = Idle, red = Offline
  • Chip type (for example esp32c6)
  • IP address and port (for example 192.168.1.100:2626)
  • The loaded modules (for example mod_network, mod_ble)

Click any card to open the Device view for that agent. When nothing is connected, the grid shows a waiting state ("No devices connected").

Live updates

Cards appear and disappear automatically as devices complete the handshake or disconnect. No manual refresh is needed.


2. Device view

File: app/views/device.py.

This is the main operational screen for a single agent.

┌──────────────┬────────────────────────────────────────────┐
│ Modules      │  Log                                        │
│ ──────────── │  [ device: esp32-home-01 ]                 │
│ mod_network  │  > status                                  │
│ mod_ble      │  chip: esp32c6  heap: 234 KB  uptime: 4m12s│
│              │  modules: mod_network, mod_ble              │
│    [+]       │                                             │
│              │  > ping 8.8.8.8                            │
│              │  PING 8.8.8.8: 42 ms                       │
│              ├────────────────────────────────────────────┤
│              │  esp32-home-01 > _                    Send  │
└──────────────┴────────────────────────────────────────────┘
Area Behavior
Modules panel (left) Modules currently loaded on this device. The + button jumps to the Modules view to inject more.
Log pane (centre) All command output for this device, color-coded by message type (success, error, info).
Command bar (bottom) Sends commands directly to this device. Up/Down arrows navigate command history.

Commands use the form command_name [arg1 arg2 ...]. Output is correlated to the request through the protobuf request_id so responses land in the correct pane.


3. Modules view

File: app/views/modules.py.

A catalog browser plus the injection workflow. The catalog is built by ModuleCatalog from every module directory under the --modules path; each directory holds a module.toml manifest and its C source.

  • Left list: all discovered modules.
  • Right panel: the selected module's name, version, and description; its supported chips (targets); the full command list with descriptions; and its required syscalls and min_heap.

Example manifest the right panel reads:

[module]
name        = "mod_network"
version     = "1.2.0"
description = "Network toolkit: ping, ARP scan, TCP flood, tunnel"
targets     = ["esp32", "esp32c6", "esp32s3", "esp32c3", "esp32s2"]
source      = "cmd_network.c"

[commands]
ping       = "ICMP echo to target host"
arp_scan   = "ARP sweep on subnet"
dos_tcp    = "TCP SYN flood <target> <port> <count>"

[requires]
syscalls = ["socket", "dns_resolve", "wifi_scan", "tcp_connect"]
min_heap = 32768

Inject flow

Pick a target device from the dropdown and press Inject. C3PO then:

sequenceDiagram
    participant UI as Modules view
    participant CC as compile_module
    participant SG as sign_module
    participant INJ as ModuleInjector
    participant DEV as ESP32 agent
    UI->>CC: compile .c -> ELF (asyncio.to_thread)
    CC-->>UI: relocatable ELF bytes
    UI->>SG: HMAC-SHA256 with master_key
    SG-->>UI: [32B HMAC][ELF]
    UI->>INJ: inject(blob, name)
    INJ->>DEV: mod_chunk (1 KB chunks)
    INJ->>DEV: mod_load name core timeout [persist]
    DEV-->>INJ: load result (up to 30 s)
  1. Compile the C source to a relocatable ELF. Compilation is blocking, so it runs in asyncio.to_thread.
  2. Sign the ELF with the device's master_key (HMAC-SHA256 via HKDF) producing a [32B HMAC][ELF] blob.
  3. Transfer the blob in 1 KB chunks via mod_chunk, then issue mod_load.
  4. Show progress in the console; wait up to 30 seconds for the load to complete.

The toolchain is selected automatically from the chip: Xtensa GCC (xtensa-esp32-elf-gcc, xtensa-esp32s3-elf-gcc, ...) for ESP32 / S2 / S3, and riscv32-esp-elf-gcc for C3 / C6 / H2. Tools are located via PATH or ~/.espressif/tools/.

Where the tunnel lives

There is no dedicated tunnel view. Reverse tunnels (tun_start / tun_stop / tun_status in mod_network) are driven as ordinary commands from the Device view or the console. Camera streaming is planned for a future version.


4. Data view

File: app/views/data.py.

Aggregated intelligence tables, populated from the structured AGENT_DATA output of module commands. Each module emits pipe-delimited records that the Data view parses into rows. The view has three tabs.

Tab Populated by Wire format Columns
WiFi mod_network scan, mod_fakeap AP\|SSID\|BSSID\|RSSI\|channel\|auth SSID, BSSID, RSSI, Ch, Auth, Device
BLE mod_ble scan BLE\|addr\|name\|RSSI\|type Address, Name, RSSI, Type, Device
Network mod_network arp_scan, mod_mitm HOST\|ip\|mac\|latency\|ports Host, MAC, Latency, Open Ports, Device

Both tabs support:

  • Sorting: click any column header.
  • CSV export: the Export CSV button writes the current table to a file.

The trailing Device column records which agent produced each record, so results from a whole fleet aggregate into one table.


5. Keystore view

File: app/views/keystore.py.

Manage device keys without editing keys.json by hand. The table lists every registered device.

Column Notes
Device ID Unique agent identifier
Key Truncated hex preview (for example a1b2c3d4...4567); click to copy the full key
Transport wifi or cellular
Board Chip type (for example esp32c6)
Status Live: Connected / Inactive / Offline

Operations:

  • Add: create an entry manually. Paste a hex key or press Generate to create a random 32-byte key.
  • Edit: double-click a row (or the Edit button) to change key, transport, or board.
  • Delete: remove an entry behind a confirmation dialog.
  • Copy Key: place the full hex key on the clipboard.
  • Reload: re-read keys.json from disk to pick up external changes.

The keystore is the trust anchor

Each entry holds a device's 32-byte master_key. The handshake identifies a device only by trying each key in the store, so losing an entry makes that device unidentifiable until it is re-added. See Security.

Status values update live through Qt signals as devices connect and disconnect.


6. Provision view

File: app/views/provision.py.

Flash and register a new device from scratch. Beyond esptool (and optionally ESP-IDF for local builds) no external tools are needed.

Form fields

Field Required Notes
Port yes Serial port (for example /dev/ttyACM0); auto-populated, with a Scan button to refresh
Chip yes Dropdown of supported variants
Device ID no Unique identifier; auto-generated as a random hex string if empty
Network yes Radio buttons: WiFi (SSID + password) or GPRS (APN, SIM PIN, board pins)
C2 Server yes IP and port (default 127.0.0.1:2626)
Firmware yes Path to a local Espilon-Firmware checkout, or empty to download the latest GitHub release
Keystore yes Path to keys.json (default keys.json)
Erase flash no Checkbox, recommended for first-time provisioning

Supported chip variants in the dropdown:

CHIPS = ["esp32c6", "esp32s3", "esp32s3-v0.2", "esp32", "esp32c3", "esp32s2",
         "esp32s3-cam-n16r8", "esp32-cam-aithinker"]

For GPRS, selecting a board profile auto-fills modem pins. Profiles include t-call and t-sim7070g. Camera board profiles (esp32s3-cam-n16r8, esp32-cam-aithinker) also exist, though runtime camera support is planned for a future version.

Provisioning sequence

flowchart TD
    A[Generate master_key, 32 random bytes] --> B{Firmware?}
    B -->|local checkout| C[idf.py build]
    B -->|empty| D[Download GitHub release]
    C --> E[Generate factory NVS, 0x14000, 24 KB]
    D --> E
    E --> F[Generate config NVS, 0x9000, 16 KB,<br/>ChaCha20-encrypted WiFi + C2 config]
    F --> G[Flash all partitions via esptool]
    G --> H[Save entry to keys.json]
    H --> I[Device connects on next boot,<br/>appears in Fleet view]
  1. Generate the master_key (32 random bytes) and store it in the keystore.
  2. Build firmware via idf.py build, or download release binaries.
  3. Generate the factory NVS partition (fctry, 0x14000, 24 KB) holding the plaintext master_key and device ID. Flash encryption secures it at boot when enabled.
  4. Generate the config NVS partition (cfg, 0x9000, 16 KB) holding the ChaCha20-encrypted WiFi credentials and C2 address, using a key derived from the master key via HKDF.
  5. Flash every partition with esptool: bootloader, partition table, NVS, OTA data, factory, firmware.
  6. Save the device_id and master_key to keys.json.

After provisioning, the device connects to C3PO on its next boot and appears in the Fleet view.


The sidebar persists on every view. It holds the navigation buttons, the logo, a server status badge showing the listening port (for example ● :2626), and a live agent count.

The console strip is also always visible, so commands can be sent from any view without switching to Device.

Console   esp32-home-01
─────────────────────────────────────────────
[+] esp32-home-01 192.168.1.100 (wifi)
> status
chip: esp32c6  heap: 234 KB  uptime: 4m12s
─────────────────────────────────────────────
[ esp32-home-01 ▾ ]  > _                 Send
Element Behavior
Header "Console" label plus the selected device ID in the accent color
Log pane Scrolling terminal output: connection events ([+] device_id addr (transport)), system logs (blue/dim), command responses (white), errors (red)
Device selector Choose the active device for console commands
Command bar Send command_name [arg1 arg2 ...] to the selected device; Up/Down navigate history

Auto-status and keep-alive probe replies are silent and do not appear in the console unless a command was user-initiated.