package config import ( "errors" "fmt" "net" "os" "strings" "github.com/miekg/dns" ) // Sentinel errors returned by validation and parsing functions. 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") ) // Config holds all runtime settings for ExploreDNS. // Populate it from CLI flags, then call Validate before use. type Config struct { // QueryType is the DNS record type to query (e.g. "A", "MX", "TXT"). QueryType string // RootServer overrides the root server IP used to begin traversal. // Empty string means auto-discover via the system resolver. RootServer string // AllRootServers queries all 13 DNS root servers instead of one. AllRootServers bool // RootAAAA includes IPv6 addresses of root servers when true. RootAAAA bool // FollowAAAA restricts referral following to AAAA glue records only. FollowAAAA bool // UDPSize is the EDNS0 advertised UDP payload size (512–4096 bytes). UDPSize int // AllowTCP enables TCP fallback when a UDP response is truncated. AllowTCP bool // AlwaysTCP forces all queries over TCP (requires AllowTCP = true). AlwaysTCP bool // MaxDepth limits the traversal depth (1–100). MaxDepth int // Retries is the number of times a failed query is retried (0–10). Retries int // Fast enables the shared-cache fast mode. When true, sibling branches // inherit glue discovered by earlier branches, trading accuracy for speed. Fast bool // Verbose enables verbose output lines. Verbose bool // Debug controls debug verbosity: 0 = off, 1 = app debug, 2 = library debug. Debug int // Quiet suppresses supplementary informational output. Quiet bool // Output visibility flags — each controls a section of the output. ShowProgress bool ShowResolves bool ShowServers bool ShowVersions bool ShowAllStats bool ShowResults bool ShowSummaryResults bool } // ParseQueryType converts a case-insensitive query type string (e.g. "A", "MX") // to its numeric DNS type constant. Returns ErrInvalidQueryType for unknown types. 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) } } // ParseUDPSize parses a string as a UDP buffer size. // Returns ErrInvalidUDPSize if the string is not an integer or is outside 512–4096. 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 } // ParseMaxDepth parses a string as a traversal depth. // Returns ErrInvalidMaxDepth if the string is not an integer or is outside 1–100. 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 } // ParseRetries parses a string as a retry count. // Returns ErrInvalidRetries if the string is not an integer or is outside 0–10. 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 } // Validate checks that all Config fields are within their accepted ranges and // that flag combinations are valid (e.g. AlwaysTCP requires AllowTCP). // Returns the first validation error encountered, or 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 } // GetDomain returns the domain name from the positional CLI arguments. // Returns ErrMissingDomain when args is empty. func (c *Config) GetDomain(args []string) (string, error) { if len(args) == 0 { return "", ErrMissingDomain } return args[0], nil } // ParseRootServer parses the RootServer field as an IP address. // Returns nil, nil when RootServer is empty (auto-discover mode). // Returns ErrInvalidRootServer when the string is non-empty but not a valid IP. 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 } // PrintUsage writes a grouped help message to stderr, listing every flag with // its description. It is registered as flag.Usage by main. 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] \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"}, }, "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]) } } } // DefaultConfig returns a Config populated with sensible defaults: // query type A, UDP size 2048, max depth 20, 2 retries, fast mode on, // TCP fallback allowed, and all output sections visible. func DefaultConfig() *Config { return &Config{ QueryType: "A", RootServer: "", AllRootServers: false, RootAAAA: false, FollowAAAA: false, 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, } }