feat: implement CLI flags and configuration handling (HAN-382) (#8)
CI / test (push) Failing after 3m57s

This commit was merged in pull request #8.
This commit is contained in:
2026-06-07 16:08:39 +00:00
parent feb5c68b39
commit 876542b2de
5 changed files with 708 additions and 28 deletions
+237
View File
@@ -1 +1,238 @@
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
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"},
},
"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,
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,
}
}