tessl install tessl/golang-github-com-miekg--dns@1.1.1Complete DNS library for Go with full protocol control, DNSSEC support, and both client and server programming capabilities
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