masterfile

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

masterfile

CI Go Reference

A Go library for parsing, editing and formatting DNS zone files (RFC 1035 "master files") without losing anything a human put there: comments in every position, blank lines, directives and the written form of values.

It is built for programs that edit zone files other people also edit by hand and that a name server (BIND, Knot, CoreDNS) then loads:

  • Parse keeps every entry and comment, never panics on any input, and gives every record a stable identity (owner relative to the apex, type, values, effective TTL) so records can be found, compared, replaced and removed regardless of position or $ORIGIN lines. New starts a zone from nothing.
  • NewRecord, NewDirective and NewComment build entries safely from parts that may come from network clients: a value can never add a second record, a directive or a comment.
  • Format writes one canonical, aligned layout that is idempotent.
  • Verify proves with an independent parser (miekg/dns) that the formatted text holds exactly the intended records and comments before it replaces the original.
  • ToASCII and ToUnicode convert internationalised names between their Unicode and xn-- forms, label by label, at your input and display boundaries.
  • InZone, LongestZone, ReverseName, ReverseAddr, IsReverseZone and ReversePrefix route names to zones and build or parse in-addr.arpa and ip6.arpa names and prefixes.
  • Apply runs patch-style operations atomically: Replace, Insert, Delete, DeleteOwner, AddRemove and Collapse edit RRsets in place, put new records where a human would (next to the owner's block, the _acme-challenge.* group, the siblings, the same type) and carry the comments of what they replace. Select, Owners, RRSets, Serial and SOA query; Check says whether a file is serveable on its own.
  • Commit renders only when something changed, through the format-and-verify path.
z, err := masterfile.Parse(data, "example.org.") // apex checked, owners resolved
results, err := z.Apply(
	masterfile.Replace{
		Selector: masterfile.Selector{Owner: "www", Types: []uint16{dns.TypeA}},
		Records:  []masterfile.RR{{Type: dns.TypeA, Content: "192.0.2.1"}},
	},
	masterfile.Insert{Owner: "_acme-challenge", Records: []masterfile.RR{{Type: dns.TypeTXT, Content: token}}},
)
out, changed, err := z.Commit(true) // nil, false when the zone already said so

Hand edits work too: range over z.Records(), splice z.Entries, set z.Changed = true, then Commit. A file can be built from nothing with New(apex) and the entry constructors.

The only dependency is github.com/miekg/dns for the RR type tables and the reference parser. Go 1.27 or later.

See docs/spec.md for the full specification and docs/DESIGN.md for implementation decisions.

License

Apache-2.0, see LICENSE.

Documentation

Overview

Package masterfile parses, edits and writes DNS zone files (RFC 1035 "master files") while keeping everything a human put there: comments in every position, blank lines, directives and the written form of values.

The package is a plain library over byte slices. It never reads or writes files itself and has no scheduling or network code. Its one third-party dependency is github.com/miekg/dns, used for the RR type and class name tables and as the independent reference parser in Verify.

Pipeline

Everything revolves around one type, Zone:

  1. Parse the text into a Zone (or start one with New). Parse finds the apex, checks it against the caller's expectation and makes every record owner explicit and relative to the apex, so entries can be found, moved, compared and replaced without regard to their position or to $ORIGIN lines.
  2. Edit it: range over Zone.Records and splice Zone.Entries by hand (setting Zone.Changed), or apply the operations [Zone.Replace], [Zone.Delete], [Zone.DeleteOwner], [Zone.AddRemove] and [Zone.Collapse], which place records where a human would and set Changed only when the zone's meaning changed. NewRecord, NewDirective and NewComment build entries safely from parts that may come from network clients.
  3. Zone.Commit renders the result: nothing while Changed is false, otherwise the canonical layout of Zone.Format, proven safe by Verify against the plain rendering of Zone.WritePlain. The caller writes the bytes.

Reformat is Parse and Format in one call for formatter commands.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrApexMismatch = errors.New("apex mismatch")
	ErrNotInZone    = errors.New("owner not in zone")
	ErrNotFound     = errors.New("record not found")
)

Errors of Parse and Zone.Apply. Zone.Commit returns errors wrapping ErrUnsafeWrite; Parse returns *ParseError for syntax errors.

View Source
var (
	ErrNoOrigin = errors.New("no origin")
	ErrNoSOA    = errors.New("no SOA record")
)

Errors of Zone.Check.

View Source
var ErrInvalidEntry = errors.New("invalid entry")

ErrInvalidEntry is wrapped by every error of NewDirective and NewComment.

View Source
var ErrInvalidName = errors.New("invalid name")

ErrInvalidName is wrapped by every error of ToASCII.

View Source
var ErrInvalidRecord = errors.New("invalid record")

ErrInvalidRecord is wrapped by every error of NewRecord.

View Source
var ErrUnsafeWrite = errors.New("unsafe write")

ErrUnsafeWrite is wrapped by every error of Verify.

Functions

func Absolute

func Absolute(name, origin string) string

Absolute resolves name against origin: "@" is the origin, an absolute name is itself, and any other name is appended to the origin. With an empty origin a relative name is taken as absolute (a trailing dot is added), which is what the reference parser does; "@" then resolves to "".

func Comments

func Comments(data []byte) ([]string, error)

Comments parses data with this package's parser and returns every comment in file order. It is exported for tests.

func EntriesEqual

func EntriesEqual(a, b []Entry) bool

EntriesEqual compares two entry lists by meaning: same length, pairwise Entry.Equal ignoring explicit TTLs, and equal effective TTLs for records (both unknown is equal; known versus unknown is not). The editing operations use it to decide whether they changed the zone.

func Fqdn

func Fqdn(name string) string

Fqdn returns name with a trailing dot added if it has none. An empty name becomes the root ".".

func InZone

func InZone(name, apex string) bool

InZone reports whether name is the apex or lies below it, both taken as absolute names and compared with NameEqual on a label boundary: "www.example.org." is in "example.org.", "myexample.org." is not.

func Increase

func Increase(serial string) string

Increase returns the serial to write after a change: a Unix time serial becomes max(now, serial+1); a date serial is incremented by one with no date logic ("2024123199" becomes "2024123200"). Anything else is returned unchanged. Two bumps within one second still increase.

func IsReverseZone

func IsReverseZone(apex string) bool

IsReverseZone reports whether apex lies under in-addr.arpa or ip6.arpa (ASCII case-insensitive; a trailing dot is optional).

func LongestZone

func LongestZone(name string, apexes []string) (apex string, ok bool)

LongestZone returns the apex among apexes that contains name (see InZone) and has the most labels, so the most specific zone wins. ok is false when no apex contains the name.

func NameEqual

func NameEqual(a, b string) bool

NameEqual compares two names case-insensitively for ASCII letters only (RFC 4343); every other byte must match exactly. No normalisation is applied: "www" and "www." differ.

func Quote

func Quote(s string) string

Quote renders s as a zone file quoted string: it is wrapped in double quotes, '"' and '\' are prefixed with a backslash, and bytes below 0x20 and byte 0x7F are written as "\DDD". All other bytes, including UTF-8 and bytes above 0x7F, are copied. Parsing the result yields exactly s.

func Records

func Records(apex string, data []byte) (map[string]int, error)

Records parses data with the reference parser (miekg/dns, origin apex, $INCLUDE disallowed) and returns the multiset of its records as rr.String() lower-cased with the SOA serial zeroed, mapped to their counts. It is exported for tests.

func Reformat

func Reformat(data []byte, apex string, w io.Writer, bumpSerial bool) error

Reformat parses data (see Parse for how apex is used) and writes the canonical layout to w. It is Parse and Zone.Format in one call, for formatter commands and tests.

func Relative

func Relative(name, origin string) string

Relative returns the part of the absolute name under the absolute origin: the origin itself gives "@", "www.example.org." under "example.org." gives "www". A name that is "@", not absolute, or not under the origin is returned unchanged. The origin must match on a label boundary: an escaped dot is not a boundary, and "myexample.org." is not under "example.org.". Under the root origin "." an absolute name loses its trailing dot, unless that would leave another one (".." stays as it is).

func ReverseAddr

func ReverseAddr(name string) (netip.Addr, bool)

ReverseAddr parses a full reverse-lookup name (four octet labels under in-addr.arpa, or 32 nibble labels under ip6.arpa) back into an address. Partial names, RFC 2317 classless labels and anything else give false.

func ReverseName

func ReverseName(addr netip.Addr) string

ReverseName returns the absolute reverse-lookup name of addr: "1.2.0.192.in-addr.arpa." for 192.0.2.1, the 32 reversed nibbles under "ip6.arpa." for an IPv6 address. An IPv4-mapped IPv6 address is unmapped first. An invalid address gives "".

func ReversePrefix

func ReversePrefix(apex string) (netip.Prefix, bool)

ReversePrefix returns the address prefix a reverse zone apex covers: "3.2.1.in-addr.arpa." is 1.2.3.0/24 (one to four octet labels give /8 to /32), "8.b.d.0.1.0.0.2.ip6.arpa." is 2001:db8::/32 (one to 32 nibble labels give /4 to /128). RFC 2317 classless names such as "0/25.2.0.192.in-addr.arpa." and anything else give false.

func StringToTTL

func StringToTTL(s string) (uint32, bool)

StringToTTL parses a TTL: a decimal number of seconds, or a BIND duration made of <digits><unit> parts with units s, m, h, d and w in any case, optionally ending in bare digits (seconds), such as "1h30m", "2W" or "90". The value must fit in 32 bits. It reports false for anything else, including an empty string.

func ToASCII

func ToASCII(name string) (string, error)

ToASCII converts the Unicode labels of a name to their A-label ("xn--") form for use in a zone file. ASCII labels are kept byte for byte, so "@", "*", "_acme-challenge", upper case and backslash escapes survive unchanged; only labels with bytes above 0x7F are converted. A non-ASCII label containing a backslash is rejected because escapes cannot be mapped. Errors wrap ErrInvalidName.

func ToUnicode

func ToUnicode(name string) string

ToUnicode converts the A-labels ("xn--") of a name to Unicode for display. Every other label is kept as it is. If any label fails to decode, the whole name is returned unchanged.

func UserComment

func UserComment(c string) string

UserComment returns the user-written part of a serial comment generated by this package: "; serial <rendering> ; note" gives "; note" and a comment holding only the generated text gives "". Any other comment is returned unchanged. Verification uses it so that regenerated renderings do not count as lost comments.

func Verify

func Verify(apex string, plain, formatted []byte) error

Verify proves that formatted is a safe replacement for plain (both renderings of the same zone, see Zone.WritePlain and Zone.Format): both must parse with the reference parser, hold the same multiset of records (SOA serial ignored), and every user comment of plain (see UserComment) must occur in a comment of formatted. Errors wrap ErrUnsafeWrite and list lost ("-") and added ("+") records.

Types

type AddRemove

type AddRemove struct {
	Owner     string
	Type      uint16
	TTL       uint32 // for added records; 0: no explicit TTL
	Add       []string
	Remove    []string
	Placement Placement
}

AddRemove is the Op that edits one RRset (Owner and Type) in place, as DNS UPDATE does: entries whose content is in Remove are dropped; then each content of Add that is still absent (and not a duplicate of an earlier one) is inserted as a record with TTL right after the last remaining entry of the set, or per Placement when the set is empty. Adding a present value and removing an absent one are no-ops.

type Anchor

type Anchor struct {
	Owner  string
	Prefix string
	Under  string
	Type   uint16
}

Anchor names records after which new records are placed: those with Owner; those whose apex-relative owner starts with Prefix (ASCII case-insensitive); those whose owner is Under or below it; those of type Type. Conditions that are set must all hold; an anchor with none set matches nothing. Owner and Under are client-form names like a selector's.

type Collapse

type Collapse struct {
	Owner     string
	Type      uint16
	Drop      []string
	Keep      string
	KeepTTL   uint32 // 0: no explicit TTL
	Placement Placement
}

Collapse is the Op that leaves the RRset (Owner and Type) with exactly one record of content Keep and none of the contents in Drop; other contents are untouched. Entries with a content in Drop are removed; of the entries equal to Keep the first stays and later ones are removed; if none stayed, Keep is inserted (with KeepTTL) where the first dropped entry was, else per Placement. Comments of every removed entry are carried onto the record that stayed or was inserted.

type Delete

type Delete struct {
	Selector
}

Delete is the Op that drops every record matching Selector; an absent record is a no-op. It is Replace with no records and ReplaceAll.

type DeleteOwner

type DeleteOwner struct {
	Owner string
}

DeleteOwner is the Op that drops every record with that owner, of any type, together with their comments.

type Entry

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

Entry is one zone file entry: a blank line, a comment line, a directive or a record. Entries are immutable values: methods that change an entry return a new one, so copies never alias each other and can be inserted into slices and compared freely. The zero Entry is a blank line.

func NewComment

func NewComment(text string) (Entry, error)

NewComment builds a Comment entry: one line holding only text, which gets a leading "; " when it does not start with ";". Line terminators and NUL are rejected, so the text can never become a second line. Errors wrap ErrInvalidEntry.

func NewDirective

func NewDirective(name string, args ...string) (Entry, error)

NewDirective builds a Directive entry from its name ($ORIGIN, $TTL, $INCLUDE or $GENERATE, any case) and arguments. Like NewRecord it is a security boundary: each argument must tokenise as exactly one item or quoted string, so it can never add a line, a comment or a second entry; a $TTL argument must be a valid TTL. Errors wrap ErrInvalidEntry.

func NewRecord

func NewRecord(owner string, ttl uint32, rtype uint16, rdata string) (Entry, error)

NewRecord builds a Record entry "owner [ttl] IN TYPE rdata" from parts, without rendering text and parsing it back. A ttl of 0 means no explicit TTL. This is a security boundary for values coming from network clients: the result is always exactly one record, never a directive, a comment or a second record.

  • owner must tokenise as exactly one unquoted item and must not start with "$".
  • For TXT and SPF, rdata is one raw character-string that becomes one quoted field (see Quote); it is not split at 255 bytes.
  • For other types, rdata must tokenise into one or more items and quoted strings. Line terminators, ";", "(", ")", NUL and unterminated quotes are rejected. Quoted fields stay quoted as written.
  • rtype must be in the type table.

Errors wrap ErrInvalidRecord.

func (Entry) Args

func (e Entry) Args() []Field

Args returns a copy of a Directive entry's arguments.

func (Entry) Class

func (e Entry) Class() string

Class returns a Record entry's class, upper-cased, or "" when absent.

func (Entry) Comment

func (e Entry) Comment() string

Comment returns the text of a Comment entry including the leading ";".

func (Entry) Comments

func (e Entry) Comments() []string

Comments returns every comment attached to the entry in file order: the comment of a Comment entry, or head, per-field and tail comments.

func (Entry) Content

func (e Entry) Content() string

Content returns the record's content in the PowerDNS form: for TXT and SPF the concatenation of the decoded strings, for every other type the decoded fields joined by one space.

func (Entry) Equal

func (e Entry) Equal(o Entry) bool

Equal compares two entries literally: same kind, and for records the same owner (ASCII case-insensitive), class, type, decoded values and the same explicit TTL in seconds (or both absent). Comments and the written form of values are ignored. Directives compare by name and decoded arguments; comment entries by text.

func (Entry) Fields

func (e Entry) Fields() []Field

Fields returns a copy of a Record entry's rdata fields.

func (Entry) Head

func (e Entry) Head() []string

Head returns a copy of the comments between "(" and the first field.

func (Entry) Kind

func (e Entry) Kind() Kind

Kind returns the entry kind.

func (Entry) Line

func (e Entry) Line() int

Line returns the 1-based line of the entry's first line in the parsed text, or 0 for entries built by hand.

func (Entry) Name

func (e Entry) Name() string

Name returns a Directive entry's name, upper-cased, such as "$ORIGIN".

func (Entry) Owner

func (e Entry) Owner() string

Owner returns a Record entry's owner: relative to the apex for an entry that came from Parse, as given for one built by hand; "" when the owner is inherited from the previous record and could not be resolved.

func (Entry) RType

func (e Entry) RType() uint16

RType returns a Record entry's numeric type.

func (Entry) RawValues

func (e Entry) RawValues() []string

RawValues returns a Record entry's rdata fields as written, or a Directive entry's arguments as written.

func (Entry) TTL

func (e Entry) TTL() (TTL, bool)

TTL returns a Record entry's explicit TTL and whether it has one.

func (Entry) Tail

func (e Entry) Tail() []string

Tail returns a copy of the comments after the last field or at the end of the entry's line.

func (Entry) Type

func (e Entry) Type() string

Type returns a Record entry's type name, upper-cased ("A", "TYPE65280").

func (Entry) Values

func (e Entry) Values() []string

Values returns a Record entry's rdata fields decoded (escapes resolved, quotes removed), or a Directive entry's decoded arguments.

func (Entry) WithComments

func (e Entry) WithComments(comments ...string) Entry

WithComments returns a copy of the entry with the comments appended to its last line as one comment joined by single spaces. Comments without a leading ";" get one. Empty comments are ignored. Line terminators and NUL inside a comment become spaces, so a comment can never end the line and start another entry (see NewComment for the strict form).

func (Entry) WithOwner

func (e Entry) WithOwner(name string) Entry

WithOwner returns a copy of a Record entry with its owner replaced; an empty name clears the owner so it is inherited. It is the caller's job to pass a single owner item (see NewRecord).

type Field

type Field struct {
	Raw      string   // as written, including quotes and escapes
	Value    string   // decoded: escapes resolved, quotes removed
	Quoted   bool     // written as a quoted string
	Comments []string // comments following this field, before the next
}

Field is one rdata field or directive argument.

type Insert

type Insert struct {
	Owner     string
	Records   []RR
	Placement Placement
}

Insert is the Op that adds Records under Owner, skipping those the zone already holds (same owner, type and content). They go per Placement, or per Zone.DefaultPlacement for the first record's type when Placement is empty. Nothing else is touched.

type Kind

type Kind int

Kind classifies an entry.

const (
	Blank     Kind = iota // an empty line (runs are collapsed into one)
	Comment               // a line holding only a comment
	Directive             // $ORIGIN, $TTL, $INCLUDE or $GENERATE
	Record                // a resource record
)

Entry kinds.

func (Kind) String

func (k Kind) String() string

String returns the kind's name.

type Op

type Op interface {
	// contains filtered or unexported methods
}

Op is one editing operation, applied with Zone.Apply. The operations are Replace, Insert, Delete, DeleteOwner, AddRemove and Collapse: plain struct values that can be built up, logged and applied together. Nothing outside this package implements Op.

type ParseError

type ParseError struct {
	Line int
	Col  int
	Msg  string
}

ParseError describes a syntax error in zone file text. Line and Col are 1-based; Col counts bytes from the start of the line.

func (*ParseError) Error

func (e *ParseError) Error() string

Error implements the error interface as "line L: msg".

type Placement

type Placement []Anchor

Placement lists anchors to try in order when an operation inserts records and found nothing to replace. The first anchor matching any Record entry decides: the new records go right after its last match, and after the records that follow it with the same owner, so an owner's block is never split. With no match, or an empty Placement, they are appended to the end. Zone.DefaultPlacement builds the list a human would use.

type RR

type RR struct {
	Type    uint16
	TTL     uint32 // 0: no explicit TTL
	Content string // PowerDNS content form: rdata fields, TXT text as is
}

RR is a record as an operation carries it: data, not an Entry. Apply builds the entry with NewRecord, so the security boundary holds for values that come from network clients.

type RRSet

type RRSet struct {
	Owner    string   // absolute
	Type     string   // upper-case
	TTL      uint32   // effective TTL of the first record, 0 when unknown
	Contents []string // in file order
}

RRSet is one group of records with the same owner and type.

type Replace

type Replace struct {
	Selector
	Records   []RR
	Mode      ReplaceMode
	MustExist bool
	Placement Placement
}

Replace is the Op that replaces the records matching Selector with Records. The first match is replaced in place by all of Records (zero or more); every later match is dropped (ReplaceAll) or kept (ReplaceFirst). Comments of dropped matches are carried onto the first inserted record. Without a match, MustExist fails with ErrNotFound; otherwise Records are inserted per Placement. Empty Records and no match is a no-op.

Owners in the selector and the anchors are absolute or apex-relative as a client would send them ("www", "www.example.org.", "@"); an owner outside the zone is ErrNotInZone, and so is an empty selector owner. Records get the selector's owner.

type ReplaceMode

type ReplaceMode int

ReplaceMode selects what happens to matches after the first.

const (
	ReplaceAll   ReplaceMode = iota // every match goes; records take the first match's place
	ReplaceFirst                    // only the first match goes; later matches stay
)

Replace modes.

type Result

type Result struct {
	Matched  int  // entries the selector matched
	Removed  int  // entries dropped
	Inserted int  // entries added
	Index    int  // position of the first inserted entry, -1 if none
	Changed  bool // the zone's meaning changed (also recorded in Zone.Changed)
}

Result reports what an operation did.

type Selector

type Selector struct {
	Owner    string
	Types    []uint16
	Contents []string
}

Selector picks Record entries. Owner is absolute or apex-relative (see Replace for the rules); "" matches any owner and is accepted by the queries only. A nil Types or Contents matches any type or content; contents are compared in the Entry.Content form, with client strings normalised the same way (TXT and SPF as given; other types tokenised like rdata and their decoded fields joined by one space).

type TTL

type TTL struct {
	Raw     string // as written, e.g. "1h" or "3600"
	Seconds uint32
}

TTL is a time-to-live as written in a zone file together with its value in seconds.

type Zone

type Zone struct {
	Entries []Entry
	Changed bool
	// contains filtered or unexported fields
}

Zone is a zone file in memory: its entries in file order and its apex. Parse builds one from text and New builds an empty one. From there the caller ranges over or edits Entries directly, or applies the editing operations ([Zone.Replace] and friends), and finally renders the result with Zone.Commit (or Zone.Format) and writes the bytes itself: the package never touches files.

Every record's owner is explicit and relative to the apex ("@" for the apex itself, "www" for www.<apex>, the absolute name for anything outside the zone), so an entry's meaning does not depend on its position or on $ORIGIN lines and entries can be moved, inserted and compared freely.

Changed records whether the zone differs in meaning from what was parsed or created: the editing operations set it when they change something, and code that edits Entries directly sets it itself. Zone.Commit reads it and renders nothing while it is false. A Zone is not safe for concurrent use.

func New

func New(apex string) *Zone

New returns an empty zone with the given apex (made absolute; "" is the root). It holds no entries, not even a $ORIGIN line: append one built with NewDirective if the file should be self-describing.

func Parse

func Parse(data []byte, apex string) (*Zone, error)

Parse parses zone file text into a Zone. Every entry is kept in order with its comments. The apex is the owner of the first SOA record, resolved with apex as the origin in force before any $ORIGIN line; with no SOA it is apex itself, else the first $ORIGIN, else the root ".". A non-empty apex is also an assertion: when the SOA names a different zone, ErrApexMismatch is returned, so a file edited over time keeps its origin. Names are resolved against the origin in force, which before any $ORIGIN line is apex, or the root when apex is empty (a relative name is then taken as absolute, as the reference parser does); the apex found only decides what the resolved owners are relative to (see Zone), so a file without origin is never reinterpreted. Syntax errors are *ParseError values naming the line. Parse never panics on any input and performs no I/O.

func (*Zone) AbsoluteOwner

func (z *Zone) AbsoluteOwner(e Entry) string

AbsoluteOwner returns an entry's absolute owner, or "" for an entry that has none.

func (*Zone) Apex

func (z *Zone) Apex() string

Apex returns the absolute apex of the zone.

func (*Zone) Apply

func (z *Zone) Apply(ops ...Op) ([]Result, error)

Apply runs the operations in order and reports what each did. It is atomic: when one fails, the error names it and the zone is left exactly as it was, with nothing applied. Errors are ErrNotInZone, ErrNotFound and, for records that cannot be built, ErrInvalidRecord.

Example

A zone is parsed, edited with a list of operations and rendered again; comments and layout survive, and only the SOA serial would change on Commit.

package main

import (
	"fmt"
	"os"

	"github.com/miekg/dns"

	"github.com/vooon/go-masterfile"
)

func main() {
	z, err := masterfile.Parse([]byte(`$ORIGIN example.org.
$TTL 1h
@    IN SOA ns1 hostmaster 2024010100 2h 30m 2w 1h
     IN NS  ns1
; hosts
www  IN A   192.0.2.1 ; web
old  IN A   192.0.2.9
`), "example.org.")
	if err != nil {
		panic(err)
	}
	results, err := z.Apply(
		masterfile.Insert{Owner: "www", Records: []masterfile.RR{{Type: dns.TypeAAAA, Content: "2001:db8::1"}}},
		masterfile.Replace{
			Selector: masterfile.Selector{Owner: "www", Types: []uint16{dns.TypeA}},
			Records:  []masterfile.RR{{Type: dns.TypeA, Content: "192.0.2.2"}},
		},
		masterfile.DeleteOwner{Owner: "old.example.org."},
	)
	if err != nil {
		panic(err)
	}
	for i, r := range results {
		fmt.Printf("op %d: matched %d removed %d inserted %d\n", i, r.Matched, r.Removed, r.Inserted)
	}
	fmt.Println("changed:", z.Changed)
	if err := z.Format(os.Stdout, false); err != nil {
		panic(err)
	}
}
Output:
op 0: matched 0 removed 0 inserted 1
op 1: matched 1 removed 1 inserted 1
op 2: matched 1 removed 1 inserted 0
changed: true
$ORIGIN example.org.
$TTL 1h
@   IN SOA  ns1 hostmaster (
    2024010100 ; serial  Mon, 01 Jan 2024 #00
    2h         ; refresh
    30m        ; retry
    2w         ; expire
    1h         ; minimum
)
    IN NS   ns1
; hosts
www IN A    192.0.2.2 ; web
    IN AAAA 2001:db8::1

func (*Zone) Check

func (z *Zone) Check() error

Check reports whether the zone is complete enough for a name server to load on its own: its origin must be known (an apex given to Parse or New, a $ORIGIN line, or an SOA whose owner resolves to a name), else ErrNoOrigin, and it must hold an SOA record, else ErrNoSOA. Parse does not insist on either, so fragments meant for $INCLUDE parse and format like any other file; callers that serve a file call Check.

func (*Zone) Commit

func (z *Zone) Commit(bumpSerial bool) (data []byte, changed bool, err error)

Commit renders the zone for writing. While Changed is false it returns nil, false, nil and the caller writes nothing. Otherwise it writes the plain and the formatted form (bumping the SOA serial when asked), verifies the pair with Verify, and returns the formatted bytes. A verification failure is returned as is (it wraps ErrUnsafeWrite) and the caller must not write. Commit leaves Changed as it is: a Zone is one read-modify-write cycle.

func (*Zone) DefaultPlacement

func (z *Zone) DefaultPlacement(owner string, rtype uint16) Placement

DefaultPlacement is where a human would add records of owner and rtype: after the owner's own records; else after the records whose first label is the same (every _acme-challenge.* together); else after the records under the same parent name (siblings); else after the last record of the same type; else at the end. The owner is in client form; one outside the zone yields the type anchor alone.

func (*Zone) Format

func (z *Zone) Format(w io.Writer, bumpSerial bool) error

Format writes the zone in the canonical layout: aligned owner, TTL, class, type and value columns; owners relative to the $ORIGIN in force and blank when repeated; TTLs equal to the $TTL in force dropped; SOA and multi-string TXT records multi-line; long DNSKEY, DS and RRSIG data split into 55-byte pieces; comments kept in place. With bumpSerial the first SOA's serial is replaced by Increase of itself. The output is idempotent: formatting it again (without a bump) reproduces it byte for byte. Write errors are returned.

func (*Zone) Owners

func (z *Zone) Owners(sel Selector) []string

Owners returns the absolute owners of the records matching sel, deduplicated (ASCII case-insensitively, first spelling wins), in file order.

func (*Zone) RRSets

func (z *Zone) RRSets() []RRSet

RRSets groups the records by owner and type in order of first appearance, with the $TTL in force applied to records without an explicit TTL. Records without an owner are skipped.

func (*Zone) Records

func (z *Zone) Records() iter.Seq2[int, Entry]

Records iterates over the Record entries with their index in Entries.

func (*Zone) SOA

func (z *Zone) SOA() (Entry, bool)

SOA returns the first SOA record and true, or false when there is none.

func (*Zone) Select

func (z *Zone) Select(sel Selector) []Entry

Select returns copies of the records matching sel in file order. An owner outside the zone matches nothing; an empty owner matches any.

func (*Zone) Serial

func (z *Zone) Serial() (uint32, bool)

Serial returns the serial of the first SOA record, or false when there is no SOA or the serial is not a 32-bit number.

func (*Zone) WritePlain

func (z *Zone) WritePlain(w io.Writer) error

WritePlain writes the zone without alignment: fields separated by one space, owners relative to the $ORIGIN in force, values as written, multi-line only when a comment position requires it, comments in place. It is deliberately simple and is the reference that Verify checks Zone.Format against. Its exact spacing is not part of the contract.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL