queryServers always tries servers in fixed order, so every resolution starts at a.root-servers.net #138

Open
opened 2026-08-10 15:17:05 +02:00 by clawbot · 0 comments
Collaborator

Raised as a question rather than filed as a defect, because it may be a deliberate simplicity choice. @sneak — a one-word answer settles it.

Observation

internal/resolver/iterative.gorootServerList() returns the thirteen roots in a fixed order beginning 198.41.0.4 (a.root-servers.net), and queryServers walks that slice strictly in order, returning on the first success:

for _, ip := range servers {
    resp, err := r.queryDNS(ctx, ip, name, qtype)
    if err == nil {
        return resp, nil
    }
    lastErr = err
}

Because the happy path succeeds on the first entry, every iterative resolution dnswatcher performs begins by querying a.root-servers.net, and the other twelve roots are contacted only when it fails. The remaining twelve are, in practice, a failover list rather than a pool.

Two consequences:

  1. Load concentration. Real resolvers randomise or RTT-rank root selection precisely so load spreads across the anycast constellation. With many targets and a short DNSWATCHER_DNS_INTERVAL, this points all root traffic at one operator.
  2. Correlated failure. If a.root-servers.net is slow or unreachable from the host, every single resolution pays that timeout before failing over, rather than one-in-thirteen of them.

This surfaced while investigating the test flakiness in #93: with ~35 parallel tests on a 48-core machine, every test fired its first query at the same IP within milliseconds of the others, which fits the observed "a different subset fails each run" signature. That issue was scoped test-side and fixed test-side, so this production behaviour was deliberately left untouched.

Note queryServers is shared — it is also used for the per-domain nameserver lists, not only the roots — so any change affects both call sites.

The question

Is the fixed order intentional (deterministic, simple, easy to reason about) or incidental? If incidental, the fix is small: shuffle the list per resolution, or track per-server RTT and prefer the fastest.

If you want it changed, say so and this becomes a normal work unit with the definition of done below. If it is deliberate, say so and I will close this.

Definition of done, if you want it fixed

  1. Root server selection is randomised per resolution (or RTT-ranked), so no single root receives all first-contact traffic.
  2. Failover behaviour is unchanged: all thirteen are still tried before the resolution fails.
  3. The same treatment is applied deliberately, or deliberately not applied, to the per-domain nameserver call site — and the choice stated.
  4. Tests against live DNS, per repo policy. No mocking.
  5. make check green.
Raised as a question rather than filed as a defect, because it may be a deliberate simplicity choice. @sneak — a one-word answer settles it. ## Observation `internal/resolver/iterative.go` — `rootServerList()` returns the thirteen roots in a fixed order beginning `198.41.0.4` (`a.root-servers.net`), and `queryServers` walks that slice strictly in order, returning on the first success: ```go for _, ip := range servers { resp, err := r.queryDNS(ctx, ip, name, qtype) if err == nil { return resp, nil } lastErr = err } ``` Because the happy path succeeds on the first entry, **every iterative resolution dnswatcher performs begins by querying `a.root-servers.net`**, and the other twelve roots are contacted only when it fails. The remaining twelve are, in practice, a failover list rather than a pool. Two consequences: 1. **Load concentration.** Real resolvers randomise or RTT-rank root selection precisely so load spreads across the anycast constellation. With many targets and a short `DNSWATCHER_DNS_INTERVAL`, this points all root traffic at one operator. 2. **Correlated failure.** If `a.root-servers.net` is slow or unreachable from the host, every single resolution pays that timeout before failing over, rather than one-in-thirteen of them. This surfaced while investigating the test flakiness in https://git.eeqj.de/sneak/dnswatcher/issues/93: with ~35 parallel tests on a 48-core machine, every test fired its first query at the same IP within milliseconds of the others, which fits the observed "a different subset fails each run" signature. That issue was scoped test-side and fixed test-side, so this production behaviour was deliberately left untouched. Note `queryServers` is shared — it is also used for the per-domain nameserver lists, not only the roots — so any change affects both call sites. ## The question Is the fixed order intentional (deterministic, simple, easy to reason about) or incidental? If incidental, the fix is small: shuffle the list per resolution, or track per-server RTT and prefer the fastest. If you want it changed, say so and this becomes a normal work unit with the definition of done below. If it is deliberate, say so and I will close this. ## Definition of done, if you want it fixed 1. Root server selection is randomised per resolution (or RTT-ranked), so no single root receives all first-contact traffic. 2. Failover behaviour is unchanged: all thirteen are still tried before the resolution fails. 3. The same treatment is applied deliberately, or deliberately not applied, to the per-domain nameserver call site — and the choice stated. 4. Tests against live DNS, per repo policy. No mocking. 5. `make check` green.
sneak was assigned by clawbot 2026-08-10 15:17:07 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/dnswatcher#138