or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

client.mdconstants.mddnssec.mddynamic-updates.mdedns0.mdindex.mdmessaging.mdrr-types.mdserver.mdtsig.mdutilities.mdzone-parsing.mdzone-transfer.md
tile.json

utilities.mddocs/

DNS Utilities

Helper functions for domain name manipulation, resource record operations, time conversions, and common DNS tasks.

Domain Name Utilities

Domain Name Validation and Formatting

// 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

Domain Name Comparison

// 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

Domain Name Parsing

// 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)

Label Navigation

// 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)

Reverse DNS

// 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)

Origin Manipulation (dnsutil package)

// 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

Resource Record Utilities

RR Operations

// 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

Message Validation

// IsMsg performs sanity checks on DNS message buffer
// Validates binary payload structure
func IsMsg(buf []byte) error

Time Utilities

DNSSEC Time Conversion

// 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)

Message ID Generation

// 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

DANE and Security Utilities

// 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)

NSEC3 Hash Utilities

// 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

RR Field Access Utilities

// 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

Deduplication Utilities

// 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

Wire Format Utilities

Domain Name Packing

// 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)

Usage Examples

Domain Name Validation

// 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

FQDN Handling

// 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.

Domain Name Comparison

// 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

Splitting Domain Names

// 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: 0

Label Navigation

name := "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.
}

Reverse DNS Lookups

// 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.

Origin Manipulation

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.

Resource Record Operations

// 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)

RRset Validation

// 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

DNSSEC Time Conversion

// 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))

Message ID Generation

// 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

Message Validation

// 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
}

Domain Name Packing/Unpacking

// 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.

Complete Zone Processing Example

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
}

Building Zone File with Origin

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)

Performance Considerations

Domain Name Operations

  • Use IsFqdn() before calling Fqdn() to avoid unnecessary allocation
  • Cache results of Split() when performing multiple label operations
  • CompareDomainName() stops at first inequality for efficiency

RR Operations

  • Copy() performs deep copy - avoid when shallow copy sufficient
  • IsDuplicate() compares full RDATA - expensive for large records
  • Use IsRRset() before DNSSEC operations to ensure valid input

Compression

  • Reuse compression maps across multiple PackDomainName calls
  • Compression most effective when packing similar domain names
  • Maximum compression offset is 16383 (14-bit pointer)

Related Topics

  • DNS Messaging - Message construction and manipulation
  • Zone Parsing - Zone file parsing with directives
  • DNSSEC Operations - DNSSEC time handling
  • Resource Record Types - All RR types for Copy/IsDuplicate