Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ make help # list all targets
| `-force` | `false` | Continue scanning even if wildcard DNS is detected |
| `-no-abort` | `false` | Keep scanning after the 20% resolver failure-rate abort (warning is still emitted) |
| `-auto-rate` | `false` | When more than 20% of the last 200 lookups failed, halve the query rate (from the rate reached so far when `-rate` is 0) and keep going, with a `rate` notice; speed back up by a quarter at a time, to the rate it started from, once a window of lookups is healthy. Only after 6 back-offs, or at 5 queries/s, does the reliability guard abort. The report counts `rate_backoffs` and the `final_rate`. Pairs well with `-retry-inconclusive`, which retries the names that failed while the resolver was overloaded |
| `-max-queries <n>` | `0` | Max candidate names to test (0 = unlimited). Each name sends one query per record type per attempt (`CNAME` costs two) |
| `-max-queries <n>` | `0` | Max candidate names to test (0 = unlimited). Each record type costs one query per attempt, except CNAME (three: A, AAAA and CNAME) |
| `-max-duration <d>` | `0` | Time budget for the whole run, e.g. `20m` (0 = unlimited). Then no new names are tested, lookups already under way finish, and the run ends normally with its results, report (`stopped_by: "duration"`) and a `degraded` verdict. Under `-dL` it covers all targets |
| `-retry-inconclusive` | `false` | After each scan, look up once more (against `-dns-server`, never the `-r` pool) every name that timed out, was refused or failed. An answer replaces the first outcome in the counts; a name that resolves is reported, but not expanded by `-recursive`. Skipped after an abort, an interrupt or once `-max-duration` runs out. The report counts `retried` and `recovered` |
| `-inconclusive <file>` | n/a | Write the names still without an answer (timeout, refused, other) to this file, one per line, sorted, so a later run can retry them with `-w <file>` |
Expand Down
13 changes: 13 additions & 0 deletions internal/dns/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,19 @@ func fqdn(domain string) string {
// dialed (see NewResolver).
var queriesPerType = map[string]int32{"A": 1, "AAAA": 1, "CNAME": 3, "NS": 1, "MX": 1, "TXT": 1}

// QueriesPerName returns the DNS queries reserved for one name across types.
// An empty type list uses DefaultTypes, as Lookup.ResolveTypes does.
func QueriesPerName(types []string) int64 {
if len(types) == 0 {
types = DefaultTypes
}
var total int64
for _, typ := range types {
total += int64(queriesPerType[typ])
}
return total
}

// Lookup is how one name is looked up: the resolver, the per-type timeout,
// how many attempts a retried lookup makes, the record types, and the verbose
// logger. Naming the fields keeps a swapped attempts/types argument from
Expand Down
19 changes: 19 additions & 0 deletions internal/dns/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@ import (
"time"
)

func TestQueriesPerName(t *testing.T) {
for _, tc := range []struct {
name string
types []string
want int64
}{
{"default types", nil, 2},
{"CNAME", []string{"CNAME"}, 3},
{"all types", []string{"A", "AAAA", "CNAME", "NS", "MX", "TXT"}, 8},
{"same types as the resolver receives", []string{"A", "AAAA", "CNAME", "CNAME"}, 8},
} {
t.Run(tc.name, func(t *testing.T) {
if got := QueriesPerName(tc.types); got != tc.want {
t.Errorf("QueriesPerName(%v) = %d, want %d", tc.types, got, tc.want)
}
})
}
}

// resolveDomain reports whether one lookup of domain (DefaultTypes, no
// retry) returns records.
func resolveDomain(ctx context.Context, r *net.Resolver, domain string, timeout time.Duration) bool {
Expand Down
7 changes: 4 additions & 3 deletions main_dryrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ type dryRunTarget struct {
Duplicates int `json:"duplicates"` // wordlist duplicates removed
Skipped int `json:"skipped"` // invalid wordlist entries
Excluded int `json:"excluded"` // candidates out of scope under -exclude
// Queries is one query per candidate and record type; QueriesMax also
// counts every retry -attempts allows. Both respect -max-queries.
// Queries counts the resolver's expected wire queries for each candidate
// and record type; QueriesMax also counts every retry -attempts allows.
// Both respect -max-queries.
Queries int64 `json:"queries"`
QueriesMax int64 `json:"queries_max"`
// Seconds and SecondsMax are the time those queries take at -rate; 0
Expand Down Expand Up @@ -57,7 +58,7 @@ func (s *session) dryRun() int {
if f.maxQueries > 0 {
names = min(names, int64(f.maxQueries))
}
p.Queries = names * int64(len(s.in.recordTypes))
p.Queries = names * dns.QueriesPerName(s.in.recordTypes)
p.QueriesMax = p.Queries * int64(max(s.in.maxAttempts, 1))
if f.rate > 0 {
p.Seconds = float64(p.Queries) / float64(f.rate)
Expand Down
16 changes: 16 additions & 0 deletions main_dryrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ func TestDryRunMaxQueriesCapsEstimate(t *testing.T) {
}
}

func TestDryRunCountsCNAMEQueriesAndRetries(t *testing.T) {
wl := writeFile(t, "wl.txt", "www\napi\n")
code, plans := dryRunPlans(t, "-simulate", "-w", wl, "-type", "A,AAAA,CNAME",
"-attempts", "2", "-rate", "10", "example.com")
if code != exitOK {
t.Fatalf("exit %d, want %d", code, exitOK)
}
if len(plans) != 1 {
t.Fatalf("got %d plans, want 1", len(plans))
}
want := dryRunTarget{Domain: "example.com", Candidates: 2, Queries: 10, QueriesMax: 20, Seconds: 1, SecondsMax: 2, PreflightOK: true}
if plans[0] != want {
t.Errorf("plan = %+v\nwant = %+v", plans[0], want)
}
}

// TestDryRunPreflight: the preflight is a dry run's only DNS traffic, and a
// resolver that fails it fails the run.
func TestDryRunPreflight(t *testing.T) {
Expand Down
Loading