Ctrl K
ComunidadGuías…

docs/adr/0069-dns-secondary-dual-redundancy.md

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

Archivo original ↗Preguntar a KiBot
# ADR-0069: DNS Secondary Dual-Redundancy for Consensus Gate Resilience

## 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-09-06                                                                               |
| **Authors**    | Alessio Brillo                                                                           |
| **Deciders**   | Alessio Brillo                                                                           |
| **Supersedes** | ADR-00XX (placeholder in config/provider-factory)                                        |
| **Relates to** | ADR-0002, ADR-0039, ADR-0044, ADR-0045, ADR-0059, ADR-0063, ADR-0064, ADR-0065, ADR-0068 |
| **Project**    | DOMINUS                                                                                  |

## Context

The 2-of-3 DNS consensus gate (ADR-0039, ADR-0045) cross-validates every `Available` verdict from the primary resolver against a secondary independent resolver. Since ADR-0068, the tertiary leg uses dual-redundant topology (two independent operators with separate circuit breakers and rate limiters).

However, the secondary leg remained single-provider: a single `NodeDnsProvider` instance built from `DNS_CONSENSUS_STRATEGY` (default `dot-alternate` = AdGuard/Mullvad/NextDNS over DoT). This created an asymmetry:

- Tertiary leg: dual-redundant, survives loss of either operator
- Secondary leg: single point of failure, loss degrades gate to 1-of-1

The dual-redundant secondary was prototyped in `buildSecondaryConsensusProviders()` (ADR-00XX placeholder) but never fully wired through `ConsensusDnsProvider` and `ConsensusEngine`. The wiring split:

1. `provider-factory.ts` creates two secondary providers but returns bare `DnsProvider[]` without groups/endpoints
2. `composition-root.ts` reconstructs array from `secondaryConfig` but drops `tertiaryConfig`
3. `consensus-engine.ts` has dual code paths (`secondaryProviders[]` vs `secondaryConfig`) with divergent semantics
4. `ConsensusDnsProvider` constructor accepts `secondaryProviders[]` but ignores `secondaryConfig`

Result: depending on code path, the gate runs single, dual, or degraded without telemetry distinction.

## Decision Drivers

1. **Conservatism (ADR-0002)** — an `Available` verdict is the risky verdict; the gate must eliminate single-resolver opinions. A single secondary leg is a SPOF that violates this principle under failure.
2. **Symmetry with tertiary (ADR-0068)** — if dual-redundant is correct for tertiary, it is correct for secondary. Asymmetry creates a weaker link.
3. **Operational honesty** — `provider-status` and metrics must reflect the gate that actually runs (cardinality: single/dual-s/dual-t), not the intended one.
4. **Zero-cost discipline (ADR-0001)** — `dot-alternate` (AdGuard) and `dot-consensus` (Mullvad/NextDNS) are free public DoT resolvers; no new infrastructure.
5. **Fail-closed quorum** — `requiredConfirmations=1` (default): ANY Available from s1 OR s2 confirms (1-of-2 rescue). `requiredConfirmations=2`: BOTH secondary AND tertiary must confirm. The secondary dual is **not** a 2-of-2 quorum — that would invert conservatism by requiring two independent confirmations instead of one rescue.

## Considered Options

### Option A: Dual-redundant secondary with 1-of-2 rescue + ANY veto (Chosen)

Create two independent `NodeDnsProvider` instances for the secondary leg:

- Provider s1: `DNS_CONSENSUS_STRATEGY_1` (default: `dot-alternate` → AdGuard over DoT)
- Provider s2: `DNS_CONSENSUS_STRATEGY_2` (default: `dot-consensus` → Mullvad/NextDNS over DoT)

Each has:

- Own rate limiter (split budget: 10 req/sec each, total 20 matching legacy secondary budget)
- Own circuit breaker (shared registry via `DnsBreakerRegistry`)
- Independent disjointness validation against primary

Race semantics (`requiredConfirmations` counts LEGS, not providers):

- **Rescue path** (`requiredConfirmations=1`): First `Available` from s1 OR s2 confirms the secondary leg.
- **Veto path**: ANY `Registered` from s1 OR s2 vetoes the domain (fail-closed).
- **Strict mode** (`requiredConfirmations=2`): Secondary leg (ANY of s1/s2) AND tertiary leg (ANY of t1/t2) must both confirm. Requiring ALL providers within a leg would make a single timeout veto every `Available` and push the `Unknown`-rate to commercially unusable levels.

**Advantages:**

- True redundancy: one operator failure doesn't disable the secondary leg.
- Independent circuit breakers: AdGuard failure doesn't block Mullvad/NextDNS.
- Independent rate limiting: each operator gets dedicated 10 req/sec budget.
- Symmetric with tertiary dual-redundant topology.
- Backward compatible: `DNS_CONSENSUS_DUAL_REDUNDANT=false` preserves legacy single-secondary behavior.
- Zero-cost: both endpoints are free public resolvers.

**Disadvantages:**

- Two extra DoT queries per `Available` domain needing secondary (bounded by `DNS_CONSENSUS_BULK_CONCURRENCY=20`).
- Slightly more complex configuration surface (two strategy env vars).
- Higher `Unknown`-rate when one secondary fails and the other is slow/timeout (mitigated by `DNS_LOOKUP_TIMEOUT_MS`).

**Cost Implications:** None — both endpoints are free public resolvers. Total rate limit budget unchanged (20 req/sec split 10/10).

**Risk Assessment:** Low. Dual-redundant mode is opt-in via `DNS_CONSENSUS_DUAL_REDUNDANT=true` (default true). Legacy mode preserved. Existing deployments using single `DNS_CONSENSUS_STRATEGY` continue to work.

---

### Option B: Keep single secondary, invest in tertiary reliability

Rely on tertiary dual-redundancy as the sole redundancy layer; accept secondary SPOF.

**Disadvantages:**

- Tertiary is only consulted when secondary fails/unknown (`consensus-engine.ts:403-420`). If secondary returns `Unknown` (timeout/error), tertiary rescues. But if secondary returns `Registered` (false positive from single operator), tertiary is never consulted — veto is immediate and irreversible.
- Asymmetric resilience contradicts ADR-0002 conservatism principle.

---

### Option C: Single secondary with internal multi-endpoint race

Modify `NodeDnsProvider` to race multiple DoT endpoints internally.

**Disadvantages:**

- Shared circuit breaker, shared rate limiter, mixed telemetry — same problems ADR-0059 and ADR-0068 solved for tertiary.
- Violates provider abstraction (one provider = one resolver group).

---

## Decision Outcome

**Chosen: Option A — Dual-redundant secondary with two independent NodeDnsProvider instances, 1-of-2 rescue + ANY veto semantics.**

Implementation details:

- New env vars already exist: `DNS_CONSENSUS_STRATEGY_1`, `DNS_CONSENSUS_STRATEGY_2`, `DNS_CONSENSUS_DUAL_REDUNDANT`, `DNS_CONSENSUS_RATE_LIMIT_TOKENS_1/2`, `_INTERVAL_MS_1/2`.
- `ConsensusConfig` unified in `consensus-engine.ts` as single source of truth; `secondaryConfig: SecondaryDnsConfig` with `primary`, `secondary`, `strategy: 'dual-redundant' | 'single'`.
- `ConsensusEngineOptions` carries `secondaryProviders: DnsProvider[]` (length 1|2) and `tertiaryProviders: DnsProvider[]` (length 0|1|2).
- `ConsensusDnsProvider` propagates `secondaryConfig`, `tertiaryConfig`, and all resolver groups/endpoints for runtime re-validation (explicit arrays win, else derived from configs).
- `buildConsensusDnsProvider()` accepts `tertiaryProviders: DnsProvider[]` plus `secondaryConfig`/`tertiaryConfig` and s2/t2 groups for re-validation.
- Quorum: `requiredConfirmations=1` → secondary leg confirms if ANY s1/s2 returns Available, else tertiary leg (ANY t1/t2) rescues; `requiredConfirmations=2` → secondary leg (ANY) AND tertiary leg (ANY) must both confirm.
- Metrics label `leg="s1|s2|t1|t2"`; alerts `DnsSecondaryLegDegraded` (1 of 2 breakers open), `DnsTertiaryLegDegraded`.

## Consequences

### Positive

- The secondary consensus leg survives loss of AdGuard or Mullvad/NextDNS independently.
- Circuit breaker isolation per operator (ADR-0059): AdGuard failure doesn't block Mullvad/NextDNS.
- Rate limiting fairness: each operator gets dedicated 10 req/sec budget.
- Symmetric dual-redundant topology for secondary and tertiary.
- Observability: `DnsSecondaryLegDegraded` alert distinguishes partial degradation from total secondary loss.
- Backward compatible: `DNS_CONSENSUS_DUAL_REDUNDANT=false` reverts to legacy single-secondary.
- Zero-cost: both operators are free public resolvers.

### Negative

- Two DoT queries per `Available` domain needing secondary (bounded by `DNS_CONSENSUS_BULK_CONCURRENCY=20`).
- Slightly more complex configuration surface (two strategy env vars).
- `secondaryConfig`/`tertiaryConfig` objects add wiring seams between factory, consensus-engine, and ConsensusDnsProvider.
- Higher `Unknown`-rate on partial secondary degradation (one timeout + one slow) — monitored via `dominus_dns_consensus_unverifiable_total` and `dominus_dns_leg_duration_ms` with `role="consensus"`.

### Compliance and Security Implications

- All queries to AdGuard/Mullvad/NextDNS are DNS lookups of candidate domains — same data class already sent to Cloudflare/Google/Quad9.
- DNSSEC validation unaffected: secondary providers are plain DoT resolver opinions like the primary's legs.
- No secrets, keys, or paid APIs involved.

### Migration and Monitoring Plan

- Rollout: `DNS_CONSENSUS_DUAL_REDUNDANT=true` by default in production override; community edition defaults to true but only activates when consensus is enabled.
- Monitoring: `dominus_dns_leg_duration_ms` with `role="consensus"` shows both endpoints; `dominus_dns_breaker_open` per endpoint tracks health.
- Alert rules: `DnsSecondaryLegDegraded` (one operator down), `DnsSecondaryBreakerOpen` (any endpoint breaker open).
- Success criteria: during a deliberate AdGuard slow-down, Mullvad/NextDNS rescues `Available` verdicts; alert fires on partial degradation; no verdict fabricated.
- Rollback: `DNS_CONSENSUS_DUAL_REDUNDANT=false` reverts to legacy single-secondary behavior; code path is env-gated and additive.

### Validation

- Unit tests: dual-redundant race rescue/veto, circuit isolation, disjointness checks, legacy mode.
- Integration test: `consensus-wiring.test.ts` boots gate with dual-redundant secondary + dual-redundant tertiary from production override env.
- Bootstrap probe: both secondary providers probed at startup with `forceRecheck=true`.
- Benchmark: `DNS_LIVE=1 npm run bench` measures p95 and query amplification before/after.

---

_This ADR was created following the MADR 4.0.0 standard. All DOMINUS ADRs should be consistent with the ADR series starting at `docs/adr/0001-project-architecture.md`. Template: `.claude/skills/adr/template.md`._