Skip to content

v0.4.0 — DNS-over-HTTPS upstream support

Choose a tag to compare

@bcrisp4 bcrisp4 released this 24 May 06:23
· 6 commits to main since this release

Highlights

DNS-over-HTTPS (RFC 8484) as upstream transport. New type: doh upstream entries alongside existing UDP. Hand-rolled DoH client over stdlib net/http, uses miekg/dns v2 dnshttp.Response for decode. HTTP/2 preferred with HTTP/1.1 fallback, TLS 1.3 floor, redirects refused, cookies disabled.

Operator-pinned endpoint_ips eliminate the bootstrap deadlock that would otherwise occur on Pi-hole-style deployments where BNS is the system resolver. The DoH URL hostname is used only for SNI / cert SAN validation; never resolved at runtime.

Per-query upstream attribution in the query log. Forwarded queries now carry upstream (URL/addr) and upstream_protocol (udp/doh) attrs. Cache hits, blocked queries, and coalesce piggybacks omit them cleanly.

Container default switched to DoH-primary with UDP fallback. Operators wanting plain UDP only can mount their own config.

Configuration

upstreams:
  - type: doh
    url: https://cloudflare-dns.com/dns-query
    endpoint_ips: [1.1.1.1, 1.0.0.1]
    timeout: 5s

  - type: udp
    addr: 1.1.1.1:53
    timeout: 2s

See examples/config.example.yaml (mixed UDP+DoH) and examples/config-doh.yaml (DoH-only).

New metrics

  • bns_doh_http_status_total{upstream, status} — HTTP-layer outcomes
  • bns_doh_tls_handshakes_total{upstream, result} — connection-churn signal

Breaking changes

  • --upstream CLI flag removed. Upstreams are YAML-only (consistent with blocklists.sources, which has been YAML-only since v0.3.0 because viper cannot index slice config via env vars).
  • bns_upstream_* metric vectors gain protocol label (udp / doh / pool). Alerts that label-match on {upstream} without protocol continue to work; alerts that group by all labels and assert cardinality need updating.

RFC 8484 compliance

Audited: POST method, ID=0 on the wire (RFC §4.1.1), Content-Type case-insensitive parse with parameters (§4.2 + RFC 7231 §3.1.1.1), 2xx status range, body cap at dns.MaxMsgSize, redirect refusal (§9), cookies disabled (§8), HTTP/2 preferred (§5.2), server push disabled by default (§5.3), TLS 1.3 minimum (BCP 195 / RFC 9325), HTTP Age header subtracted from RR TTLs (§5.1).

Spec + plan

  • docs/specs/2026-05-24-doh-upstream-design.md
  • docs/plans/2026-05-24-doh-upstream-implementation.md

Container image

ghcr.io/bcrisp4/bns:0.4.0, :0.4, :latest (linux/amd64 + linux/arm64) — built and pushed automatically by .github/workflows/docker.yml on this tag.

🤖 Generated with Claude Code