api

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

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

View Source
const DefaultMaxPageSize = 1000

DefaultMaxPageSize bounds one page of the query log.

View Source
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

func New(opts Options) (*API, error)

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

type MetricsSource interface {
	Snapshot() metrics.Snapshot
}

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.

func (Options) Validate

func (o Options) Validate() error

Validate reports every problem with o rather than the first.

type PolicySource

type PolicySource interface {
	Stats() (evaluated, blocked uint64)
	Reload(ctx context.Context) error
}

PolicySource reports on filtering and reloads it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL