Skip to content

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/>&#40;LuaJIT&#41;]
    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:

  1. The transport (HTTP handler or NATS subscriber) parses the request parameters into a typed Go struct.
  2. The Go side normalizes the domain (IDNA-encodes it) and validates the request.
  3. 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.
  4. The Lua function returns a JSON-encoded result.
  5. 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_maintenance on 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_info and stashes the returned JSON. The HTTP /api/v1/info endpoint and the pdns_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_total and 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.