Architecture¶
The Simulator HTTP Server is a thin Go shell around the Platform Filter Lua filter engine. It accepts request types over HTTP or NATS, dispatches each one to a pool of long-lived LuaJIT workers, and returns the JSON result the Lua side produces.
Role in Platform Filter¶
The service does not produce data and is not on the live DNS path. It reads the same local LMDB databases the recursor reads — the OX Feed categorization databases, the static settings database, and the subscriber settings the Settings Syncer populates from Redis — and answers queries against them.
The Admin Portal is the main consumer. Operators also use it directly when debugging customer reports about a domain being unexpectedly filtered or unfiltered.
There is one live-traffic use case outside the DNS path: a proxy can
delegate filtering decisions to the simulator over HTTP, using
/api/v1/simulate, and then apply the returned verdict to the proxied
HTTP or TLS/SNI request. The current OpenResty implementation enables
this mode with proxyremotebaseurl.
flowchart LR
A[Admin Portal /<br/>operator] -- HTTP --> S[(simulator-server)]
A2[Other component] -. NATS .-> S
P[Proxy] -- HTTP<br/>remote decision --> S
S --> W[Lua worker pool<br/>(LuaJIT)]
W -. read .-> L1[(OX Feed LMDBs)]
W -. read .-> L2[(Settings LMDB)]
A simulator request never reaches the recursor and never modifies state outside of the worker's own in-process Lua tables. Ad-hoc Admin Portal and operator requests only read local LMDB state. Remote proxy requests also return their verdict to the proxy, which may allow, block, or reject the live proxied connection based on that response.
Remote proxy decision backend¶
A proxy using this backend sends the client IP, requested domain,
optional path, profile, settings override, and vhost to
/api/v1/simulate with verbose=0. If the proxy sends an X-API-Key
header, it must match one of the simulator's configured http.api_keys.
This mode lets the proxy process avoid local filter-engine and LMDB
decision work, centralizing that work in the simulator-server process.
In the current OpenResty implementation, the platform setting
proxyremotebaseurl selects this backend and proxyremoteapikey
populates the X-API-Key header. Proxy implementations that do not
support this remote backend continue to run their local decision logic.
Request flow¶
Every API call follows the same five steps:
- The transport (HTTP handler or NATS subscriber) parses the request parameters into a typed Go struct.
- The Go side normalizes the domain (IDNA-encodes it) and validates the request.
- The request is pushed onto an internal Go channel. The next available Lua worker pulls it off and calls the matching Lua function in its own LuaJIT state.
- The Lua function returns a JSON-encoded result.
- The transport writes that JSON back to the caller verbatim.
Workers are blocking and synchronous from the Lua side — each worker processes one request at a time. Concurrency comes from running more than one worker.
Lua worker pool¶
At startup the service creates lua.workers independent LuaJIT states,
each of which loads lua.file (typically the shipped
pdns-pf/simulator/server.lua). Each worker is a goroutine that loops
on the request channels.
- Independent states. Workers share no Lua-level state. The LMDB databases the Lua side reads are shared (read-only at the LMDB layer), but every Lua table and cached value lives in exactly one worker.
- Maintenance tick. Once per second the worker calls
call_maintenanceon its Lua state. This is what advances the Lua side's "now" timestamp and runs whatever maintenance callbacks the filter engine has registered (cache TTLs, log flushers, etc.). - Info refresh. Immediately after each maintenance tick, the worker
calls
call_infoand stashes the returned JSON. The HTTP/api/v1/infoendpoint and thepdns_pf_simulator_info{region}gauge are both populated from that cached payload. - Crash semantics. A worker that fails to start (Lua import or
parse error) increments
pdns_pf_simulator_lua_worker_create_error_totaland the process exits — the service refuses to run with fewer workers than configured.
Core concepts¶
These terms are used throughout the configuration and operations pages.
Request types¶
The service understands four request types. Each one maps to a Lua function exposed by the loaded entry-point file:
| Request | HTTP endpoint | Lua function |
|---|---|---|
| Domain lookup | /api/v1/domain |
call_lookup_domain |
| User simulation | /api/v1/simulate |
call_simulate_user |
| Code mappings | /api/v1/codemappings |
call_codemappings |
| Info | /api/v1/info |
call_info (called by the worker, not by HTTP) |
The NATS transport exposes the same four under subjects of the form
<prefix>.simulator.<request> (see NATS subjects).
Domain lookup¶
A categorization query for one (domain, optional path) pair. Returns
the OX Feed classification codes that apply, without applying any
subscriber-specific rules. This is the call the
--test-and-exit flag fires before
exiting.
User simulation¶
The full filter pipeline for a given subscriber. Inputs are the domain,
optional path, an identifier the filter engine can resolve to a
subscriber (one of ip, username, or deviceid), and a number of
modifiers (override settings, override rules_ip, target a specific
profile or vhost, request verbose rule-evaluation logs). The
return value carries the verdict plus, when verbose is set, a trace of
every rule that fired. This is also the endpoint used by proxy
implementations that delegate live decisions to the simulator.
Code mappings¶
A dump of the OX Feed classification code → human-readable name table from the local LMDB. The Admin Portal calls this on a slow schedule to keep its category labels in sync with the active feed.
Info¶
A small JSON object the Lua side updates on its maintenance tick. At
minimum it carries the deployment region; downstream code reads it via
the /api/v1/info HTTP endpoint or via the
pdns_pf_simulator_info{region} metric.
NATS subjects¶
When the NATS transport is enabled (see Configuration → nats),
the service registers under the itsy service name simulator and serves
the following request subjects, all prefixed with itsy.prefix:
| Subject | Request type | Payload |
|---|---|---|
<prefix>.simulator.domain |
Domain lookup | JSON {"domain": "...", "path": "..."} |
<prefix>.simulator.simulate |
User simulation | JSON SimulateUserReq (same fields as the HTTP form params) |
<prefix>.simulator.codemappings |
Code mappings | Empty |
<prefix>.simulator.info |
Info | Empty |
<prefix>.simulator.echo |
— | Returns the request bytes unchanged. Used by itsy for health-checks. |
The default itsy.prefix is svc. The shipped example sets it to
pdnspf.