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:
- 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.
- 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.
- 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 ¶
- Variables
- func Absolute(name, origin string) string
- func Comments(data []byte) ([]string, error)
- func EntriesEqual(a, b []Entry) bool
- func Fqdn(name string) string
- func InZone(name, apex string) bool
- func Increase(serial string) string
- func IsReverseZone(apex string) bool
- func LongestZone(name string, apexes []string) (apex string, ok bool)
- func NameEqual(a, b string) bool
- func Quote(s string) string
- func Records(apex string, data []byte) (map[string]int, error)
- func Reformat(data []byte, apex string, w io.Writer, bumpSerial bool) error
- func Relative(name, origin string) string
- func ReverseAddr(name string) (netip.Addr, bool)
- func ReverseName(addr netip.Addr) string
- func ReversePrefix(apex string) (netip.Prefix, bool)
- func StringToTTL(s string) (uint32, bool)
- func ToASCII(name string) (string, error)
- func ToUnicode(name string) string
- func UserComment(c string) string
- func Verify(apex string, plain, formatted []byte) error
- type AddRemove
- type Anchor
- type Collapse
- type Delete
- type DeleteOwner
- type Entry
- func (e Entry) Args() []Field
- func (e Entry) Class() string
- func (e Entry) Comment() string
- func (e Entry) Comments() []string
- func (e Entry) Content() string
- func (e Entry) Equal(o Entry) bool
- func (e Entry) Fields() []Field
- func (e Entry) Head() []string
- func (e Entry) Kind() Kind
- func (e Entry) Line() int
- func (e Entry) Name() string
- func (e Entry) Owner() string
- func (e Entry) RType() uint16
- func (e Entry) RawValues() []string
- func (e Entry) TTL() (TTL, bool)
- func (e Entry) Tail() []string
- func (e Entry) Type() string
- func (e Entry) Values() []string
- func (e Entry) WithComments(comments ...string) Entry
- func (e Entry) WithOwner(name string) Entry
- type Field
- type Insert
- type Kind
- type Op
- type ParseError
- type Placement
- type RR
- type RRSet
- type Replace
- type ReplaceMode
- type Result
- type Selector
- type TTL
- type Zone
- func (z *Zone) AbsoluteOwner(e Entry) string
- func (z *Zone) Apex() string
- func (z *Zone) Apply(ops ...Op) ([]Result, error)
- func (z *Zone) Check() error
- func (z *Zone) Commit(bumpSerial bool) (data []byte, changed bool, err error)
- func (z *Zone) DefaultPlacement(owner string, rtype uint16) Placement
- func (z *Zone) Format(w io.Writer, bumpSerial bool) error
- func (z *Zone) Owners(sel Selector) []string
- func (z *Zone) RRSets() []RRSet
- func (z *Zone) Records() iter.Seq2[int, Entry]
- func (z *Zone) SOA() (Entry, bool)
- func (z *Zone) Select(sel Selector) []Entry
- func (z *Zone) Serial() (uint32, bool)
- func (z *Zone) WritePlain(w io.Writer) error
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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.
var ( ErrNoOrigin = errors.New("no origin") ErrNoSOA = errors.New("no SOA record") )
Errors of Zone.Check.
var ErrInvalidEntry = errors.New("invalid entry")
ErrInvalidEntry is wrapped by every error of NewDirective and NewComment.
var ErrInvalidName = errors.New("invalid name")
ErrInvalidName is wrapped by every error of ToASCII.
var ErrInvalidRecord = errors.New("invalid record")
ErrInvalidRecord is wrapped by every error of NewRecord.
var ErrUnsafeWrite = errors.New("unsafe write")
ErrUnsafeWrite is wrapped by every error of Verify.
Functions ¶
func Absolute ¶
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 ¶
Comments parses data with this package's parser and returns every comment in file order. It is exported for tests.
func EntriesEqual ¶
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 ¶
Fqdn returns name with a trailing dot added if it has none. An empty name becomes the root ".".
func InZone ¶
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 ¶
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 ¶
IsReverseZone reports whether apex lies under in-addr.arpa or ip6.arpa (ASCII case-insensitive; a trailing dot is optional).
func LongestZone ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) Comments ¶
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 ¶
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 ¶
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) Line ¶
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) Owner ¶
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) RawValues ¶
RawValues returns a Record entry's rdata fields as written, or a Directive entry's arguments as written.
func (Entry) Tail ¶
Tail returns a copy of the comments after the last field or at the end of the entry's line.
func (Entry) Values ¶
Values returns a Record entry's rdata fields decoded (escapes resolved, quotes removed), or a Directive entry's decoded arguments.
func (Entry) WithComments ¶
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).
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 ¶
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.
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 ¶
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 ¶
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 Zone ¶
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 ¶
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 ¶
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 ¶
AbsoluteOwner returns an entry's absolute owner, or "" for an entry that has none.
func (*Zone) Apply ¶
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 ¶
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 ¶
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 ¶
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.
An owner whose first label starts with "_" is an attribute leaf (RFC 8552: _acme-challenge, _dmarc, _25._tcp), protocol metadata rather than a host. It groups only with its own label family and otherwise goes to the end: the type anchor would put it after an unrelated record that happens to share the type, such as the last TXT of a commented section.
func (*Zone) Format ¶
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 ¶
Owners returns the absolute owners of the records matching sel, deduplicated (ASCII case-insensitively, first spelling wins), in file order.
func (*Zone) RRSets ¶
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) Select ¶
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 ¶
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 ¶
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.