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
Complete zone file parser supporting RFC 1035 syntax with directives and error handling.
Iterative parser for RFC 1035 style zone files.
type ZoneParser struct {
// contains filtered or unexported fields
}
// NewZoneParser creates a new zone parser
// r: reader containing zone data
// origin: initial origin (default zone)
// file: filename for error reporting
func NewZoneParser(r io.Reader, origin, file string) *ZoneParser
// Next returns next RR from zone
// Returns (RR, true) if successful, (nil, false) at end or error
func (zp *ZoneParser) Next() (RR, bool)
// Err returns first non-EOF error encountered
func (zp *ZoneParser) Err() error
// Comment returns comment text from last parsed RR
func (zp *ZoneParser) Comment() string
// SetDefaultTTL sets default TTL for zone
func (zp *ZoneParser) SetDefaultTTL(ttl uint32)
// SetIncludeAllowed controls $INCLUDE directive support
// Default: false (disabled for security)
func (zp *ZoneParser) SetIncludeAllowed(v bool)
// SetIncludeFS provides fs.FS for $INCLUDE file resolution
func (zp *ZoneParser) SetIncludeFS(fsys fs.FS)Error type with location information.
type ParseError struct {
// contains filtered or unexported fields
}
func (e *ParseError) Error() string
func (e *ParseError) Unwrap() error// NewRR parses single RR from string
// Default class: IN, default TTL: 3600, default origin: .
func NewRR(s string) (RR, error)
// ReadRR reads single RR from reader
// file: used in error reporting and $INCLUDE resolution
func ReadRR(r io.Reader, file string) (RR, error)
// ParseZone parses complete zone from reader
// Returns slice of all RRs
func ParseZone(r io.Reader, origin, file string) ([]RR, error)Sets origin for relative domain names.
$ORIGIN example.com.
www IN A 192.0.2.1 ; expands to www.example.com.Sets default TTL for subsequent records.
$TTL 3600
example.com. IN A 192.0.2.1 ; uses 3600 TTLIncludes external zone file (disabled by default).
$INCLUDE /path/to/hosts.zone
$INCLUDE hosts.zone example.com. ; with custom originGenerates multiple RRs from template.
$GENERATE 1-100 host-$.example.com. IN A 192.0.2.$
; Creates host-1.example.com through host-100.example.comzoneData := `
$ORIGIN example.com.
$TTL 3600
@ IN SOA ns1 admin 2024010101 3600 1800 604800 86400
@ IN NS ns1
@ IN NS ns2
ns1 IN A 192.0.2.1
ns2 IN A 192.0.2.2
www IN A 192.0.2.10
mail IN A 192.0.2.20
@ IN MX 10 mail
`
zp := dns.NewZoneParser(strings.NewReader(zoneData), "", "")
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
fmt.Println(rr.String())
}
if err := zp.Err(); err != nil {
log.Fatal(err)
}file, err := os.Open("/etc/bind/db.example.com")
if err != nil {
log.Fatal(err)
}
defer file.Close()
zp := dns.NewZoneParser(file, "example.com.", file.Name())
zp.SetDefaultTTL(3600)
var records []dns.RR
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
records = append(records, rr)
}
if err := zp.Err(); err != nil {
log.Fatal(err)
}
fmt.Printf("Parsed %d records\n", len(records))// Simple parsing
rr, err := dns.NewRR("example.com. 3600 IN A 192.0.2.1")
if err != nil {
log.Fatal(err)
}
if a, ok := rr.(*dns.A); ok {
fmt.Printf("IPv4: %s\n", a.A)
}zoneData := `
example.com. IN A 192.0.2.1 ; web server
example.com. IN MX 10 mail.example.com. ; mail server
`
zp := dns.NewZoneParser(strings.NewReader(zoneData), "", "")
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
comment := zp.Comment()
if comment != "" {
fmt.Printf("%s ; %s\n", rr.String(), comment)
} else {
fmt.Println(rr.String())
}
}// Create filesystem rooted at zone directory
fsys := os.DirFS("/var/named/zones")
zp := dns.NewZoneParser(file, "example.com.", "db.example.com")
zp.SetIncludeAllowed(true)
zp.SetIncludeFS(fsys)
// Zone file can now use: $INCLUDE hosts.zone
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
fmt.Println(rr.String())
}zoneData := `
$ORIGIN example.com.
$GENERATE 1-254 host-$ IN A 192.0.2.$
`
zp := dns.NewZoneParser(strings.NewReader(zoneData), "", "")
count := 0
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
count++
// Generates host-1 through host-254
}
fmt.Printf("Generated %d records\n", count) // Output: Generated 254 recordszoneData := `
; Reverse DNS entries
$GENERATE 1-254 $.2.0.192.in-addr.arpa. IN PTR host-$.example.com.
; IPv6 addresses
$GENERATE 0-255 host-${0,2,x}.example.com. IN AAAA 2001:db8::${0,2,x}
; With step value
$GENERATE 0-100/10 host$ IN A 192.0.2.$
; Creates host0, host10, host20, ..., host100
`
zp := dns.NewZoneParser(strings.NewReader(zoneData), "", "")
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
fmt.Println(rr.String())
}zoneData := `
example.com. IN A 192.0.2.1
invalid line here
example.com. IN MX 10 mail.example.com.
`
zp := dns.NewZoneParser(strings.NewReader(zoneData), "", "")
var records []dns.RR
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
records = append(records, rr)
}
if err := zp.Err(); err != nil {
if pe, ok := err.(*dns.ParseError); ok {
fmt.Printf("Parse error: %v\n", pe)
// Error includes line and column information
}
}zoneData := `
@ IN SOA ns1 admin 2024010101 3600 1800 604800 86400
www IN A 192.0.2.1
mail IN A 192.0.2.2
`
// Origin is prepended to relative names
zp := dns.NewZoneParser(strings.NewReader(zoneData), "example.com.", "")
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
// @ expands to example.com.
// www expands to www.example.com.
fmt.Println(rr.String())
}zoneData := `
example.com. IN A 192.0.2.1
www.example.com. IN A 192.0.2.10
`
zp := dns.NewZoneParser(strings.NewReader(zoneData), "", "")
zp.SetDefaultTTL(7200) // 2 hours
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
// Records without explicit TTL use 7200
fmt.Printf("TTL: %d, RR: %s\n", rr.Header().Ttl, rr.String())
}zoneData := `
$ORIGIN example.com.
$TTL 3600
@ IN SOA ns1 admin 2024010101 3600 1800 604800 86400
@ IN NS ns1
@ IN DNSKEY 256 3 8 AwEAA...
@ IN DNSKEY 257 3 8 AwEBB... ; KSK
@ IN DS 12345 8 2 ABC123...
@ IN A 192.0.2.1
@ IN RRSIG A 8 2 3600 20240201000000 20240101000000 12345 example.com. signature...
`
zp := dns.NewZoneParser(strings.NewReader(zoneData), "", "")
var dnskeys []*dns.DNSKEY
var rrsigs []*dns.RRSIG
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
switch v := rr.(type) {
case *dns.DNSKEY:
dnskeys = append(dnskeys, v)
case *dns.RRSIG:
rrsigs = append(rrsigs, v)
}
}
fmt.Printf("Found %d DNSKEYs and %d RRSIGs\n", len(dnskeys), len(rrsigs))func LoadZone(filename string) (map[string][]dns.RR, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
zone := make(map[string][]dns.RR)
zp := dns.NewZoneParser(file, "", filename)
zp.SetDefaultTTL(3600)
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
name := rr.Header().Name
zone[name] = append(zone[name], rr)
}
if err := zp.Err(); err != nil {
return nil, err
}
return zone, nil
}
// Usage
zone, err := LoadZone("/etc/bind/db.example.com")
if err != nil {
log.Fatal(err)
}
// Query zone
if rrs, ok := zone["www.example.com."]; ok {
for _, rr := range rrs {
fmt.Println(rr.String())
}
}func ParseAndValidateZone(r io.Reader) error {
zp := dns.NewZoneParser(r, "example.com.", "")
zp.SetDefaultTTL(3600)
var soaCount int
nameSet := make(map[string]bool)
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
// Track names
nameSet[rr.Header().Name] = true
// Count SOA records
if _, isSoa := rr.(*dns.SOA); isSoa {
soaCount++
}
// Validate specific record types
switch v := rr.(type) {
case *dns.A:
if v.A == nil || v.A.To4() == nil {
return fmt.Errorf("invalid A record: %s", rr.String())
}
case *dns.MX:
if v.Mx == "" {
return fmt.Errorf("MX record missing exchanger: %s", rr.String())
}
}
}
if err := zp.Err(); err != nil {
return err
}
// Validate zone structure
if soaCount == 0 {
return fmt.Errorf("zone missing SOA record")
}
if soaCount > 1 {
return fmt.Errorf("zone has multiple SOA records")
}
return nil
}// Process zone without loading everything into memory
func ProcessZoneIncremental(filename string, processor func(dns.RR) error) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
zp := dns.NewZoneParser(file, "", filename)
zp.SetDefaultTTL(3600)
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
if err := processor(rr); err != nil {
return err
}
}
return zp.Err()
}
// Usage: Count record types
typeCounts := make(map[uint16]int)
err := ProcessZoneIncremental("/var/named/db.example.com", func(rr dns.RR) error {
typeCounts[rr.Header().Rrtype]++
return nil
}); Absolute name
example.com. IN A 192.0.2.1
; Relative name (appended to $ORIGIN)
www IN A 192.0.2.2
; @ represents current origin
@ IN A 192.0.2.3
; Blank owner inherits from previous
example.com. IN A 192.0.2.1
IN MX 10 mail.example.com.; Explicit TTL
example.com. 7200 IN A 192.0.2.1
; Default TTL from $TTL
$TTL 3600
example.com. IN A 192.0.2.1
; TTL can appear after class
example.com. IN 7200 A 192.0.2.1; Default is IN (Internet)
example.com. IN A 192.0.2.1
; Other classes
example.com. CH TXT "Chaos class"
example.com. HS A 192.0.2.1; Using parentheses
example.com. IN SOA ns1.example.com. admin.example.com. (
2024010101 ; serial
3600 ; refresh
1800 ; retry
604800 ; expire
86400 ; minimum
)
; TXT records
example.com. IN TXT (
"v=spf1"
" mx"
" -all"
)// $INCLUDE is disabled by default for security
// When enabled, it can read arbitrary files
// Recommended: Use fs.FS to restrict access
restrictedFS := os.DirFS("/var/named/zones")
zp.SetIncludeFS(restrictedFS)
zp.SetIncludeAllowed(true)
// This prevents access to files outside the zone directory