From a5f42ddfa6be4994fe24af28e89408c2fe9273ab Mon Sep 17 00:00:00 2001 From: vmfunc Date: Wed, 10 Jun 2026 15:21:24 -0700 Subject: [PATCH] feat(dnslist): async dns resolution with wildcard filtering dnslist previously http-probed every wordlist candidate through the blocking os resolver, so a big list meant a request per dead name and a wildcard zone flooded results. resolve each candidate first via a new internal/dnsx (retryabledns over a bundled 1.1.1.1/8.8.8.8/9.9.9.9 pool, promoted to a direct dep), fingerprint the apex with random nonexistent labels to detect catch-all zones, and http-probe only the names that actually resolve and aren't wildcard. add -resolvers to override the pool. resolverFn is a package-level seam so the dnsx tests stay hermetic; the dnslist newDNSResolver seam keeps the integration test network-free. --- go.mod | 2 +- internal/config/config.go | 2 + internal/dnsx/dnsx.go | 270 ++++++++++++++++++++++++++++++ internal/dnsx/dnsx_test.go | 176 +++++++++++++++++++ internal/scan/dnslist.go | 53 +++++- internal/scan/integration_test.go | 16 +- sif.go | 3 +- 7 files changed, 516 insertions(+), 6 deletions(-) create mode 100644 internal/dnsx/dnsx.go create mode 100644 internal/dnsx/dnsx_test.go diff --git a/go.mod b/go.mod index 35bb982..14b15d1 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/likexian/whois v1.15.7 github.com/projectdiscovery/goflags v0.1.74 github.com/projectdiscovery/nuclei/v3 v3.8.0 + github.com/projectdiscovery/retryabledns v1.0.114 github.com/projectdiscovery/utils v0.10.1 github.com/rocketlaunchr/google-search v1.1.6 golang.org/x/net v0.53.0 @@ -288,7 +289,6 @@ require ( github.com/projectdiscovery/ratelimit v0.0.85 // indirect github.com/projectdiscovery/rawhttp v0.1.90 // indirect github.com/projectdiscovery/rdap v0.9.1-0.20221108103045-9865884d1917 // indirect - github.com/projectdiscovery/retryabledns v1.0.114 // indirect github.com/projectdiscovery/retryablehttp-go v1.3.8 // indirect github.com/projectdiscovery/sarif v0.0.1 // indirect github.com/projectdiscovery/tlsx v1.2.2 // indirect diff --git a/internal/config/config.go b/internal/config/config.go index 7eeea8d..d9742b7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,6 +30,7 @@ type Settings struct { DirWordlist string // -w dirlist: custom wordlist (file path or url) DirExtensions string // -e dirlist: extensions appended to each word Dnslist string + Resolvers string // -resolvers dnslist: comma list overriding the bundled pool Debug bool LogDir string NoScan bool @@ -120,6 +121,7 @@ func Parse() *Settings { flagSet.StringVar(&settings.DirWordlist, "w", "", "Dirlist: custom wordlist (local file path or url; overrides -dirlist size)"), flagSet.StringVar(&settings.DirExtensions, "e", "", "Dirlist: extensions appended to each word (comma list, e.g. php,bak,env)"), flagSet.EnumVar(&settings.Dnslist, "dnslist", Nil, "DNS fuzzing scan size (small/medium/large)", listSizes), + flagSet.StringVar(&settings.Resolvers, "resolvers", "", "Dnslist: DNS resolvers to use (comma list, e.g. 1.1.1.1,8.8.8.8; overrides the bundled pool)"), flagSet.EnumVar(&settings.Ports, "ports", Nil, "Port scanning scope (common/full)", portScopes), flagSet.BoolVar(&settings.Dorking, "dork", false, "Enable Google dorking"), flagSet.BoolVar(&settings.Git, "git", false, "Enable git repository scanning"), diff --git a/internal/dnsx/dnsx.go b/internal/dnsx/dnsx.go new file mode 100644 index 0000000..c3c3ae7 --- /dev/null +++ b/internal/dnsx/dnsx.go @@ -0,0 +1,270 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +// Package dnsx resolves subdomain candidates against a bundled resolver pool +// before anything is probed over http, so the slow/inaccurate path of HTTP-ing +// every wordlist entry through the OS resolver is gone. it also fingerprints +// wildcard zones (a zone that answers every random label) so a catch-all +// nameserver can't flood the caller with phantom subdomains. +package dnsx + +import ( + "crypto/rand" + "fmt" + "math/big" + "sort" + "strings" + + retryabledns "github.com/projectdiscovery/retryabledns" +) + +// bundled default resolver pool. anycast cloudflare/google/quad9 - fast, public, +// and unlikely to rate-limit a recon sweep. -resolvers overrides this set. +const ( + resolverCloudflare = "1.1.1.1:53" + resolverGoogle = "8.8.8.8:53" + resolverQuad9 = "9.9.9.9:53" +) + +// defaultResolvers is the bundled pool used when the caller passes none. +var defaultResolvers = []string{resolverCloudflare, resolverGoogle, resolverQuad9} + +const ( + // defaultRetries is how many times retryabledns rotates through the pool on a + // timeout before giving up on a name. low enough to stay fast on a big list. + defaultRetries = 3 + + // wildcardProbes is how many random nonexistent labels we resolve to + // fingerprint a wildcard zone. more samples make a rotating catch-all (one + // that hands back a different ip per query) harder to miss, but each is a + // real lookup so this stays small. + wildcardProbes = 3 + + // randomLabelLen is the length of each random wildcard-probe label. long + // enough that a collision with a real host is astronomically unlikely. + randomLabelLen = 16 +) + +// randomLabelAlphabet is the lowercase-alnum set wildcard probe labels draw +// from; a valid dns label so the query isn't rejected before it leaves. +const randomLabelAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789" + +// defaultDNSPort is appended to any resolver entry given without an explicit +// port, so "1.1.1.1" and "1.1.1.1:53" both work on the cli. +const defaultDNSPort = "53" + +// ParseResolvers splits a comma list of resolvers into a normalized slice, +// appending the default port to bare ips/hosts. an empty or blank input returns +// nil so the caller falls back to the bundled pool. +func ParseResolvers(raw string) []string { + if strings.TrimSpace(raw) == "" { + return nil + } + parts := strings.Split(raw, ",") + out := make([]string, 0, len(parts)) + for i := 0; i < len(parts); i++ { + entry := strings.TrimSpace(parts[i]) + if entry == "" { + continue + } + // a bare ip/host gets the default port; an entry already carrying ":port" + // (or a bracketed ipv6 literal) is left as-is. + if !strings.Contains(entry, ":") { + entry += ":" + defaultDNSPort + } + out = append(out, entry) + } + + return out +} + +// resolution is the resolved address set for one host. empty Addrs means the +// name did not resolve (nxdomain / no records). +type resolution struct { + Addrs []string +} + +// resolved reports whether the name returned any address. +func (r resolution) resolved() bool { + return len(r.Addrs) > 0 +} + +// resolverFn is the test seam: every lookup the package makes goes through this +// var, so a fake can answer without touching the network. real runs point it at +// a retryabledns-backed client via NewResolver. +var resolverFn func(host string) (resolution, error) + +// Resolver resolves candidates against a pool and filters wildcard answers. it +// is built once per scan and shared across the worker goroutines; the +// underlying retryabledns client is safe for concurrent use. +type Resolver struct { + // wildcardSigs holds the address sets a wildcard zone answers random labels + // with. nil/empty means the zone is not wildcard. a candidate whose answer is + // covered by one of these is a catch-all hit, not a real host. + wildcardSigs []map[string]struct{} +} + +// NewResolver wires resolverFn to a retryabledns client over the given pool +// (bundled default when resolvers is empty) and returns a Resolver. it does not +// fingerprint anything yet - call FingerprintWildcard with the apex first. +func NewResolver(resolvers []string) (*Resolver, error) { + pool := resolvers + if len(pool) == 0 { + pool = defaultResolvers + } + + client, err := retryabledns.New(pool, defaultRetries) + if err != nil { + return nil, fmt.Errorf("dnsx: build resolver over %v: %w", pool, err) + } + + // only install the real client when a test hasn't already injected a fake; + // the seam wins so hermetic tests never reach this client. + if resolverFn == nil { + resolverFn = func(host string) (resolution, error) { + data, err := client.Resolve(host) + if err != nil { + return resolution{}, fmt.Errorf("dnsx: resolve %q: %w", host, err) + } + return resolution{Addrs: mergeAddrs(data)}, nil + } + } + + return &Resolver{}, nil +} + +// FingerprintWildcard resolves wildcardProbes random labels under apex. any that +// answer mean the zone is a catch-all, so their address sets are recorded as +// signatures to filter real candidates against later. a clean zone leaves the +// signature list empty and nothing gets filtered. +func (r *Resolver) FingerprintWildcard(apex string) error { + apex = strings.TrimSuffix(apex, ".") + for i := 0; i < wildcardProbes; i++ { + label, err := randomLabel(randomLabelLen) + if err != nil { + return fmt.Errorf("dnsx: wildcard probe label: %w", err) + } + + res, err := resolverFn(label + "." + apex) + if err != nil { + // a probe failure (timeout / nxdomain surfaced as error) just means this + // sample says "not wildcard"; don't abort the whole fingerprint on it. + continue + } + if res.resolved() { + r.wildcardSigs = append(r.wildcardSigs, toSet(res.Addrs)) + } + } + + return nil +} + +// Resolve looks up host and reports whether it is a real, non-wildcard hit. a +// name that doesn't resolve, or whose answer matches a recorded wildcard +// signature, returns false so the caller skips probing it. +func (r *Resolver) Resolve(host string) (bool, error) { + res, err := resolverFn(host) + if err != nil { + return false, fmt.Errorf("dnsx: resolve %q: %w", host, err) + } + if !res.resolved() { + return false, nil + } + if r.isWildcard(res.Addrs) { + return false, nil + } + + return true, nil +} + +// isWildcard reports whether addrs is covered by any recorded wildcard +// signature. a candidate whose every address appears in a wildcard answer is a +// catch-all hit; a host with even one address outside the signature is a real, +// distinct record and survives. +func (r *Resolver) isWildcard(addrs []string) bool { + if len(r.wildcardSigs) == 0 { + return false + } + for i := 0; i < len(r.wildcardSigs); i++ { + if subset(addrs, r.wildcardSigs[i]) { + return true + } + } + + return false +} + +// mergeAddrs flattens the A and AAAA answers into one sorted, deduped slice so +// two equal answers compare equal regardless of record ordering. +func mergeAddrs(data *retryabledns.DNSData) []string { + if data == nil { + return nil + } + seen := make(map[string]struct{}, len(data.A)+len(data.AAAA)) + for i := 0; i < len(data.A); i++ { + seen[data.A[i]] = struct{}{} + } + for i := 0; i < len(data.AAAA); i++ { + seen[data.AAAA[i]] = struct{}{} + } + + addrs := make([]string, 0, len(seen)) + for addr := range seen { + addrs = append(addrs, addr) + } + sort.Strings(addrs) + + return addrs +} + +// toSet turns addrs into a lookup set for subset checks. +func toSet(addrs []string) map[string]struct{} { + set := make(map[string]struct{}, len(addrs)) + for i := 0; i < len(addrs); i++ { + set[addrs[i]] = struct{}{} + } + + return set +} + +// subset reports whether every addr is present in sig (and addrs is non-empty); +// an empty addrs can't be a wildcard match. +func subset(addrs []string, sig map[string]struct{}) bool { + if len(addrs) == 0 { + return false + } + for i := 0; i < len(addrs); i++ { + if _, ok := sig[addrs[i]]; !ok { + return false + } + } + + return true +} + +// randomLabel returns a cryptographically-random lowercase-alnum dns label of +// length n. crypto/rand (not math/rand) so a target can't predict the probe +// labels and special-case them to defeat wildcard detection. +func randomLabel(n int) (string, error) { + var b strings.Builder + b.Grow(n) + alphabetLen := big.NewInt(int64(len(randomLabelAlphabet))) + for i := 0; i < n; i++ { + idx, err := rand.Int(rand.Reader, alphabetLen) + if err != nil { + return "", fmt.Errorf("dnsx: random index: %w", err) + } + b.WriteByte(randomLabelAlphabet[idx.Int64()]) + } + + return b.String(), nil +} diff --git a/internal/dnsx/dnsx_test.go b/internal/dnsx/dnsx_test.go new file mode 100644 index 0000000..f104f13 --- /dev/null +++ b/internal/dnsx/dnsx_test.go @@ -0,0 +1,176 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package dnsx + +import ( + "reflect" + "strings" + "testing" +) + +// withFakeResolver swaps resolverFn for fn for the duration of one test, then +// restores it - the seam that keeps every case below network-free. +func withFakeResolver(t *testing.T, fn func(host string) (resolution, error)) { + t.Helper() + orig := resolverFn + resolverFn = fn + t.Cleanup(func() { resolverFn = orig }) +} + +// newFingerprinted builds a Resolver and runs the wildcard fingerprint against +// apex using the already-injected fake; fatal on error. +func newFingerprinted(t *testing.T, apex string) *Resolver { + t.Helper() + r := &Resolver{} + if err := r.FingerprintWildcard(apex); err != nil { + t.Fatalf("FingerprintWildcard: %v", err) + } + + return r +} + +const testApex = "example.com" + +// a host that resolves to a real address, in a clean (non-wildcard) zone, is a +// genuine hit. +func TestResolve_FoundInCleanZone(t *testing.T) { + withFakeResolver(t, func(host string) (resolution, error) { + // nothing answers a random wildcard probe -> clean zone. + if strings.HasSuffix(host, "."+testApex) && host != "www."+testApex { + return resolution{}, nil + } + if host == "www."+testApex { + return resolution{Addrs: []string{"93.184.216.34"}}, nil + } + return resolution{}, nil + }) + + r := newFingerprinted(t, testApex) + if len(r.wildcardSigs) != 0 { + t.Fatalf("clean zone should record no wildcard signatures, got %d", len(r.wildcardSigs)) + } + + ok, err := r.Resolve("www." + testApex) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if !ok { + t.Error("a resolving host in a clean zone should be a hit") + } +} + +// nxdomain (no addresses) is not a hit, so the caller skips probing it. +func TestResolve_NxdomainSkipped(t *testing.T) { + withFakeResolver(t, func(string) (resolution, error) { + // every name, probes included, returns no records. + return resolution{}, nil + }) + + r := newFingerprinted(t, testApex) + + ok, err := r.Resolve("ghost." + testApex) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if ok { + t.Error("an nxdomain host must not count as found") + } +} + +// a wildcard zone answers the random probe labels, so a candidate that resolves +// to the same catch-all address is filtered out. +func TestResolve_WildcardFiltered(t *testing.T) { + const catchAll = "10.0.0.1" + withFakeResolver(t, func(string) (resolution, error) { + // the zone answers everything - probes and candidates alike - with one ip. + return resolution{Addrs: []string{catchAll}}, nil + }) + + r := newFingerprinted(t, testApex) + if len(r.wildcardSigs) == 0 { + t.Fatal("wildcard zone should record at least one signature") + } + + ok, err := r.Resolve("anything." + testApex) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if ok { + t.Error("a candidate matching the wildcard answer must be filtered") + } +} + +// a real host in a wildcard zone that resolves to a distinct address (not the +// catch-all) still survives the filter - one address outside the signature is +// enough to be a genuine record. +func TestResolve_DistinctHostSurvivesWildcard(t *testing.T) { + const catchAll = "10.0.0.1" + const realHost = "api." + testApex + withFakeResolver(t, func(host string) (resolution, error) { + if host == realHost { + return resolution{Addrs: []string{"203.0.113.7"}}, nil + } + // everything else (probes + other candidates) hits the catch-all. + return resolution{Addrs: []string{catchAll}}, nil + }) + + r := newFingerprinted(t, testApex) + if len(r.wildcardSigs) == 0 { + t.Fatal("wildcard zone should record at least one signature") + } + + ok, err := r.Resolve(realHost) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if !ok { + t.Error("a host resolving to a distinct address should survive the wildcard filter") + } +} + +func TestParseResolvers(t *testing.T) { + tests := []struct { + name string + in string + want []string + }{ + {"empty falls back to bundled", "", nil}, + {"blank falls back to bundled", " ", nil}, + {"bare ips get default port", "1.1.1.1,8.8.8.8", []string{"1.1.1.1:53", "8.8.8.8:53"}}, + {"explicit port preserved", "9.9.9.9:5353", []string{"9.9.9.9:5353"}}, + {"whitespace and empties trimmed", " 1.1.1.1 , ,8.8.8.8 ", []string{"1.1.1.1:53", "8.8.8.8:53"}}, + {"mixed bare and ported", "1.1.1.1,9.9.9.9:5353", []string{"1.1.1.1:53", "9.9.9.9:5353"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParseResolvers(tt.in); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseResolvers(%q) = %v, want %v", tt.in, got, tt.want) + } + }) + } +} + +func TestNewResolver_DefaultsToBundledPool(t *testing.T) { + // keep the seam already installed so New doesn't replace it with a real + // client; we only assert the constructor accepts an empty override. + withFakeResolver(t, func(string) (resolution, error) { return resolution{}, nil }) + + r, err := NewResolver(nil) + if err != nil { + t.Fatalf("NewResolver(nil): %v", err) + } + if r == nil { + t.Fatal("NewResolver returned nil resolver") + } +} diff --git a/internal/scan/dnslist.go b/internal/scan/dnslist.go index 2f88a6d..ba1e1ab 100644 --- a/internal/scan/dnslist.go +++ b/internal/scan/dnslist.go @@ -21,6 +21,7 @@ import ( "time" charmlog "github.com/charmbracelet/log" + "github.com/dropalldatabases/sif/internal/dnsx" "github.com/dropalldatabases/sif/internal/httpx" "github.com/dropalldatabases/sif/internal/logger" "github.com/dropalldatabases/sif/internal/output" @@ -33,6 +34,27 @@ var dnsURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/mai // local server instead of resolving real DNS. nil keeps http.DefaultTransport. var dnsTransport http.RoundTripper +// hostResolver is the small slice of dnsx the dnslist worker needs: resolve a +// candidate and report whether it's a real, non-wildcard hit. +type hostResolver interface { + Resolve(host string) (bool, error) +} + +// newDNSResolver builds the resolver for one run; it's a var so integration +// tests inject a fake that answers without touching real dns. the apex is +// fingerprinted for wildcards before any candidate is checked. +var newDNSResolver = func(apex string, resolvers []string) (hostResolver, error) { + r, err := dnsx.NewResolver(resolvers) + if err != nil { + return nil, fmt.Errorf("dns resolver: %w", err) + } + if err := r.FingerprintWildcard(apex); err != nil { + return nil, fmt.Errorf("wildcard fingerprint: %w", err) + } + + return r, nil +} + const ( dnsSmallFile = "subdomains-100.txt" dnsMediumFile = "subdomains-1000.txt" @@ -56,8 +78,11 @@ func meaningfulStatus(code int) bool { return code >= http.StatusOK && code < http.StatusMultipleChoices } -// Dnslist performs DNS subdomain enumeration on the target domain. -func Dnslist(size string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) { +// Dnslist performs DNS subdomain enumeration on the target domain. each +// candidate is resolved first; only names that actually resolve (and aren't a +// wildcard catch-all) are http-probed, so a big wordlist no longer means a +// http request per dead name. +func Dnslist(size string, url string, timeout time.Duration, threads int, logdir string, resolvers []string) ([]string, error) { log := output.Module("DNS") log.Start() @@ -92,6 +117,15 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir sanitizedURL := stripScheme(url) + // resolve against dns first, fingerprinting the apex for wildcards so a + // catch-all zone can't flood the probe step. build it once and share across + // the workers - the underlying client is concurrency-safe. + resolver, err := newDNSResolver(sanitizedURL, resolvers) + if err != nil { + log.Error("Error building DNS resolver: %s", err) + return nil, err + } + if logdir != "" { if err := logger.WriteHeader(sanitizedURL, logdir, size+" subdomain fuzzing"); err != nil { log.Error("Error creating log file: %v", err) @@ -132,10 +166,23 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir charmlog.Debugf("Looking up: %s", domain) + host := domain + "." + sanitizedURL + + // dns gate: skip the http probe entirely for names that don't + // resolve or that a wildcard zone answers. this is the whole point - + // no request per dead candidate. + ok, err := resolver.Resolve(host) + if err != nil { + charmlog.Debugf("resolve %s: %s", host, err) + continue + } + if !ok { + continue + } + // probe http first, then https - but a subdomain is recorded at // most once. firing both schemes and appending on each is what // double-counted every host on the old path. - host := domain + "." + sanitizedURL foundURL, scheme := probeSubdomain(client, host) if foundURL == "" { continue diff --git a/internal/scan/integration_test.go b/internal/scan/integration_test.go index 93f46b8..be168c0 100644 --- a/internal/scan/integration_test.go +++ b/internal/scan/integration_test.go @@ -424,7 +424,15 @@ func TestIntegrationDnslist(t *testing.T) { } defer func() { dnsTransport = origTr }() - found, err := Dnslist("small", "http://example.com", 5*time.Second, 2, "") + // inject a fake resolver so the run never touches real dns: every candidate + // resolves, nothing is wildcard, so all wordlist names reach the probe step. + origResolver := newDNSResolver + newDNSResolver = func(_ string, _ []string) (hostResolver, error) { + return resolveAllStub{}, nil + } + defer func() { newDNSResolver = origResolver }() + + found, err := Dnslist("small", "http://example.com", 5*time.Second, 2, "", nil) if err != nil { t.Fatalf("Dnslist: %v", err) } @@ -435,6 +443,12 @@ func TestIntegrationDnslist(t *testing.T) { } } +// resolveAllStub answers every host as a real, non-wildcard hit so the dns gate +// is a pass-through and the probe step gets the full wordlist. +type resolveAllStub struct{} + +func (resolveAllStub) Resolve(string) (bool, error) { return true, nil } + func contains(s []string, v string) bool { for i := 0; i < len(s); i++ { if s[i] == v { diff --git a/sif.go b/sif.go index 8bdbf9c..4be3e97 100644 --- a/sif.go +++ b/sif.go @@ -25,6 +25,7 @@ import ( "github.com/charmbracelet/log" "github.com/dropalldatabases/sif/internal/config" + "github.com/dropalldatabases/sif/internal/dnsx" "github.com/dropalldatabases/sif/internal/finding" "github.com/dropalldatabases/sif/internal/httpx" "github.com/dropalldatabases/sif/internal/logger" @@ -270,7 +271,7 @@ func (app *App) Run() error { var dnsResults []string if app.settings.Dnslist != "none" { - result, err := scan.Dnslist(app.settings.Dnslist, url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir) + result, err := scan.Dnslist(app.settings.Dnslist, url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir, dnsx.ParseResolvers(app.settings.Resolvers)) if err != nil { log.Errorf("Error while running dns scan: %s", err) } else {