Skip to content

Releases: bcrisp4/bns

v0.5.1 — fix entries gauge on 304 cache loads

Choose a tag to compare

@bcrisp4 bcrisp4 released this 02 Jun 04:52
2ddda80

Fix

bns_blocklist_entries_by_source{source=...} no longer goes absent after a restart for HTTP blocklist sources that load from cache.

Previously the gauge was only set on the 200 OK fetch path. Any source already on disk in cache_dir with a valid ETag returns 304 Not Modified on its first conditional GET after restart — and the gauge for that source was never populated, even though the list was fully loaded and actively matching. With conditional GETs plus a multi-hour refresh_interval, the gauge stayed absent until the upstream content actually changed (ETag differs → 200).

The 304 path now re-reports the entry count already persisted in the cache metadata (CacheMeta.Entries). A 304 is only reachable when If-None-Match was sent from the cached ETag, so the cached count is current.

Upgrade

Drop-in. No config changes, no behavioural change beyond the metric now being present after a cache-load restart.

v0.5.0 — direct cache hit/miss metric

Choose a tag to compare

@bcrisp4 bcrisp4 released this 24 May 07:27

Highlights

  • New metric: bns_cache_lookups_total{result="hit"|"miss"} — direct cache effectiveness signal emitted by cachestage on every single-question lookup. Hit-rate is now a clean Prometheus query:
    rate(bns_cache_lookups_total{result="hit"}[5m])
    / rate(bns_cache_lookups_total[5m])
    
    Replaces the indirect derivation from bns_upstream_queries_total / bns_queries_total, which was noisy under coalesced piggybacks and upstream errors.
  • Help-text fix: bns_cache_evictions_total now correctly documents both eviction paths (LRU pressure on Store and lazy TTL expiry on Get).

Internal

  • cachestage.New gained a *metrics.Metrics parameter (nil-safe, matching the coalesce stage precedent). Multi-question requests bypass the cache and the counter — they are not lookups.
  • CLAUDE.md compressed + improver pass.

Changelog

3bb1663 metrics: add bns_cache_lookups_total{result=hit|miss}
9971540 docs: /caveman:compress pass over CLAUDE.md
f1b349e docs: CLAUDE.md improver pass — v0.4.0 release, make build-stress, compress DoH gotchas
b325bd1 docs: 3 learnings from DoH session (test helpers, TLS h2, Pack-error)
b30dd43 metrics: clarify cache eviction help text covers TTL expiry

Container image

ghcr.io/bcrisp4/bns:0.5.0 (multi-arch linux/amd64 + linux/arm64) — published automatically by CI on tag push.

v0.4.0 — DNS-over-HTTPS upstream support

Choose a tag to compare

@bcrisp4 bcrisp4 released this 24 May 06:23

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

v0.3.1 — quieter cold-start logs

Choose a tag to compare

@bcrisp4 bcrisp4 released this 21 May 07:00

Fix

Cold-start with HTTP-only blocklist sources no longer emits a spurious WARN. When every configured source is type: http and the on-disk cache is empty (first run, or no volume mounted), the matcher is empty by design — the fetcher populates it within a few seconds. Previously the startup log line was:

```
WARN blocklist sources configured but yielded zero entries — check paths and file contents
```

which falsely implied operator misconfiguration. The expected case now logs at INFO:

```
INFO blocklist cache cold — fetcher will populate; block stage is a no-op until first fetch lands
```

The original WARN still fires for genuine misconfiguration (file source pointing at an empty/missing path, or mixed sources where the file half yields nothing).

Upgrade

Drop-in. No config changes. No behavioural change beyond the startup log line.

```
docker pull ghcr.io/bcrisp4/bns:0.3.1
```

v0.3.0 — HTTP blocklist source

Choose a tag to compare

@bcrisp4 bcrisp4 released this 21 May 06:40

Highlights

Adds a polite, self-refreshing HTTP blocklist source so operators no longer need to bake lists into the container image or babysit git pull/SIGHUP. The default container now fetches hagezi pro.txt at startup, caches it to disk, and refreshes every 24h with conditional GET.

What's new

  • type: http blocklist source — fetches lists from any HTTP(S) URL with conditional GET (ETag / If-Modified-Since). Configurable global refresh interval (blocklists.refresh_interval, default 24h).
  • On-disk persistent cache at blocklists.cache_dir (default /var/cache/bns/blocklists). Bodies + sidecar metadata written atomically; readers never observe a partial write. Survives restarts.
  • Bootstrap dialer — fetcher resolves URL hostnames via configured upstream IPs directly, bypassing the system stub. BNS can fetch its own lists even when it's the sole resolver on the LAN.
  • Always fail-open — HTTP fetch failures never block DNS service. A bns_blocklist_last_success_timestamp_seconds{source} gauge supports staleness alerts.
  • Per-source observability — three new metric families:
    • bns_blocklist_fetch_total{source,outcome} (success / not_modified / failure)
    • bns_blocklist_last_success_timestamp_seconds{source}
    • bns_blocklist_entries_by_source{source}
  • Required name field on every blocklist source — used as the {source="..."} label on every bns_blocklist_* metric. Validation rejects missing or duplicate names at startup.
  • SIGHUP now does both — reload disk state (file-source edits) and trigger an immediate fetcher cycle (HTTP sources).

Breaking changes

  • --blocklist CLI flag removed. Use YAML (-c file.yaml) for blocklist source configuration. The flag is incompatible with the new required name field; YAML is the canonical declaration form.
  • Container no longer bakes the hagezi list. Cold start with no cache volume serves DNS without blocking for a few seconds while the first fetch lands. Mount a named volume at /var/cache/bns to preserve cache across restarts:
    docker volume create bns-cache
    docker run -v bns-cache:/var/cache/bns ghcr.io/bcrisp4/bns:0.3.0
  • HAGEZI_TAG Dockerfile build arg dropped.

Config schema

blocklists:
  refresh_interval: 24h
  cache_dir: /var/cache/bns/blocklists
  sources:
    - type: http
      name: hagezi-pro
      url: https://raw.githubusercontent.com/hagezi/dns-blocklists/main/domains/pro.txt
    - type: file
      name: custom-blocklist
      path: /etc/bns/custom-blocklist.txt

Container images

Multi-arch (linux/amd64, linux/arm64) at ghcr.io/bcrisp4/bns:0.3.0, :0.3, :latest.

Spec + plan

  • Design: docs/specs/2026-05-21-bns-http-blocklist-source-design.md
  • Implementation plan: docs/plans/2026-05-21-bns-http-blocklist-source-implementation.md
  • 16 TDD tasks plus a simplify pass and a code-review-driven fix-up.

v0.2.0

Choose a tag to compare

@bcrisp4 bcrisp4 released this 20 May 06:01

Full Changelog: v0.1.0...v0.2.0

v0.1.0

Choose a tag to compare

@bcrisp4 bcrisp4 released this 20 May 02:22

First MVP release of BNS — caching DNS forwarder with ad-block, intended for small private networks (Raspberry Pi-class hardware).

Features

  • UDP + TCP listeners (single shared resolver chain)
  • In-memory LRU cache with TTL-aware expiry and negative caching
  • Singleflight upstream coalescing
  • Blocklist matcher (exact + subdomain wildcard) with hot-reload on SIGHUP
  • Prometheus /metrics, /healthz, /readyz
  • Configurable upstream pool with primary + fallback
  • Multi-arch container image (linux/amd64, linux/arm64) — see below

Container

Image published at ghcr.io/bcrisp4/bns:0.1.0 (also 0.1 and latest).

docker run -d \
  -p 53:5354/udp -p 53:5354/tcp -p 9090:9090 \
  --name bns ghcr.io/bcrisp4/bns:0.1.0

Built on gcr.io/distroless/static-debian12:nonroot. Hagezi pro.txt (~459K domains) baked in at /etc/bns/blocklists/pro.txt. Override config via bind-mount, BNS_* env vars, or trailing CLI flags.

Caveats

  • Container listens on :5354 (nonroot can't bind privileged ports). Map host :53 at run time.
  • Blocklist refresh requires image rebuild (URL fetcher source not yet implemented).
  • DoH/DoT/DoQ upstream not yet implemented — UDP plaintext only.