Documentation
¶
Overview ¶
Package api exposes a resolver's statistics and controls over HTTP.
It is optional. Nothing in the engine imports it, and a deployment that wants no management surface simply does not construct one — which is a supported configuration rather than a degraded one, because the smallest attack surface is the one that does not exist.
This is the most dangerous surface in the product ¶
Everything else here answers DNS queries. This answers questions about what a network's people looked up, and lets a caller change what gets blocked. An exposed instance leaks a browsing history and hands over the filtering policy at the same time, and unlike a resolver it is reachable by a browser on the same LAN following a link.
So the defaults are chosen to fail closed, and three of them are deliberately inconvenient:
- Authentication is REQUIRED. New refuses to build a server with no credential rather than starting an open one. There is no "disable_auth" — an operator who genuinely wants an unauthenticated API can put one in front of this, where they will at least have had to think about it.
- The token is compared in constant time, because a management token compared with == leaks itself one byte at a time to anyone who can measure a response.
- Cross-origin requests are refused unless origins are named. Without that, any page a user visits can drive this API from inside their network, which is the shape of every home-router compromise of the last decade.
The query log is gated separately from everything else (Options.ExposeLog), because "how many queries were blocked" and "what did the person in the next room look up" are different questions and deserve different answers.
What it does not do ¶
It does not listen on a socket of its own: New returns an http.Handler and the caller decides where it is served, which is what lets a daemon put it on a unix socket, behind TLS, or on a separate interface. It holds no state, starts no goroutines, and every read it performs is bounded — an unbounded read of a query log is how a management API turns into a memory exhaustion.
Bounded means bounded by the DEPLOYMENT, not by the caller: the page size a request asks for is clamped to Options.MaxPageSize, and so is the page size it does not ask for. A cap a caller escapes by sending a zero, or by omitting the parameter, is not a cap.
Index ¶
Constants ¶
const DefaultMaxPageSize = 1000
DefaultMaxPageSize bounds one page of the query log.
const MinTokenLen = 32
MinTokenLen is the shortest credential New will accept.
Thirty-two characters is not arbitrary: this token is the whole of the authentication, it protects a browsing history, and anything short enough to be guessed or typed from memory is short enough to be brute-forced by something on the same network. Refusing a short one at construction is the only moment anybody is paying attention.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type API ¶
type API struct {
// contains filtered or unexported fields
}
API serves the management endpoints.
func New ¶
New returns an http.Handler serving the management API.
It does not listen: the caller decides where this is served, which is what lets a daemon put it on a unix socket, behind TLS, or on an interface that is not the one answering DNS.
func (*API) ServeHTTP ¶
func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler.
type CacheSource ¶
type CacheSource interface {
Stats() cache.Stats
Len() int
Flush()
FlushSubtree(n dnsmsg.Name) int
}
CacheSource reports on and controls the cache.
type LogSource ¶
type LogSource interface {
Query(ctx context.Context, f storage.Filter) ([]storage.QueryEntry, error)
Count(ctx context.Context, f storage.Filter) (uint64, error)
TopNames(ctx context.Context, f storage.Filter, n int) ([]storage.Stat, error)
TopDevices(ctx context.Context, f storage.Filter, n int) ([]storage.Stat, error)
}
LogSource reads the query log.
type MetricsSource ¶
MetricsSource reports engine-wide counters.
type Options ¶
type Options struct {
// Token authenticates every request but /healthz. Required, and at least
// [MinTokenLen] characters; see the package documentation for why there is
// no way to turn it off.
Token string `json:"token"`
// AllowedOrigins enables CORS for exactly these origins. Empty — the
// default — refuses every cross-origin request, so a page a user happens to
// visit cannot drive this API from inside their network.
AllowedOrigins []string `json:"allowed_origins"`
// ExposeLog enables the query-log endpoints. It is separate from everything
// else because "how many queries were blocked" and "what did the person in
// the next room look up" are different questions.
ExposeLog bool `json:"expose_log"`
// ReadOnly refuses every mutating request. A dashboard needs nothing else,
// and a token that can only read is a token worth less to whoever steals it.
ReadOnly bool `json:"read_only"`
// MaxPageSize caps how many query-log rows one request may return. Zero
// selects [DefaultMaxPageSize].
MaxPageSize int `json:"max_page_size"`
Metrics MetricsSource `json:"-"`
Cache CacheSource `json:"-"`
Log LogSource `json:"-"`
Policy PolicySource `json:"-"`
Clock clock.Clock `json:"-"`
Logger *slog.Logger `json:"-"`
}
Options configure the API.