Helper functions for domain name manipulation, resource record operations, time conversions, and common DNS tasks.
// IsDomainName checks if a string is a valid DNS domain name
// Returns label count and whether valid
// Validates length limits: 63 chars per label, 255 octets total
func IsDomainName(s string) (labels int, ok bool)
// IsFqdn checks if domain name is fully qualified (ends with .)
func IsFqdn(s string) bool
// Fqdn returns fully qualified domain name from s
// If already FQDN, returns s unchanged
func Fqdn(s string) string
// CanonicalName returns domain name in canonical form
// Canonical form is lowercase and fully qualified (RFC 4034 Section 6.2)
func CanonicalName(s string) string// CompareDomainName compares s1 and s2 from the right
// Returns number of labels in common starting from the right
// Names are downcased before comparison
// s1 and s2 must be syntactically valid domain names
func CompareDomainName(s1, s2 string) (n int)
// IsSubDomain checks if child is subdomain of parent
// Returns true if child is subdomain or equal to parent
func IsSubDomain(parent, child string) bool// SplitDomainName splits name into labels
// "www.miek.nl." returns []string{"www", "miek", "nl"}
// ".www.miek.nl." returns []string{"", "www", "miek", "nl"}
// Root label (.) returns nil
// s must be syntactically valid domain name
func SplitDomainName(s string) (labels []string)
// Split splits name into label indexes
// "www.miek.nl." returns []int{0, 4, 9}
// Root name (.) returns nil
// s must be syntactically valid domain name
func Split(s string) []int
// CountLabel counts number of labels in domain name
// s must be syntactically valid domain name
func CountLabel(s string) (labels int)// NextLabel returns index of start of next label
// offset: starting position (negative causes panic)
// Returns (index, end) where end=true at string end
func NextLabel(s string, offset int) (i int, end bool)
// PrevLabel returns index of label jumping n labels left from right
// n: number of labels to jump
// Returns (index, start) where start=true when overshot beginning
func PrevLabel(s string, n int) (i int, start bool)// ReverseAddr returns in-addr.arpa or ip6.arpa hostname for IP address
// Suitable for reverse DNS (PTR) record lookups
// Returns error if IP address cannot be parsed
func ReverseAddr(addr string) (arpa string, err error)// AddOrigin adds origin to s if s is not already FQDN
// "@" represents the apex domain (RFC 1035 Section 5.1)
func AddOrigin(s, origin string) string
// TrimDomainName trims origin from s if s is subdomain
// Never returns empty string, returns "@" for apex instead
func TrimDomainName(s, origin string) string// Copy creates deep copy of RR
func Copy(r RR) RR
// Len returns wire format length of RR in bytes
func Len(r RR) int
// IsDuplicate checks if r1 and r2 are duplicates (excluding TTL)
// Header and RDATA must be identical
// Having identical RRs in message is protocol violation
func IsDuplicate(r1, r2 RR) bool
// IsRRset reports whether RRs form valid RRset per RFC 2181
// RRs must have same type, name, and class
func IsRRset(rrset []RR) bool// IsMsg performs sanity checks on DNS message buffer
// Validates binary payload structure
func IsMsg(buf []byte) error// TimeToString converts RRSIG inception/expiration to string
// Format: "20060102150405" (YYYYMMDDHHmmss)
// Takes serial arithmetic (RFC 1982) into account
func TimeToString(t uint32) string
// StringToTime converts string to RRSIG inception/expiration time
// Format: "20060102150405" (YYYYMMDDHHmmss)
// Returns 32-bit Unix timestamp
// Takes serial arithmetic (RFC 1982) into account
func StringToTime(s string) (uint32, error)// Id returns 16-bit random number for message ID
// Drawn from cryptographically secure random number generator
// This is a variable that can be reassigned for testing
var Id = func() uint16// CertificateToDANE generates TLSA/SMIMEA record data from certificate
// selector: 0 (full cert), 1 (SubjectPublicKeyInfo)
// matchingType: 0 (exact), 1 (SHA-256), 2 (SHA-512)
// Returns hex-encoded hash or certificate data
func CertificateToDANE(selector, matchingType uint8, cert *x509.Certificate) (string, error)
// TLSAName generates TLSA record name from service, network, and host
// service: port number or service name (e.g., "443" or "https")
// network: "tcp" or "udp"
// name: hostname
// Returns "_<port>._<protocol>.<name>" format
func TLSAName(name, service, network string) (string, error)
// SMIMEAName generates SMIMEA record name from email address
// email: email address (local@domain)
// domain: base domain for SMIMEA lookups
// Returns hash-based name for SMIMEA record
func SMIMEAName(email, domain string) (string, error)// HashName generates NSEC3 hash of domain name
// label: domain name to hash
// ha: hash algorithm (1 = SHA1)
// iter: iterations count
// salt: hex-encoded salt
// Returns base32-encoded hash (no padding)
func HashName(label string, ha uint8, iter uint16, salt string) string// NumField returns number of rdata fields in RR
// Useful for generic RR inspection
func NumField(r RR) int
// Field returns i-th rdata field as string
// i: zero-based field index
// Returns empty string if index out of range
func Field(r RR, i int) string// Dedup removes duplicate RRs from slice
// m: optional map for tracking (pass nil for new map)
// Returns new slice with duplicates removed
// Uses IsDuplicate for comparison
func Dedup(rrs []RR, m map[string]RR) []RR// PackDomainName packs domain name into wire format
// msg: buffer to pack into
// off: offset in buffer
// compression: compression map (nil to disable)
// compress: enable/disable compression
// Returns new offset and error
func PackDomainName(s string, msg []byte, off int, compression map[string]int, compress bool) (off1 int, err error)
// UnpackDomainName unpacks domain name from wire format
// msg: buffer containing packed name
// off: offset to start unpacking
// Returns domain name, new offset, error
func UnpackDomainName(msg []byte, off int) (string, int, error)// Check if valid domain name
labels, ok := dns.IsDomainName("www.example.com")
if ok {
fmt.Printf("Valid domain with %d labels\n", labels) // Output: Valid domain with 3 labels
}
// Invalid domain
_, ok = dns.IsDomainName("invalid..domain")
fmt.Println(ok) // Output: false// Check if FQDN
fmt.Println(dns.IsFqdn("example.com.")) // true
fmt.Println(dns.IsFqdn("example.com")) // false
// Convert to FQDN
name := dns.Fqdn("example.com")
fmt.Println(name) // Output: example.com.
// Canonical form (lowercase + FQDN)
canonical := dns.CanonicalName("WWW.Example.COM")
fmt.Println(canonical) // Output: www.example.com.// Compare domains from right
n := dns.CompareDomainName("www.miek.nl.", "miek.nl.")
fmt.Println(n) // Output: 2 (common labels: miek, nl)
n = dns.CompareDomainName("www.miek.nl.", "www.bla.nl.")
fmt.Println(n) // Output: 1 (common label: nl)
// Check subdomain relationship
isChild := dns.IsSubDomain("example.com.", "www.example.com.")
fmt.Println(isChild) // Output: true
isChild = dns.IsSubDomain("example.com.", "example.org.")
fmt.Println(isChild) // Output: false
// Same domain is considered subdomain
isChild = dns.IsSubDomain("example.com.", "example.com.")
fmt.Println(isChild) // Output: true// Split into labels
labels := dns.SplitDomainName("www.miek.nl.")
fmt.Println(labels) // Output: [www miek nl]
// Split into indexes
indexes := dns.Split("www.miek.nl.")
fmt.Println(indexes) // Output: [0 4 9]
// Count labels
count := dns.CountLabel("www.example.com.")
fmt.Println(count) // Output: 3
// Root label
fmt.Println(dns.CountLabel(".")) // Output: 0name := "www.example.com."
// Navigate forward through labels
offset := 0
for {
next, end := dns.NextLabel(name, offset)
if end {
break
}
label := name[offset:next-1]
fmt.Println(label)
offset = next
}
// Output: www, example, com
// Navigate backward
idx, start := dns.PrevLabel("www.example.com.", 1)
if !start {
fmt.Println(name[idx:]) // Output: com.
}
idx, start = dns.PrevLabel("www.example.com.", 2)
if !start {
fmt.Println(name[idx:]) // Output: example.com.
}// IPv4 reverse lookup
arpa, err := dns.ReverseAddr("192.0.2.1")
if err != nil {
log.Fatal(err)
}
fmt.Println(arpa) // Output: 1.2.0.192.in-addr.arpa.
// Query the reverse
m := new(dns.Msg)
m.SetQuestion(arpa, dns.TypePTR)
r, err := dns.Exchange(m, "8.8.8.8:53")
// IPv6 reverse lookup
arpa, err = dns.ReverseAddr("2001:db8::1")
if err != nil {
log.Fatal(err)
}
fmt.Println(arpa)
// Output: 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.import "github.com/miekg/dns/dnsutil"
// Add origin to relative names
name := dnsutil.AddOrigin("www", "example.com.")
fmt.Println(name) // Output: www.example.com.
// Already FQDN
name = dnsutil.AddOrigin("www.example.com.", "example.com.")
fmt.Println(name) // Output: www.example.com.
// @ represents apex
name = dnsutil.AddOrigin("@", "example.com.")
fmt.Println(name) // Output: example.com.
// Trim origin from subdomain
short := dnsutil.TrimDomainName("www.example.com.", "example.com.")
fmt.Println(short) // Output: www
// Apex returns @
short = dnsutil.TrimDomainName("example.com.", "example.com.")
fmt.Println(short) // Output: @
// Not a subdomain - returns original
short = dnsutil.TrimDomainName("www.example.org.", "example.com.")
fmt.Println(short) // Output: www.example.org.// Copy RR
original, _ := dns.NewRR("example.com. 3600 IN A 192.0.2.1")
copied := dns.Copy(original)
// Modify copy without affecting original
copied.Header().Ttl = 7200
// Check wire format length
length := dns.Len(original)
fmt.Printf("RR wire length: %d bytes\n", length)
// Check for duplicates (excluding TTL)
rr1, _ := dns.NewRR("example.com. 3600 IN A 192.0.2.1")
rr2, _ := dns.NewRR("example.com. 7200 IN A 192.0.2.1")
rr3, _ := dns.NewRR("example.com. 3600 IN A 192.0.2.2")
fmt.Println(dns.IsDuplicate(rr1, rr2)) // true (same except TTL)
fmt.Println(dns.IsDuplicate(rr1, rr3)) // false (different IP)// Valid RRset - same name, type, class
rrset := []dns.RR{
&dns.A{
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 3600},
A: net.ParseIP("192.0.2.1"),
},
&dns.A{
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 3600},
A: net.ParseIP("192.0.2.2"),
},
}
fmt.Println(dns.IsRRset(rrset)) // Output: true
// Invalid RRset - different types
invalidSet := []dns.RR{
&dns.A{
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 3600},
A: net.ParseIP("192.0.2.1"),
},
&dns.AAAA{
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 3600},
AAAA: net.ParseIP("2001:db8::1"),
},
}
fmt.Println(dns.IsRRset(invalidSet)) // Output: false// Convert Unix timestamp to DNSSEC time string
unixTime := uint32(time.Now().Unix())
timeStr := dns.TimeToString(unixTime)
fmt.Println(timeStr) // Output: 20240101120000 (YYYYMMDDHHmmss format)
// Parse DNSSEC time string
parsed, err := dns.StringToTime("20240101120000")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Unix timestamp: %d\n", parsed)
// Use in RRSIG records
rrsig := &dns.RRSIG{
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeRRSIG, Class: dns.ClassINET, Ttl: 3600},
TypeCovered: dns.TypeA,
Algorithm: dns.RSASHA256,
Labels: 2,
OrigTtl: 3600,
Expiration: parsed + 86400, // expires in 24 hours
Inception: parsed,
KeyTag: 12345,
SignerName: "example.com.",
Signature: "base64signature...",
}
// Convert back to string for display
fmt.Printf("Inception: %s\n", dns.TimeToString(rrsig.Inception))
fmt.Printf("Expiration: %s\n", dns.TimeToString(rrsig.Expiration))// Generate random message ID
id := dns.Id()
fmt.Printf("Message ID: %d\n", id)
// Use in message
m := new(dns.Msg)
m.Id = dns.Id()
m.SetQuestion("example.com.", dns.TypeA)
// Override for testing
oldId := dns.Id
dns.Id = func() uint16 { return 12345 } // Fixed ID for tests
testMsg := new(dns.Msg)
testMsg.Id = dns.Id()
fmt.Printf("Test message ID: %d\n", testMsg.Id) // Output: 12345
dns.Id = oldId // Restore// Validate DNS message buffer
buf, err := m.Pack()
if err != nil {
log.Fatal(err)
}
err = dns.IsMsg(buf)
if err != nil {
log.Printf("Invalid DNS message: %v\n", err)
} else {
fmt.Println("Valid DNS message")
}
// Check truncated buffer
err = dns.IsMsg(buf[:5])
if err != nil {
fmt.Println(err) // Output: dns: bad message header
}// Pack domain name with compression
msg := make([]byte, 512)
compression := make(map[string]int)
off, err := dns.PackDomainName("www.example.com.", msg, 0, compression, true)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Packed %d bytes\n", off)
// Pack another name with compression
off, err = dns.PackDomainName("mail.example.com.", msg, off, compression, true)
if err != nil {
log.Fatal(err)
}
// "example.com." is compressed using pointer to first occurrence
// Unpack domain name
name, newOff, err := dns.UnpackDomainName(msg, 0)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Unpacked: %s\n", name) // Output: www.example.com.func ProcessZoneFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
zp := dns.NewZoneParser(file, "", filename)
zp.SetDefaultTTL(3600)
// Group records by name
zone := make(map[string][]dns.RR)
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
// Get canonical name for case-insensitive grouping
name := dns.CanonicalName(rr.Header().Name)
zone[name] = append(zone[name], rr)
}
if err := zp.Err(); err != nil {
return err
}
// Validate and process RRsets
for name, rrset := range zone {
// Group by type
byType := make(map[uint16][]dns.RR)
for _, rr := range rrset {
byType[rr.Header().Rrtype] = append(byType[rr.Header().Rrtype], rr)
}
// Check each type forms valid RRset
for rrtype, rrs := range byType {
if !dns.IsRRset(rrs) {
return fmt.Errorf("invalid RRset for %s type %d", name, rrtype)
}
// Check for duplicates
for i := 0; i < len(rrs); i++ {
for j := i + 1; j < len(rrs); j++ {
if dns.IsDuplicate(rrs[i], rrs[j]) {
return fmt.Errorf("duplicate RR: %s", rrs[i].String())
}
}
}
}
}
fmt.Printf("Processed %d unique names\n", len(zone))
return nil
}import "github.com/miekg/dns/dnsutil"
func BuildZoneFile(origin string) string {
var zone strings.Builder
zone.WriteString(fmt.Sprintf("$ORIGIN %s\n", origin))
zone.WriteString("$TTL 3600\n\n")
// Relative names will be expanded
records := []struct {
name string
rrtype uint16
rdata string
}{
{"@", dns.TypeSOA, "ns1 admin 2024010101 3600 1800 604800 86400"},
{"@", dns.TypeNS, "ns1"},
{"@", dns.TypeNS, "ns2"},
{"ns1", dns.TypeA, "192.0.2.1"},
{"ns2", dns.TypeA, "192.0.2.2"},
{"www", dns.TypeA, "192.0.2.10"},
{"mail", dns.TypeA, "192.0.2.20"},
{"@", dns.TypeMX, "10 mail"},
}
for _, rec := range records {
// Use dnsutil to handle relative names
fullName := dnsutil.AddOrigin(rec.name, origin)
zone.WriteString(fmt.Sprintf("%s IN %s %s\n",
dnsutil.TrimDomainName(fullName, origin),
dns.TypeToString[rec.rrtype],
rec.rdata))
}
return zone.String()
}
// Usage
zoneData := BuildZoneFile("example.com.")
fmt.Println(zoneData)IsFqdn() before calling Fqdn() to avoid unnecessary allocationSplit() when performing multiple label operationsCompareDomainName() stops at first inequality for efficiencyCopy() performs deep copy - avoid when shallow copy sufficientIsDuplicate() compares full RDATA - expensive for large recordsIsRRset() before DNSSEC operations to ensure valid input