mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
198 lines
5.1 KiB
Go
198 lines
5.1 KiB
Go
// Package resutil provides shared DNS resolution utilities
|
|
package resutil
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
|
|
"github.com/miekg/dns"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// GenerateRequestID creates a random 8-character hex string for request tracing.
|
|
func GenerateRequestID() string {
|
|
bytes := make([]byte, 4)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
log.Errorf("generate request ID: %v", err)
|
|
return ""
|
|
}
|
|
return hex.EncodeToString(bytes)
|
|
}
|
|
|
|
// IPsToRRs converts a slice of IP addresses to DNS resource records.
|
|
// IPv4 addresses become A records, IPv6 addresses become AAAA records.
|
|
func IPsToRRs(name string, ips []netip.Addr, ttl uint32) []dns.RR {
|
|
var result []dns.RR
|
|
|
|
for _, ip := range ips {
|
|
if ip.Is6() {
|
|
result = append(result, &dns.AAAA{
|
|
Hdr: dns.RR_Header{
|
|
Name: name,
|
|
Rrtype: dns.TypeAAAA,
|
|
Class: dns.ClassINET,
|
|
Ttl: ttl,
|
|
},
|
|
AAAA: ip.AsSlice(),
|
|
})
|
|
} else {
|
|
result = append(result, &dns.A{
|
|
Hdr: dns.RR_Header{
|
|
Name: name,
|
|
Rrtype: dns.TypeA,
|
|
Class: dns.ClassINET,
|
|
Ttl: ttl,
|
|
},
|
|
A: ip.AsSlice(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// NetworkForQtype returns the network string ("ip4" or "ip6") for a DNS query type.
|
|
// Returns empty string for unsupported types.
|
|
func NetworkForQtype(qtype uint16) string {
|
|
switch qtype {
|
|
case dns.TypeA:
|
|
return "ip4"
|
|
case dns.TypeAAAA:
|
|
return "ip6"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
type resolver interface {
|
|
LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
|
|
}
|
|
|
|
// chainedWriter is implemented by ResponseWriters that carry request metadata
|
|
type chainedWriter interface {
|
|
RequestID() string
|
|
SetMeta(key, value string)
|
|
}
|
|
|
|
// GetRequestID extracts a request ID from the ResponseWriter if available,
|
|
// otherwise generates a new one.
|
|
func GetRequestID(w dns.ResponseWriter) string {
|
|
if cw, ok := w.(chainedWriter); ok {
|
|
if id := cw.RequestID(); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
return GenerateRequestID()
|
|
}
|
|
|
|
// SetMeta sets metadata on the ResponseWriter if it supports it.
|
|
func SetMeta(w dns.ResponseWriter, key, value string) {
|
|
if cw, ok := w.(chainedWriter); ok {
|
|
cw.SetMeta(key, value)
|
|
}
|
|
}
|
|
|
|
// LookupResult contains the result of an external DNS lookup
|
|
type LookupResult struct {
|
|
IPs []netip.Addr
|
|
Rcode int
|
|
Err error // Original error for caller's logging needs
|
|
}
|
|
|
|
// LookupIP performs a DNS lookup and determines the appropriate rcode.
|
|
func LookupIP(ctx context.Context, r resolver, network, host string, qtype uint16) LookupResult {
|
|
ips, err := r.LookupNetIP(ctx, network, host)
|
|
if err != nil {
|
|
return LookupResult{
|
|
Rcode: getRcodeForError(ctx, r, host, qtype, err),
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
// Unmap IPv4-mapped IPv6 addresses that some resolvers may return
|
|
for i, ip := range ips {
|
|
ips[i] = ip.Unmap()
|
|
}
|
|
|
|
return LookupResult{
|
|
IPs: ips,
|
|
Rcode: dns.RcodeSuccess,
|
|
}
|
|
}
|
|
|
|
func getRcodeForError(ctx context.Context, r resolver, host string, qtype uint16, err error) int {
|
|
var dnsErr *net.DNSError
|
|
if !errors.As(err, &dnsErr) {
|
|
return dns.RcodeServerFailure
|
|
}
|
|
|
|
if dnsErr.IsNotFound {
|
|
return getRcodeForNotFound(ctx, r, host, qtype)
|
|
}
|
|
|
|
return dns.RcodeServerFailure
|
|
}
|
|
|
|
// getRcodeForNotFound distinguishes between NXDOMAIN (domain doesn't exist) and NODATA
|
|
// (domain exists but no records of requested type) by checking the opposite record type.
|
|
//
|
|
// musl libc (the reason we need this distinction) only queries A/AAAA pairs in getaddrinfo,
|
|
// so checking the opposite A/AAAA type is sufficient. Other record types (MX, TXT, etc.)
|
|
// are not queried by musl and don't need this handling.
|
|
func getRcodeForNotFound(ctx context.Context, r resolver, domain string, originalQtype uint16) int {
|
|
// Try querying for a different record type to see if the domain exists
|
|
// If the original query was for AAAA, try A. If it was for A, try AAAA.
|
|
// This helps distinguish between NXDOMAIN and NODATA.
|
|
var alternativeNetwork string
|
|
switch originalQtype {
|
|
case dns.TypeAAAA:
|
|
alternativeNetwork = "ip4"
|
|
case dns.TypeA:
|
|
alternativeNetwork = "ip6"
|
|
default:
|
|
return dns.RcodeNameError
|
|
}
|
|
|
|
if _, err := r.LookupNetIP(ctx, alternativeNetwork, domain); err != nil {
|
|
var dnsErr *net.DNSError
|
|
if errors.As(err, &dnsErr) && dnsErr.IsNotFound {
|
|
// Alternative query also returned not found - domain truly doesn't exist
|
|
return dns.RcodeNameError
|
|
}
|
|
// Some other error (timeout, server failure, etc.) - can't determine, assume domain exists
|
|
return dns.RcodeSuccess
|
|
}
|
|
|
|
// Alternative query succeeded - domain exists but has no records of this type
|
|
return dns.RcodeSuccess
|
|
}
|
|
|
|
// FormatAnswers formats DNS resource records for logging.
|
|
func FormatAnswers(answers []dns.RR) string {
|
|
if len(answers) == 0 {
|
|
return "[]"
|
|
}
|
|
|
|
parts := make([]string, 0, len(answers))
|
|
for _, rr := range answers {
|
|
switch r := rr.(type) {
|
|
case *dns.A:
|
|
parts = append(parts, r.A.String())
|
|
case *dns.AAAA:
|
|
parts = append(parts, r.AAAA.String())
|
|
case *dns.CNAME:
|
|
parts = append(parts, "CNAME:"+r.Target)
|
|
case *dns.PTR:
|
|
parts = append(parts, "PTR:"+r.Ptr)
|
|
default:
|
|
parts = append(parts, dns.TypeToString[rr.Header().Rrtype])
|
|
}
|
|
}
|
|
return "[" + strings.Join(parts, ", ") + "]"
|
|
}
|