- Update go.mod module declaration - Update all internal import paths in .go files - Update go install lines in README.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai>
257 lines
5.6 KiB
Go
257 lines
5.6 KiB
Go
package traverse
|
|
|
|
import (
|
|
"net"
|
|
|
|
"gitea.hansenits.com.au/hits/ExploreDNS/internal/dns"
|
|
miekgdns "github.com/miekg/dns"
|
|
)
|
|
|
|
type ResponseType int
|
|
|
|
const (
|
|
RespReferral ResponseType = iota
|
|
RespAnswer
|
|
RespCNAMEFollow
|
|
RespNODATA
|
|
RespNXDOMAIN
|
|
RespSERVFAIL
|
|
RespREFUSED
|
|
RespNOTIMPL
|
|
RespCNAMELoop
|
|
RespError
|
|
// RespNSResolutionFailed indicates that the traversal could not resolve the
|
|
// IP address of an in-bailiwick nameserver. The domain may still be
|
|
// reachable in practice (e.g. via glue records held by the registry), but
|
|
// the iterative traversal could not complete that path.
|
|
RespNSResolutionFailed
|
|
)
|
|
|
|
func (rt ResponseType) String() string {
|
|
switch rt {
|
|
case RespReferral:
|
|
return "referral"
|
|
case RespAnswer:
|
|
return "answer"
|
|
case RespCNAMEFollow:
|
|
return "cname_follow"
|
|
case RespNODATA:
|
|
return "nodata"
|
|
case RespNXDOMAIN:
|
|
return "nxdomain"
|
|
case RespSERVFAIL:
|
|
return "servfail"
|
|
case RespREFUSED:
|
|
return "refused"
|
|
case RespNOTIMPL:
|
|
return "notimp"
|
|
case RespCNAMELoop:
|
|
return "cname_loop"
|
|
case RespError:
|
|
return "error"
|
|
case RespNSResolutionFailed:
|
|
return "ns_error"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
type Response struct {
|
|
Referral *Referral
|
|
Server net.IP
|
|
Cache *InfoCache
|
|
Decoded *dns.DecodedResponse
|
|
Type ResponseType
|
|
ErrorMessage string
|
|
}
|
|
|
|
func NewResponse(ref *Referral, server net.IP, cache *InfoCache) *Response {
|
|
return &Response{
|
|
Referral: ref,
|
|
Server: server,
|
|
Cache: cache,
|
|
}
|
|
}
|
|
|
|
func (r *Response) Process(msg *miekgdns.Msg) *Response {
|
|
if msg == nil {
|
|
r.Type = RespError
|
|
r.ErrorMessage = "nil DNS response"
|
|
return r
|
|
}
|
|
|
|
r.Decoded = dns.DecodeResponse(msg)
|
|
if r.Decoded == nil {
|
|
r.Type = RespError
|
|
r.ErrorMessage = "failed to decode DNS response"
|
|
return r
|
|
}
|
|
|
|
// Synthesize CNAME from DNAME when the server didn't include a synthesized CNAME record.
|
|
if len(r.Decoded.CNAMEChain) == 0 && r.Referral != nil && len(r.Decoded.DNAMEMappings) > 0 {
|
|
for _, dm := range r.Decoded.DNAMEMappings {
|
|
synthesized := dns.SynthesizeCNAMEFromDNAME(r.Referral.Name, dm.Owner, dm.Target)
|
|
if synthesized != "" {
|
|
r.Decoded.CNAMEChain = append(r.Decoded.CNAMEChain, synthesized)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
r.Type = r.classify()
|
|
return r
|
|
}
|
|
|
|
func (r *Response) classify() ResponseType {
|
|
switch r.Decoded.Classification {
|
|
case dns.ResponseNXDOMAIN:
|
|
return RespNXDOMAIN
|
|
case dns.ResponseSERVFAIL:
|
|
return RespSERVFAIL
|
|
case dns.ResponseREFUSED:
|
|
return RespREFUSED
|
|
case dns.ResponseNOTIMPL:
|
|
return RespNOTIMPL
|
|
case dns.ResponseAnswer:
|
|
if len(r.Decoded.CNAMEChain) > 0 && !r.hasFinalAnswer() {
|
|
return RespCNAMEFollow
|
|
}
|
|
return RespAnswer
|
|
case dns.ResponseReferral:
|
|
return RespReferral
|
|
case dns.ResponseNODATA:
|
|
return RespNODATA
|
|
default:
|
|
return RespError
|
|
}
|
|
}
|
|
|
|
func (r *Response) hasFinalAnswer() bool {
|
|
for _, rr := range r.Decoded.Answers {
|
|
switch rr.(type) {
|
|
case *miekgdns.CNAME, *miekgdns.DNAME, *miekgdns.RRSIG:
|
|
// CNAME and DNAME are redirect records, not final answers.
|
|
// RRSIG is a DNSSEC signature record — it covers the CNAME/DNAME
|
|
// but is not itself the answer to the original question type.
|
|
continue
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r *Response) ChildReferrals() []*Referral {
|
|
if r.Type != RespReferral {
|
|
return nil
|
|
}
|
|
if r.Referral == nil {
|
|
return nil
|
|
}
|
|
|
|
var nameservers []string
|
|
for _, rr := range r.Decoded.Authority {
|
|
if ns, ok := rr.(*miekgdns.NS); ok {
|
|
if r.Referral.InBailiwick(ns.Ns) {
|
|
nameservers = append(nameservers, ns.Ns)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(nameservers) == 0 {
|
|
for _, rr := range r.Decoded.Authority {
|
|
if ns, ok := rr.(*miekgdns.NS); ok {
|
|
nameservers = append(nameservers, ns.Ns)
|
|
}
|
|
}
|
|
}
|
|
|
|
r.storeAuthority(nameservers)
|
|
|
|
prob := r.childProb(len(nameservers))
|
|
var children []*Referral
|
|
for _, ns := range nameservers {
|
|
child := NewReferral(
|
|
r.Referral.Name,
|
|
r.Referral.Qtype,
|
|
ns,
|
|
r.Referral.Depth+1,
|
|
prob,
|
|
r.Referral,
|
|
)
|
|
r.resolveGlue(child)
|
|
children = append(children, child)
|
|
}
|
|
return children
|
|
}
|
|
|
|
func (r *Response) CNAMEFollowReferral() *Referral {
|
|
if r.Type != RespCNAMEFollow || len(r.Decoded.CNAMEChain) == 0 {
|
|
return nil
|
|
}
|
|
target := r.Decoded.CNAMEChain[len(r.Decoded.CNAMEChain)-1]
|
|
follow := NewReferral(
|
|
target,
|
|
r.Referral.Qtype,
|
|
r.Referral.Bailiwick,
|
|
r.Referral.Depth+1,
|
|
r.Referral.Prob,
|
|
r.Referral,
|
|
)
|
|
if len(r.Referral.Addresses) > 0 {
|
|
follow.Addresses = make([]net.IP, len(r.Referral.Addresses))
|
|
copy(follow.Addresses, r.Referral.Addresses)
|
|
follow.State = StateResolved
|
|
}
|
|
return follow
|
|
}
|
|
|
|
func (r *Response) storeAuthority(nameservers []string) {
|
|
if r.Cache == nil {
|
|
return
|
|
}
|
|
zone := r.Referral.Name
|
|
r.Cache.StoreNS(zone, nameservers)
|
|
}
|
|
|
|
func (r *Response) resolveGlue(child *Referral) {
|
|
if r.Cache == nil {
|
|
return
|
|
}
|
|
nsName := child.Bailiwick
|
|
for _, rr := range r.Decoded.Additional {
|
|
switch v := rr.(type) {
|
|
case *miekgdns.A:
|
|
if normalize(v.Header().Name) == normalize(nsName) {
|
|
child.Addresses = append(child.Addresses, v.A)
|
|
}
|
|
case *miekgdns.AAAA:
|
|
if normalize(v.Header().Name) == normalize(nsName) {
|
|
child.Addresses = append(child.Addresses, v.AAAA)
|
|
}
|
|
}
|
|
}
|
|
if child.HasAddresses() {
|
|
child.State = StateResolved
|
|
}
|
|
r.Cache.StoreGlue(nsName, child.Addresses)
|
|
}
|
|
|
|
func (r *Response) IsTerminal() bool {
|
|
switch r.Type {
|
|
case RespAnswer, RespNODATA, RespNXDOMAIN, RespSERVFAIL, RespREFUSED, RespNOTIMPL, RespCNAMELoop, RespError, RespNSResolutionFailed:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (r *Response) childProb(n int) float64 {
|
|
if n <= 0 {
|
|
return 0
|
|
}
|
|
if r.Referral == nil {
|
|
return 1.0 / float64(n)
|
|
}
|
|
return r.Referral.Prob / float64(n)
|
|
}
|