Gates / frontend (push) Successful in 1m34s
Gates / test (push) Successful in 2m3s
Gates / test-aarch64 (push) Failing after 3h13m33s
Gates / package (push) Successful in 5m20s
Gates / container (push) Successful in 15s
CI / gates (push) Failing after 6h30m45s
3058 lines
97 KiB
YAML
3058 lines
97 KiB
YAML
openapi: 3.0.3
|
|
info:
|
|
title: nxdns admin API
|
|
version: "1.0"
|
|
description: |
|
|
The REST API of the nxdns DNS sinkhole, served by nxdns itself on
|
|
`web.bind:web.port` (plain HTTP; TLS termination is the operator's proxy).
|
|
|
|
Conventions, which every endpoint follows:
|
|
|
|
- All JSON field names are snake_case, matching settings keys and SQL
|
|
column names.
|
|
- Every error response carries the envelope `{"error": "<message>"}`.
|
|
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.
|
|
- Authentication is enabled iff `web.password_hash` is set. When it is,
|
|
every operation marked with `sessionCookie` security answers 401
|
|
without a valid session cookie. When it is not, no operation requires
|
|
authentication. Monitoring endpoints, the login, this document and the
|
|
static assets are always open.
|
|
- Requests spend one token of the per-address bucket
|
|
(`web.api_rate_limit_per_min`); over-budget requests answer 429 with a
|
|
`Retry-After` header. `/metrics` and `/api/health` are exempt so a
|
|
Prometheus scrape can never be throttled; `/api/queries/live` is
|
|
exempt because one long-lived stream is bounded by
|
|
`web.sse_max_connections_per_ip` instead.
|
|
- Mutations to groups, blocklists, rules, local records, forward zones,
|
|
clients and client prefixes take effect live. Upstreams and
|
|
`/api/settings` are restart-required.
|
|
- nxdns runs under one of two configuration authorities. Started with
|
|
`--config=<file>`, that file is the sole declarative source, and every
|
|
operation that writes configuration answers 403 with the same error
|
|
envelope, naming the file. Operations that change runtime state —
|
|
`/api/pause`, `POST /api/blocklists/update`, `/api/certs/reload`, the
|
|
login and the logout — stay live, as does `DELETE /api/clients/{id}`
|
|
for a client the file does not declare. `GET /api/config/status`
|
|
reports the live authority, so a client reads the mode rather than
|
|
discovering it from a rejection.
|
|
|
|
servers:
|
|
- url: /
|
|
|
|
security:
|
|
- sessionCookie: []
|
|
|
|
paths:
|
|
/metrics:
|
|
get:
|
|
summary: Prometheus metrics
|
|
description: |
|
|
Prometheus text exposition format 0.0.4, `nxdns_` prefix. Always
|
|
unauthenticated and never rate limited.
|
|
security: []
|
|
responses:
|
|
"200":
|
|
description: Metrics exposition.
|
|
content:
|
|
text/plain:
|
|
schema:
|
|
type: string
|
|
|
|
/api/health:
|
|
get:
|
|
summary: Health rollup
|
|
description: |
|
|
Always 200. `status` is `degraded` when, and only when, one of the five
|
|
condition objects is in a degrading state: protection `unavailable`,
|
|
upstreams `unavailable`, query history `losing` or `failed`,
|
|
diagnostics `unavailable`, or disk `low` or `critical`. A paused
|
|
protection is an operator's own choice and does not degrade. Always
|
|
unauthenticated and never rate limited.
|
|
security: []
|
|
responses:
|
|
"200":
|
|
description: Health snapshot.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Health"
|
|
|
|
/api/version:
|
|
get:
|
|
summary: Build and uptime
|
|
security: []
|
|
responses:
|
|
"200":
|
|
description: Version information.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Version"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
|
|
/api/openapi.yaml:
|
|
get:
|
|
summary: This document
|
|
security: []
|
|
responses:
|
|
"200":
|
|
description: The OpenAPI contract, verbatim.
|
|
content:
|
|
application/yaml:
|
|
schema:
|
|
type: string
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
|
|
/api/auth/login:
|
|
post:
|
|
summary: Log in
|
|
description: |
|
|
Verifies the admin password. On success the response sets the
|
|
`nxdns_session` cookie (`HttpOnly; SameSite=Lax; Path=/`). When no
|
|
password is configured the request succeeds without minting a
|
|
session and `auth_required` is false.
|
|
security: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/LoginRequest"
|
|
responses:
|
|
"200":
|
|
description: Authenticated; the cookie is set iff `auth_required`.
|
|
headers:
|
|
Set-Cookie:
|
|
schema:
|
|
type: string
|
|
description: "`nxdns_session=<token>; HttpOnly; SameSite=Lax; Path=/; Max-Age=<ttl>`"
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/LoginResponse"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
description: Wrong or empty password.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/auth/logout:
|
|
post:
|
|
summary: Log out
|
|
description: Deletes the session and clears the cookie. Always succeeds.
|
|
responses:
|
|
"200":
|
|
description: Logged out; the cookie is cleared.
|
|
headers:
|
|
Set-Cookie:
|
|
schema:
|
|
type: string
|
|
description: "`nxdns_session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`"
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [authenticated]
|
|
properties:
|
|
authenticated:
|
|
type: boolean
|
|
enum: [false]
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
|
|
/api/queries:
|
|
get:
|
|
summary: Query log page
|
|
description: |
|
|
Keyset pagination over the query log, newest first. Follow
|
|
`next_before` until it is null.
|
|
parameters:
|
|
- name: limit
|
|
in: query
|
|
schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
|
|
- name: before
|
|
in: query
|
|
description: Return rows with id strictly below this cursor.
|
|
schema: { type: integer, minimum: 1 }
|
|
- name: domain
|
|
in: query
|
|
description: Substring match; `%` and `_` are literal.
|
|
schema: { type: string, maxLength: 253 }
|
|
- name: client
|
|
in: query
|
|
description: Exact client address.
|
|
schema: { type: string, maxLength: 64 }
|
|
- name: blocked
|
|
in: query
|
|
schema: { type: boolean }
|
|
- name: since
|
|
in: query
|
|
description: Unix seconds, inclusive.
|
|
schema: { type: integer }
|
|
- name: until
|
|
in: query
|
|
description: Unix seconds, exclusive.
|
|
schema: { type: integer }
|
|
responses:
|
|
"200":
|
|
description: One page.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/QueriesPage"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/queries/{id}:
|
|
get:
|
|
summary: One query, fully explained
|
|
description: |
|
|
The full provenance of one logged query: what was asked, which group's
|
|
policy applied, what that policy decided and matched on, what was
|
|
rewritten, where the answer came from, and what the client received.
|
|
Every field is a fact recorded when the query was answered, so a group
|
|
or blocklist renamed since keeps the name it had.
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema: { type: integer, minimum: 1 }
|
|
responses:
|
|
"200":
|
|
description: The query.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/QueryDetail"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/queries/live:
|
|
get:
|
|
summary: Live query stream (server-sent events)
|
|
description: |
|
|
`text/event-stream`. The stream opens with `retry: 3000`, then sends
|
|
one `event: query` frame per resolved query. The `data:` line is one
|
|
query fully explained, in the order a query meets the pipeline: the six
|
|
objects `QueryDetail` documents — `request`, `group`, `policy`,
|
|
`rewrites`, `route`, `response` — all of them required, and without
|
|
that schema's `id`, which does not exist yet because the entry precedes
|
|
its own insert.
|
|
A `: ping` comment goes out every 15 seconds.
|
|
A client that falls more than 64 events behind is disconnected and
|
|
should re-sync via `/api/queries` after reconnecting. Connections
|
|
per address are capped by `web.sse_max_connections_per_ip`; the
|
|
stream does not spend rate-limit tokens.
|
|
responses:
|
|
"200":
|
|
description: An event stream; stays open until either side closes.
|
|
content:
|
|
text/event-stream:
|
|
schema:
|
|
type: string
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
description: This address already holds its maximum number of live streams.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/diagnostics:
|
|
get:
|
|
summary: Operational event log
|
|
description: |
|
|
Failure episodes, newest first. One event is one subject failing
|
|
continuously: it opens on the first failure, counts repeats in
|
|
`occurrences`, and gets a `resolved_at` when the subject recovers. A
|
|
subject that fails again opens a new event rather than reopening the
|
|
old one. Keyset pagination — follow `next_before` until it is null.
|
|
parameters:
|
|
- name: state
|
|
in: query
|
|
schema: { type: string, enum: [active, resolved, all], default: all }
|
|
- name: severity
|
|
in: query
|
|
schema: { type: string, enum: [warning, error] }
|
|
- name: component
|
|
in: query
|
|
description: Matches the part of `code` before the dot, exactly.
|
|
schema: { type: string, maxLength: 64 }
|
|
- name: since
|
|
in: query
|
|
description: |
|
|
Unix seconds. With `until`, selects episodes overlapping the
|
|
window; an episode resolved exactly at `since` does not overlap.
|
|
schema: { type: integer }
|
|
- name: until
|
|
in: query
|
|
description: Unix seconds, exclusive.
|
|
schema: { type: integer }
|
|
- name: limit
|
|
in: query
|
|
schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
|
|
- name: before
|
|
in: query
|
|
description: Return events with id strictly below this cursor.
|
|
schema: { type: integer, minimum: 1 }
|
|
responses:
|
|
"200":
|
|
description: One page.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/DiagnosticsPage"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
delete:
|
|
summary: Purge every resolved event
|
|
description: |
|
|
Deletes the resolved history and answers with how many rows went.
|
|
Active events are never touched, so clearing the page cannot lose an
|
|
episode that is still failing. Resolution stays automatic; this only
|
|
decides when the history disappears. A runtime action, served in file
|
|
mode too — the event log is not configuration.
|
|
responses:
|
|
"200":
|
|
description: How many resolved events were removed.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/DiagnosticsPurge"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/diagnostics/{id}:
|
|
parameters:
|
|
- $ref: "#/components/parameters/RowId"
|
|
get:
|
|
summary: One operational event
|
|
description: |
|
|
404 for an id that never existed and for one retention has removed —
|
|
the API does not distinguish them.
|
|
responses:
|
|
"200":
|
|
description: The event.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/DiagnosticEvent"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
delete:
|
|
summary: Purge one resolved event
|
|
description: |
|
|
Deletes a resolved event. An event that is still active answers 409 —
|
|
an open episode is the current state of the box, not history — and an
|
|
id no row holds answers 404. A runtime action, served in file mode too.
|
|
responses:
|
|
"204":
|
|
description: Purged.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/stats:
|
|
get:
|
|
summary: Totals for a period
|
|
parameters:
|
|
- $ref: "#/components/parameters/Period"
|
|
responses:
|
|
"200":
|
|
description: Totals over the period's UTC-aligned window.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/StatsTotals"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/stats/timeseries:
|
|
get:
|
|
summary: Bucketed counts for a period
|
|
description: |
|
|
Fixed-width UTC buckets covering the same window `/api/stats`
|
|
reports for the period: 1h into 60 one-minute buckets, 24h into 48
|
|
half-hour buckets, 7d into 168 one-hour buckets, 30d into 120
|
|
six-hour buckets. Empty buckets are zero-filled.
|
|
parameters:
|
|
- $ref: "#/components/parameters/Period"
|
|
responses:
|
|
"200":
|
|
description: The bucket series.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/StatsTimeseries"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/stats/types:
|
|
get:
|
|
summary: Query-type breakdown for a period
|
|
description: |
|
|
How many queries of each DNS type the period's window holds, over the
|
|
same UTC-aligned window `/api/stats` reports for. Rows carry the numeric
|
|
type only: the type-name table lives in the admin, and a second copy
|
|
here would drift out of agreement with it. `qtype` is nullable in the
|
|
query log, so the rows that carry no type group into a row of their own
|
|
rather than vanishing from a breakdown that claims to add up. Ordered by
|
|
count descending, then type ascending with the null row last. Types
|
|
absent from the window are absent from the list.
|
|
parameters:
|
|
- $ref: "#/components/parameters/Period"
|
|
responses:
|
|
"200":
|
|
description: The type breakdown.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/StatsTypes"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/stats/routes:
|
|
get:
|
|
summary: How the period's queries were answered
|
|
description: |
|
|
A breakdown by answering route over the same window `/api/stats`
|
|
reports for. `source` is the answering resolver's identity — the
|
|
upstream url on `upstream` rows, the zone on `forward_zone` rows, null
|
|
on every other kind and on rows whose identity the log did not record.
|
|
It is not the blocklist a block came from. Ordered by count descending,
|
|
then route ascending, then source ascending with nulls last.
|
|
parameters:
|
|
- $ref: "#/components/parameters/Period"
|
|
responses:
|
|
"200":
|
|
description: The route breakdown.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/StatsRoutes"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/stats/clients:
|
|
get:
|
|
summary: Per-client bucketed counts for a period
|
|
description: |
|
|
One zero-filled series per client, bucketed exactly like
|
|
`/api/stats/timeseries` so the two charts share an x-axis. The eight
|
|
clients with the most queries in the window are named, ranked by count
|
|
descending then address ascending; every other client sums into
|
|
`other`, which is always present and always holds one entry per bucket
|
|
in the window — including when `clients` is empty, when no client fell
|
|
outside the named eight, and when the window holds no queries at all.
|
|
parameters:
|
|
- $ref: "#/components/parameters/Period"
|
|
responses:
|
|
"200":
|
|
description: The per-client series.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/StatsClients"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/lookup:
|
|
get:
|
|
summary: Explain a domain
|
|
description: |
|
|
Runs the full pipeline view for a domain without answering DNS:
|
|
local records, forward zone, block decision with reason and matched
|
|
pattern, source URL when a blocklist matched, and any safe-search
|
|
rewrite.
|
|
parameters:
|
|
- name: domain
|
|
in: query
|
|
required: true
|
|
schema: { type: string, maxLength: 253 }
|
|
- name: group_id
|
|
in: query
|
|
description: Group row id; the default group when absent.
|
|
schema: { type: integer, minimum: 1 }
|
|
responses:
|
|
"200":
|
|
description: The pipeline's view of the domain.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Lookup"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/groups:
|
|
get:
|
|
summary: List groups
|
|
responses:
|
|
"200":
|
|
description: Every group.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [groups]
|
|
properties:
|
|
groups:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Group"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
post:
|
|
summary: Create a group
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/GroupInput"
|
|
responses:
|
|
"201":
|
|
description: Created; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Group"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/groups/{id}:
|
|
parameters:
|
|
- $ref: "#/components/parameters/RowId"
|
|
get:
|
|
summary: Read a group
|
|
responses:
|
|
"200":
|
|
description: The group.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Group"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
put:
|
|
summary: Update a group
|
|
description: The `default` group cannot be renamed (409).
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/GroupInput"
|
|
responses:
|
|
"200":
|
|
description: Updated; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Group"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
delete:
|
|
summary: Delete a group
|
|
description: |
|
|
The `default` group cannot be deleted, and neither can a group that
|
|
still has clients (409 either way).
|
|
responses:
|
|
"204":
|
|
description: Deleted; applied live.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/groups/{id}/sources:
|
|
parameters:
|
|
- $ref: "#/components/parameters/RowId"
|
|
get:
|
|
summary: Blocklist sources assigned to a group
|
|
responses:
|
|
"200":
|
|
description: The assignment.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/GroupSources"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
put:
|
|
summary: Replace the assignment
|
|
description: Idempotent full-set replace; duplicates collapse.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/GroupSources"
|
|
responses:
|
|
"200":
|
|
description: Replaced; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/GroupSources"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/blocklists:
|
|
get:
|
|
summary: List blocklist sources
|
|
responses:
|
|
"200":
|
|
description: Every source with its load counters.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [blocklists]
|
|
properties:
|
|
blocklists:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Blocklist"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
post:
|
|
summary: Add a blocklist source
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/BlocklistInput"
|
|
responses:
|
|
"201":
|
|
description: Created; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/BlocklistEcho"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/blocklists/update:
|
|
post:
|
|
summary: Refresh every enabled source now
|
|
description: |
|
|
Runs a fetch-and-compile pass over all enabled sources, reloads the
|
|
filter, and reports the per-source status snapshot.
|
|
responses:
|
|
"202":
|
|
description: Refresh ran; the snapshot reflects it.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [sources]
|
|
properties:
|
|
sources:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/SourceStatus"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/blocklists/{id}:
|
|
parameters:
|
|
- $ref: "#/components/parameters/RowId"
|
|
get:
|
|
summary: Read a blocklist source
|
|
responses:
|
|
"200":
|
|
description: The source with its load counters.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Blocklist"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
put:
|
|
summary: Update a blocklist source
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/BlocklistInput"
|
|
responses:
|
|
"200":
|
|
description: Updated; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/BlocklistEcho"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
delete:
|
|
summary: Delete a blocklist source
|
|
responses:
|
|
"204":
|
|
description: Deleted; applied live.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/rules:
|
|
get:
|
|
summary: List rules
|
|
responses:
|
|
"200":
|
|
description: Every allow/block rule.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [rules]
|
|
properties:
|
|
rules:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Rule"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
post:
|
|
summary: Create a rule
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RuleInput"
|
|
responses:
|
|
"201":
|
|
description: Created; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RuleEcho"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/rules/{id}:
|
|
parameters:
|
|
- $ref: "#/components/parameters/RowId"
|
|
get:
|
|
summary: Read a rule
|
|
responses:
|
|
"200":
|
|
description: The rule.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Rule"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
put:
|
|
summary: Update a rule
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RuleInput"
|
|
responses:
|
|
"200":
|
|
description: Updated; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/RuleEcho"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
delete:
|
|
summary: Delete a rule
|
|
responses:
|
|
"204":
|
|
description: Deleted; applied live.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/local-records:
|
|
get:
|
|
summary: List local DNS records
|
|
responses:
|
|
"200":
|
|
description: Every local record.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [local_records]
|
|
properties:
|
|
local_records:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/LocalRecord"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
post:
|
|
summary: Create a local record
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/LocalRecordInput"
|
|
responses:
|
|
"201":
|
|
description: Created; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/LocalRecord"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/local-records/{id}:
|
|
parameters:
|
|
- $ref: "#/components/parameters/RowId"
|
|
get:
|
|
summary: Read a local record
|
|
responses:
|
|
"200":
|
|
description: The record.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/LocalRecord"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
put:
|
|
summary: Update a local record
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/LocalRecordInput"
|
|
responses:
|
|
"200":
|
|
description: Updated; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/LocalRecord"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
delete:
|
|
summary: Delete a local record
|
|
responses:
|
|
"204":
|
|
description: Deleted; applied live.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/forward-zones:
|
|
get:
|
|
summary: List forward zones
|
|
responses:
|
|
"200":
|
|
description: Every forward zone.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [forward_zones]
|
|
properties:
|
|
forward_zones:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ForwardZone"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
post:
|
|
summary: Create a forward zone
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ForwardZoneInput"
|
|
responses:
|
|
"201":
|
|
description: Created; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ForwardZone"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/forward-zones/{id}:
|
|
parameters:
|
|
- $ref: "#/components/parameters/RowId"
|
|
get:
|
|
summary: Read a forward zone
|
|
responses:
|
|
"200":
|
|
description: The zone.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ForwardZone"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
put:
|
|
summary: Update a forward zone
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ForwardZoneInput"
|
|
responses:
|
|
"200":
|
|
description: Updated; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ForwardZone"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
delete:
|
|
summary: Delete a forward zone
|
|
responses:
|
|
"204":
|
|
description: Deleted; applied live.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/clients:
|
|
get:
|
|
summary: List clients
|
|
description: |
|
|
Every known client, materialized ones included, each with
|
|
`hand_edited`. Clients have no POST: rows appear through DNS
|
|
activity or configuration import.
|
|
responses:
|
|
"200":
|
|
description: Every client.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [clients]
|
|
properties:
|
|
clients:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Client"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/clients/{id}:
|
|
parameters:
|
|
- $ref: "#/components/parameters/RowId"
|
|
get:
|
|
summary: Read a client
|
|
responses:
|
|
"200":
|
|
description: The client.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Client"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
put:
|
|
summary: Rename or regroup a client
|
|
description: |
|
|
Sets `hand_edited`. The address is the client's identity and cannot
|
|
be changed.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ClientEdit"
|
|
responses:
|
|
"200":
|
|
description: The updated row; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Client"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
delete:
|
|
summary: Forget a client
|
|
description: A live client re-materializes on its next query.
|
|
responses:
|
|
"204":
|
|
description: Deleted; applied live.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/client-prefixes:
|
|
get:
|
|
summary: List client prefixes
|
|
responses:
|
|
"200":
|
|
description: The whole prefix table.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [client_prefixes]
|
|
properties:
|
|
client_prefixes:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ClientPrefix"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
put:
|
|
summary: Replace the prefix table
|
|
description: |
|
|
Atomic whole-list replace; an empty array clears the table. A prefix
|
|
listed twice is a 409.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [client_prefixes]
|
|
properties:
|
|
client_prefixes:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ClientPrefixInput"
|
|
responses:
|
|
"200":
|
|
description: The stored table, re-read; applied live.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [client_prefixes]
|
|
properties:
|
|
client_prefixes:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ClientPrefix"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/upstreams:
|
|
get:
|
|
summary: List upstream resolvers
|
|
responses:
|
|
"200":
|
|
description: Every configured upstream.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [upstreams]
|
|
properties:
|
|
upstreams:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Upstream"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
post:
|
|
summary: Add an upstream
|
|
description: Restart-required; the running pool is not changed.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpstreamInput"
|
|
responses:
|
|
"201":
|
|
description: Created; takes effect on restart.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpstreamEcho"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/upstreams/{id}:
|
|
parameters:
|
|
- $ref: "#/components/parameters/RowId"
|
|
get:
|
|
summary: Read an upstream
|
|
responses:
|
|
"200":
|
|
description: The upstream.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Upstream"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
put:
|
|
summary: Update an upstream
|
|
description: Restart-required; the running pool is not changed.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpstreamInput"
|
|
responses:
|
|
"200":
|
|
description: Updated; takes effect on restart.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/UpstreamEcho"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
delete:
|
|
summary: Delete an upstream
|
|
description: The last enabled upstream cannot be removed (409).
|
|
responses:
|
|
"204":
|
|
description: Deleted; takes effect on restart.
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"404":
|
|
$ref: "#/components/responses/NotFound"
|
|
"409":
|
|
$ref: "#/components/responses/Conflict"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/pause:
|
|
get:
|
|
summary: Read the pause state
|
|
responses:
|
|
"200":
|
|
description: |
|
|
`until` is null while unpaused and while paused indefinitely;
|
|
`paused` disambiguates.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Pause"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
post:
|
|
summary: Pause or resume blocking
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/PauseRequest"
|
|
responses:
|
|
"200":
|
|
description: The state after applying.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Pause"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/settings:
|
|
get:
|
|
summary: Read the scalar settings
|
|
description: |
|
|
Every stored settings key, plus the derived `web.auth_enabled`.
|
|
`restart_required` lists every key, because all scalar settings are
|
|
restart-required this milestone; live behavior comes from the
|
|
resource endpoints and `/api/pause`. Passwords and hashes are never
|
|
serialized.
|
|
responses:
|
|
"200":
|
|
description: The settings and the restart-required key list.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SettingsEnvelope"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
put:
|
|
summary: Update settings
|
|
description: |
|
|
Partial update: omitted sections and fields keep their stored
|
|
values. The merged configuration is validated before anything is
|
|
written; on any failure nothing changes. A non-empty `web.password`
|
|
is hashed with argon2id and stored as the hash; changing it clears
|
|
every session. `web.password_hash` itself is not accepted.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SettingsPatch"
|
|
responses:
|
|
"200":
|
|
description: The merged settings, as `GET` would report them.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/SettingsEnvelope"
|
|
"400":
|
|
$ref: "#/components/responses/BadRequest"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"403":
|
|
$ref: "#/components/responses/ManagedByFile"
|
|
"413":
|
|
$ref: "#/components/responses/BodyTooLarge"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
"500":
|
|
$ref: "#/components/responses/Internal"
|
|
"503":
|
|
$ref: "#/components/responses/Unavailable"
|
|
|
|
/api/config/status:
|
|
get:
|
|
summary: Read the configuration authority and restart state
|
|
description: |
|
|
Which source governs this process's configuration, and whether a
|
|
change already written waits for a restart. Both are per-process
|
|
facts the database cannot answer, and this is their one home: a
|
|
client reads the mode here rather than discovering it from a 403.
|
|
responses:
|
|
"200":
|
|
description: The live authority and the pending-restart flag.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/ConfigStatus"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
|
|
/api/certs/reload:
|
|
post:
|
|
summary: Reload the TLS certificates from disk
|
|
description: |
|
|
Reloads the certificate and key of every enabled DoH/DoT endpoint.
|
|
Always answers 200: the per-endpoint outcome is the payload, and a
|
|
failed reload leaves the previous certificate serving.
|
|
responses:
|
|
"200":
|
|
description: The outcome for each endpoint.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CertsReload"
|
|
"401":
|
|
$ref: "#/components/responses/Unauthorized"
|
|
"429":
|
|
$ref: "#/components/responses/RateLimited"
|
|
|
|
components:
|
|
securitySchemes:
|
|
sessionCookie:
|
|
type: apiKey
|
|
in: cookie
|
|
name: nxdns_session
|
|
description: |
|
|
Minted by `POST /api/auth/login`. Only meaningful when
|
|
`web.password_hash` is configured; without it, every endpoint is
|
|
open and the cookie is ignored.
|
|
|
|
parameters:
|
|
RowId:
|
|
name: id
|
|
in: path
|
|
required: true
|
|
description: Positive integer row id.
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
Period:
|
|
name: period
|
|
in: query
|
|
description: Statistics window; the default is `24h`.
|
|
schema:
|
|
type: string
|
|
enum: [1h, 24h, 7d, 30d]
|
|
default: 24h
|
|
|
|
responses:
|
|
BadRequest:
|
|
description: Malformed path, query parameter or body; validation failure.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
Unauthorized:
|
|
description: |
|
|
Authentication is enabled and the request carried no valid session
|
|
cookie.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
ManagedByFile:
|
|
description: |
|
|
nxdns is running under file authority and this operation writes
|
|
configuration. The message names the file. Authentication is checked
|
|
first, so an unauthenticated request to a protected route still
|
|
answers 401 rather than disclosing that the route exists.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
NotFound:
|
|
description: No row has this id.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
Conflict:
|
|
description: |
|
|
The change contradicts stored data: a duplicate key, a reference to
|
|
a missing row, or a delete the data still depends on.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
BodyTooLarge:
|
|
description: The request body exceeds 1 MiB.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
RateLimited:
|
|
description: |
|
|
The per-address token bucket is empty. `Retry-After` says when one
|
|
token will be available.
|
|
headers:
|
|
Retry-After:
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
description: Seconds until a retry can succeed.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
Internal:
|
|
description: |
|
|
Something on the server failed; the detail is in the server log,
|
|
never on the wire.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
Unavailable:
|
|
description: |
|
|
A collaborator this endpoint needs is not running: no configuration
|
|
database, no query log, no snapshot loaded, or the server is
|
|
shutting down.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/Error"
|
|
|
|
schemas:
|
|
Error:
|
|
type: object
|
|
required: [error]
|
|
properties:
|
|
error:
|
|
type: string
|
|
description: Operator-facing message.
|
|
|
|
Health:
|
|
type: object
|
|
required: [status, protection, upstreams, query_history, diagnostics, disk]
|
|
properties:
|
|
status:
|
|
type: string
|
|
enum: [ok, degraded]
|
|
protection:
|
|
$ref: "#/components/schemas/HealthProtection"
|
|
upstreams:
|
|
$ref: "#/components/schemas/HealthUpstreams"
|
|
query_history:
|
|
$ref: "#/components/schemas/HealthQueryHistory"
|
|
diagnostics:
|
|
$ref: "#/components/schemas/HealthDiagnostics"
|
|
disk:
|
|
$ref: "#/components/schemas/HealthDisk"
|
|
|
|
HealthProtection:
|
|
type: object
|
|
required: [state, until]
|
|
properties:
|
|
state:
|
|
type: string
|
|
enum: [active, paused, unavailable]
|
|
description: >
|
|
unavailable when no filter snapshot exists for the query path to
|
|
evaluate against, which outranks any pause and is the only one of
|
|
the three that degrades health. An expired timed pause is active.
|
|
until:
|
|
type: integer
|
|
nullable: true
|
|
description: >
|
|
The second filtering resumes at. Null for an indefinite pause and
|
|
for every state other than paused.
|
|
|
|
HealthUpstreams:
|
|
type: object
|
|
required: [state, available, total]
|
|
properties:
|
|
state:
|
|
type: string
|
|
enum: [ok, unavailable]
|
|
description: unavailable exactly when `available` is 0; that degrades health.
|
|
available: { type: integer }
|
|
total:
|
|
type: integer
|
|
description: Enabled upstreams, which is what the routing pool is built from.
|
|
|
|
HealthQueryHistory:
|
|
type: object
|
|
required: [state, dropped_total, last_drop_s]
|
|
properties:
|
|
state:
|
|
type: string
|
|
enum: [recording, losing, failed]
|
|
description: >
|
|
failed when the query-log writer never started; losing while the
|
|
disk gate is holding writes back and has already cost rows in the
|
|
episode open now. Both degrade health. Drops from an earlier
|
|
episode do not change the state - they are reported by the two
|
|
fields below.
|
|
dropped_total:
|
|
type: integer
|
|
description: Query rows lost since this process started, cumulative.
|
|
last_drop_s:
|
|
type: integer
|
|
nullable: true
|
|
description: >
|
|
The newest drop, unix seconds; null until one happens. Stamped by a
|
|
separate atomic from the count, so a non-zero `dropped_total` beside
|
|
a null here is a legal momentary answer.
|
|
|
|
HealthDiagnostics:
|
|
type: object
|
|
required: [state, active_warnings, active_errors]
|
|
properties:
|
|
state:
|
|
type: string
|
|
enum: [recording, unavailable]
|
|
description: unavailable when the event store failed to open or its writes are failing; either state degrades health.
|
|
active_warnings: { type: integer }
|
|
active_errors: { type: integer }
|
|
|
|
HealthDisk:
|
|
type: object
|
|
required: [state, free_bytes]
|
|
properties:
|
|
state:
|
|
type: string
|
|
enum: [ok, low, critical]
|
|
description: >
|
|
The disk monitor's own states; its `warn` is renamed `low` here,
|
|
because `warn` reads as a log level rather than as a quantity of
|
|
disk. Both `low` and `critical` degrade health.
|
|
free_bytes: { type: integer }
|
|
|
|
Version:
|
|
type: object
|
|
required: [version, git_commit, zig_version, uptime_seconds]
|
|
properties:
|
|
version: { type: string }
|
|
git_commit: { type: string }
|
|
zig_version: { type: string }
|
|
uptime_seconds: { type: integer }
|
|
|
|
LoginRequest:
|
|
type: object
|
|
required: [password]
|
|
properties:
|
|
password: { type: string }
|
|
|
|
LoginResponse:
|
|
type: object
|
|
required: [authenticated, auth_required]
|
|
properties:
|
|
authenticated:
|
|
type: boolean
|
|
enum: [true]
|
|
auth_required:
|
|
type: boolean
|
|
description: False when no password is configured; no cookie is set.
|
|
|
|
PolicyAction:
|
|
type: string
|
|
description: |
|
|
Whether the filtering policy reached a verdict. `not_evaluated` is the
|
|
honest answer for a query answered before filtering could apply, and is
|
|
not the same as `allow`.
|
|
enum: [not_evaluated, allow, block]
|
|
|
|
PolicyReason:
|
|
type: string
|
|
description: |
|
|
Why the policy landed where it did. The first nine are the matcher's own
|
|
verdicts; the rest name a pipeline step that decided without consulting
|
|
the matcher.
|
|
enum: [rule_allow_exact, rule_block_exact, rule_allow_wildcard, rule_block_wildcard, rule_allow_regex, rule_block_regex, blocklist_exception, blocklist_domain, blocklist_wildcard, local_record, forward_zone, non_in_class, paused, snapshot_unavailable, no_match, protocol_error]
|
|
|
|
RouteKind:
|
|
type: string
|
|
description: Where the answer the client received came from.
|
|
enum: [blocked, local, forward_zone, upstream, cache, rejected]
|
|
|
|
QueryRow:
|
|
type: object
|
|
description: |
|
|
The summary projection the query-log table scans. The full provenance of
|
|
a row is one request away at `/api/queries/{id}`.
|
|
required: [id, ts, domain, client_ip, qtype, qclass, rcode, blocked, response_time_us, cache_hit, upstream, policy_action, policy_reason, route_kind]
|
|
properties:
|
|
id: { type: integer }
|
|
ts:
|
|
type: integer
|
|
description: Unix seconds.
|
|
domain: { type: string }
|
|
client_ip: { type: string }
|
|
qtype:
|
|
type: integer
|
|
nullable: true
|
|
qclass: { type: integer }
|
|
rcode:
|
|
type: integer
|
|
description: The twelve-bit EDNS extended code, not the four header bits alone.
|
|
blocked: { type: boolean }
|
|
response_time_us:
|
|
type: integer
|
|
nullable: true
|
|
cache_hit:
|
|
type: boolean
|
|
nullable: true
|
|
upstream:
|
|
type: string
|
|
description: Empty for cache hits, local answers and blocked queries.
|
|
policy_action:
|
|
$ref: "#/components/schemas/PolicyAction"
|
|
policy_reason:
|
|
$ref: "#/components/schemas/PolicyReason"
|
|
route_kind:
|
|
$ref: "#/components/schemas/RouteKind"
|
|
|
|
ProvenanceRequest:
|
|
type: object
|
|
required: [time, domain, client, qtype, qclass]
|
|
properties:
|
|
time:
|
|
type: integer
|
|
description: Unix seconds.
|
|
domain: { type: string }
|
|
client: { type: string }
|
|
qtype:
|
|
type: integer
|
|
nullable: true
|
|
qclass: { type: integer }
|
|
|
|
ProvenanceGroup:
|
|
type: object
|
|
description: |
|
|
The client's filtering group at the time of the query, as a historical
|
|
fact: the id may name a group since renamed or deleted.
|
|
required: [id, name]
|
|
properties:
|
|
id:
|
|
type: integer
|
|
nullable: true
|
|
name: { type: string }
|
|
|
|
ProvenancePolicy:
|
|
type: object
|
|
required: [action, reason, matched, source_id, source_name]
|
|
properties:
|
|
action:
|
|
$ref: "#/components/schemas/PolicyAction"
|
|
reason:
|
|
$ref: "#/components/schemas/PolicyReason"
|
|
matched:
|
|
type: string
|
|
description: The rule pattern or list entry that decided; empty when nothing matched.
|
|
source_id:
|
|
type: integer
|
|
nullable: true
|
|
source_name:
|
|
type: string
|
|
description: The blocklist the match came from; empty for a rule.
|
|
|
|
ProvenanceRewrites:
|
|
type: object
|
|
required: [cname_target, safe_search_target]
|
|
properties:
|
|
cname_target:
|
|
type: string
|
|
description: Set when the decision was made about a CNAME target rather than the queried name.
|
|
safe_search_target: { type: string }
|
|
|
|
ProvenanceRoute:
|
|
type: object
|
|
required: [kind, forward_zone, upstream]
|
|
properties:
|
|
kind:
|
|
$ref: "#/components/schemas/RouteKind"
|
|
forward_zone: { type: string }
|
|
upstream:
|
|
type: string
|
|
description: |
|
|
Non-empty only for an attempted upstream or forward-zone exchange,
|
|
including one that failed. Already redacted: the userinfo, path,
|
|
query and fragment of a resolver url never reach here.
|
|
|
|
ProvenanceResponse:
|
|
type: object
|
|
required: [rcode, duration_us]
|
|
properties:
|
|
rcode:
|
|
type: integer
|
|
description: The twelve-bit EDNS extended code, not the four header bits alone.
|
|
duration_us:
|
|
type: integer
|
|
nullable: true
|
|
|
|
QueryDetail:
|
|
type: object
|
|
description: |
|
|
One query, fully explained, in the order a query meets the pipeline,
|
|
plus the row id. Written out rather than composed with `allOf` so the
|
|
drift guard reads one property list per schema.
|
|
required: [id, request, group, policy, rewrites, route, response]
|
|
properties:
|
|
id: { type: integer }
|
|
request:
|
|
$ref: "#/components/schemas/ProvenanceRequest"
|
|
group:
|
|
$ref: "#/components/schemas/ProvenanceGroup"
|
|
policy:
|
|
$ref: "#/components/schemas/ProvenancePolicy"
|
|
rewrites:
|
|
$ref: "#/components/schemas/ProvenanceRewrites"
|
|
route:
|
|
$ref: "#/components/schemas/ProvenanceRoute"
|
|
response:
|
|
$ref: "#/components/schemas/ProvenanceResponse"
|
|
|
|
Coverage:
|
|
type: object
|
|
description: |
|
|
How much of the requested window the query log can still answer for.
|
|
Retention deletes rows and advances the watermark in one transaction, so
|
|
a client can tell an empty window from a pruned one instead of charting
|
|
the gap as zero.
|
|
required: [complete, available_since]
|
|
properties:
|
|
complete:
|
|
type: boolean
|
|
description: |
|
|
True only when the window's lower bound is at or after
|
|
`available_since`. A request with no lower bound asks about all of
|
|
history, which no file that has ever pruned can promise.
|
|
available_since:
|
|
type: integer
|
|
description: The oldest instant the file is complete for, unix seconds.
|
|
|
|
QueriesPage:
|
|
type: object
|
|
required: [queries, next_before, coverage]
|
|
properties:
|
|
queries:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/QueryRow"
|
|
next_before:
|
|
type: integer
|
|
nullable: true
|
|
description: Cursor for the next page; null on the last page.
|
|
coverage:
|
|
$ref: "#/components/schemas/Coverage"
|
|
|
|
DiagnosticEvent:
|
|
type: object
|
|
required: [id, code, component, subject, severity, first_seen, last_seen, occurrences, resolved_at, detail]
|
|
properties:
|
|
id: { type: integer }
|
|
code:
|
|
type: string
|
|
description: |
|
|
The failure kind, as `component.name`. One of a fixed set of
|
|
fifteen; new codes are added with new releases.
|
|
enum:
|
|
- disk.space
|
|
- disk.probe
|
|
- blocklist.refresh
|
|
- blocklist.snapshot
|
|
- blocklist.storage
|
|
- certificate.reload
|
|
- query_log.write
|
|
- query_log.maintenance
|
|
- query_log.recreated
|
|
# Legacy: nothing emits this any more (milestone 30 deleted the
|
|
# upstream-minute history subsystem), but stored rows survive and
|
|
# the list endpoint passes their code through.
|
|
- upstream_history.write
|
|
- upstream.exchange
|
|
- client_names.storage
|
|
- clients.storage
|
|
- listener.start
|
|
- configuration.load
|
|
component:
|
|
type: string
|
|
description: The part of `code` before the dot, repeated for filtering.
|
|
subject:
|
|
type: string
|
|
description: |
|
|
What failed, as a display name: a blocklist source name, an
|
|
endpoint, an operation. Redacted where it derives from a url; the
|
|
store's internal identity for the subject is never exposed.
|
|
severity:
|
|
type: string
|
|
enum: [warning, error]
|
|
first_seen:
|
|
type: integer
|
|
description: When this episode opened, unix seconds.
|
|
last_seen:
|
|
type: integer
|
|
description: The most recent failure of this episode, unix seconds.
|
|
occurrences:
|
|
type: integer
|
|
description: How many failures this episode has held; at least 1.
|
|
resolved_at:
|
|
type: integer
|
|
nullable: true
|
|
description: |
|
|
When the subject recovered, unix seconds. Null while the episode is
|
|
still open. A subject that fails again opens a new event rather than
|
|
reopening this one.
|
|
detail:
|
|
type: string
|
|
description: The last error of this episode, truncated to 512 bytes.
|
|
|
|
DiagnosticsPage:
|
|
type: object
|
|
required: [events, next_before, active]
|
|
properties:
|
|
events:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/DiagnosticEvent"
|
|
next_before:
|
|
type: integer
|
|
nullable: true
|
|
description: Cursor for the next page; null on the last page.
|
|
active:
|
|
type: object
|
|
required: [warnings, errors]
|
|
description: Episodes open right now, whatever this page filtered to.
|
|
properties:
|
|
warnings: { type: integer }
|
|
errors: { type: integer }
|
|
|
|
DiagnosticsPurge:
|
|
type: object
|
|
required: [purged]
|
|
properties:
|
|
purged:
|
|
type: integer
|
|
description: How many resolved events the purge removed; zero when there were none.
|
|
|
|
StatsTotals:
|
|
type: object
|
|
required: [period, since, until, queries, blocked, clients, avg_response_time_us, coverage]
|
|
properties:
|
|
period:
|
|
type: string
|
|
enum: [1h, 24h, 7d, 30d]
|
|
since:
|
|
type: integer
|
|
description: Window start, unix seconds, inclusive.
|
|
until:
|
|
type: integer
|
|
description: Window end, unix seconds, exclusive.
|
|
queries: { type: integer }
|
|
blocked: { type: integer }
|
|
clients:
|
|
type: integer
|
|
description: Distinct client addresses in the window.
|
|
avg_response_time_us:
|
|
type: integer
|
|
nullable: true
|
|
description: Null when no query in the window recorded a time.
|
|
coverage:
|
|
$ref: "#/components/schemas/Coverage"
|
|
|
|
Bucket:
|
|
type: object
|
|
required: [ts, queries, blocked, cached]
|
|
properties:
|
|
ts:
|
|
type: integer
|
|
description: Bucket start, unix seconds.
|
|
queries: { type: integer }
|
|
blocked: { type: integer }
|
|
cached: { type: integer }
|
|
|
|
StatsTimeseries:
|
|
type: object
|
|
required: [period, since, until, bucket_seconds, buckets, coverage]
|
|
properties:
|
|
period:
|
|
type: string
|
|
enum: [1h, 24h, 7d, 30d]
|
|
since: { type: integer }
|
|
until: { type: integer }
|
|
bucket_seconds: { type: integer }
|
|
buckets:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/Bucket"
|
|
coverage:
|
|
$ref: "#/components/schemas/Coverage"
|
|
|
|
TypeCount:
|
|
type: object
|
|
required: [qtype, count]
|
|
properties:
|
|
qtype:
|
|
type: integer
|
|
nullable: true
|
|
description: |
|
|
The numeric DNS type. Null is the group of logged queries that
|
|
recorded no type, not an absent row.
|
|
count: { type: integer }
|
|
|
|
StatsTypes:
|
|
type: object
|
|
required: [period, since, until, types, coverage]
|
|
properties:
|
|
period:
|
|
type: string
|
|
enum: [1h, 24h, 7d, 30d]
|
|
since: { type: integer }
|
|
until: { type: integer }
|
|
types:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/TypeCount"
|
|
coverage:
|
|
$ref: "#/components/schemas/Coverage"
|
|
|
|
RouteCount:
|
|
type: object
|
|
required: [route, source, count]
|
|
properties:
|
|
route:
|
|
$ref: "#/components/schemas/RouteKind"
|
|
source:
|
|
type: string
|
|
nullable: true
|
|
description: |
|
|
The answering upstream's url or the forward zone, and null on every
|
|
other route kind. Also null when an `upstream` or `forward_zone`
|
|
row recorded no identity.
|
|
count: { type: integer }
|
|
|
|
StatsRoutes:
|
|
type: object
|
|
required: [period, since, until, routes, coverage]
|
|
properties:
|
|
period:
|
|
type: string
|
|
enum: [1h, 24h, 7d, 30d]
|
|
since: { type: integer }
|
|
until: { type: integer }
|
|
routes:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/RouteCount"
|
|
coverage:
|
|
$ref: "#/components/schemas/Coverage"
|
|
|
|
ClientSeries:
|
|
type: object
|
|
required: [client, buckets]
|
|
properties:
|
|
client:
|
|
type: string
|
|
description: The client address as the log recorded it, redaction included.
|
|
buckets:
|
|
type: array
|
|
description: |
|
|
One count per bucket in the window, zero-filled. Every series in a
|
|
response has this same length, `other` included.
|
|
items:
|
|
type: integer
|
|
|
|
StatsClients:
|
|
type: object
|
|
required: [period, since, until, bucket_seconds, clients, other, coverage]
|
|
properties:
|
|
period:
|
|
type: string
|
|
enum: [1h, 24h, 7d, 30d]
|
|
since: { type: integer }
|
|
until: { type: integer }
|
|
bucket_seconds: { type: integer }
|
|
clients:
|
|
type: array
|
|
items:
|
|
$ref: "#/components/schemas/ClientSeries"
|
|
other:
|
|
type: array
|
|
description: |
|
|
Every client outside the named eight, summed per bucket. Always
|
|
present, and always one entry per bucket in the window — including
|
|
when `clients` is empty, when no client fell outside the named
|
|
eight, and when the window holds no queries at all.
|
|
items:
|
|
type: integer
|
|
coverage:
|
|
$ref: "#/components/schemas/Coverage"
|
|
|
|
Lookup:
|
|
type: object
|
|
required: [domain, group_id, local_records, forward_zone, blocked, reason, matched, source_url, safe_search_rewrite]
|
|
properties:
|
|
domain:
|
|
type: string
|
|
description: The normalized form of the queried domain.
|
|
group_id: { type: integer }
|
|
local_records:
|
|
type: boolean
|
|
description: Whether a local record answers this name.
|
|
forward_zone:
|
|
type: string
|
|
nullable: true
|
|
blocked: { type: boolean }
|
|
reason:
|
|
type: string
|
|
description: The pipeline's decision, e.g. `none` or `blocklist_domain`.
|
|
matched:
|
|
type: string
|
|
description: The matched pattern; empty when nothing matched.
|
|
source_url:
|
|
type: string
|
|
nullable: true
|
|
safe_search_rewrite:
|
|
type: string
|
|
nullable: true
|
|
|
|
Group:
|
|
type: object
|
|
required: [id, name, safe_search]
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
safe_search: { type: boolean }
|
|
|
|
GroupInput:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name: { type: string }
|
|
safe_search:
|
|
type: boolean
|
|
default: false
|
|
|
|
GroupSources:
|
|
type: object
|
|
required: [source_ids]
|
|
properties:
|
|
source_ids:
|
|
type: array
|
|
items: { type: integer }
|
|
|
|
Blocklist:
|
|
type: object
|
|
required: [id, url, name, enabled, is_suggested, last_updated, domain_count, wildcard_count, exception_count, skipped_regex_count, skipped_unsupported_count, checksum]
|
|
properties:
|
|
id: { type: integer }
|
|
url: { type: string }
|
|
name: { type: string }
|
|
enabled: { type: boolean }
|
|
is_suggested: { type: boolean }
|
|
last_updated:
|
|
type: integer
|
|
nullable: true
|
|
domain_count: { type: integer }
|
|
wildcard_count: { type: integer }
|
|
exception_count: { type: integer }
|
|
skipped_regex_count: { type: integer }
|
|
skipped_unsupported_count: { type: integer }
|
|
checksum:
|
|
type: string
|
|
nullable: true
|
|
|
|
BlocklistInput:
|
|
type: object
|
|
required: [url, name]
|
|
properties:
|
|
url: { type: string }
|
|
name: { type: string }
|
|
enabled:
|
|
type: boolean
|
|
default: true
|
|
is_suggested:
|
|
type: boolean
|
|
default: false
|
|
|
|
BlocklistEcho:
|
|
type: object
|
|
required: [id, url, name, enabled, is_suggested]
|
|
properties:
|
|
id: { type: integer }
|
|
url: { type: string }
|
|
name: { type: string }
|
|
enabled: { type: boolean }
|
|
is_suggested: { type: boolean }
|
|
|
|
SourceStatus:
|
|
type: object
|
|
required: [id, state, loaded, last_attempt, last_success, url, last_error, domains, wildcards, exceptions, skipped_regex, skipped_unsupported]
|
|
properties:
|
|
id: { type: integer }
|
|
state:
|
|
type: string
|
|
description: The refresh outcome, e.g. `loaded` or `fetch_failed`.
|
|
loaded: { type: boolean }
|
|
last_attempt: { type: integer }
|
|
last_success: { type: integer }
|
|
url: { type: string }
|
|
last_error:
|
|
type: string
|
|
description: Empty when the last attempt succeeded.
|
|
domains: { type: integer }
|
|
wildcards: { type: integer }
|
|
exceptions: { type: integer }
|
|
skipped_regex: { type: integer }
|
|
skipped_unsupported: { type: integer }
|
|
|
|
Rule:
|
|
type: object
|
|
required: [id, group_id, group, pattern, kind, action, created_at]
|
|
properties:
|
|
id: { type: integer }
|
|
group_id: { type: integer }
|
|
group: { type: string }
|
|
pattern: { type: string }
|
|
kind:
|
|
type: string
|
|
enum: [exact, wildcard, regex]
|
|
action:
|
|
type: string
|
|
enum: [allow, block]
|
|
created_at: { type: integer }
|
|
|
|
RuleInput:
|
|
type: object
|
|
required: [group_id, pattern, kind, action]
|
|
properties:
|
|
group_id: { type: integer }
|
|
pattern: { type: string }
|
|
kind:
|
|
type: string
|
|
enum: [exact, wildcard, regex]
|
|
action:
|
|
type: string
|
|
enum: [allow, block]
|
|
|
|
RuleEcho:
|
|
type: object
|
|
required: [id, group_id, pattern, kind, action]
|
|
properties:
|
|
id: { type: integer }
|
|
group_id: { type: integer }
|
|
pattern: { type: string }
|
|
kind:
|
|
type: string
|
|
enum: [exact, wildcard, regex]
|
|
action:
|
|
type: string
|
|
enum: [allow, block]
|
|
|
|
LocalRecord:
|
|
type: object
|
|
required: [id, name, rtype, value, ttl]
|
|
properties:
|
|
id: { type: integer }
|
|
name: { type: string }
|
|
rtype:
|
|
type: string
|
|
enum: [A, AAAA, CNAME]
|
|
value: { type: string }
|
|
ttl: { type: integer }
|
|
|
|
LocalRecordInput:
|
|
type: object
|
|
required: [name, rtype, value]
|
|
properties:
|
|
name: { type: string }
|
|
rtype:
|
|
type: string
|
|
enum: [A, AAAA, CNAME]
|
|
value: { type: string }
|
|
ttl:
|
|
type: integer
|
|
default: 300
|
|
|
|
ForwardZone:
|
|
type: object
|
|
required: [id, zone, resolver]
|
|
properties:
|
|
id: { type: integer }
|
|
zone: { type: string }
|
|
resolver: { type: string }
|
|
|
|
ForwardZoneInput:
|
|
type: object
|
|
required: [zone, resolver]
|
|
properties:
|
|
zone: { type: string }
|
|
resolver: { type: string }
|
|
|
|
Client:
|
|
type: object
|
|
required: [id, ip, name, learned_name, group_id, group, hand_edited, first_seen, last_seen]
|
|
properties:
|
|
id: { type: integer }
|
|
ip: { type: string }
|
|
name:
|
|
type: string
|
|
description: Empty when the client was never named.
|
|
learned_name:
|
|
type: string
|
|
description: |
|
|
The name learned over reverse DNS, or empty when nothing was
|
|
learned. Display-only runtime state: `name` wins whenever it is
|
|
non-empty, and a learned name never appears in an export. The
|
|
server writes it; a client cannot.
|
|
group_id: { type: integer }
|
|
group: { type: string }
|
|
hand_edited: { type: boolean }
|
|
first_seen: { type: integer }
|
|
last_seen: { type: integer }
|
|
|
|
ClientEdit:
|
|
type: object
|
|
required: [group_id]
|
|
properties:
|
|
name:
|
|
type: string
|
|
default: ""
|
|
group_id: { type: integer }
|
|
|
|
ClientPrefix:
|
|
type: object
|
|
required: [id, prefix, group_id, group, priority]
|
|
properties:
|
|
id: { type: integer }
|
|
prefix: { type: string }
|
|
group_id: { type: integer }
|
|
group: { type: string }
|
|
priority: { type: integer }
|
|
|
|
ClientPrefixInput:
|
|
type: object
|
|
required: [prefix, group_id]
|
|
properties:
|
|
prefix: { type: string }
|
|
group_id: { type: integer }
|
|
priority:
|
|
type: integer
|
|
default: 100
|
|
|
|
Upstream:
|
|
type: object
|
|
required: [id, url, priority, enabled, tls_name]
|
|
properties:
|
|
id: { type: integer }
|
|
url: { type: string }
|
|
priority: { type: integer }
|
|
enabled: { type: boolean }
|
|
tls_name:
|
|
type: string
|
|
description: Only meaningful for DoT upstreams; empty otherwise.
|
|
|
|
UpstreamInput:
|
|
type: object
|
|
required: [url]
|
|
properties:
|
|
url: { type: string }
|
|
priority:
|
|
type: integer
|
|
default: 100
|
|
enabled:
|
|
type: boolean
|
|
default: true
|
|
tls_name:
|
|
type: string
|
|
default: ""
|
|
|
|
UpstreamEcho:
|
|
type: object
|
|
required: [id, url, priority, enabled, tls_name, restart_required]
|
|
properties:
|
|
id: { type: integer }
|
|
url: { type: string }
|
|
priority: { type: integer }
|
|
enabled: { type: boolean }
|
|
tls_name: { type: string }
|
|
restart_required:
|
|
type: boolean
|
|
enum: [true]
|
|
|
|
Pause:
|
|
type: object
|
|
required: [paused, until]
|
|
properties:
|
|
paused: { type: boolean }
|
|
until:
|
|
type: integer
|
|
nullable: true
|
|
description: |
|
|
Unix second filtering resumes; null while unpaused and while
|
|
paused indefinitely.
|
|
|
|
PauseRequest:
|
|
type: object
|
|
required: [paused]
|
|
properties:
|
|
paused: { type: boolean }
|
|
duration_seconds:
|
|
type: integer
|
|
nullable: true
|
|
minimum: 1
|
|
maximum: 604800
|
|
description: Only meaningful with `paused = true`; absent means indefinite.
|
|
|
|
CertReloadOutcome:
|
|
type: object
|
|
required: [enabled, reloaded, error]
|
|
properties:
|
|
enabled:
|
|
type: boolean
|
|
description: Whether the endpoint is enabled in the configuration.
|
|
reloaded:
|
|
type: boolean
|
|
error:
|
|
type: string
|
|
nullable: true
|
|
description: Why the reload failed; null on success and while disabled.
|
|
|
|
CertsReload:
|
|
type: object
|
|
required: [doh, dot]
|
|
properties:
|
|
doh:
|
|
$ref: "#/components/schemas/CertReloadOutcome"
|
|
dot:
|
|
$ref: "#/components/schemas/CertReloadOutcome"
|
|
|
|
Settings:
|
|
type: object
|
|
required: [upstream, dns, blocking, cache, web, doh_server, dot_server, edns, logging, disk, blocklist_update]
|
|
properties:
|
|
upstream:
|
|
type: object
|
|
required: [attempt_timeout_ms, read_timeout_ms, total_timeout_ms]
|
|
properties:
|
|
attempt_timeout_ms: { type: integer }
|
|
read_timeout_ms: { type: integer }
|
|
total_timeout_ms: { type: integer }
|
|
dns:
|
|
type: object
|
|
required: [bind_ipv4, bind_ipv6, port, rate_limit, rate_window_seconds]
|
|
properties:
|
|
bind_ipv4: { type: string }
|
|
bind_ipv6: { type: string }
|
|
port: { type: integer }
|
|
rate_limit: { type: integer }
|
|
rate_window_seconds: { type: integer }
|
|
blocking:
|
|
type: object
|
|
required: [response, ttl]
|
|
properties:
|
|
response:
|
|
type: string
|
|
enum: [zero, nxdomain]
|
|
ttl: { type: integer }
|
|
cache:
|
|
type: object
|
|
required: [size, negative_ttl_max]
|
|
properties:
|
|
size: { type: integer }
|
|
negative_ttl_max: { type: integer }
|
|
web:
|
|
type: object
|
|
required: [enabled, bind, port, session_ttl_hours, api_rate_limit_per_min, api_localhost_exempt, sse_max_connections_per_ip, trusted_proxies, auth_enabled]
|
|
properties:
|
|
enabled: { type: boolean }
|
|
bind: { type: string }
|
|
port: { type: integer }
|
|
session_ttl_hours: { type: integer }
|
|
api_rate_limit_per_min: { type: integer }
|
|
api_localhost_exempt: { type: boolean }
|
|
sse_max_connections_per_ip: { type: integer }
|
|
trusted_proxies:
|
|
type: string
|
|
description: |
|
|
Comma-separated IP literals. A request from one of these peers
|
|
is identified by the last entry of its X-Forwarded-For header.
|
|
Empty trusts no proxy.
|
|
auth_enabled:
|
|
type: boolean
|
|
description: Derived, read-only; true iff a password hash is stored.
|
|
doh_server:
|
|
$ref: "#/components/schemas/TlsListenerSettings"
|
|
dot_server:
|
|
$ref: "#/components/schemas/TlsListenerSettings"
|
|
edns:
|
|
type: object
|
|
required: [ecs_mode]
|
|
properties:
|
|
ecs_mode:
|
|
type: string
|
|
enum: [strip, forward]
|
|
logging:
|
|
type: object
|
|
required: [level, retention_days, query_log_buffer_max, query_log_flush_interval_s, hide_domains, hide_client_ips, output, file_path, max_size_mb, max_files]
|
|
properties:
|
|
level:
|
|
type: string
|
|
enum: [error, warn, info, debug]
|
|
retention_days: { type: integer }
|
|
query_log_buffer_max: { type: integer }
|
|
query_log_flush_interval_s: { type: integer }
|
|
hide_domains: { type: boolean }
|
|
hide_client_ips: { type: boolean }
|
|
output:
|
|
type: string
|
|
enum: [stderr, syslog, file]
|
|
file_path: { type: string }
|
|
max_size_mb: { type: integer }
|
|
max_files: { type: integer }
|
|
disk:
|
|
type: object
|
|
required: [min_free_mb, warn_free_mb]
|
|
properties:
|
|
min_free_mb: { type: integer }
|
|
warn_free_mb: { type: integer }
|
|
blocklist_update:
|
|
type: object
|
|
required: [enabled, interval_hours]
|
|
properties:
|
|
enabled: { type: boolean }
|
|
interval_hours: { type: integer }
|
|
|
|
TlsListenerSettings:
|
|
type: object
|
|
required: [enabled, bind, port, cert_path, key_path]
|
|
properties:
|
|
enabled: { type: boolean }
|
|
bind: { type: string }
|
|
port: { type: integer }
|
|
cert_path: { type: string }
|
|
key_path: { type: string }
|
|
|
|
SettingsEnvelope:
|
|
type: object
|
|
required: [settings, restart_required]
|
|
properties:
|
|
settings:
|
|
$ref: "#/components/schemas/Settings"
|
|
restart_required:
|
|
type: array
|
|
items: { type: string }
|
|
description: |
|
|
Every `section.field` key that needs a restart to take effect —
|
|
currently all of them. Whether a restart is *owed* right now is
|
|
process state, and lives on `/api/config/status`.
|
|
|
|
ConfigStatus:
|
|
type: object
|
|
description: |
|
|
Which source governs this process's configuration, and whether a
|
|
committed change waits for a restart. This is how a client learns
|
|
that configuration is read-only; it never has to probe a write route
|
|
for a 403. It rides an authenticated endpoint because `path` is a
|
|
filesystem path, and never appears on the open `/api/version` or
|
|
`/api/health`.
|
|
required: [authority, path, reconciled_at, restart_pending]
|
|
properties:
|
|
authority:
|
|
type: string
|
|
enum: [database, managed_file]
|
|
description: |
|
|
`database` when nxdns runs without `--config`; `managed_file`
|
|
when it runs with it, in which case every configuration write
|
|
answers 403.
|
|
path:
|
|
type: string
|
|
nullable: true
|
|
description: The managed file, or null in `database` mode.
|
|
reconciled_at:
|
|
type: integer
|
|
nullable: true
|
|
description: |
|
|
When this process loaded the managed file, in epoch seconds, and
|
|
null in `database` mode. It means exactly that: a file whose
|
|
mtime is newer has not been loaded by the running process. It
|
|
cannot answer whether the file matches what the server serves —
|
|
a stepped clock or a preserved mtime defeats the comparison
|
|
either way, and `nxdns import` can move the database without
|
|
moving either timestamp.
|
|
restart_pending:
|
|
type: boolean
|
|
description: |
|
|
True once this process has committed a configuration change that
|
|
takes effect only at the next start — an upstream write or a
|
|
settings key. Nothing clears it but process exit, and it is
|
|
never persisted, so a false after a restart is the truth.
|
|
|
|
SettingsPatch:
|
|
type: object
|
|
description: |
|
|
Partial update; every section and every field is optional. The
|
|
shape is `Settings` without the derived `web.auth_enabled`, plus
|
|
the write-only `web.password`. `web.password_hash` is rejected as
|
|
an unknown field.
|
|
properties:
|
|
upstream:
|
|
type: object
|
|
properties:
|
|
attempt_timeout_ms: { type: integer }
|
|
read_timeout_ms: { type: integer }
|
|
total_timeout_ms: { type: integer }
|
|
dns:
|
|
type: object
|
|
properties:
|
|
bind_ipv4: { type: string }
|
|
bind_ipv6: { type: string }
|
|
port: { type: integer }
|
|
rate_limit: { type: integer }
|
|
rate_window_seconds: { type: integer }
|
|
blocking:
|
|
type: object
|
|
properties:
|
|
response: { type: string }
|
|
ttl: { type: integer }
|
|
cache:
|
|
type: object
|
|
properties:
|
|
size: { type: integer }
|
|
negative_ttl_max: { type: integer }
|
|
web:
|
|
type: object
|
|
properties:
|
|
enabled: { type: boolean }
|
|
bind: { type: string }
|
|
port: { type: integer }
|
|
password:
|
|
type: string
|
|
description: |
|
|
Write-only. Hashed with argon2id and stored as the hash;
|
|
never echoed. An empty string is ignored, not a removal.
|
|
session_ttl_hours: { type: integer }
|
|
api_rate_limit_per_min: { type: integer }
|
|
api_localhost_exempt: { type: boolean }
|
|
sse_max_connections_per_ip: { type: integer }
|
|
trusted_proxies: { type: string }
|
|
doh_server:
|
|
$ref: "#/components/schemas/TlsListenerPatch"
|
|
dot_server:
|
|
$ref: "#/components/schemas/TlsListenerPatch"
|
|
edns:
|
|
type: object
|
|
properties:
|
|
ecs_mode: { type: string }
|
|
logging:
|
|
type: object
|
|
properties:
|
|
level: { type: string }
|
|
retention_days: { type: integer }
|
|
query_log_buffer_max: { type: integer }
|
|
query_log_flush_interval_s: { type: integer }
|
|
hide_domains: { type: boolean }
|
|
hide_client_ips: { type: boolean }
|
|
output: { type: string }
|
|
file_path: { type: string }
|
|
max_size_mb: { type: integer }
|
|
max_files: { type: integer }
|
|
disk:
|
|
type: object
|
|
properties:
|
|
min_free_mb: { type: integer }
|
|
warn_free_mb: { type: integer }
|
|
blocklist_update:
|
|
type: object
|
|
properties:
|
|
enabled: { type: boolean }
|
|
interval_hours: { type: integer }
|
|
|
|
TlsListenerPatch:
|
|
type: object
|
|
properties:
|
|
enabled: { type: boolean }
|
|
bind: { type: string }
|
|
port: { type: integer }
|
|
cert_path: { type: string }
|
|
key_path: { type: string }
|