Add INI Config Format and Custom Services pages
132
Custom-Services.md
Normal file
132
Custom-Services.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Custom Services (Bring-Your-Own)
|
||||
|
||||
DECNET ships 25+ first-class service plugins (see
|
||||
[Services catalog](Services-Catalog)), but you can add your own **without
|
||||
writing a plugin** by declaring a `[custom-<name>]` section in the INI.
|
||||
|
||||
The source of truth is `decnet/custom_service.py` (the `CustomService`
|
||||
class) and the `[custom-*]` branch of `decnet/ini_loader.py`.
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
At INI load time the parser scans every section starting with `custom-`:
|
||||
|
||||
```python
|
||||
# decnet/ini_loader.py
|
||||
if section.startswith("custom-"):
|
||||
s = cp[section]
|
||||
svc_name = section[len("custom-"):]
|
||||
image = s.get("binary", "")
|
||||
exec_cmd = s.get("exec", "")
|
||||
ports_raw = s.get("ports", "")
|
||||
ports = [int(p.strip()) for p in ports_raw.split(",") if p.strip().isdigit()]
|
||||
cfg.custom_services.append(
|
||||
CustomServiceSpec(name=svc_name, image=image, exec_cmd=exec_cmd, ports=ports)
|
||||
)
|
||||
```
|
||||
|
||||
Each resulting `CustomServiceSpec` is wrapped into a `CustomService`
|
||||
(subclass of `BaseService`) and registered dynamically at deploy time
|
||||
(`register_custom_service()` in the service registry). Once registered, the
|
||||
custom slug behaves like any built-in service — you can list it in a decky's
|
||||
`services=` line and reference it in the `--services` CLI flag.
|
||||
|
||||
`CustomService.compose_fragment()` produces a Docker Compose fragment of the
|
||||
shape:
|
||||
|
||||
```yaml
|
||||
image: <binary>
|
||||
container_name: <decky>-<slug>
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_NAME: <decky>
|
||||
LOG_TARGET: <optional, from --log-target>
|
||||
command: [<tokens from exec=>] # only emitted if exec= is non-empty
|
||||
```
|
||||
|
||||
Underscores in the custom name are converted to dashes in the container
|
||||
suffix (`slug = name.replace("_", "-")`).
|
||||
|
||||
Note: `CustomService.dockerfile_context()` returns `None`, so DECNET will
|
||||
**not** try to build the image. The image you name in `binary=` must be
|
||||
pullable (public registry, or present in the local Docker daemon).
|
||||
|
||||
---
|
||||
|
||||
## Accepted keys
|
||||
|
||||
| Key | Required | Meaning |
|
||||
|----------|----------|---------|
|
||||
| `binary` | yes | Docker image reference, e.g. `myorg/weirdapp:1.2.3`. Becomes the container's `image:`. |
|
||||
| `exec` | no | Command to run inside the container. Space-split into a list and emitted as Compose `command:`. If omitted, the image's default `CMD`/`ENTRYPOINT` is used. |
|
||||
| `ports` | no | Comma-separated list of integer ports. Non-numeric tokens are silently dropped. Used by the deploy layer to expose/route the ports. |
|
||||
|
||||
There are no other keys. Anything else in the section is ignored by
|
||||
`ini_loader.py`.
|
||||
|
||||
The custom service slug (the part after `custom-`) must not collide with a
|
||||
built-in service name.
|
||||
|
||||
---
|
||||
|
||||
## Minimal working example
|
||||
|
||||
Ship a lightweight HTTP echo server as a decoy on port 9000:
|
||||
|
||||
```ini
|
||||
[general]
|
||||
net = 192.168.1.0/24
|
||||
gw = 192.168.1.1
|
||||
interface = eth0
|
||||
|
||||
# Define the custom service
|
||||
[custom-echoweb]
|
||||
binary = ealen/echo-server:0.9.2
|
||||
exec = node index.js
|
||||
ports = 80
|
||||
|
||||
# Use it on a decky, alongside a built-in service
|
||||
[decky-app01]
|
||||
ip = 192.168.1.130
|
||||
services = ssh, echoweb
|
||||
nmap_os = linux
|
||||
|
||||
[decky-app01.ssh]
|
||||
ssh_version = OpenSSH_8.9p1 Ubuntu-3ubuntu0.6
|
||||
kernel_version = 5.15.0-91-generic
|
||||
users = root:toor
|
||||
```
|
||||
|
||||
Deploy:
|
||||
|
||||
```bash
|
||||
sudo decnet deploy --config echo.ini --interface eth0 \
|
||||
--log-target 192.168.1.200:5140
|
||||
```
|
||||
|
||||
At deploy time, `decky-app01` will get two containers: a standard `ssh`
|
||||
decoy and a `decky-app01-echoweb` container running
|
||||
`ealen/echo-server:0.9.2` with `command: ["node", "index.js"]`.
|
||||
|
||||
---
|
||||
|
||||
## Caveats
|
||||
|
||||
- No per-service persona subsection is honoured for custom services — the
|
||||
`service_cfg` argument in `CustomService.compose_fragment()` is accepted
|
||||
but ignored. If you need persona/override behaviour, write a real plugin.
|
||||
- No Dockerfile build step. `binary=` is pulled as-is.
|
||||
- If `exec=` is empty, no `command:` key is emitted — the image's default
|
||||
entrypoint runs.
|
||||
- `LOG_TARGET` is injected into the container env **only** if a CLI
|
||||
`--log-target` was passed to `decnet deploy`; otherwise the custom service
|
||||
is responsible for its own logging.
|
||||
|
||||
---
|
||||
|
||||
See also:
|
||||
- [INI Config Format](INI-Config-Format)
|
||||
- [Services catalog](Services-Catalog)
|
||||
- [Service personas](Service-Personas)
|
||||
255
INI-Config-Format.md
Normal file
255
INI-Config-Format.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# INI Config Format
|
||||
|
||||
DECNET deployments are described by an INI file passed via `--config`. The file
|
||||
is parsed by `decnet/ini_loader.py` (structural validation in
|
||||
`decnet/models.py::validate_ini_string`) and converted into an `IniConfig` of
|
||||
`DeckySpec` and `CustomServiceSpec` objects.
|
||||
|
||||
This page is an exhaustive reference. **Every key documented here is accepted
|
||||
by the parser.** Keys not listed are ignored (or cause a validation error for
|
||||
typed fields).
|
||||
|
||||
Related pages:
|
||||
- [Services catalog](Services-Catalog)
|
||||
- [Archetypes](Archetypes)
|
||||
- [Service personas](Service-Personas)
|
||||
- [Custom services](Custom-Services)
|
||||
|
||||
---
|
||||
|
||||
## Section types
|
||||
|
||||
A DECNET INI file contains four kinds of sections:
|
||||
|
||||
1. `[general]` — fleet-wide network settings (optional, but recommended).
|
||||
2. `[<decky-name>]` — one decky (or a group of deckies if `amount=N`).
|
||||
3. `[<decky-name>.<service>]` — persona/override config for one service on
|
||||
one decky (or group).
|
||||
4. `[custom-<name>]` — a bring-your-own service definition. See
|
||||
[Custom services](Custom-Services).
|
||||
|
||||
Section names must match `^[A-Za-z0-9\-_.]+$` (enforced on the decky `name`).
|
||||
|
||||
### How the parser distinguishes a service subsection from a decky
|
||||
|
||||
A section with a dot in its name is treated as a **per-service subsection**
|
||||
if and only if the last dotted segment is a **known service name** registered
|
||||
in `decnet.services.registry.all_services()`. Otherwise the whole section
|
||||
name is treated as a decky name.
|
||||
|
||||
Example: `[decky-01.ssh]` → subsection (because `ssh` is a registered service).
|
||||
`[decky.webmail]` → decky section (because `webmail` is not a registered
|
||||
service).
|
||||
|
||||
---
|
||||
|
||||
## `[general]` section
|
||||
|
||||
Fleet-wide settings. All keys optional.
|
||||
|
||||
| Key | Type | Meaning |
|
||||
|-------------|--------|------------------------------------------------------|
|
||||
| `net` | string | LAN CIDR for the decoy network, e.g. `192.168.1.0/24`. Maps to `IniConfig.subnet`. |
|
||||
| `gw` | string | LAN gateway IP. Maps to `IniConfig.gateway`. |
|
||||
| `interface` | string | Host interface to bind MACVLAN/IPVLAN to, e.g. `eth0`, `wlp6s0`. |
|
||||
|
||||
> **Note:** `log_target` is **not** an INI key. It is a CLI flag
|
||||
> (`--log-target HOST:PORT`). You may leave a commented `#log_target=` reminder
|
||||
> in your INI, but the parser will not read it.
|
||||
|
||||
Example:
|
||||
|
||||
```ini
|
||||
[general]
|
||||
net = 192.168.1.0/24
|
||||
gw = 192.168.1.1
|
||||
interface = eth0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decky sections
|
||||
|
||||
Each non-general, non-`custom-*` section that is not a service subsection
|
||||
defines a decky (or a group). The section name becomes the decky's `name`.
|
||||
|
||||
| Key | Type | Default | Meaning |
|
||||
|--------------------|----------|---------|---------|
|
||||
| `ip` | string | auto | Static IP inside the subnet. **Forbidden when `amount>1`** — raises `ValueError`. |
|
||||
| `services` | csv list | `None` | Comma-separated service slugs. If omitted, falls back to the `--randomize-services` pool or the archetype's service list. |
|
||||
| `archetype` | string | `None` | Archetype slug (e.g. `linux-server`, `windows-workstation`). Archetypes pre-seed services, distro, and `nmap_os`. See [Archetypes](Archetypes). |
|
||||
| `amount` | int | `1` | Number of deckies to spawn from this section. Must be `1..100`. When `>1`, the section acts as a template and deckies are named `<section>-01`, `<section>-02`, …. |
|
||||
| `nmap_os` | string | `linux` | TCP/IP stack family for fingerprint spoofing. Accepted: `linux`, `windows`, `embedded`, `bsd`, `cisco`. Also accepted as `nmap-os` (alias). |
|
||||
| `mutate_interval` | int | global | Auto-rotation interval in minutes (`>=1`). Also accepted as `mutate-interval` (alias). |
|
||||
|
||||
### `amount` expansion rules
|
||||
|
||||
```ini
|
||||
[corp-workstations]
|
||||
archetype = windows-workstation
|
||||
amount = 5
|
||||
```
|
||||
|
||||
Expands to `corp-workstations-01` … `corp-workstations-05`. Any
|
||||
`[corp-workstations.<service>]` subsection propagates to **all five**
|
||||
expanded deckies (see Resolution order below).
|
||||
|
||||
Setting `ip=` together with `amount>1` is rejected — one IP cannot be shared.
|
||||
|
||||
---
|
||||
|
||||
## Per-service subsections `[<decky>.<service>]`
|
||||
|
||||
Persona overrides for a single service on one decky (or a group). The last
|
||||
dotted segment must be a registered service slug — otherwise the subsection
|
||||
is ignored.
|
||||
|
||||
Every key inside such a subsection is passed through verbatim as a `dict`
|
||||
into `DeckySpec.service_config[<service>]` and then consumed by the service
|
||||
plugin's `compose_fragment(..., service_cfg=...)`. The available keys are
|
||||
therefore **service-specific** — see [Service personas](Service-Personas) for
|
||||
the full per-service key list.
|
||||
|
||||
Examples (excerpt from `development/test-full.ini`):
|
||||
|
||||
```ini
|
||||
[decky-webmail.http]
|
||||
server_header = Apache/2.4.54 (Debian)
|
||||
response_code = 200
|
||||
fake_app = wordpress
|
||||
|
||||
[decky-webmail.smtp]
|
||||
smtp_banner = 220 mail.corp.local ESMTP Postfix (Debian/GNU)
|
||||
smtp_mta = mail.corp.local
|
||||
|
||||
[decky-ldapdc.ssh]
|
||||
ssh_version = OpenSSH_8.9p1 Ubuntu-3ubuntu0.6
|
||||
kernel_version = 5.15.0-91-generic
|
||||
users = root:toor,admin:admin123,svc_backup:backup2024
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resolution order
|
||||
|
||||
When the runtime configuration for a decky's service is built, values are
|
||||
layered in this order (later wins):
|
||||
|
||||
1. **Service defaults** — hard-coded inside the service plugin.
|
||||
2. **Archetype** — if the decky's section had `archetype=…`, the archetype's
|
||||
default services/distro/`nmap_os` apply.
|
||||
3. **Per-decky keys** — `ip`, `services`, `nmap_os`, `mutate_interval` from
|
||||
the decky section override archetype defaults.
|
||||
4. **Per-service subsection** — keys in `[<decky>.<service>]` override the
|
||||
service's built-in defaults. For a group (`amount>N`), the subsection
|
||||
`[<group>.<service>]` is copied into every expanded decky.
|
||||
|
||||
---
|
||||
|
||||
## Full realistic example
|
||||
|
||||
A 10-decky heterogeneous fleet, mixing an archetype pool with role-themed
|
||||
boxes. This is a trimmed version of `development/test-full.ini`.
|
||||
|
||||
```ini
|
||||
# full.ini — 10-decky heterogeneous fleet
|
||||
#
|
||||
# sudo decnet deploy --config full.ini --interface eth0 \
|
||||
# --log-target 192.168.1.200:5140
|
||||
|
||||
[general]
|
||||
net = 192.168.1.0/24
|
||||
gw = 192.168.1.1
|
||||
interface = eth0
|
||||
|
||||
# ── Archetype pool: 5 Windows workstations ───────────────────────────
|
||||
[windows-workstation]
|
||||
archetype = windows-workstation
|
||||
amount = 5
|
||||
|
||||
# ── Web / Mail stack ─────────────────────────────────────────────────
|
||||
[decky-webmail]
|
||||
ip = 192.168.1.110
|
||||
services = http, smtp, imap, pop3
|
||||
nmap_os = linux
|
||||
|
||||
[decky-webmail.http]
|
||||
server_header = Apache/2.4.54 (Debian)
|
||||
fake_app = wordpress
|
||||
|
||||
[decky-webmail.smtp]
|
||||
smtp_banner = 220 mail.corp.local ESMTP Postfix (Debian/GNU)
|
||||
|
||||
# ── Windows file server ──────────────────────────────────────────────
|
||||
[decky-fileserv]
|
||||
ip = 192.168.1.111
|
||||
services = smb, ftp, tftp
|
||||
nmap_os = windows
|
||||
|
||||
[decky-fileserv.smb]
|
||||
workgroup = CORP
|
||||
server_name = FILESERV01
|
||||
os_version = Windows Server 2019
|
||||
|
||||
# ── Postgres / Mongo / Elastic ───────────────────────────────────────
|
||||
[decky-dbsrv02]
|
||||
ip = 192.168.1.113
|
||||
services = postgres, mongodb, elasticsearch
|
||||
nmap_os = linux
|
||||
|
||||
[decky-dbsrv02.postgres]
|
||||
pg_version = 14.5
|
||||
|
||||
[decky-dbsrv02.elasticsearch]
|
||||
es_version = 8.4.3
|
||||
cluster_name = prod-search
|
||||
|
||||
# ── IoT / SCADA (embedded TCP stack) ─────────────────────────────────
|
||||
[decky-iot]
|
||||
ip = 192.168.1.117
|
||||
services = mqtt, snmp, conpot
|
||||
nmap_os = embedded
|
||||
mutate_interval = 60
|
||||
|
||||
[decky-iot.snmp]
|
||||
snmp_community = public
|
||||
sys_descr = Linux router 5.4.0 #1 SMP x86_64
|
||||
|
||||
# ── Legacy admin box ─────────────────────────────────────────────────
|
||||
[decky-legacy]
|
||||
ip = 192.168.1.119
|
||||
services = telnet, vnc, ssh
|
||||
nmap_os = bsd
|
||||
|
||||
[decky-legacy.ssh]
|
||||
ssh_version = OpenSSH_7.4p1 Debian-10+deb9u7
|
||||
kernel_version = 4.9.0-19-amd64
|
||||
users = root:root,admin:password,pi:raspberry
|
||||
|
||||
# ── Bring-your-own service (see Custom-Services) ─────────────────────
|
||||
[custom-weirdapp]
|
||||
binary = myorg/weirdapp:1.2.3
|
||||
exec = /opt/weirdapp/run --listen 0.0.0.0:9000
|
||||
ports = 9000
|
||||
```
|
||||
|
||||
Deploy with:
|
||||
|
||||
```bash
|
||||
sudo decnet deploy --config full.ini --interface eth0 \
|
||||
--log-target 192.168.1.200:5140
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation and errors
|
||||
|
||||
- Empty INI (no sections): `"The provided INI content must contain at least
|
||||
one section"`.
|
||||
- Total payload > 512 KB: rejected.
|
||||
- At least one decky section is mandatory (`IniConfig.at_least_one_decky`).
|
||||
- `amount` must be a positive integer `<=100`.
|
||||
- `ip=` with `amount>1` is rejected.
|
||||
- `mutate_interval` must be an integer.
|
||||
- `DeckySpec` and `CustomServiceSpec` both have `extra="forbid"` — unknown
|
||||
*typed* fields on the parser's output raise Pydantic errors.
|
||||
Reference in New Issue
Block a user