Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3f2a7ab16 |
@@ -407,13 +407,6 @@ it watches. Instead, it 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, so no one root server gets every first query. A server that does
|
|
||||||
not reply, refuses the query, or gives an error reply such as SERVFAIL or a
|
|
||||||
referral that leads no closer to the name is passed over for the next one. When
|
|
||||||
a referral names a zone's nameservers without their addresses, the addresses of
|
|
||||||
all of them are looked up, so that each can be asked.
|
|
||||||
|
|
||||||
This approach ensures:
|
This approach ensures:
|
||||||
|
|
||||||
- Independence from any upstream resolver's cache or filtering.
|
- Independence from any upstream resolver's cache or filtering.
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
- 2026-10-02: a query a server refuses is not resent asking for recursion, and
|
- 2026-10-02: a query a server refuses is not resent asking for recursion, and
|
||||||
every root server refusing is reported as DNS interception (closes #206).
|
every root server refusing is reported as DNS interception (closes #206).
|
||||||
- 2026-10-02: 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-02: a name listed more than once in `DNSWATCHER_TARGETS`, in any
|
- 2026-10-02: a name listed more than once in `DNSWATCHER_TARGETS`, in any
|
||||||
letter case or with a trailing dot, is watched once (closes #207).
|
letter case or with a trailing dot, is watched once (closes #207).
|
||||||
- 2026-10-01: README checked against the code and corrected: metrics, CORS,
|
- 2026-10-01: README checked against the code and corrected: metrics, CORS,
|
||||||
@@ -135,4 +133,5 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
- 1.0 readiness: run it with a real config and read the logs:
|
- 1.0 readiness: run it with a real config and read the logs:
|
||||||
https://git.eeqj.de/sneak/dnswatcher/issues/66
|
https://git.eeqj.de/sneak/dnswatcher/issues/66
|
||||||
|
- 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
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
//
|
//
|
||||||
// 1. Bounded concurrency. Tests run in parallel and the build hosts
|
// 1. Bounded concurrency. Tests run in parallel and the build hosts
|
||||||
// have many cores, so without a limit every test starts its own
|
// have many cores, so without a limit every test starts its own
|
||||||
// iterative resolution at the same instant and they all send their
|
// iterative resolution at the same instant and they all hit the
|
||||||
// first queries to the root servers within a few milliseconds of
|
// first root server within a few milliseconds of each other. Root
|
||||||
// each other. Root servers rate-limit that, which shows up as a
|
// servers rate-limit that, which shows up as a different arbitrary
|
||||||
// different arbitrary subset of tests failing on each run. Run caps
|
// subset of tests failing on each run. Run caps how many live
|
||||||
// how many live operations are in flight at once in one test binary.
|
// operations are in flight at once in one test binary.
|
||||||
//
|
//
|
||||||
// 2. Retry with exponential backoff. Each live operation gets several
|
// 2. Retry with exponential backoff. Each live operation gets several
|
||||||
// attempts with its own timeout. An attempt is retried when it
|
// attempts with its own timeout. An attempt is retried when it
|
||||||
|
|||||||
@@ -47,24 +47,3 @@ func (r *Resolver) QueryEachNS(
|
|||||||
) (map[string]*NameserverResponse, error) {
|
) (map[string]*NameserverResponse, error) {
|
||||||
return r.queryEachNS(ctx, nameservers, hostname)
|
return r.queryEachNS(ctx, nameservers, hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveNSIPs exports resolveNSIPs for testing.
|
|
||||||
func (r *Resolver) ResolveNSIPs(
|
|
||||||
ctx context.Context,
|
|
||||||
nsNames []string,
|
|
||||||
) []string {
|
|
||||||
return r.resolveNSIPs(ctx, nsNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,9 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand/v2"
|
|
||||||
"net"
|
"net"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -247,26 +245,10 @@ func (r *Resolver) followDelegation(
|
|||||||
return nil, ErrNoNameservers
|
return nil, ErrNoNameservers
|
||||||
}
|
}
|
||||||
|
|
||||||
// shuffled returns a copy of servers in the order shuffle puts them
|
// queryServers asks servers, the servers of zone, about name until one
|
||||||
// in. The resolver passes rand.Shuffle, so each time it walks a list of
|
// gives a usable reply. A server that times out, refuses or gives a
|
||||||
// servers it starts at a random one, and no one server gets every
|
// reply that is not usable is passed over for the next. When every
|
||||||
// first query.
|
// server refused, the error says so, and when they are the root
|
||||||
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 servers, the servers of zone, about name in a random
|
|
||||||
// order until one gives a usable reply. A server that times out, refuses
|
|
||||||
// or gives a reply that is not usable is passed over for the next. When
|
|
||||||
// every server refused, the error says so, and when they are the root
|
|
||||||
// servers it is ErrIntercepted.
|
// servers it is ErrIntercepted.
|
||||||
func (r *Resolver) queryServers(
|
func (r *Resolver) queryServers(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@@ -279,7 +261,7 @@ func (r *Resolver) queryServers(
|
|||||||
|
|
||||||
refused := 0
|
refused := 0
|
||||||
|
|
||||||
for _, ip := range shuffled(servers, rand.Shuffle) {
|
for _, ip := range servers {
|
||||||
if checkCtx(ctx) != nil {
|
if checkCtx(ctx) != nil {
|
||||||
return nil, ErrContextCanceled
|
return nil, ErrContextCanceled
|
||||||
}
|
}
|
||||||
@@ -366,10 +348,6 @@ func nsSetFrom(resp *dns.Msg, domain string) []string {
|
|||||||
return extractNSSet(resp.Answer)
|
return extractNSSet(resp.Answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveNSIPs returns the addresses of every nameserver in nsNames
|
|
||||||
// whose name resolves, for a referral that carries none. The walk can
|
|
||||||
// then go on to the zone's other nameservers when one gives no usable
|
|
||||||
// reply.
|
|
||||||
func (r *Resolver) resolveNSIPs(
|
func (r *Resolver) resolveNSIPs(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
nsNames []string,
|
nsNames []string,
|
||||||
@@ -381,6 +359,10 @@ func (r *Resolver) resolveNSIPs(
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
ips = append(ips, resolved...)
|
ips = append(ips, resolved...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(ips) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ips
|
return ips
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package resolver_test
|
package resolver_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand/v2"
|
|
||||||
"slices"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -237,30 +235,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -383,37 +383,3 @@ func liveResolveIPsAllowingEmpty(
|
|||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// liveResolveNSIPs looks up the addresses of the nameservers named
|
|
||||||
// names, retrying until there are at least atLeast of them: a name
|
|
||||||
// whose lookup got no reply is left out of the result, not an error.
|
|
||||||
func liveResolveNSIPs(
|
|
||||||
t *testing.T,
|
|
||||||
r *resolver.Resolver,
|
|
||||||
names []string,
|
|
||||||
atLeast int,
|
|
||||||
) []string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var out []string
|
|
||||||
|
|
||||||
livednstest.Retry(
|
|
||||||
t,
|
|
||||||
"ResolveNSIPs("+strings.Join(names, ", ")+")",
|
|
||||||
func(ctx context.Context) error {
|
|
||||||
ips := r.ResolveNSIPs(ctx, names)
|
|
||||||
if len(ips) < atLeast {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%w: %d addresses, expected at least %d",
|
|
||||||
livednstest.ErrNoAnswer, len(ips), atLeast,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
out = ips
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -140,28 +140,6 @@ func TestFindAuthoritativeNameservers_CloudflareDomain(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestResolveNSIPs_EveryNameserver looks up the addresses of two of
|
|
||||||
// google.com's nameservers together, as the walk does when a referral
|
|
||||||
// names a zone's nameservers without their addresses, and compares them
|
|
||||||
// with each looked up alone. Together they must give the addresses of
|
|
||||||
// both, not only of the first that resolves, so that when one gives no
|
|
||||||
// usable reply the walk goes on to the other.
|
|
||||||
func TestResolveNSIPs_EveryNameserver(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
r := newTestResolver(t)
|
|
||||||
names := []string{"ns3.google.com.", "ns4.google.com."}
|
|
||||||
want := make([]string, 0, len(names))
|
|
||||||
|
|
||||||
for _, name := range names {
|
|
||||||
want = append(want, liveResolveNSIPs(t, r, []string{name}, 1)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := liveResolveNSIPs(t, r, names, len(want))
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, want, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// QueryNameserver tests
|
// QueryNameserver tests
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user