Skip to content

Add automatic wildcard DNS detection and filtering via a new -aw/--auto-wildcard flag#947

Open
Aqil-Ahmad wants to merge 2 commits intoprojectdiscovery:devfrom
Aqil-Ahmad:autoWildcard
Open

Add automatic wildcard DNS detection and filtering via a new -aw/--auto-wildcard flag#947
Aqil-Ahmad wants to merge 2 commits intoprojectdiscovery:devfrom
Aqil-Ahmad:autoWildcard

Conversation

@Aqil-Ahmad
Copy link

@Aqil-Ahmad Aqil-Ahmad commented Feb 17, 2026

fixes #924
/claim #924

Proposed changes

Instead of re-implementing wildcard logic, this imports and extends the stable wildcard resolver from
https://github.com/projectdiscovery/shuffledns/tree/dev/pkg/wildcards into pkg/wildcards/.

When enabled:

  • Root domains are auto-detected from input (from -d flag in bruteforce mode, or extracted via public suffix list in -l list mode)
  • Each resolved host is checked inline during resolution — random subdomain queries are used to detect
    wildcard IPs, with caching for performance
  • Wildcard matches are silently filtered from output
  • Works with all output modes (plain, -json, -resp, etc.)
  • Mutually exclusive with the existing -wd flag; not supported in stream mode

Checklist

  • Pull request is created against the dev branch
  • All checks passed (lint, unit/integration/regression tests etc.) with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Summary by CodeRabbit

  • New Features
    • Added an --auto-wildcard (-aw) option to auto-detect and filter wildcard DNS responses, skipping wildcard subdomains during scans.
    • Auto-wildcard is incompatible with existing wildcard-domain options and is disabled in stream mode to avoid conflicts.
    • Wildcard detection now reports filtered counts and persists wildcard data to/from a file for reuse.

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

No actionable comments were generated in the recent review. 🎉


Walkthrough

Auto-wildcard detection was added: a new CLI flag enables runtime discovery of wildcard DNS across multiple domains, a wildcard resolver and store were introduced, and the runner integrates detection to skip/filter wildcard-derived hosts during processing.

Changes

Cohort / File(s) Summary
Dependency Management
go.mod
Moved golang.org/x/net v0.47.0 from indirect to direct requirement.
CLI / Options
internal/runner/options.go
Added AutoWildcard bool and --auto-wildcard (-aw) flag; validated mutual exclusion with wildcard-domain flag and disallowed in stream mode.
Runner Integration
internal/runner/runner.go
Added wildcard resolver fields and atomic filtered counter; collect root domains during input prep; initialize resolver before run; inline worker filtering using resolver; added isAutoWildcard helper and logging of filtered counts.
Wildcard Engine
pkg/wildcards/resolver.go, pkg/wildcards/util.go, pkg/wildcards/store.go
New Resolver with wildcard permutation generation, DNS querying and caching; thread-safe Store with persistence; utility to load resolver IPs from file.
Tests
pkg/wildcards/resolver_test.go
Added unit tests for permutation generation and LookupHost behavior covering wildcard and non-wildcard scenarios.

Sequence Diagram

sequenceDiagram
    actor User
    participant Runner as "Runner\n(runner.go)"
    participant Input as "InputProcessor\n(prepareInput)"
    participant Resolver as "WildcardResolver\n(pkg/wildcards)"
    participant DNS as "DNS Server\n(external)"

    User->>Runner: Start with --auto-wildcard
    Runner->>Input: prepareInput(hosts)
    Input->>Input: collect root domains
    Input-->>Runner: processed hosts + domains

    Runner->>Resolver: NewResolver(domains)
    Resolver->>DNS: query wildcard permutations
    DNS-->>Resolver: responses (IPs)
    Resolver->>Resolver: cache wildcard IPs

    loop per host
        Runner->>Resolver: LookupHost(host, observedIP)
        Resolver-->>Runner: isWildcard (bool), wildcardIPs
        alt isWildcard
            Runner->>Runner: increment wildcardFilteredCount
            Runner-->>DNS: skip further probes for host
        else not wildcard
            Runner->>DNS: continue normal DNS probes
        end
    end

    Runner-->>User: results + wildcard filter stats
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A hop to sniff DNS trails,

wild patterns traced in moonlit veils.
Auto-wild finds the phantom seas,
I prune the hops with nimble ease.
Hooray — fewer weeds, more real leaves!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and specifically describes the main change: adding automatic wildcard DNS detection and filtering via a new flag.
Linked Issues check ✅ Passed The pull request meets all primary objectives from issue #924: auto-detection of wildcards across multiple domains [#924], automatic filtering of wildcard results [#924], and exposure via --auto-wildcard flag [#924].
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the auto-wildcard feature. The new wildcard resolver package, flag addition, runner integration, and supporting utilities are all necessary and in scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
pkg/wildcards/resolver_test.go (1)

36-66: Integration test depends on external DNS infrastructure — will be flaky in CI.

Test_Resolver_LookupHost makes live DNS queries to 8.8.8.8 and depends on campaigns.google.com being a wildcard. If network access is unavailable or Google changes their DNS configuration, this test will fail. Consider gating it behind a build tag (e.g., //go:build integration) or a testing.Short() skip, and adding a mock-based unit test for the core logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/wildcards/resolver_test.go` around lines 36 - 66,
Test_Resolver_LookupHost performs live DNS queries (via NewResolver and
r.client.Lookup) and so is flaky in CI; modify the test to avoid external
dependency by either adding a build tag (e.g., //go:build integration) so it
only runs in integration builds or early-skipping it when testing.Short() is
true, and add a new unit test that mocks Resolver.client.Lookup to exercise
Resolver.LookupHost logic deterministically (create a fake client that returns
controlled IPs and verify LookupHost behavior).
pkg/wildcards/resolver.go (1)

107-118: Using errors.New("found domain") as a control-flow signal is fragile.

If Each ever starts inspecting/logging these errors, this pattern will produce misleading diagnostics. A dedicated sentinel (e.g., var errFound = errors.New("found")) or a break-supporting iteration pattern would be clearer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/wildcards/resolver.go` around lines 107 - 118, The LookupHost function
uses errors.New("found domain") as a control-flow signal inside w.Domains.Each;
replace that ad-hoc error with a package-level sentinel (e.g., var errFound =
errors.New("domain found")) and return errFound from the Each callback when the
domain is matched, then after Each finishes check if the returned error equals
errFound and ignore it (treat as normal early-exit) while propagating any other
real errors; reference Resolver.LookupHost, w.Domains.Each, and the new errFound
sentinel when making the change.
internal/runner/runner.go (3)

499-519: Wildcard resolver is set up with a separate DNSX client — verify this is intentional.

A new DNSX client is created specifically for wildcard resolution rather than reusing r.dnsx. This doubles resolver connections but isolates wildcard probe traffic from main resolution. If intentional, this is fine. If not, NewResolverWithClient(r.autoWildcardDomains, r.dnsx) would be simpler and avoid extra resource usage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/runner/runner.go` around lines 499 - 519, The code creates a
separate DNSX client for wildcard detection (using dnsx.DefaultOptions, dnsx.New
and assigning to wcClient) instead of reusing the existing r.dnsx client; decide
whether this is intentional and either (A) keep it but add a clear comment
explaining why isolation is required (e.g., to separate probe traffic and
retries/timeouts) or (B) reuse the existing resolver by passing r.dnsx into
wildcards.NewResolverWithClient(r.autoWildcardDomains, r.dnsx) and remove the
dnsx.DefaultOptions/dnsx.New block to avoid extra connections; update the code
around r.options.AutoWildcard, r.autoWildcardDomains, NewResolverWithClient and
r.wildcardResolver accordingly.

1032-1043: Auto-wildcard filtering only checks A records — AAAA wildcards will pass through undetected.

isAutoWildcard iterates over dnsData.A only, and the underlying LookupHost in the resolver also only inspects in.A responses. If a host resolves exclusively via AAAA, wildcard filtering won't apply. This matches the forced TypeA addition on line 126, but it's a known limitation worth documenting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/runner/runner.go` around lines 1032 - 1043, isAutoWildcard currently
only inspects IPv4 A records (dnsData.A) so AAAA-only wildcard responses bypass
filtering; update isAutoWildcard to also iterate dnsData.AAAA and call
r.wildcardResolver.LookupHost for each IPv6 address, and if the resolver's
LookupHost or underlying logic only examines in.A responses, update it to also
consider in.AAAA (or provide a separate lookup path) so both IPv4 and IPv6
wildcard detections are applied; reference isAutoWildcard, dnsData.A,
dnsData.AAAA and wildcardResolver.LookupHost when making the changes.

809-815: Inline auto-wildcard check issues a DNS query per IP per host — could amplify request volume.

Each resolved host triggers LookupHost, which may issue random-subdomain DNS probes for each permutation level (until a cache hit or NXDOMAIN). For large input lists, this can significantly increase total DNS traffic beyond what the rate limiter accounts for, since the wildcard resolver uses its own DNSX client. Consider documenting this behavior or accounting for these probes in rate-limit calculations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/runner/runner.go` around lines 809 - 815, The inline auto-wildcard
check currently calls r.isAutoWildcard (which uses LookupHost via the wildcard
resolver's DNSX client) per resolved host/IP, causing extra DNS probes outside
the main rate limiter; update the logic so that wildcard detection is either
memoized per domain (cache results in a map keyed by domain and consult before
calling isAutoWildcard) or re-use the main rate-limited DNS client instead of
the wildcardResolver's separate client, and ensure any DNS calls from
isAutoWildcard are passed through the same rate-limiter used by the runner;
adjust r.isAutoWildcard, r.wildcardResolver usage, and where LookupHost is
invoked to implement caching or shared client so per-IP/stub probes stop
amplifying request volume.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/runner/runner.go`:
- Around line 292-304: The code currently adds every normalized item from sc
into awDomainSet when r.options.AutoWildcard and r.options.Domains are set,
which wrongly includes FUZZ patterns; update the loop in the runner (where
awDomainSet, normalize, sc are used) to skip entries containing the literal
"FUZZ" (or other placeholder tokens used for bruteforce) before inserting into
awDomainSet so only real root domains are collected for auto-wildcard detection;
i.e., check strings.Contains(item, "FUZZ") (or your placeholder list) and
continue the loop if true, otherwise add item to awDomainSet.

In `@pkg/wildcards/resolver_test.go`:
- Line 65: Remove the debug fmt.Printf call that prints the variable wildcards
in the test; either delete the line fmt.Printf("wildcards: %v\n", wildcards) or
replace it with t.Logf("wildcards: %v", wildcards) so the output is only shown
via the testing framework (use the test's *testing.T instance `t`).

In `@pkg/wildcards/resolver.go`:
- Around line 179-188: The code has a TOCTOU race when initializing
wildcardAnswersCache entries: if !cachedValueOk two goroutines can each create a
new cachedValue.IPS and Set it, losing one goroutine's additions; update the
logic around wildcardAnswersCache, cachedValue and IPS to atomically
get-or-create the entry instead of blind Set — e.g., implement a GetOrCreate or
LoadOrStore pattern for wildcardAnswersCache keyed by original, or use a per-key
mutex to ensure only one goroutine initializes cachedValue.IPS (check after
creating whether another goroutine stored a value and merge into that existing
IPS or reuse it), then populate IPS and call Get/Set/GetSyncLockMapValues
(referencing wildcardAnswersCache, cachedValue, IPS, mapsutil.NewSyncLockMap,
Set, Get, and getSyncLockMapValues) so initialization and population are atomic
and no entries are overwritten.
- Around line 199-211: GetAllWildcardIPs is reading value.IPS.Map directly which
bypasses SyncLockMap's concurrency protection and causes a data race; update
Resolver.GetAllWildcardIPs to iterate the thread-safe map via the SyncLockMap
API (e.g., call value.IPS.Iterate or reuse the existing getSyncLockMapValues
helper) instead of accessing the underlying Map field, and collect IPs into ips
using those safe accessors when walking wildcardAnswersCache entries
(referencing wildcardAnswersCache, wildcardAnswerCacheValue, and value.IPS).

In `@pkg/wildcards/store.go`:
- Around line 80-87: The LoadFromFile loop currently ignores scanner.Err(),
allowing read errors to be swallowed and returning partial data; after the for
scanner.Scan() loop in pkg/wildcards/store.go (the code that calls
s.wildcards.Set(item, struct{}{})), call scanner.Err() and if non-nil return
that error (or wrap it for context) so file read failures surface instead of
returning nil.

In `@pkg/wildcards/util.go`:
- Around line 17-26: The loop that builds the servers slice fails to check
scanner.Err() after scanning and doesn't sanitize lines; update the scanner loop
to TrimSpace each scanned line (use strings.TrimSpace), skip if trimmed line is
empty, and only append ":53" when the trimmed line does not already include a
port (e.g., check strings.Contains(trimmed, ":") or use net.SplitHostPort).
After the loop, call scanner.Err() and return that error if non-nil so partial
results aren't silently accepted. Add the "strings" import (and net if you
choose SplitHostPort) and update uses of scanner/Text/servers accordingly.

---

Nitpick comments:
In `@internal/runner/runner.go`:
- Around line 499-519: The code creates a separate DNSX client for wildcard
detection (using dnsx.DefaultOptions, dnsx.New and assigning to wcClient)
instead of reusing the existing r.dnsx client; decide whether this is
intentional and either (A) keep it but add a clear comment explaining why
isolation is required (e.g., to separate probe traffic and retries/timeouts) or
(B) reuse the existing resolver by passing r.dnsx into
wildcards.NewResolverWithClient(r.autoWildcardDomains, r.dnsx) and remove the
dnsx.DefaultOptions/dnsx.New block to avoid extra connections; update the code
around r.options.AutoWildcard, r.autoWildcardDomains, NewResolverWithClient and
r.wildcardResolver accordingly.
- Around line 1032-1043: isAutoWildcard currently only inspects IPv4 A records
(dnsData.A) so AAAA-only wildcard responses bypass filtering; update
isAutoWildcard to also iterate dnsData.AAAA and call
r.wildcardResolver.LookupHost for each IPv6 address, and if the resolver's
LookupHost or underlying logic only examines in.A responses, update it to also
consider in.AAAA (or provide a separate lookup path) so both IPv4 and IPv6
wildcard detections are applied; reference isAutoWildcard, dnsData.A,
dnsData.AAAA and wildcardResolver.LookupHost when making the changes.
- Around line 809-815: The inline auto-wildcard check currently calls
r.isAutoWildcard (which uses LookupHost via the wildcard resolver's DNSX client)
per resolved host/IP, causing extra DNS probes outside the main rate limiter;
update the logic so that wildcard detection is either memoized per domain (cache
results in a map keyed by domain and consult before calling isAutoWildcard) or
re-use the main rate-limited DNS client instead of the wildcardResolver's
separate client, and ensure any DNS calls from isAutoWildcard are passed through
the same rate-limiter used by the runner; adjust r.isAutoWildcard,
r.wildcardResolver usage, and where LookupHost is invoked to implement caching
or shared client so per-IP/stub probes stop amplifying request volume.

In `@pkg/wildcards/resolver_test.go`:
- Around line 36-66: Test_Resolver_LookupHost performs live DNS queries (via
NewResolver and r.client.Lookup) and so is flaky in CI; modify the test to avoid
external dependency by either adding a build tag (e.g., //go:build integration)
so it only runs in integration builds or early-skipping it when testing.Short()
is true, and add a new unit test that mocks Resolver.client.Lookup to exercise
Resolver.LookupHost logic deterministically (create a fake client that returns
controlled IPs and verify LookupHost behavior).

In `@pkg/wildcards/resolver.go`:
- Around line 107-118: The LookupHost function uses errors.New("found domain")
as a control-flow signal inside w.Domains.Each; replace that ad-hoc error with a
package-level sentinel (e.g., var errFound = errors.New("domain found")) and
return errFound from the Each callback when the domain is matched, then after
Each finishes check if the returned error equals errFound and ignore it (treat
as normal early-exit) while propagating any other real errors; reference
Resolver.LookupHost, w.Domains.Each, and the new errFound sentinel when making
the change.

@Aqil-Ahmad
Copy link
Author

FYI, this is the only pr i currently have open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support auto wildcard detection similar to PureDNS

1 participant

Comments