Ctrl K
ComunidadGuías…

docs/adr/0048-resolver-groups-wire-format.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# ADR-0048: Custom Resolver Groups Accept the DoH Wire Format (ADR-0047 Gap)

## Metadata

| Field | Value |
|-------|-------|
| **Status** | Superseded by [ADR-0072](0072-dns-consensus-revalidation-hardening.md) and [ADR-0075](0075-dns-unbound-single-source-completion.md) |
| **Date** | 2026-08-08 |
| **Authors** | AlessioBrillo |
| **Deciders** | AlessioBrillo |
| **Supersedes** | N/A |
| **Relates to** | ADR-0040, ADR-0045, ADR-0046, ADR-0047 |
| **Project** | DOMINUS |

## Context

ADR-0047 shipped `DnsLookupSpec.format?: 'json' | 'wire'` for the **default**
DoH group: Google maps to its JSON API on `/resolve` and Quad9 is marked
`format: 'wire'` so the RFC 8484 wire leg is used. The runtime honours the
field in `node-dns-provider.ts` (`spec.format ?? 'json'`), and
`strategyToResolverGroups` populates it for every built-in strategy.

However, the **configuration schema rejected the field**: the `DNS_RESOLVER_
GROUPS` Zod schema describes each lookup as `{ type: 'doh', endpoint }`
without `format`, and Zod strips unknown keys by default. Consequences for
operators who override the resolver groups via `DNS_RESOLVER_GROUPS`:

1. A custom wire-only endpoint (e.g. `dns.quad9.net/dns-query`) could not
   express its format: the JSON leg was always used, the server answers
   505, the leg votes neutrally — the same silently-inert-leg failure mode
   ADR-0047 fixed for the defaults, now reachable through configuration.
2. A typo such as `format: "spdy"` was silently stripped instead of
   rejected, so a misconfiguration produced a degraded (inert) leg rather
   than a startup error.
3. Documentation drift: the parking-IP doc string promised a built-in
   default list that the implementation never applied (absent
   `DNS_PARKING_IPS_PATH` disables parking detection), `.env.example`
   suggested `DNS_TERTIARY_STRATEGY=dot-only` which duplicates the
   consensus secondary's default transport and gets dropped by the
   disjointness check (ADR-0045), and the compose recursor comment still
   referenced "candidate ADR-0047" although ADR-0047 had been accepted.

This ADR closes the gap: the wire format must be configurable per lookup,
drift between the schema and its documentation must be removed, and the
local probe validation (`validateResolverGroups`) must be covered by tests.

## Decision

1. **`DNS_RESOLVER_GROUPS` accepts `format` on DoH lookups** — the Zod
   schema's `doh` variant becomes
   `z.object({ type: z.literal('doh'), endpoint: z.string().url(), format: z.enum(['json', 'wire']).optional() })`
   with docs. `json` stays the default (unchanged behaviour); `wire` selects
   the RFC 8484 base64url leg so custom wire-only endpoints (Quad9, AdGuard,
   Mullvad) behave exactly like the built-in Quad9 leg from ADR-0047. A
   value outside the enum is now a **config error at load time**, not a
   silently stripped field.
2. **The wire leg is propagated end to end** — a custom resolver group read
   from config with `format: 'wire'` issues `GET /dns-query?dns=<base64url>`
   with no `name=` parameter, verified by provider-level tests; the runtime
   path already honoured `spec.format`.
3. **Config-surface docs align with behaviour** (best-effort, no
   behavioural change):
   - `DNS_PARKING_IPS_PATH` doc no longer promises a built-in default list
     (there is none; absence disables parking detection).
   - `.env.example` `DNS_TERTIARY_STRATEGY` example changed from
     `dot-only` (duplicates the consensus secondary default) to `native`
     with a comment explaining the trap.
   - `docker-compose.dns-consensus.yml` recursor comment drops the
     "candidate ADR-0047" back-reference; multi-arch tracking stays under
     ADR-0046.
   - The unused `getDefaultDohProviders`/`getDefaultDotProviders` exports
     are removed (no callers; the constants remain internal).
4. **`validateResolverGroups` matches decisions:** happy-path probe status,
   unexpected status, network failure, and the 5 s abort timeout are each
   covered by unit tests with a fake provider.

## Alternatives Considered

- **Reject unknown fields entirely in the schema** (`.strict()`). Rejected:
  would break other reserved fields and fails to be backwards-compatible
  with forward-looking configs.
- **Force wire by endpoint heuristic** (e.g. always wire for non-Google
  hosts). Rejected: brittle, undocumented, and silently changes behaviour
  of deployers who pinned a JSON-serving endpoint.
- **Keep the docs/example inconsistencies**: Rejected: `.env.example`
  `dot-only` duplicating the secondary would make the tertiary leg fail the
  disjointness gate needlessly — a documented trap that costs users the
  third opinion ADR-0045 added.

## Consequences

- **Positive** — resolver overrides are the full first-class surface
  ADR-0047 gave them: an operator can run a Quad9-only group correctly,
  typos surface as errors instead of silent inert legs, and docs no longer
  send users to the tertiary trap. Coverage for the startup probe grows.
- **Negative** — schema accepts `'json' | 'wire'` only; any third DoH
  format (there is none in use) needs a future ADR. Removing the two
  exports is an API change for any external consumer of the barrel file
  (none in-repo); semver practice tracked in the changelog.

## Verification

- Reproducer committed before this ADR (`test: add reproducer for format
  stripping in DNS_RESOLVER_GROUPS`): runs with `format: 'wire'` and
  asserts the field survives `loadConfig()` (previously stripped).
- `npx vitest run src/config.test.ts src/providers/dns` — 30 + 63 + 15
  tests green.
- Full backend suite — 2049 tests green (2037 + 30 − shifts); `npx tsc
  --noEmit`, `npx eslint` on the touched providers, Prettier clean.