Management API
OxiDNS exposes a standalone control plane for:
- Process and startup health checks
- Config checks and raw config text validation
- Reload and shutdown control
- Plugin extension APIs
- Prometheus metrics export
This chapter covers management API enablement, authentication and transport, core endpoints, and metrics export.
How to Enable It
Shorthand
api:
http: "127.0.0.1:9088"
Listen addresses support ip:port, [ipv6]:port, and :port. http: ":9088" binds as dual-stack [::]:9088; use 0.0.0.0:9088 for IPv4-only.
Expanded Form
api:
http:
listen: "127.0.0.1:9443"
ssl:
cert: "/etc/oxidns/api.crt"
key: "/etc/oxidns/api.key"
client_ca: "/etc/oxidns/client-ca.crt"
require_client_cert: true
auth:
type: basic
username: "admin"
password: "secret"
webui:
root: "/etc/oxidns/webui"
index: "index.html"
Authentication and Transport
TLS
When both ssl.cert and ssl.key are configured, the API is served over HTTPS.
Optional hardening:
client_ca- Configures the client CA.
require_client_cert- Enforces mutual TLS.
Basic Auth
auth:
type: basic
username: "admin"
password: "secret"
When enabled, all API requests require Basic Auth.
The request header looks like this:
Authorization: Basic YWRtaW46c2VjcmV0
Encoding rules:
- Concatenate the raw string as
username:password - Base64-encode the whole string
- Prefix the header value with
Basic
In the example above, the Base64 value for admin:secret is YWRtaW46c2VjcmV0.
- This uses standard Base64, not URL-safe Base64.
- Do not encode
usernameandpasswordseparately. - Do not percent-encode or URL-encode first.
- The server compares the fully decoded value directly against
username:password.
Examples:
curl -u admin:secret http://127.0.0.1:9088/api/healthz
Or:
curl -H 'Authorization: Basic YWRtaW46c2VjcmV0' \
http://127.0.0.1:9088/api/healthz
Static WebUI Files
The management API can serve an external WebUI static directory. The WebUI is mounted at /, and management API routes are under /api/*:
api:
http:
listen: "0.0.0.0:9199"
webui:
root: "/etc/oxidns/webui"
index: "index.html"
After enabling it, open http://server:9199/ for the WebUI. The WebUI uses same-origin /api requests to reach the backend. Static files are not protected by Basic Auth, while /api/* keeps the management API authentication and CORS behavior. If webui.root is relative, it resolves against OxiDNS -d/--working-dir, not the configuration file directory. See WebUI Deployment for the full configuration, build steps, and standalone nginx example.
CORS / WebUI Cross-Origin Access
By default, the management API infers WebUI CORS behavior from api.http.listen:
- When listening on
0.0.0.0or[::], it returnsAccess-Control-Allow-Origin: *. - When listening on a specific IP, it allows WebUI origins on the same host without constraining the WebUI port. For example, if the API listens on
192.168.1.10:8080, bothhttp://192.168.1.10:3000andhttp://192.168.1.10:5173are allowed. - When listening on
127.0.0.1or[::1],localhostis also allowed.
To tighten or override the automatic policy, configure cors.allowed_origins explicitly:
api:
http:
listen: "0.0.0.0:8080"
cors:
allowed_origins:
- "http://localhost:3000"
- "http://192.168.1.100:3000"
When configured explicitly, allowed_origins is matched exactly against the browser's Origin header. Use "*" to allow any origin, but browsers will not accept credentialed cross-origin requests with a wildcard origin.
Route Layout
API routes fall into three groups:
- Global routes
- For example
/api/healthzand/api/control
- For example
- Plugin routes
- Uniform format:
/api/plugins/<plugin_tag>/<subpath>
- Uniform format:
- Observability routes
- For example
/api/metrics
- For example
Built-In Health Endpoints
GET /api/healthz
Purpose:
- Checks only whether the API listener has been established.
Responses:
200 OK:ok503 Service Unavailable:not_listening
GET /api/readyz
Purpose:
- Checks whether plugin initialization and server startup are complete.
Responses:
200 OK:ready503 Service Unavailable:not_ready
GET /api/health
Purpose:
- Returns JSON health details.
Example shape:
{
"status": "ok",
"version": "x.y.z",
"build_bundle": "full",
"uptime_ms": 12345,
"checks": {
"api": "ok",
"plugin_init": "ok",
"server_startup": "ok"
},
"plugins": {
"total": 12,
"servers": 4
}
}
build_bundle is the primary build bundle of the running binary: minimal, standard, full, or custom. Use GET /api/build when you need the full feature and plugin capability snapshot.
Build Capability Endpoint
GET /api/build
Purpose:
- Returns the package version, compiled feature bundle, enabled public Cargo features, and plugin types compiled into the running binary.
- The WebUI uses this endpoint to decide whether the current build supports each plugin kind and disables plugin entries that were not compiled in.
Example response:
{
"ok": true,
"build": {
"version": "1.1.4",
"bundle": "standard",
"enabled_bundles": ["standard"],
"enabled_features": [
"standard",
"api",
"metrics",
"server-dot",
"server-doh"
],
"supported_plugins": {
"servers": ["tcp_server", "udp_server"],
"executors": ["cache", "fallback", "forward", "sequence"],
"matchers": ["has_resp", "qname", "qtype"],
"providers": ["domain_set", "ip_set"]
}
}
}
Field notes:
version- Cargo package version.
bundle- Primary bundle for this binary:
minimal,standard,full, orcustom.
- Primary bundle for this binary:
enabled_bundles- Bundle features explicitly enabled at compile time. The default
fullbuild usually includes bothstandardandfull.
- Bundle features explicitly enabled at compile time. The default
enabled_features- Enabled public Cargo features. Internal
_features are intentionally omitted.
- Enabled public Cargo features. Internal
supported_plugins- Plugin types registered in the current binary, grouped into
servers,executors,matchers, andproviders.
- Plugin types registered in the current binary, grouped into
Built-In Control Endpoints
GET /api/control
Purpose:
- Returns the current process control-plane state.
The payload includes:
- Running state
- Uptime
- Active config path
- Whether shutdown has been requested
- Reload status snapshots
GET /api/system
Purpose:
- Returns process, platform, config path, reload status, resource usage, and compiled capability summary for the running binary.
The payload includes:
version- Cargo package version.
build- Same build capability object as the
buildfield returned byGET /api/build.
- Same build capability object as the
os/arch- Current runtime platform.
uptime_ms- Process uptime.
config_path- Active config file path.
reload- Reload status snapshot.
process_cpu_percent/process_memory_mb/system_memory_total_mb- Process and system resource usage information.
POST /api/shutdown
Purpose:
- Requests graceful shutdown.
Response:
202 Accepted
POST /api/reload
Purpose:
- Requests a config reload and reinitializes all plugins.
Responses:
202 Accepted- The request has been accepted.
409 Conflict- A reload is already
pendingorin_progress.
- A reload is already
GET /api/reload/status
Purpose:
- Returns the status of the most recent reload attempt.
Fields include:
statusidlependingin_progressokfailed
pendingin_progresslast_started_mslast_completed_mslast_success_mslast_error
Config Check Endpoints
GET /api/config
Purpose:
- Reads the config file referenced by the current startup options.
- Returns the raw YAML text, config path, content version, and file update time.
- Does not expand environment variable placeholders;
contentmatches the file on disk.
Example response:
{
"ok": true,
"path": "/etc/oxidns/config.yaml",
"format": "yaml",
"content": "plugins:\n - tag: forward\n type: forward\n",
"version": "sha256-hex",
"updated_at_ms": 1760000000000
}
PUT /api/config
Purpose:
- Saves the full YAML config file.
- Runs the same validation as
POST /api/config/validatebefore writing by default. - Can request an application-level reload after a successful save.
- Writes the original request text, not the expanded values of
${VAR}placeholders.
Request body:
{
"format": "yaml",
"content": "plugins:\n - tag: debug_main\n type: debug_print\n",
"base_version": "sha256-hex",
"validate": true,
"reload": false
}
Responses:
200 OK- The config was saved. The response includes the new version, plugin count, and init order.
400 Bad Request- The YAML cannot be parsed, validation failed, or
formatis notyaml.
- The YAML cannot be parsed, validation failed, or
409 Conflictbase_versiondoes not match the current file version, or a reload was requested while another reload is already running.
GET /api/config/check
Purpose:
- Validates the config file at the current config path.
- Expands environment variable placeholders in memory for validation, without modifying the file on disk.
Good fit:
- Check whether the on-disk config parses correctly and passes plugin dependency validation.
POST /api/config/validate
Purpose:
- Validates YAML config text sent directly in the request body.
- Also accepts the JSON envelope used by
PUT /api/config. - Expands environment variable placeholders in memory for validation, without returning or saving expanded config text.
Request body requirements:
- Non-empty UTF-8 YAML text; or
- JSON:
{"format":"yaml","content":"...yaml..."}
Good fit:
- Validate a config in the control plane before writing it to disk.
Plugin Extension APIs
Unified Format
/api/plugins/<plugin_tag>/<route>
- A few plugins also expose prefix routes. For example,
query_recorderuses/api/plugins/<tag>/records/<id>.
cache
GET /api/plugins/<cache_tag>/entries
Reads cache entries with pagination.
Query parameters:
limit: Page size. Defaults to100, maximum500.cursor: Pagination cursor.qname: Case-insensitive substring filter for the query domain in the cache key.
GET /api/plugins/<cache_tag>/flush
Clears the cache.
provider
POST /api/plugins/<provider_tag>/reload
Purpose:
- Reloads that provider's internal snapshot with the same configuration it used at startup.
- Does not rebuild unrelated plugins and does not change provider tags, dependency topology, or config structure.
Responses:
200 OK- The provider reload succeeded.
400 Bad Request- The provider does not exist, is not a live provider, or returned an error while reloading.
Good fit:
- Refreshing only the affected
domain_set,ip_set,geosite,geoip, oradguard_ruleprovider after downloading new rule files. - Avoiding the blast radius of an application-wide
POST /api/reload.
- When the change also updates
config.yaml, provider topology, the plugin list, or other non-provider structures,POST /api/reloadis still required.
GET /api/plugins/<cache_tag>/dump
Exports a cache dump.
POST /api/plugins/<cache_tag>/load_dump
Imports a cache dump.
reverse_lookup
GET /api/plugins/<tag>?ip=<ip_addr>
Looks up the domain cached for an IP address.
Example:
GET /api/plugins/reverse_lookup_main?ip=8.8.8.8
Responses:
- Hit: domain text, usually a fully-qualified domain name
- Miss: empty response body
- Invalid parameter:
400 Bad Request
query_recorder
GET /api/plugins/<tag>/records
Returns recorder rows ordered by created_at_ms descending and does not include steps.
Query parameters:
cursor=<created_at_ms>:<id>- Continue pagination after the last row from the previous page.
limit=<n>- Default
100, maximum500.
- Default
since_ms=<unix_ms>- Only return rows at or after this timestamp.
until_ms=<unix_ms>- Only return rows at or before this timestamp.
qname=<text>- Case-insensitive substring match against request question names.
client_ip=<text>- Case-insensitive substring match against the client IP string; IPv4/IPv6 fragments are accepted.
qtype=<type>- Exact match against request question type.
rcode=<rcode>- Exact match against response code.
status=all|error|has_response|no_response- Filter by recorder row status.
client_ip is the transport peer observed by the DNS server. If record lists or /stats/top_clients show only 127.0.0.1, the queries are usually passing through a local forwarder first, such as systemd-resolved, dnsmasq, AdGuardHome, dae, or clash. Check the deployment chain, point clients directly at OxiDNS, or configure a trusted src_ip_header for HTTP/DoH reverse-proxy deployments.
Responses:
200 OK- JSON shaped like:
{
"ok": true,
"next_cursor": "1713510000123:42",
"records": [
{
"id": 42,
"created_at_ms": 1713510000123,
"elapsed_ms": 12,
"request_id": 1234,
"client_ip": "192.0.2.10",
"questions_json": [
{ "name": "www.example.com.", "qtype": "A", "qclass": "IN" }
],
"req_rd": true,
"req_cd": false,
"req_ad": false,
"req_opcode": "Query",
"req_edns_json": null,
"error": null,
"has_response": true,
"rcode": "NoError",
"resp_aa": false,
"resp_tc": false,
"resp_ra": true,
"resp_ad": false,
"resp_cd": false,
"answer_count": 1,
"authority_count": 0,
"additional_count": 0,
"answers_json": [
{
"name": "www.example.com.",
"class": "IN",
"ttl": 300,
"rr_type": "A",
"payload_kind": "A",
"payload_text": "192.0.2.1",
"payload": { "ip": "192.0.2.1" }
}
],
"authorities_json": [],
"additionals_json": [],
"signature_json": [],
"resp_edns_json": null
}
]
}
GET /api/plugins/<tag>/records/<id>
Returns one full record plus its steps array.
Responses:
200 OK- JSON containing a
recordobject.record.recordholds the fixed main-table fields andrecord.stepsholds path events.
- JSON containing a
404 Not Found- The record does not exist.
DELETE /api/plugins/<tag>/records
Clears all persisted query-history rows and steps path events for the current recorder. The operation first flushes the background writer queue, then deletes all rows from the SQLite records table and clears the in-memory tail.
Responses:
200 OK- JSON shaped like:
{
"ok": true,
"cleared_records": 128
}
GET /api/plugins/<tag>/stats/plugins
Aggregates plugin hit information from recorded path events.
Query parameters:
since_ms=<unix_ms>until_ms=<unix_ms>kind=matcher|executor|builtin|all- Same as
/records, supportsqname,client_ip,qtype,rcode, andstatusfilters.
Response fields:
kindtagcheckedmatchedexecutedquery_totalquery_share
GET /api/plugins/<tag>/stats/top_clients
Aggregates query counts by client IP.
Query parameters:
limit=<n>- Number of buckets to return. Defaults to
20. The backend no longer enforces a200cap; large values increase SQLite sorting and response-size cost.
- Number of buckets to return. Defaults to
- Same as
/records, supportssince_ms,until_ms,qname,client_ip,qtype,rcode,status, andmatcher_tagfilters.
Response fields:
sample_sizerows[].keyrows[].countrows[].share
GET /api/plugins/<tag>/stats/top_qnames
Aggregates query counts by question name. Query parameters and response fields match /stats/top_clients.
GET /api/plugins/<tag>/stats/qtype
Aggregates distribution by QTYPE. Supports the same time range and filter parameters as /records, and returns sample_size plus rows[].key/count/share.
GET /api/plugins/<tag>/stats/rcode
Aggregates distribution by response code or special status bucket. Supports the same time range and filter parameters as /records, and returns sample_size plus rows[].key/count/share.
GET /api/plugins/<tag>/stats/latency
Returns latency summary values, histogram buckets, and slow-query ranking.
Query parameters:
slow_limit=<n>orlimit=<n>- Number of slow-query rows to return. Defaults to
20. The backend no longer enforces a200cap.
- Number of slow-query rows to return. Defaults to
- Same as
/records, supports time range and filter parameters.
GET /api/plugins/<tag>/stats/timeseries
Aggregates query trends into time buckets.
Query parameters:
bucket=minute|hourbuckets=<n>- Number of buckets to return. Defaults to
60, maximum720.
- Number of buckets to return. Defaults to
- Same as
/records, supports time range and filter parameters.
GET /api/plugins/<tag>/stream
Streams newly written records over SSE.
Query parameters:
tail=<n>- Replay the most recent
nrecords from the in-memory tail first, then continue streaming.
- Replay the most recent
event: recorduses the fullRecordDetailJSON asdata.- Heartbeat comment frames are sent periodically to keep the connection alive.
- Clients should send
Accept: text/event-streamand tolerate heartbeat frames, error events, empty payloads, and brief reconnects.
Prometheus Metrics
GET /api/metrics
This endpoint is registered when the API is enabled. It is the single Prometheus text endpoint; plugins do not expose separate stats/metrics HTTP endpoints.
Current exported metrics include:
query_totalquery_error_totalquery_inflightquery_latency_countquery_latency_sum_mscache_lookup_totalcache_hit_totalcache_miss_totalcache_expired_totalcache_insert_totalcache_skip_totalcache_lazy_refresh_totalcache_entry_countforward_query_totalforward_success_totalforward_error_totalforward_timeout_totalforward_latency_countforward_latency_sum_msforward_upstream_query_totalforward_upstream_success_totalforward_upstream_error_totalforward_upstream_timeout_totalforward_upstream_latency_countforward_upstream_latency_sum_msfallback_primary_totalfallback_primary_error_totalfallback_secondary_totalblackhole_block_totalhosts_hit_totalhosts_miss_totalratelimit_allowed_totalratelimit_rejected_totalserver_request_totalserver_completed_totalserver_controlled_totalserver_failed_totalserver_inflightserver_latency_countserver_latency_sum_msipset_entries_totalipset_dropped_totalipset_write_totalipset_write_error_totalnftset_entries_totalnftset_dropped_totalnftset_write_totalnftset_write_error_totalros_address_list_observe_totalros_address_list_dropped_totalros_address_list_sync_error_totalros_address_list_sync_timeout_totalreverse_lookup_ptr_hit_totalreverse_lookup_ptr_miss_totalreverse_lookup_cache_insert_totalreverse_lookup_cache_entriesdownload_success_totaldownload_failure_totaldownload_timeout_totalhttp_request_dispatch_totalhttp_request_error_totalhttp_request_dropped_totalscript_run_totalscript_success_totalscript_error_totalscript_timeout_totalreload_trigger_totalreload_error_totalreload_provider_reload_totalreload_provider_reload_error_totalcron_job_run_totalcron_job_skipped_totalcron_executor_error_total
These metrics use low-cardinality plugin-level labels such as plugin_tag, name, kind, reason, result, and protocol. server_* additionally carries a protocol label (udp/tcp/dot/quic/doh). forward_upstream_* carries an upstream label whose value is the upstream tag (or its resolved address when no tag is configured); since the upstream set is fixed at startup and bounded, this stays a low-cardinality dimension. High-cardinality values such as qname or client IP are intentionally excluded from the generic metrics layer; use query_recorder when per-query detail is needed.
Config Reference
Minimal Management Plane
api:
http: "127.0.0.1:9088"
Good fit:
- Local operations
- Process self-checks
- Metrics scraping
Protected Control Plane
api:
http:
listen: "0.0.0.0:9443"
ssl:
cert: "/etc/oxidns/api.crt"
key: "/etc/oxidns/api.key"
auth:
type: basic
username: "admin"
password: "secret"
Good fit:
- Remote control
- Integration with external operations platforms
Mutual-TLS Control Plane
api:
http:
listen: "0.0.0.0:9443"
ssl:
cert: "/etc/oxidns/api.crt"
key: "/etc/oxidns/api.key"
client_ca: "/etc/oxidns/client-ca.crt"
require_client_cert: true
Good fit:
- Strictly controlled automation systems
- Multi-tenant or high-sensitivity operational environments