-
Notifications
You must be signed in to change notification settings - Fork 310
feat: add auto wildcard detection and filtering #948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,7 @@ type Runner struct { | |
| wildcards *mapsutil.SyncLockMap[string, struct{}] | ||
| wildcardscache map[string][]string | ||
| wildcardscachemutex sync.Mutex | ||
| autoWildcardDomains *mapsutil.SyncLockMap[string, bool] | ||
| limiter *ratelimit.Limiter | ||
| hm *hybrid.HybridMap | ||
| stats clistats.StatisticsClient | ||
|
|
@@ -159,6 +160,7 @@ func New(options *Options) (*Runner, error) { | |
| wildcardworkerchan: make(chan string), | ||
| wildcards: mapsutil.NewSyncLockMap[string, struct{}](), | ||
| wildcardscache: make(map[string][]string), | ||
| autoWildcardDomains: mapsutil.NewSyncLockMap[string, bool](), | ||
| limiter: limiter, | ||
| hm: hm, | ||
| stats: stats, | ||
|
|
@@ -548,6 +550,11 @@ func (r *Runner) run() error { | |
| gologger.Print().Msgf("%d wildcard subdomains removed\n", numRemovedSubdomains) | ||
| } | ||
|
|
||
| if r.options.AutoWildcard { | ||
| gologger.Print().Msgf("Starting auto wildcard detection and filtering\n") | ||
| r.runAutoWildcardFiltering() | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
|
|
@@ -572,6 +579,91 @@ func (r *Runner) lookupAndOutput(host string) error { | |
| return nil | ||
| } | ||
|
|
||
| func (r *Runner) runAutoWildcardFiltering() { | ||
| ipDomain := make(map[string]map[string]struct{}) | ||
| domainHosts := make(map[string][]string) | ||
| listIPs := []string{} | ||
|
|
||
| // Scan all stored DNS data and group by IP and domain | ||
| r.hm.Scan(func(k, v []byte) error { | ||
| var dnsdata retryabledns.DNSData | ||
| if err := json.Unmarshal(v, &dnsdata); err != nil { | ||
| return nil | ||
| } | ||
|
|
||
| host := string(k) | ||
| baseDomain := r.GetBaseDomain(host) | ||
|
|
||
| // Track hosts per base domain | ||
| if _, ok := domainHosts[baseDomain]; !ok { | ||
| domainHosts[baseDomain] = make([]string, 0) | ||
| } | ||
| domainHosts[baseDomain] = append(domainHosts[baseDomain], host) | ||
|
|
||
| for _, a := range dnsdata.A { | ||
| _, ok := ipDomain[a] | ||
| if !ok { | ||
| ipDomain[a] = make(map[string]struct{}) | ||
| listIPs = append(listIPs, a) | ||
| } | ||
| ipDomain[a][host] = struct{}{} | ||
| } | ||
|
|
||
| return nil | ||
| }) | ||
|
|
||
| gologger.Debug().Msgf("Found %d unique IPs across %d domains\n", len(listIPs), len(domainHosts)) | ||
|
|
||
| // Start wildcard workers for auto detection | ||
| numThreads := r.options.Threads | ||
| if numThreads > len(listIPs) { | ||
| numThreads = len(listIPs) | ||
| } | ||
| for i := 0; i < numThreads; i++ { | ||
| r.wgwildcardworker.Add(1) | ||
| go r.wildcardWorkerAuto() | ||
| } | ||
|
|
||
| // Send hosts to wildcard worker for checking | ||
| seen := make(map[string]struct{}) | ||
| for _, hosts := range domainHosts { | ||
| for _, host := range hosts { | ||
| if _, ok := seen[host]; !ok { | ||
| seen[host] = struct{}{} | ||
| r.wildcardworkerchan <- host | ||
| } | ||
| } | ||
| } | ||
| close(r.wildcardworkerchan) | ||
| r.wgwildcardworker.Wait() | ||
|
|
||
| // Restart output worker | ||
| r.startOutputWorker() | ||
|
|
||
| // Output non-wildcard results | ||
| seen = make(map[string]struct{}) | ||
| seenRemovedSubdomains := make(map[string]struct{}) | ||
| numRemovedSubdomains := 0 | ||
|
|
||
| for _, hosts := range domainHosts { | ||
| for _, host := range hosts { | ||
| if _, ok := seen[host]; !ok { | ||
| seen[host] = struct{}{} | ||
| if !r.wildcards.Has(host) { | ||
| _ = r.lookupAndOutput(host) | ||
| } else { | ||
| numRemovedSubdomains++ | ||
| seenRemovedSubdomains[host] = struct{}{} | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| close(r.outputchan) | ||
| r.wgoutputworker.Wait() | ||
| gologger.Print().Msgf("%d wildcard subdomains removed\n", numRemovedSubdomains) | ||
| } | ||
|
|
||
| func (r *Runner) runStream() error { | ||
| r.startWorkers() | ||
|
|
||
|
|
@@ -731,7 +823,7 @@ func (r *Runner) worker() { | |
| } | ||
| } | ||
| // if wildcard filtering just store the data | ||
| if r.options.WildcardDomain != "" { | ||
| if r.options.WildcardDomain != "" || r.options.AutoWildcard { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A-record query type is not enforced when Line 119 ensures A records are queried when 🐛 Proposed fix (in the `New` function around line 119)- if len(questionTypes) == 0 || options.WildcardDomain != "" {
+ if len(questionTypes) == 0 || options.WildcardDomain != "" || options.AutoWildcard {
options.A = true
questionTypes = append(questionTypes, dns.TypeA)
}🤖 Prompt for AI Agents |
||
| if err := r.storeDNSData(dnsData.DNSData); err != nil { | ||
| gologger.Debug().Msgf("Failed to store DNS data for %s: %v\n", domain, err) | ||
| } | ||
|
|
@@ -945,3 +1037,18 @@ func (r *Runner) wildcardWorker() { | |
| } | ||
| } | ||
| } | ||
|
|
||
| func (r *Runner) wildcardWorkerAuto() { | ||
| defer r.wgwildcardworker.Done() | ||
|
|
||
| for { | ||
| host, more := <-r.wildcardworkerchan | ||
| if !more { | ||
| break | ||
| } | ||
| if r.IsWildcardAuto(host) { | ||
| // mark this host as a wildcard subdomain | ||
| _ = r.wildcards.Set(host, struct{}{}) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,6 +4,7 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/rs/xid" | ||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/weppos/publicsuffix-go/publicsuffix" | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // IsWildcard checks if a host is wildcard | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -69,3 +70,120 @@ func (r *Runner) IsWildcard(host string) bool { | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // IsWildcardDomain checks if a domain has wildcard DNS records by querying a random subdomain | ||||||||||||||||||||||||||||||||||||||||||||||
| func (r *Runner) IsWildcardDomain(domain string) bool { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Check cache first | ||||||||||||||||||||||||||||||||||||||||||||||
| if hasWildcard, ok := r.autoWildcardDomains.Get(domain); ok { | ||||||||||||||||||||||||||||||||||||||||||||||
| return hasWildcard | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Generate a random subdomain to test for wildcards | ||||||||||||||||||||||||||||||||||||||||||||||
| randomSubdomain := xid.New().String() + "." + domain | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| in, err := r.dnsx.QueryOne(randomSubdomain) | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil || in == nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Random subdomain doesn't resolve, not a wildcard domain | ||||||||||||||||||||||||||||||||||||||||||||||
| _ = r.autoWildcardDomains.Set(domain, false) | ||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+84
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid caching If 🔧 Proposed adjustment in, err := r.dnsx.QueryOne(randomSubdomain)
- if err != nil || in == nil {
- // Random subdomain doesn't resolve, not a wildcard domain
- _ = r.autoWildcardDomains.Set(domain, false)
- return false
- }
+ if err != nil {
+ // Transient failure: don't cache
+ return false
+ }
+ if in == nil {
+ // Random subdomain doesn't resolve, not a wildcard domain
+ _ = r.autoWildcardDomains.Set(domain, false)
+ return false
+ }🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Random subdomain resolved, this is a wildcard domain | ||||||||||||||||||||||||||||||||||||||||||||||
| _ = r.autoWildcardDomains.Set(domain, true) | ||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // GetBaseDomain extracts the base domain from a subdomain | ||||||||||||||||||||||||||||||||||||||||||||||
| func (r *Runner) GetBaseDomain(host string) string { | ||||||||||||||||||||||||||||||||||||||||||||||
| // Trim trailing dot if present | ||||||||||||||||||||||||||||||||||||||||||||||
| host = strings.TrimSuffix(host, ".") | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Use publicsuffix library for accurate base domain extraction | ||||||||||||||||||||||||||||||||||||||||||||||
| if domain, err := publicsuffix.Parse(host); err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| // domain has fields: TLD, SLD, TRD | ||||||||||||||||||||||||||||||||||||||||||||||
| // For co.uk, TLD is "co.uk", SLD is "example", TRD is "sub" | ||||||||||||||||||||||||||||||||||||||||||||||
| // We want eTLD+1: SLD + "." + TLD | ||||||||||||||||||||||||||||||||||||||||||||||
| if domain.TLD != "" && domain.SLD != "" { | ||||||||||||||||||||||||||||||||||||||||||||||
| return domain.SLD + "." + domain.TLD | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Fallback to original logic if publicsuffix parsing fails | ||||||||||||||||||||||||||||||||||||||||||||||
| parts := strings.Split(host, ".") | ||||||||||||||||||||||||||||||||||||||||||||||
| if len(parts) <= 2 { | ||||||||||||||||||||||||||||||||||||||||||||||
| return host | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| // Return the last two parts as the base domain | ||||||||||||||||||||||||||||||||||||||||||||||
| return strings.Join(parts[len(parts)-2:], ".") | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+97
to
+118
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find and read the wildcard.go file
find . -name "wildcard.go" -path "*/runner/*" -type fRepository: projectdiscovery/dnsx Length of output: 94 🏁 Script executed: # Check the file exists and get its line count
wc -l internal/runner/wildcard.go 2>/dev/null || echo "File not found at expected path"Repository: projectdiscovery/dnsx Length of output: 96 🏁 Script executed: # Read the entire file to verify the code and function implementations
cat -n internal/runner/wildcard.goRepository: projectdiscovery/dnsx Length of output: 5729 🏁 Script executed: # Check publicsuffix dependency in go.mod
rg -A2 -B2 "publicsuffix" go.mod go.sum 2>/dev/null || echo "publicsuffix dependency not found"Repository: projectdiscovery/dnsx Length of output: 1251 🏁 Script executed: # Also check what DNS/resolver libraries are imported in the wildcard.go file
rg "^import" -A20 internal/runner/wildcard.goRepository: projectdiscovery/dnsx Length of output: 538 🏁 Script executed: # Check if publicsuffix-go is imported anywhere
rg "publicsuffix-go|weppos/publicsuffix" --type=goRepository: projectdiscovery/dnsx Length of output: 47 🏁 Script executed: # Also check for publicsuffix imports
rg "import.*publicsuffix" --type=goRepository: projectdiscovery/dnsx Length of output: 47 🏁 Script executed: # Check what publicsuffix variant is available
rg "golang.org/x/net/publicsuffix" --type=goRepository: projectdiscovery/dnsx Length of output: 47 🏁 Script executed: # Check the documentation/usage of publicsuffix-go package
# First, find if there's any documentation or examples in go.mod comments
rg "weppos" go.mod go.sum
# Also search for any comment about the publicsuffix-go dependency
rg -B5 -A5 "publicsuffix-go" go.modRepository: projectdiscovery/dnsx Length of output: 1239 🏁 Script executed: # Let's check if there are any tests or examples using publicsuffix
find . -name "*.go" -type f -exec grep -l "publicsuffix\|publicSuffix" {} \;Repository: projectdiscovery/dnsx Length of output: 47 🏁 Script executed: # Since publicsuffix-go is in the dependency, let's check what the actual API should be
# by looking at the common patterns in similar projects
# First, let me verify the exact import path and common function names
rg -n "github.com/weppos" . --type go 2>/dev/null | head -20Repository: projectdiscovery/dnsx Length of output: 47 Fix two critical bugs in Issue 1: Taking the last two labels fails for domains like Issue 2: The loop uses Suggested fixesFor import "github.com/weppos/publicsuffix-go/publicsuffix"
func (r *Runner) GetBaseDomain(host string) string {
rule, err := publicsuffix.DefaultList.Find(host)
if err != nil || rule == nil {
// Fallback: return the last two parts
parts := strings.Split(host, ".")
if len(parts) <= 2 {
return host
}
return strings.Join(parts[len(parts)-2:], ".")
}
return rule.Prevailing(host)
}For for i := 1; i < len(subdomainTokens); i++ {🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // IsWildcardAuto checks if a host is wildcard using auto detection | ||||||||||||||||||||||||||||||||||||||||||||||
| func (r *Runner) IsWildcardAuto(host string) bool { | ||||||||||||||||||||||||||||||||||||||||||||||
| baseDomain := r.GetBaseDomain(host) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // First check if this domain has wildcards | ||||||||||||||||||||||||||||||||||||||||||||||
| if !r.IsWildcardDomain(baseDomain) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Domain has wildcards, now check if this specific host is a wildcard | ||||||||||||||||||||||||||||||||||||||||||||||
| orig := make(map[string]struct{}) | ||||||||||||||||||||||||||||||||||||||||||||||
| wildcards := make(map[string]struct{}) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| in, err := r.dnsx.QueryOne(host) | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil || in == nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| for _, A := range in.A { | ||||||||||||||||||||||||||||||||||||||||||||||
| orig[A] = struct{}{} | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Get wildcard IPs by querying random subdomains at each level | ||||||||||||||||||||||||||||||||||||||||||||||
| subdomainPart := strings.TrimSuffix(host, "."+baseDomain) | ||||||||||||||||||||||||||||||||||||||||||||||
| subdomainTokens := strings.Split(subdomainPart, ".") | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| var hosts []string | ||||||||||||||||||||||||||||||||||||||||||||||
| hosts = append(hosts, baseDomain) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if len(subdomainTokens) > 0 && subdomainTokens[0] != "" { | ||||||||||||||||||||||||||||||||||||||||||||||
| for i := 1; i <= len(subdomainTokens); i++ { | ||||||||||||||||||||||||||||||||||||||||||||||
| newhost := strings.Join(subdomainTokens[i:], ".") + "." + baseDomain | ||||||||||||||||||||||||||||||||||||||||||||||
| if newhost != "" { | ||||||||||||||||||||||||||||||||||||||||||||||
| hosts = append(hosts, newhost) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+145
to
+155
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Off-by-one in loop produces a host with a leading dot. The loop uses The existing 🐛 Proposed fix- for i := 1; i <= len(subdomainTokens); i++ {
+ for i := 1; i < len(subdomainTokens); i++ {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Iterate over all the hosts generated for rand. | ||||||||||||||||||||||||||||||||||||||||||||||
| for _, h := range hosts { | ||||||||||||||||||||||||||||||||||||||||||||||
| r.wildcardscachemutex.Lock() | ||||||||||||||||||||||||||||||||||||||||||||||
| listip, ok := r.wildcardscache[h] | ||||||||||||||||||||||||||||||||||||||||||||||
| r.wildcardscachemutex.Unlock() | ||||||||||||||||||||||||||||||||||||||||||||||
| if !ok { | ||||||||||||||||||||||||||||||||||||||||||||||
| in, err := r.dnsx.QueryOne(xid.New().String() + "." + h) | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil || in == nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| listip = in.A | ||||||||||||||||||||||||||||||||||||||||||||||
| r.wildcardscachemutex.Lock() | ||||||||||||||||||||||||||||||||||||||||||||||
| r.wildcardscache[h] = in.A | ||||||||||||||||||||||||||||||||||||||||||||||
| r.wildcardscachemutex.Unlock() | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Get all the records and add them to the wildcard map | ||||||||||||||||||||||||||||||||||||||||||||||
| for _, A := range listip { | ||||||||||||||||||||||||||||||||||||||||||||||
| if _, ok := wildcards[A]; !ok { | ||||||||||||||||||||||||||||||||||||||||||||||
| wildcards[A] = struct{}{} | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // check if original ip are among wildcards | ||||||||||||||||||||||||||||||||||||||||||||||
| for a := range orig { | ||||||||||||||||||||||||||||||||||||||||||||||
| if _, ok := wildcards[a]; ok { | ||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential deadlock: worker count is bounded by
len(listIPs), but work is driven bydomainHosts.numThreadsis capped atlen(listIPs)(Line 619), butlistIPsonly contains A-record IPs. If all resolved hosts have only AAAA records (or other non-A types),listIPsis empty →numThreads == 0→ no workers started. The subsequent loop (Lines 629–636) then tries to send on the unbufferedwildcardworkerchanwith no reader, causing a deadlock.Even outside the deadlock scenario, bounding threads by IP count doesn't make sense for auto mode — the work unit is hosts, not IPs.
🐛 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents