- 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>
This commit is contained in:
co-authored by
Copilot
multica-agent
parent
fe1afe2a97
commit
9aa85d8e5d
+34
-10
@@ -7,17 +7,20 @@ import (
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// ResponseClassification categorises a DNS response at a high level,
|
||||
// independent of the raw RCODE.
|
||||
type ResponseClassification int
|
||||
|
||||
// Classification constants, in order from most to least specific.
|
||||
const (
|
||||
ResponseAnswer ResponseClassification = iota
|
||||
ResponseReferral
|
||||
ResponseNODATA
|
||||
ResponseNXDOMAIN
|
||||
ResponseSERVFAIL
|
||||
ResponseREFUSED
|
||||
ResponseNOTIMPL
|
||||
ResponseOther
|
||||
ResponseAnswer ResponseClassification = iota // answer section contains records
|
||||
ResponseReferral // non-authoritative NS referral
|
||||
ResponseNODATA // NOERROR with empty answer section
|
||||
ResponseNXDOMAIN // name does not exist (RCODE 3)
|
||||
ResponseSERVFAIL // server failure (RCODE 2)
|
||||
ResponseREFUSED // query refused (RCODE 5)
|
||||
ResponseNOTIMPL // not implemented (RCODE 4)
|
||||
ResponseOther // any other RCODE
|
||||
)
|
||||
|
||||
func (rc ResponseClassification) String() string {
|
||||
@@ -41,6 +44,7 @@ func (rc ResponseClassification) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// DecodedResponse holds the extracted, structured fields from a raw *dns.Msg.
|
||||
type DecodedResponse struct {
|
||||
Rcode int
|
||||
RcodeName string
|
||||
@@ -51,8 +55,10 @@ type DecodedResponse struct {
|
||||
Answers []dns.RR
|
||||
Authority []dns.RR
|
||||
Additional []dns.RR
|
||||
CNAMEChain []string
|
||||
DNAMEMappings []DNAMEMapping
|
||||
// CNAMEChain contains the ordered CNAME targets from the answer section.
|
||||
CNAMEChain []string
|
||||
// DNAMEMappings contains any DNAME records for redirect synthesis.
|
||||
DNAMEMappings []DNAMEMapping
|
||||
}
|
||||
|
||||
// DNAMEMapping holds a DNAME record's owner and target for redirect synthesis.
|
||||
@@ -61,6 +67,8 @@ type DNAMEMapping struct {
|
||||
Target string // e.g., "example.net."
|
||||
}
|
||||
|
||||
// DecodeResponse converts a raw *dns.Msg into a DecodedResponse.
|
||||
// Returns nil when msg is nil.
|
||||
func DecodeResponse(msg *dns.Msg) *DecodedResponse {
|
||||
if msg == nil {
|
||||
return nil
|
||||
@@ -171,10 +179,13 @@ func SynthesizeCNAMEFromDNAME(queryName, dnameOwner, dnameTarget string) string
|
||||
return prefix + target
|
||||
}
|
||||
|
||||
// IsTruncated reports whether msg has the TC (truncated) bit set.
|
||||
func IsTruncated(msg *dns.Msg) bool {
|
||||
return msg != nil && msg.Truncated
|
||||
}
|
||||
|
||||
// RcodeName returns the string representation of the RCODE in msg (e.g. "NOERROR").
|
||||
// Returns "UNKNOWN" when msg is nil.
|
||||
func RcodeName(msg *dns.Msg) string {
|
||||
if msg == nil {
|
||||
return "UNKNOWN"
|
||||
@@ -182,6 +193,7 @@ func RcodeName(msg *dns.Msg) string {
|
||||
return dns.RcodeToString[msg.Rcode]
|
||||
}
|
||||
|
||||
// ExtractAnswers returns the answer section of msg, or nil when msg is nil.
|
||||
func ExtractAnswers(msg *dns.Msg) []dns.RR {
|
||||
if msg == nil {
|
||||
return nil
|
||||
@@ -189,6 +201,7 @@ func ExtractAnswers(msg *dns.Msg) []dns.RR {
|
||||
return msg.Answer
|
||||
}
|
||||
|
||||
// ExtractAuthority returns the authority section of msg, or nil when msg is nil.
|
||||
func ExtractAuthority(msg *dns.Msg) []dns.RR {
|
||||
if msg == nil {
|
||||
return nil
|
||||
@@ -196,6 +209,8 @@ func ExtractAuthority(msg *dns.Msg) []dns.RR {
|
||||
return msg.Ns
|
||||
}
|
||||
|
||||
// ExtractCNAMEChain returns the ordered list of CNAME targets from the answer section.
|
||||
// Returns nil when msg is nil.
|
||||
func ExtractCNAMEChain(msg *dns.Msg) []string {
|
||||
if msg == nil {
|
||||
return nil
|
||||
@@ -203,6 +218,8 @@ func ExtractCNAMEChain(msg *dns.Msg) []string {
|
||||
return extractCNAMEChain(msg)
|
||||
}
|
||||
|
||||
// IsReferral reports whether msg is a non-authoritative NS referral
|
||||
// (NOERROR, empty answer section, NS records in authority, AA=false).
|
||||
func IsReferral(msg *dns.Msg) bool {
|
||||
if msg == nil || msg.Rcode != dns.RcodeSuccess || len(msg.Answer) > 0 {
|
||||
return false
|
||||
@@ -210,6 +227,8 @@ func IsReferral(msg *dns.Msg) bool {
|
||||
return hasNSRecords(msg.Ns) && !msg.Authoritative
|
||||
}
|
||||
|
||||
// IsNODATA reports whether msg is a NODATA response
|
||||
// (NOERROR with an empty answer section and no referral).
|
||||
func IsNODATA(msg *dns.Msg) bool {
|
||||
if msg == nil || msg.Rcode != dns.RcodeSuccess {
|
||||
return false
|
||||
@@ -223,6 +242,8 @@ func IsNODATA(msg *dns.Msg) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// HasCNAMEChain reports whether msg contains at least one CNAME record in its
|
||||
// answer section.
|
||||
func HasCNAMEChain(msg *dns.Msg) bool {
|
||||
if msg == nil {
|
||||
return false
|
||||
@@ -230,6 +251,9 @@ func HasCNAMEChain(msg *dns.Msg) bool {
|
||||
return len(extractCNAMEChain(msg)) > 0
|
||||
}
|
||||
|
||||
// FormatRecord returns a human-readable representation of a DNS resource record
|
||||
// in the form: NAME TTL CLASS TYPE RDATA.
|
||||
// Returns an empty string when rr is nil.
|
||||
func FormatRecord(rr dns.RR) string {
|
||||
if rr == nil {
|
||||
return ""
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
// Package dns provides low-level DNS query primitives, response decoding, root
|
||||
// server discovery, and type constants used throughout ExploreDNS.
|
||||
//
|
||||
// The package is organised around four concerns:
|
||||
//
|
||||
// - types.go — DNS record-type constants and helpers (QNameType, etc.)
|
||||
// - query.go — Query / IterativeQuery and retry/back-off logic
|
||||
// - resolver.go — Resolver interface plus BasicResolver and CachingResolver
|
||||
// - decode.go — DecodeResponse, ResponseClassification, and related helpers
|
||||
// - roots.go — DiscoverRoots for bootstrapping iterative traversal
|
||||
package dns
|
||||
|
||||
+23
-4
@@ -9,14 +9,22 @@ import (
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// QueryConfig controls the transport-level behaviour of DNS queries.
|
||||
type QueryConfig struct {
|
||||
UDPSize int
|
||||
Timeout time.Duration
|
||||
Retries int
|
||||
UseTCP bool
|
||||
// UDPSize is the EDNS0 advertised UDP payload size in bytes.
|
||||
UDPSize int
|
||||
// Timeout is the per-attempt network timeout.
|
||||
Timeout time.Duration
|
||||
// Retries is the total number of query attempts (first try + retries - 1 extra attempts).
|
||||
Retries int
|
||||
// UseTCP forces all queries over TCP when true.
|
||||
UseTCP bool
|
||||
// AllowTCP enables automatic TCP retry when a UDP response is truncated.
|
||||
AllowTCP bool
|
||||
}
|
||||
|
||||
// DefaultQueryConfig returns a QueryConfig with sensible defaults:
|
||||
// 2048-byte UDP buffer, 5-second timeout, 3 retries, UDP with TCP fallback.
|
||||
func DefaultQueryConfig() *QueryConfig {
|
||||
return &QueryConfig{
|
||||
UDPSize: DefaultEDNS0UDPSize(),
|
||||
@@ -27,8 +35,13 @@ func DefaultQueryConfig() *QueryConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// ExchangeFunc is a function that sends a DNS message to server and returns
|
||||
// the response. Injecting an ExchangeFunc in tests avoids real network calls.
|
||||
type ExchangeFunc func(ctx context.Context, server string, msg *dns.Msg, useTCP bool) (*dns.Msg, error)
|
||||
|
||||
// Query sends a standard (recursion-desired) DNS query to server for name/qtype
|
||||
// using the provided cfg. cfg may be nil; DefaultQueryConfig is used in that case.
|
||||
// Retries with exponential back-off are performed on transient errors.
|
||||
func Query(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig) (*dns.Msg, error) {
|
||||
if cfg == nil {
|
||||
cfg = DefaultQueryConfig()
|
||||
@@ -75,6 +88,8 @@ func realExchange(ctx context.Context, server string, msg *dns.Msg, useTCP bool)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// QueryWithExchange is the testable core of Query. It uses exchangeFn instead
|
||||
// of the real network, enabling deterministic unit tests.
|
||||
func QueryWithExchange(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig, exchangeFn ExchangeFunc) (*dns.Msg, error) {
|
||||
if cfg == nil {
|
||||
cfg = DefaultQueryConfig()
|
||||
@@ -127,6 +142,9 @@ func QueryWithExchange(ctx context.Context, server net.IP, name string, qtype ui
|
||||
return nil, fmt.Errorf("query %s %s failed after %d retries: %w", name, QNameType(qtype), cfg.Retries, lastErr)
|
||||
}
|
||||
|
||||
// IterativeQuery sends a non-recursive (RD=false) DNS query intended for
|
||||
// authoritative nameservers. Use this during iterative traversal to prevent
|
||||
// resolvers from answering on behalf of the authoritative server.
|
||||
func IterativeQuery(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig) (*dns.Msg, error) {
|
||||
if cfg == nil {
|
||||
cfg = DefaultQueryConfig()
|
||||
@@ -137,6 +155,7 @@ func IterativeQuery(ctx context.Context, server net.IP, name string, qtype uint1
|
||||
return IterativeQueryWithExchange(ctx, server, name, qtype, cfg, realExchange)
|
||||
}
|
||||
|
||||
// IterativeQueryWithExchange is the testable core of IterativeQuery.
|
||||
func IterativeQueryWithExchange(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig, exchangeFn ExchangeFunc) (*dns.Msg, error) {
|
||||
if cfg == nil {
|
||||
cfg = DefaultQueryConfig()
|
||||
|
||||
@@ -10,12 +10,16 @@ import (
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Resolver is the interface for sending DNS queries.
|
||||
// Implementations may cache, mock, or delegate to the real network.
|
||||
type Resolver interface {
|
||||
Query(ctx context.Context, server net.IP, name string, qtype uint16, cfg *QueryConfig) (*dns.Msg, error)
|
||||
}
|
||||
|
||||
// BasicResolver implements Resolver by issuing real UDP/TCP DNS queries.
|
||||
type BasicResolver struct{}
|
||||
|
||||
// NewBasicResolver returns a BasicResolver ready for use.
|
||||
func NewBasicResolver() *BasicResolver {
|
||||
return &BasicResolver{}
|
||||
}
|
||||
@@ -40,6 +44,10 @@ func (e *cacheEntry) expired() bool {
|
||||
return time.Now().After(e.expireAt)
|
||||
}
|
||||
|
||||
// CachingResolver wraps another Resolver and caches successful responses.
|
||||
// Responses are evicted based on the minimum DNS TTL in the message, with a
|
||||
// configurable fallback default TTL for zero-TTL responses.
|
||||
// All operations are safe for concurrent use.
|
||||
type CachingResolver struct {
|
||||
inner Resolver
|
||||
mu sync.RWMutex
|
||||
@@ -47,6 +55,9 @@ type CachingResolver struct {
|
||||
defaultTTL time.Duration
|
||||
}
|
||||
|
||||
// NewCachingResolver creates a CachingResolver wrapping inner.
|
||||
// When inner is nil, a BasicResolver is used.
|
||||
// Optional opts may be supplied to customise the default TTL (see WithDefaultTTL).
|
||||
func NewCachingResolver(inner Resolver, opts ...CachingResolverOption) *CachingResolver {
|
||||
if inner == nil {
|
||||
inner = NewBasicResolver()
|
||||
@@ -119,6 +130,7 @@ func (cr *CachingResolver) store(key cacheKey, msg *dns.Msg) {
|
||||
cr.mu.Unlock()
|
||||
}
|
||||
|
||||
// Len returns the current number of entries in the cache, including expired ones.
|
||||
func (cr *CachingResolver) Len() int {
|
||||
cr.mu.RLock()
|
||||
n := len(cr.cache)
|
||||
@@ -126,12 +138,14 @@ func (cr *CachingResolver) Len() int {
|
||||
return n
|
||||
}
|
||||
|
||||
// Clear removes all entries from the cache.
|
||||
func (cr *CachingResolver) Clear() {
|
||||
cr.mu.Lock()
|
||||
cr.cache = make(map[cacheKey]*cacheEntry)
|
||||
cr.mu.Unlock()
|
||||
}
|
||||
|
||||
// PurgeExpired removes all expired entries from the cache and returns the count removed.
|
||||
func (cr *CachingResolver) PurgeExpired() int {
|
||||
cr.mu.Lock()
|
||||
count := 0
|
||||
@@ -145,12 +159,15 @@ func (cr *CachingResolver) PurgeExpired() int {
|
||||
return count
|
||||
}
|
||||
|
||||
// CachingResolverOption is a functional option for NewCachingResolver.
|
||||
type CachingResolverOption func(*cachingResolverConfig)
|
||||
|
||||
type cachingResolverConfig struct {
|
||||
defaultTTL time.Duration
|
||||
}
|
||||
|
||||
// WithDefaultTTL sets the TTL used for cache entries when the DNS response
|
||||
// contains no TTL information (or when TTL is zero).
|
||||
func WithDefaultTTL(d time.Duration) CachingResolverOption {
|
||||
return func(c *cachingResolverConfig) {
|
||||
c.defaultTTL = d
|
||||
|
||||
+19
-2
@@ -10,12 +10,18 @@ import (
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// RootServer holds the name and IP addresses of a DNS root nameserver.
|
||||
type RootServer struct {
|
||||
// Name is the FQDN of the root nameserver (e.g. "a.root-servers.net.").
|
||||
Name string
|
||||
// IPv4 holds the IPv4 addresses for the server.
|
||||
IPv4 []net.IP
|
||||
// IPv6 holds the IPv6 addresses for the server.
|
||||
IPv6 []net.IP
|
||||
}
|
||||
|
||||
// AllIPs returns all IP addresses for the server.
|
||||
// When includeAAAA is false, only IPv4 addresses are returned.
|
||||
func (rs *RootServer) AllIPs(includeAAAA bool) []net.IP {
|
||||
var ips []net.IP
|
||||
ips = append(ips, rs.IPv4...)
|
||||
@@ -25,12 +31,19 @@ func (rs *RootServer) AllIPs(includeAAAA bool) []net.IP {
|
||||
return ips
|
||||
}
|
||||
|
||||
// RootDiscoveryConfig controls how DiscoverRoots selects root servers.
|
||||
type RootDiscoveryConfig struct {
|
||||
Server string
|
||||
AllRoots bool
|
||||
// Server overrides which root server is used. An empty string means
|
||||
// auto-select the first root server returned by the system resolver.
|
||||
Server string
|
||||
// AllRoots queries all 13 root servers instead of just one.
|
||||
AllRoots bool
|
||||
// IncludeAAAA includes IPv6 addresses of root servers when true.
|
||||
IncludeAAAA bool
|
||||
}
|
||||
|
||||
// DefaultRootDiscoveryConfig returns a RootDiscoveryConfig that auto-selects a
|
||||
// single IPv4-only root server.
|
||||
func DefaultRootDiscoveryConfig() *RootDiscoveryConfig {
|
||||
return &RootDiscoveryConfig{
|
||||
AllRoots: false,
|
||||
@@ -38,6 +51,10 @@ func DefaultRootDiscoveryConfig() *RootDiscoveryConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// DiscoverRoots discovers DNS root servers to use as traversal starting points.
|
||||
// When cfg.Server is set, that specific root server is used.
|
||||
// When cfg.AllRoots is true, all 13 root servers are returned.
|
||||
// Otherwise, a single root server is selected from the system resolver's NS response.
|
||||
func DiscoverRoots(ctx context.Context, cfg *RootDiscoveryConfig) ([]RootServer, error) {
|
||||
if cfg == nil {
|
||||
cfg = DefaultRootDiscoveryConfig()
|
||||
|
||||
@@ -4,6 +4,9 @@ import (
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// DNS record-type constants mirroring github.com/miekg/dns values.
|
||||
// Using local aliases keeps the rest of ExploreDNS independent of the
|
||||
// upstream library's type system.
|
||||
const (
|
||||
TypeA uint16 = dns.TypeA
|
||||
TypeAAAA uint16 = dns.TypeAAAA
|
||||
@@ -17,6 +20,7 @@ const (
|
||||
TypeANY uint16 = dns.TypeANY
|
||||
)
|
||||
|
||||
// QNameTypes maps numeric DNS type constants to their canonical uppercase names.
|
||||
var QNameTypes = map[uint16]string{
|
||||
TypeA: "A",
|
||||
TypeAAAA: "AAAA",
|
||||
@@ -30,6 +34,8 @@ var QNameTypes = map[uint16]string{
|
||||
TypeANY: "ANY",
|
||||
}
|
||||
|
||||
// QNameType returns the string name for a numeric DNS query type.
|
||||
// Falls back to the upstream dns.TypeToString map for types not in QNameTypes.
|
||||
func QNameType(qtype uint16) string {
|
||||
if name, ok := QNameTypes[qtype]; ok {
|
||||
return name
|
||||
@@ -37,10 +43,12 @@ func QNameType(qtype uint16) string {
|
||||
return dns.TypeToString[qtype]
|
||||
}
|
||||
|
||||
// DefaultEDNS0UDPSize returns the default EDNS0 advertised UDP payload size (2048 bytes).
|
||||
func DefaultEDNS0UDPSize() int {
|
||||
return 2048
|
||||
}
|
||||
|
||||
// MinEDNS0UDPSize returns the minimum accepted EDNS0 UDP payload size (512 bytes).
|
||||
func MinEDNS0UDPSize() int {
|
||||
return 512
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user