170 lines
6.9 KiB
Markdown
170 lines
6.9 KiB
Markdown
# Architecture
|
|
|
|
This page covers how RconDataBridge is built internally: the module map,
|
|
the boot sequence, the security model, and the Project Zomboid engine
|
|
quirks the mod works around. It's aimed at anyone reading or modifying the
|
|
mod's own Lua code, rather than consuming the protocol from outside; for
|
|
that, see [Protocol Reference](Protocol-Reference) and [Operations
|
|
Reference](Operations-Reference).
|
|
|
|
## Boot sequence
|
|
|
|
On `Events.OnServerStarted`, bootstrap code runs, in order:
|
|
|
|
1. Run internal self-tests (JSON and base64 codecs), if enabled.
|
|
2. Register the base RCON options (`ProtocolVersion`, `WorldStats`,
|
|
`Request`, `Response`, `LastResponse`).
|
|
3. Register every operation into the operation registry.
|
|
4. Publish the initial `RconDataBridge_ProtocolVersion` value.
|
|
5. Start the request pipeline and the world and player telemetry pollers.
|
|
|
|
`Events.OnServerStarted` is known to fire more than once per boot in
|
|
Project Zomboid, so every step above guards against running twice: option
|
|
registration checks for an existing option first and wraps the underlying
|
|
engine call in a protected call as a second line of defense, and each
|
|
poller's `init()` function is guarded by a local flag, so its event
|
|
listeners are only ever added once.
|
|
|
|
## Request flow
|
|
|
|
Once booted, a single request travels through the modules like this:
|
|
|
|
```text
|
|
RCON client
|
|
|
|
|
| changeoption RconDataBridge_Request <base64 JSON>
|
|
v
|
|
RconDataBridge_Request option (single mailbox slot)
|
|
|
|
|
| polled every N ticks (Poll Interval setting)
|
|
v
|
|
Request pipeline
|
|
|-- base64 decode, size check ----> reject: SCHEMA_ERROR / PAYLOAD_TOO_LARGE
|
|
|-- JSON decode, envelope check --> reject: SCHEMA_ERROR
|
|
|-- idempotency check ------------> known id: replay the cached response
|
|
|-- rate limit check -------------> reject: RATE_LIMITED
|
|
v
|
|
Operation registry (allowlist, exact-name lookup)
|
|
|-- unknown op -------------------> reject: OP_NOT_ALLOWED
|
|
v
|
|
Operation handler (Ops/RDB_Op*.lua)
|
|
|-- handler throws ---------------> reject: INTERNAL_ERROR
|
|
v
|
|
Response envelope { ok, data } or { ok: false, error }
|
|
|
|
|
| every outcome above, success or rejection, is also written to the audit log
|
|
v
|
|
RconDataBridge_Response / RconDataBridge_LastResponse
|
|
|
|
|
| showoptions, base64 decode
|
|
v
|
|
RCON client
|
|
```
|
|
|
|
See [Protocol Reference](Protocol-Reference) for what each error code
|
|
means and how a client should consume this cycle.
|
|
|
|
## Module responsibilities
|
|
|
|
### Options registry
|
|
|
|
A thin wrapper over Project Zomboid's server options store. Registering an
|
|
option needs both an "add option" call and a separate "add to public
|
|
options" call, or it never shows up in `showoptions`, since that command
|
|
reads a different internal list than the one a plain add populates.
|
|
|
|
### Request pipeline
|
|
|
|
Polls `RconDataBridge_Request` on a configurable tick interval, since
|
|
there's no "option changed" event. Decodes base64 to JSON, validates the
|
|
envelope, checks idempotency and the rate limit, dispatches through the
|
|
operation registry, publishes the response, and always logs the outcome to
|
|
the audit trail, including rejections.
|
|
|
|
### Operation registry
|
|
|
|
The allowlist; Dispatch is an exact-name lookup into a table, nothing more;
|
|
new operations are added one file per operation.
|
|
|
|
### Security
|
|
|
|
Payload size cap (4096 bytes decoded), envelope schema validation, and a
|
|
global, not per-connection, sliding-window rate limit, since RCON requests
|
|
carry no caller identity to limit by individually.
|
|
|
|
### Idempotency
|
|
|
|
Replays the cached response for a repeated request `id` within a
|
|
configurable window, protecting side-effecting operations, such as
|
|
`save_server`, from retries. After the window expires, the same `id` runs
|
|
fresh.
|
|
|
|
### Config
|
|
|
|
A facade over admin-tunable settings. A small set of values, such as the
|
|
maximum payload size, are fixed constants; everything else is read live
|
|
from Sandbox Options. See [Configuration](Configuration).
|
|
|
|
### World stats
|
|
|
|
Refreshed roughly once an in-game minute from the engine's game clock,
|
|
cached in memory, and mirrored directly to `RconDataBridge_WorldStats`, so
|
|
`get_world_stats` and the option always agree, and reading the option skips
|
|
a request/response round trip entirely.
|
|
|
|
### Player snapshot
|
|
|
|
The source of truth for player data, both online and offline, persisted so
|
|
it survives a restart, since the engine has no API for offline player data
|
|
on its own.
|
|
|
|
### JSON and base64 codecs
|
|
|
|
Self-contained shared codecs with no external dependencies. Each runs its
|
|
own self-test at boot when the debug self-test setting is enabled.
|
|
|
|
## Security model
|
|
|
|
A handful of rules define the mod's security boundary and are treated as
|
|
hard constraints rather than style preferences:
|
|
|
|
- **No new transport, port, or authentication layer.** RCON access is the
|
|
only access control, by design.
|
|
- **No dynamic operation dispatch.** The operation registry only ever does
|
|
an exact-name table lookup; an RCON request can never cause arbitrary
|
|
Lua code to run.
|
|
- **All operations are explicitly allowlisted.** An unrecognized `op` value
|
|
returns `OP_NOT_ALLOWED` rather than being interpreted in any way.
|
|
- **Every remote action is logged**, with its request id, operation,
|
|
result, and timestamp, including rejected requests, through the audit
|
|
log described above.
|
|
- **Requests are validated and bounded** on payload size, envelope schema,
|
|
and rate, before an operation handler ever runs.
|
|
|
|
## Engine constraints the mod works around
|
|
|
|
A number of behaviours in the code exist specifically to work around gaps
|
|
or quirks in the Project Zomboid server engine, rather than being
|
|
arbitrary design choices. The straightforward ones:
|
|
|
|
| Constraint | Why | How it's handled |
|
|
|-------------------------------------------------|-----------------------------|------------------------------------------------------|
|
|
| No "option changed" event | Doesn't exist in the engine | Tick-throttled polling in the request pipeline |
|
|
| No connect or disconnect event | Doesn't exist in the engine | Diffing the online roster against the last known set |
|
|
| No offline-player registry API | Doesn't exist in the engine | Player snapshots are persisted independently |
|
|
| `OnServerStarted` fires more than once per boot | Engine behavior | Every initializer guards on a local flag |
|
|
|
|
As a special client facing quirk:
|
|
|
|
### RCON responses can span multiple packets
|
|
|
|
Confirmed from the server's own RCON response handling: a large response,
|
|
such as a `showoptions` call once this mod's options are added to the
|
|
roughly 140 built-in ones, gets chunked across multiple packets with no
|
|
explicit end-of-response marker.
|
|
|
|
This is a client-side concern rather than a mod-side one. Any RCON client
|
|
must accumulate every packet sharing a request id rather than resolving on
|
|
the first one it receives. See the client checklist in [Protocol
|
|
Reference](Protocol-Reference#9-client-implementation-checklist).
|