- Replace hardcoded 127.0.0.1:53 in systemResolver() with actual system DNS from /etc/resolv.conf (falls back to 127.0.0.1:53 if unavailable) - Add Resolver field to RootDiscoveryConfig so callers can override the upstream resolver used during root server discovery - Add --dns-upstream flag (e.g. --dns-upstream 8.8.8.8:53) to exploredns CLI and DNSUpstream field to Config - Add internal/dns/hints.go with all 13 IANA root server IPv4/IPv6 addresses as embedded constants (RootHints []RootServer) - Update tests: fix real_exchange_test.go call site; add hints_test.go covering RootHints correctness and resolver helper functions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai>
246 lines
6.6 KiB
Go
246 lines
6.6 KiB
Go
// Package config defines the configuration types and defaults for ExploreDNS,
|
|
// along with validation helpers and the CLI usage text.
|
|
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidQueryType = errors.New("invalid query type")
|
|
ErrInvalidUDPSize = errors.New("UDP size must be between 512 and 4096")
|
|
ErrInvalidMaxDepth = errors.New("max depth must be between 1 and 100")
|
|
ErrInvalidRetries = errors.New("retries must be between 0 and 10")
|
|
ErrMissingDomain = errors.New("domain is required")
|
|
ErrAlwaysTCPRequiresTCP = errors.New("--always-tcp requires --allow-tcp")
|
|
ErrInvalidRootServer = errors.New("invalid root server IP address")
|
|
)
|
|
|
|
type Config struct {
|
|
QueryType string
|
|
RootServer string
|
|
AllRootServers bool
|
|
RootAAAA bool
|
|
FollowAAAA bool
|
|
// DNSUpstream is the upstream resolver used for root server discovery.
|
|
// Format: "host:port" (e.g. "8.8.8.8:53"). Empty means use the system resolver.
|
|
DNSUpstream string
|
|
UDPSize int
|
|
AllowTCP bool
|
|
AlwaysTCP bool
|
|
MaxDepth int
|
|
Retries int
|
|
Fast bool
|
|
Verbose bool
|
|
Debug int
|
|
Quiet bool
|
|
|
|
ShowProgress bool
|
|
ShowResolves bool
|
|
ShowServers bool
|
|
ShowVersions bool
|
|
ShowAllStats bool
|
|
ShowResults bool
|
|
ShowSummaryResults bool
|
|
}
|
|
|
|
func ParseQueryType(s string) (uint16, error) {
|
|
s = strings.ToUpper(s)
|
|
switch s {
|
|
case "A":
|
|
return dns.TypeA, nil
|
|
case "AAAA":
|
|
return dns.TypeAAAA, nil
|
|
case "NS":
|
|
return dns.TypeNS, nil
|
|
case "CNAME":
|
|
return dns.TypeCNAME, nil
|
|
case "MX":
|
|
return dns.TypeMX, nil
|
|
case "TXT":
|
|
return dns.TypeTXT, nil
|
|
case "SOA":
|
|
return dns.TypeSOA, nil
|
|
case "PTR":
|
|
return dns.TypePTR, nil
|
|
case "ANY":
|
|
return dns.TypeANY, nil
|
|
default:
|
|
return 0, fmt.Errorf("%w: %s", ErrInvalidQueryType, s)
|
|
}
|
|
}
|
|
|
|
func ParseUDPSize(s string) (int, error) {
|
|
var size int
|
|
if _, err := fmt.Sscanf(s, "%d", &size); err != nil {
|
|
return 0, fmt.Errorf("%w: %s", ErrInvalidUDPSize, s)
|
|
}
|
|
if size < 512 || size > 4096 {
|
|
return 0, fmt.Errorf("%w: %d (must be 512-4096)", ErrInvalidUDPSize, size)
|
|
}
|
|
return size, nil
|
|
}
|
|
|
|
func ParseMaxDepth(s string) (int, error) {
|
|
var depth int
|
|
if _, err := fmt.Sscanf(s, "%d", &depth); err != nil {
|
|
return 0, fmt.Errorf("%w: %s", ErrInvalidMaxDepth, s)
|
|
}
|
|
if depth < 1 || depth > 100 {
|
|
return 0, fmt.Errorf("%w: %d (must be 1-100)", ErrInvalidMaxDepth, depth)
|
|
}
|
|
return depth, nil
|
|
}
|
|
|
|
func ParseRetries(s string) (int, error) {
|
|
var retries int
|
|
if _, err := fmt.Sscanf(s, "%d", &retries); err != nil {
|
|
return 0, fmt.Errorf("%w: %s", ErrInvalidRetries, s)
|
|
}
|
|
if retries < 0 || retries > 10 {
|
|
return 0, fmt.Errorf("%w: %d (must be 0-10)", ErrInvalidRetries, retries)
|
|
}
|
|
return retries, nil
|
|
}
|
|
|
|
func (c *Config) Validate() error {
|
|
if _, err := ParseQueryType(c.QueryType); err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.UDPSize < 512 || c.UDPSize > 4096 {
|
|
return fmt.Errorf("%w: %d", ErrInvalidUDPSize, c.UDPSize)
|
|
}
|
|
|
|
if c.MaxDepth < 1 || c.MaxDepth > 100 {
|
|
return fmt.Errorf("%w: %d", ErrInvalidMaxDepth, c.MaxDepth)
|
|
}
|
|
|
|
if c.Retries < 0 || c.Retries > 10 {
|
|
return fmt.Errorf("%w: %d", ErrInvalidRetries, c.Retries)
|
|
}
|
|
|
|
if c.AlwaysTCP && !c.AllowTCP {
|
|
return ErrAlwaysTCPRequiresTCP
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Config) GetDomain(args []string) (string, error) {
|
|
if len(args) == 0 {
|
|
return "", ErrMissingDomain
|
|
}
|
|
return args[0], nil
|
|
}
|
|
|
|
func (c *Config) ParseRootServer() (net.IP, error) {
|
|
if c.RootServer == "" {
|
|
return nil, nil
|
|
}
|
|
ip := net.ParseIP(c.RootServer)
|
|
if ip == nil {
|
|
return nil, fmt.Errorf("%w: %s", ErrInvalidRootServer, c.RootServer)
|
|
}
|
|
return ip, nil
|
|
}
|
|
|
|
// ParseDebugLevel returns the debug verbosity level from the -d and -dd flag values.
|
|
// dd=true → 2 (library debug), d=true → 1 (application debug), neither → 0.
|
|
func ParseDebugLevel(d, dd bool) int {
|
|
if dd {
|
|
return 2
|
|
}
|
|
if d {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func PrintUsage() {
|
|
fmt.Fprintf(os.Stderr, "ExploreDNS - DNS reconnaissance and exploration tool\n\n")
|
|
fmt.Fprintf(os.Stderr, "Usage:\n")
|
|
fmt.Fprintf(os.Stderr, " exploredns [flags] <domain>\n\n")
|
|
fmt.Fprintf(os.Stderr, "Flags:\n")
|
|
|
|
flagGroups := map[string][][2]string{
|
|
"Query Options": {
|
|
{"--type", "Record type (default A)"},
|
|
{"--root-server", "Override root server"},
|
|
{"--all-root-servers", "Use all 13 root servers"},
|
|
{"--root-aaaa", "Include IPv6 root addresses"},
|
|
{"--follow-aaaa", "Only follow AAAA for referrals"},
|
|
{"--dns-upstream", "Upstream resolver for root discovery (default: system)"},
|
|
},
|
|
"Transport Options": {
|
|
{"--udp-size", "EDNS0 buffer size (default 2048)"},
|
|
{"--allow-tcp", "TCP fallback on truncation (default true)"},
|
|
{"--always-tcp", "Always use TCP"},
|
|
{"--retries", "Retry count (default 2)"},
|
|
},
|
|
"Traversal Options": {
|
|
{"--max-depth", "Max traversal depth (default 20)"},
|
|
{"--fast", "Fast mode: reuse earlier branch cache (default true)"},
|
|
},
|
|
"Output Options": {
|
|
{"--verbose, -v", "Verbose output"},
|
|
{"--debug, -d", "Debug mode (stackable: -dd for library debug)"},
|
|
{"--quiet, -q", "Suppress supplementary info"},
|
|
{"--show-progress", "Show traversal progress"},
|
|
{"--no-show-progress", "Hide traversal progress"},
|
|
{"--show-resolves", "Show glue resolution details"},
|
|
{"--no-show-resolves", "Hide glue resolution details"},
|
|
{"--show-servers", "Show servers queried"},
|
|
{"--no-show-servers", "Hide servers queried"},
|
|
{"--show-versions", "Show server versions"},
|
|
{"--no-show-versions", "Hide server versions"},
|
|
{"--show-all-stats", "Show all statistics"},
|
|
{"--no-show-all-stats", "Hide all statistics"},
|
|
{"--show-results", "Show query results"},
|
|
{"--no-show-results", "Hide query results"},
|
|
{"--show-summary-results", "Show summary of results"},
|
|
{"--no-show-summary-results", "Hide summary of results"},
|
|
},
|
|
}
|
|
|
|
for group, flags := range flagGroups {
|
|
fmt.Fprintf(os.Stderr, "\n%s:\n", group)
|
|
for _, f := range flags {
|
|
fmt.Fprintf(os.Stderr, " %-25s %s\n", f[0], f[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
func DefaultConfig() *Config {
|
|
return &Config{
|
|
QueryType: "A",
|
|
RootServer: "",
|
|
AllRootServers: false,
|
|
RootAAAA: false,
|
|
FollowAAAA: false,
|
|
DNSUpstream: "",
|
|
UDPSize: 2048,
|
|
AllowTCP: true,
|
|
AlwaysTCP: false,
|
|
MaxDepth: 20,
|
|
Retries: 2,
|
|
Fast: true,
|
|
Verbose: false,
|
|
Debug: 0,
|
|
Quiet: false,
|
|
ShowProgress: true,
|
|
ShowResolves: true,
|
|
ShowServers: true,
|
|
ShowVersions: true,
|
|
ShowAllStats: true,
|
|
ShowResults: true,
|
|
ShowSummaryResults: true,
|
|
}
|
|
}
|