DoH and tokens
DNS over HTTPS (RFC 8484) is served by the dnsy-server DoH listener on /dns-query. It speaks
HTTP/1.1 and HTTP/2 over TLS (ALPN h2, http/1.1) and uses the same certificate loading and hot
reload as DoT: replacing the PEM files takes effect for new connections, and a bad replacement
keeps the previous certificate.
Requests
| Method | Request |
|---|---|
GET | /dns-query?dns=<base64url>, the DNS message in base64url without padding |
POST | /dns-query, body is the DNS message, Content-Type: application/dns-message |
Responses are 200 with Content-Type: application/dns-message and
Cache-Control: max-age=<n>, where n is the smallest TTL of any record in the response (0 when
there are none). It is Cache-Control: private, max-age=<n> when the request presented a token
or trusted_proxies is set, since the answer may then depend on who asked. Responses are padded (RFC 8467) when the query carries an EDNS Padding option.
| Status | Meaning |
|---|---|
| 400 | bad base64, missing dns, not a DNS message, empty body, or more than 65535 octets (body or decoded parameter) |
| 401 | no token (when required) or a rejected token; WWW-Authenticate: Bearer |
| 403 | valid token without the dns:query scope |
| 404 | any path other than the configured [doh] path (default /dns-query) and {path}/{token} |
| 405 | method other than GET and POST (Allow: GET, POST) |
| 415 | POST with a content type other than application/dns-message |
| 429 | too many rejected tokens from this client address (Retry-After) |
Tokens
Tokens are created through the API (scope dns:query; see Users, tokens and sessions). A token is
required by default. Send it either way:
Authorization: Bearer <token>- in the path,
https://dns.example/dns-query/<token>, for clients that cannot set headers (browsers’ DoH settings, many mobile resolvers)
A presented token is always verified, even when tokens are not required. Verification and secret
comparison are constant time and done by the token verifier in dnsy-auth. The authenticated
principal is attached to the request, so the query log shows which token asked.
A successful verification is cached for 5 s (DOH_TOKEN_CACHE_TTL, at most 4096 tokens,
DOH_TOKEN_CACHE_CAPACITY), so a busy client costs one database read per token every 5 s instead
of one per query. Rejected tokens are never cached. A revoked token, a disabled or deleted owner or
a role change takes effect on the next request when done through the API or the web UI. Changes
made with the offline CLI (dnsy user disable, dnsy token create and friends) while the server
is running apply to DoH within 5 s (verified tokens are cached for 5 s); dnsy user disable says
so when it runs.
Rejected tokens are counted per client (an IPv4 address or an IPv6 /64; default 10 per 60 s). Past that limit the client gets
429 before any token is looked at, so the response never reveals whether a token exists. Malformed,
unknown, revoked, expired and wrong-secret tokens all give the same 401.
The table holds at most 10 000 clients; clients that reached the limit are never evicted to make
room, so flooding it with other addresses cannot unblock them. If a part of the table is full of
blocked clients, failures from further clients share one counter that blocks them all once it
reaches the limit (the table fails closed).
Secrets are not logged
The request URI and the Authorization header are never logged, never copied into error bodies,
and never passed to the engine, so path tokens cannot reach the query log. Access logging records
only the client address and status. Prefer the header where the client allows it: path tokens can
still be recorded by reverse proxies or browser history in front of or around dnsy.
Settings
The listener is configured through DohConfig: listen addresses, certificate and key, the token
authenticator, require_token (default true), max_auth_failures, auth_window and
trusted_proxies ([doh] trusted_proxies, empty by default). A request from a listed proxy is
attributed to the rightmost X-Forwarded-For entry that is not a listed proxy: that address keys
the auth-failure throttle, ACL rules, per-client QPS and the query log (rules and spoofing risks
in Security). It shares
the idle timeout, connection cap and drain time of the TCP and DoT listeners.