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