Compare commits
1
Commits
next
...
017eede779
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
017eede779 |
@@ -399,6 +399,10 @@ performs full iterative resolution:
|
|||||||
4. **Authoritative query**: Queries all discovered authoritative nameservers
|
4. **Authoritative query**: Queries all discovered authoritative nameservers
|
||||||
directly for the requested records.
|
directly for the requested records.
|
||||||
|
|
||||||
|
In steps 2 and 3 the servers are asked one at a time in a random order, chosen
|
||||||
|
anew each time, until one replies; the lookup fails only when none does. No one
|
||||||
|
root server gets every first query.
|
||||||
|
|
||||||
This approach ensures:
|
This approach ensures:
|
||||||
|
|
||||||
- Independence from any upstream resolver's cache or filtering.
|
- Independence from any upstream resolver's cache or filtering.
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-01: the resolver tries root servers, and every other server list it
|
||||||
|
walks, in a random order each time, not always from the top (closes #138).
|
||||||
- 2026-10-01: `ResolveIPAddresses` returns an error, not no addresses, when no
|
- 2026-10-01: `ResolveIPAddresses` returns an error, not no addresses, when no
|
||||||
nameserver of the name's zone answered (closes #190).
|
nameserver of the name's zone answered (closes #190).
|
||||||
- 2026-10-01: `make fmt` and `make fmt-check` cover Markdown with prettier, run
|
- 2026-10-01: `make fmt` and `make fmt-check` cover Markdown with prettier, run
|
||||||
@@ -120,5 +122,4 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
- README accuracy sweep: https://git.eeqj.de/sneak/dnswatcher/issues/108
|
- README accuracy sweep: https://git.eeqj.de/sneak/dnswatcher/issues/108
|
||||||
- README sections required by policy:
|
- README sections required by policy:
|
||||||
https://git.eeqj.de/sneak/dnswatcher/issues/173
|
https://git.eeqj.de/sneak/dnswatcher/issues/173
|
||||||
- fixed root server order: https://git.eeqj.de/sneak/dnswatcher/issues/138
|
|
||||||
- review toward 1.0: https://git.eeqj.de/sneak/dnswatcher/issues/144
|
- review toward 1.0: https://git.eeqj.de/sneak/dnswatcher/issues/144
|
||||||
|
|||||||
@@ -26,3 +26,16 @@ func (r *Resolver) QueryEachNS(
|
|||||||
) (map[string]*NameserverResponse, error) {
|
) (map[string]*NameserverResponse, error) {
|
||||||
return r.queryEachNS(ctx, nameservers, hostname)
|
return r.queryEachNS(ctx, nameservers, hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RootServerList exports rootServerList for testing.
|
||||||
|
func RootServerList() []string {
|
||||||
|
return rootServerList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffled exports shuffled for testing.
|
||||||
|
func Shuffled(
|
||||||
|
servers []string,
|
||||||
|
shuffle func(n int, swap func(i, j int)),
|
||||||
|
) []string {
|
||||||
|
return shuffled(servers, shuffle)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -255,6 +257,24 @@ func (r *Resolver) followDelegation(
|
|||||||
return nil, ErrNoNameservers
|
return nil, ErrNoNameservers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shuffled returns a copy of servers in the order shuffle puts them
|
||||||
|
// in. The resolver passes rand.Shuffle, so each time it walks a list of
|
||||||
|
// servers it starts at a random one, and no one server gets every
|
||||||
|
// first query.
|
||||||
|
func shuffled(
|
||||||
|
servers []string,
|
||||||
|
shuffle func(n int, swap func(i, j int)),
|
||||||
|
) []string {
|
||||||
|
order := slices.Clone(servers)
|
||||||
|
shuffle(len(order), func(i, j int) {
|
||||||
|
order[i], order[j] = order[j], order[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
return order
|
||||||
|
}
|
||||||
|
|
||||||
|
// queryServers asks the servers in a random order and returns the
|
||||||
|
// first reply; it fails only when every server has failed.
|
||||||
func (r *Resolver) queryServers(
|
func (r *Resolver) queryServers(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
servers []string,
|
servers []string,
|
||||||
@@ -263,7 +283,7 @@ func (r *Resolver) queryServers(
|
|||||||
) (*dns.Msg, error) {
|
) (*dns.Msg, error) {
|
||||||
var lastErr error
|
var lastErr error
|
||||||
|
|
||||||
for _, ip := range servers {
|
for _, ip := range shuffled(servers, rand.Shuffle) {
|
||||||
if checkCtx(ctx) != nil {
|
if checkCtx(ctx) != nil {
|
||||||
return nil, ErrContextCanceled
|
return nil, ErrContextCanceled
|
||||||
}
|
}
|
||||||
@@ -279,13 +299,15 @@ func (r *Resolver) queryServers(
|
|||||||
return nil, fmt.Errorf("all servers failed: %w", lastErr)
|
return nil, fmt.Errorf("all servers failed: %w", lastErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveNSIPs returns the addresses of one of the nameservers, trying
|
||||||
|
// their names in a random order until one resolves.
|
||||||
func (r *Resolver) resolveNSIPs(
|
func (r *Resolver) resolveNSIPs(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
nsNames []string,
|
nsNames []string,
|
||||||
) []string {
|
) []string {
|
||||||
var ips []string
|
var ips []string
|
||||||
|
|
||||||
for _, ns := range nsNames {
|
for _, ns := range shuffled(nsNames, rand.Shuffle) {
|
||||||
resolved, err := r.resolveARecord(ctx, ns)
|
resolved, err := r.resolveARecord(ctx, ns)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ips = append(ips, resolved...)
|
ips = append(ips, resolved...)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package resolver_test
|
package resolver_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/rand/v2"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -92,3 +94,30 @@ func TestExtractRecordValue_LetterCase(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestShuffled shuffles the root servers with many seeds. Every order
|
||||||
|
// must hold each root server once, so each is tried before a
|
||||||
|
// resolution fails; each root server must come first for some seed, so
|
||||||
|
// no one root server gets every first query; and the list passed in
|
||||||
|
// must be left as it was.
|
||||||
|
func TestShuffled(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const seeds = 1000
|
||||||
|
|
||||||
|
roots := resolver.RootServerList()
|
||||||
|
before := slices.Clone(roots)
|
||||||
|
first := make(map[string]bool)
|
||||||
|
|
||||||
|
for seed := range uint64(seeds) {
|
||||||
|
rng := rand.New(rand.NewPCG(seed, 0)) //nolint:gosec // seeded on purpose
|
||||||
|
order := resolver.Shuffled(roots, rng.Shuffle)
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, roots, order)
|
||||||
|
|
||||||
|
first[order[0]] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, first, len(roots))
|
||||||
|
assert.Equal(t, before, roots)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user