CI / test (pull_request) Failing after 2m13s
- 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>
280 lines
8.8 KiB
Go
280 lines
8.8 KiB
Go
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,
|
||
}
|
||
}
|