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

zone-parsing.mddocs/

Zone File Parsing

Complete zone file parser supporting RFC 1035 syntax with directives and error handling.

Core Types

ZoneParser

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)

ParseError

Error type with location information.

type ParseError struct {
	// contains filtered or unexported fields
}

func (e *ParseError) Error() string
func (e *ParseError) Unwrap() error

Convenience Functions

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

Supported Directives

$ORIGIN

Sets origin for relative domain names.

$ORIGIN example.com.
www    IN A 192.0.2.1    ; expands to www.example.com.

$TTL

Sets default TTL for subsequent records.

$TTL 3600
example.com. IN A 192.0.2.1  ; uses 3600 TTL

$INCLUDE

Includes external zone file (disabled by default).

$INCLUDE /path/to/hosts.zone
$INCLUDE hosts.zone example.com.  ; with custom origin

$GENERATE

Generates multiple RRs from template.

$GENERATE 1-100 host-$.example.com. IN A 192.0.2.$
; Creates host-1.example.com through host-100.example.com

Usage Examples

Basic Parsing

zoneData := `
$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)
}

Parsing from File

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

Parsing Single RR

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

Handling Comments

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())
	}
}

Using $INCLUDE with fs.FS

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

Generating Records with $GENERATE

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 records

Advanced $GENERATE Patterns

zoneData := `
; 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())
}

Error Handling

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

Parsing with Custom Origin

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())
}

Setting Custom Default TTL

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())
}

Parsing DNSSEC Zone

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

Complete Zone Loading Function

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())
	}
}

Parsing Zone with Validation

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
}

Incremental Zone Processing

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

Zone File Syntax

Owner Name Rules

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

TTL Specification

; 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

Class Specification

; 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

Multi-line Records

; 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"
)

Parser Limitations

  • Maximum include depth: 7 levels
  • Maximum $GENERATE range: 65535 steps
  • Comments are preserved only for RRs, not standalone comment lines
  • Relative $INCLUDE paths depend on fs.FS implementation

Security Considerations

$INCLUDE Directive

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

Related Topics

  • Resource Record Types - All supported RR types
  • DNSSEC Operations - Parsing DNSSEC records
  • Zone Transfers - AXFR/IXFR zone loading
  • DNS Messaging - Using parsed RRs in messages