Files
ExploreDNS/internal/dns/decode.go
2026-06-07 17:24:00 +00:00

246 lines
4.9 KiB
Go

package dns
import (
"fmt"
"strings"
"github.com/miekg/dns"
)
type ResponseClassification int
const (
ResponseAnswer ResponseClassification = iota
ResponseReferral
ResponseNODATA
ResponseNXDOMAIN
ResponseSERVFAIL
ResponseREFUSED
ResponseNOTIMPL
ResponseOther
)
func (rc ResponseClassification) String() string {
switch rc {
case ResponseAnswer:
return "answer"
case ResponseReferral:
return "referral"
case ResponseNODATA:
return "nodata"
case ResponseNXDOMAIN:
return "nxdomain"
case ResponseSERVFAIL:
return "servfail"
case ResponseREFUSED:
return "refused"
case ResponseNOTIMPL:
return "notimp"
default:
return "other"
}
}
type DecodedResponse struct {
Rcode int
RcodeName string
Truncated bool
RecursionAvailable bool
Authoritative bool
Classification ResponseClassification
Answers []dns.RR
Authority []dns.RR
Additional []dns.RR
CNAMEChain []string
DNAMEMappings []DNAMEMapping
}
// DNAMEMapping holds a DNAME record's owner and target for redirect synthesis.
type DNAMEMapping struct {
Owner string // e.g., "example.com."
Target string // e.g., "example.net."
}
func DecodeResponse(msg *dns.Msg) *DecodedResponse {
if msg == nil {
return nil
}
d := &DecodedResponse{
Rcode: msg.Rcode,
RcodeName: dns.RcodeToString[msg.Rcode],
Truncated: msg.Truncated,
RecursionAvailable: msg.RecursionAvailable,
Authoritative: msg.Authoritative,
Answers: msg.Answer,
Authority: msg.Ns,
Additional: msg.Extra,
CNAMEChain: extractCNAMEChain(msg),
DNAMEMappings: extractDNAMEMappings(msg),
}
d.Classification = classify(msg)
return d
}
func classify(msg *dns.Msg) ResponseClassification {
switch msg.Rcode {
case dns.RcodeNameError:
return ResponseNXDOMAIN
case dns.RcodeServerFailure:
return ResponseSERVFAIL
case dns.RcodeRefused:
return ResponseREFUSED
case dns.RcodeNotImplemented:
return ResponseNOTIMPL
case dns.RcodeSuccess:
return classifySuccess(msg)
default:
return ResponseOther
}
}
func classifySuccess(msg *dns.Msg) ResponseClassification {
hasAnswers := len(msg.Answer) > 0
if hasAnswers {
return ResponseAnswer
}
hasNS := hasNSRecords(msg.Ns)
if hasNS && !msg.Authoritative {
return ResponseReferral
}
if hasNS {
return ResponseNODATA
}
return ResponseNODATA
}
func hasNSRecords(rrs []dns.RR) bool {
for _, rr := range rrs {
if _, ok := rr.(*dns.NS); ok {
return true
}
}
return false
}
func extractCNAMEChain(msg *dns.Msg) []string {
var chain []string
seen := make(map[string]bool)
for _, rr := range msg.Answer {
if cname, ok := rr.(*dns.CNAME); ok {
target := cname.Target
if !seen[target] {
seen[target] = true
chain = append(chain, target)
}
}
}
return chain
}
func extractDNAMEMappings(msg *dns.Msg) []DNAMEMapping {
var mappings []DNAMEMapping
for _, rr := range msg.Answer {
if dname, ok := rr.(*dns.DNAME); ok {
mappings = append(mappings, DNAMEMapping{
Owner: dns.Fqdn(dname.Hdr.Name),
Target: dns.Fqdn(dname.Target),
})
}
}
return mappings
}
// SynthesizeCNAMEFromDNAME computes the CNAME target for queryName given a DNAME mapping.
// Returns empty string if queryName is not a strict subdomain of dnameOwner.
func SynthesizeCNAMEFromDNAME(queryName, dnameOwner, dnameTarget string) string {
q := strings.ToLower(dns.Fqdn(queryName))
owner := strings.ToLower(dns.Fqdn(dnameOwner))
target := strings.ToLower(dns.Fqdn(dnameTarget))
if !dns.IsSubDomain(owner, q) || q == owner {
return ""
}
prefix := strings.TrimSuffix(q, owner)
return prefix + target
}
func IsTruncated(msg *dns.Msg) bool {
return msg != nil && msg.Truncated
}
func RcodeName(msg *dns.Msg) string {
if msg == nil {
return "UNKNOWN"
}
return dns.RcodeToString[msg.Rcode]
}
func ExtractAnswers(msg *dns.Msg) []dns.RR {
if msg == nil {
return nil
}
return msg.Answer
}
func ExtractAuthority(msg *dns.Msg) []dns.RR {
if msg == nil {
return nil
}
return msg.Ns
}
func ExtractCNAMEChain(msg *dns.Msg) []string {
if msg == nil {
return nil
}
return extractCNAMEChain(msg)
}
func IsReferral(msg *dns.Msg) bool {
if msg == nil || msg.Rcode != dns.RcodeSuccess || len(msg.Answer) > 0 {
return false
}
return hasNSRecords(msg.Ns) && !msg.Authoritative
}
func IsNODATA(msg *dns.Msg) bool {
if msg == nil || msg.Rcode != dns.RcodeSuccess {
return false
}
if len(msg.Answer) > 0 {
return false
}
if IsReferral(msg) {
return false
}
return true
}
func HasCNAMEChain(msg *dns.Msg) bool {
if msg == nil {
return false
}
return len(extractCNAMEChain(msg)) > 0
}
func FormatRecord(rr dns.RR) string {
if rr == nil {
return ""
}
header := rr.Header()
return fmt.Sprintf("%s %d %s %s %s",
header.Name,
header.Ttl,
dns.ClassToString[header.Class],
QNameType(header.Rrtype),
rr.String(),
)
}