Files
ExploreDNS/internal/config/config.go
T
9aa85d8e5d
CI / test (pull_request) Failing after 2m13s
docs: comprehensive documentation for ExploreDNS
- Rewrite README.md with overview, features, installation (go install +
  build from source), quick start, full CLI flag reference table,
  output section descriptions, project structure, and development guide

- Add package-level doc comments to all five internal packages:
  config, dns, fingerprint, output, traverse (via doc.go or existing
  package-declaration files)

- Add GoDoc comments on every exported type, constant, function, and
  method across all packages:
  - internal/config: Config struct fields, all Parse*/Default/Validate
  - internal/dns: QueryConfig, Resolver, BasicResolver, CachingResolver,
    ExchangeFunc, RootServer, RootDiscoveryConfig, DecodedResponse,
    ResponseClassification, all exported helpers
  - internal/fingerprint: Fingerprinter, New, NewWithTimeout, Query,
    FingerprintAll
  - internal/traverse: Traverser, TraverserConfig, TraversalResult,
    Referral, ResolutionState, Response, ResponseType, InfoCache,
    Stack, TraverserHooks, EventStage, TraversalEvent, EventHandler,
    CircularReferralError, UnresolvableNameserverError
  - internal/output: Format, Config, Formatter, SummaryStats,
    NewFormatter, AttachHooks, RunTraversal, DefaultConfig,
    ComputeSummary

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-08 04:01:51 +10:00

280 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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] <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"},
},
"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,
}
}