Files
nxdns/docs/api.md
T

9.5 KiB

nxdns REST API

nxdns serves its admin API itself, on web.bind:web.port (default port 8080), as plain HTTP. TLS termination, where an operator wants it, belongs to a reverse proxy in front; the session cookie deliberately omits the Secure attribute so the supported plain-HTTP LAN deployment works.

This page is orientation. The machine-readable contract is src/web/openapi.yaml, which the running server hands out unauthenticated at GET /api/openapi.yaml. When this page and the yaml disagree, the yaml wins.

Conventions

  • All request and response bodies are JSON (application/json), except /metrics (Prometheus text format), /api/openapi.yaml (YAML) and /api/queries/live (text/event-stream).
  • Field names are snake_case, matching settings keys and SQL column names.
  • Every error response carries the envelope {"error": "<message>"}. The message is operator-facing text; internal detail never reaches the wire — a 500 body is generic and the cause goes to the server log.
  • Request bodies are strict: an unknown field is a 400, a body over 1 MiB is a 413.
  • A request whose path matches but whose method does not answers 405 with an Allow header. An unknown /api path is a JSON 404; unknown non-/api paths fall through to the embedded SPA (index.html), so client-side routing works.
  • Item routes ({id}) match a positive integer id only.
  • Mutations to groups, blocklists, rules, local records, forward zones, clients and client prefixes take effect live. Upstreams and /api/settings are restart-required.

Authentication

Cookie sessions, in memory, no accounts — one operator password.

  • Authentication is on exactly when web.password_hash is set. When no password is set, every route is open and POST /api/auth/login answers {"authenticated": true, "auth_required": false} without setting a cookie.
  • POST /api/auth/login takes {"password": "..."}. A correct password answers 200 with a Set-Cookie for nxdns_session (HttpOnly; SameSite=Lax; Path=/, Max-Age = the session TTL). A wrong password is a 401; a stored hash the server cannot read is a 500, never a 401. Login attempts spend rate-limit tokens like any other request, and argon2id verification is deliberately slow.
  • Every route whose auth policy is session answers 401 {"error": "authentication required"} without a valid cookie.
  • Sessions live web.session_ttl_hours (default 24) from login; use does not extend the lifetime. The table holds 32 sessions; a 33rd login evicts the least recently used. Nothing is persisted — a server restart logs every operator out.
  • Changing the password through PUT /api/settings revokes every live session immediately; the new password applies without a restart.
  • POST /api/auth/logout ends the cookie's session and clears the cookie. It answers 200 whether or not the session was live.

Rate limiting

A token bucket per client address: capacity and refill are both web.api_rate_limit_per_min (default 300) per minute, so a page-load burst up to the capacity is admitted and the long-run rate holds.

  • An over-budget request answers 429 {"error": "rate limited"} with a Retry-After header giving the seconds until a token is available (rounded up, never zero).
  • Loopback addresses (127.0.0.0/8 and ::1) are exempt while web.api_localhost_exempt is true (the default).
  • Exempt routes, which never consult a bucket: /metrics and /api/health (a Prometheus scrape must never see 429) and /api/queries/live (one long-lived stream must not drain its address's bucket; it is bounded by the SSE connection cap instead).
  • The limiter tracks at most 4096 addresses. When the table is full and no slot is reclaimable, requests from unknown addresses are refused with 429.

Live query stream (SSE)

GET /api/queries/live is server-sent events over chunked transfer, Content-Type: text/event-stream, Cache-Control: no-store.

  • The stream opens with retry: 3000, so a browser EventSource reconnects on its own after a drop.
  • Each query is one frame: event: query and a single data: line of JSON. The payload carries the GET /api/queries row fields minus id (a live entry precedes persistence): ts, domain, client_ip, qtype, blocked, block_reason, response_time_us, cache_hit, upstream.
  • A : ping comment heartbeat goes out after 15 s of quiet, keeping middleboxes from reaping the idle connection.
  • Each subscriber buffers up to 64 entries. A client too slow for the query rate overflows its buffer and the server ends the stream cleanly after delivering what the buffer held — queries are never held back for a slow reader. There is no gap marker: on reconnect, re-sync through GET /api/queries, which has the missed rows.
  • Connections per client address are capped at web.sse_max_connections_per_ip (default 3); over the cap is a 429. The cap binds loopback too. The server holds at most 32 concurrent streams in total; when all slots are taken, the answer is a 503.

Operations

Auth open means no session required; session means a valid session cookie is required whenever a password is set. Rate limit counted spends a token; exempt never consults the limiter.

Method Path Auth Rate limit Purpose
GET /metrics open exempt Prometheus metrics
GET /api/health open exempt Health rollup
GET /api/version open counted Build and uptime
GET /api/openapi.yaml open counted This API's OpenAPI document
POST /api/auth/login open counted Log in
POST /api/auth/logout session counted Log out
GET /api/queries session counted Query log page
GET /api/queries/live session exempt Live query stream (server-sent events)
GET /api/stats session counted Totals for a period
GET /api/stats/timeseries session counted Bucketed counts for a period
GET /api/lookup session counted Explain a domain
GET /api/upstream/health session counted Upstream pool health
GET /api/groups session counted List groups
POST /api/groups session counted Create a group
GET /api/groups/{id} session counted Read a group
PUT /api/groups/{id} session counted Update a group
DELETE /api/groups/{id} session counted Delete a group
GET /api/groups/{id}/sources session counted Blocklist sources assigned to a group
PUT /api/groups/{id}/sources session counted Replace the assignment
GET /api/blocklists session counted List blocklist sources
POST /api/blocklists session counted Add a blocklist source
POST /api/blocklists/update session counted Refresh every enabled source now
GET /api/blocklists/{id} session counted Read a blocklist source
PUT /api/blocklists/{id} session counted Update a blocklist source
DELETE /api/blocklists/{id} session counted Delete a blocklist source
GET /api/rules session counted List rules
POST /api/rules session counted Create a rule
GET /api/rules/{id} session counted Read a rule
PUT /api/rules/{id} session counted Update a rule
DELETE /api/rules/{id} session counted Delete a rule
GET /api/local-records session counted List local DNS records
POST /api/local-records session counted Create a local record
GET /api/local-records/{id} session counted Read a local record
PUT /api/local-records/{id} session counted Update a local record
DELETE /api/local-records/{id} session counted Delete a local record
GET /api/forward-zones session counted List forward zones
POST /api/forward-zones session counted Create a forward zone
GET /api/forward-zones/{id} session counted Read a forward zone
PUT /api/forward-zones/{id} session counted Update a forward zone
DELETE /api/forward-zones/{id} session counted Delete a forward zone
GET /api/clients session counted List clients
GET /api/clients/{id} session counted Read a client
PUT /api/clients/{id} session counted Rename or regroup a client
DELETE /api/clients/{id} session counted Forget a client
GET /api/client-prefixes session counted List client prefixes
PUT /api/client-prefixes session counted Replace the prefix table
GET /api/upstreams session counted List upstream resolvers
POST /api/upstreams session counted Add an upstream
GET /api/upstreams/{id} session counted Read an upstream
PUT /api/upstreams/{id} session counted Update an upstream
DELETE /api/upstreams/{id} session counted Delete an upstream
GET /api/pause session counted Read the pause state
POST /api/pause session counted Pause or resume blocking
GET /api/settings session counted Read the scalar settings
PUT /api/settings session counted Update settings
POST /api/certs/reload session counted Reload the TLS certificates from disk

There is no POST /api/clients: client rows come from DNS activity or import, never from the API.

Schemas

Request and response schemas for every operation live in the OpenAPI document: src/web/openapi.yaml in the repository, or GET /api/openapi.yaml from a running server.