246 lines
4.9 KiB
Go
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(),
|
|
)
|
|
}
|