zone

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2026 License: AGPL-3.0 Imports: 10 Imported by: 0

Documentation

Overview

Package zone holds the domain model: names, zones, records, validation and the reverse-mapping rules.

Index

Constants

View Source
const (
	// MaxLabelLen is the largest permitted label, in octets.
	MaxLabelLen = 63
	// MaxNameWireLen is the largest permitted encoded name, in octets. It
	// counts the length octet of every label and the terminating zero octet,
	// which is why the printable form of a maximal name is shorter than this.
	MaxNameWireLen = 255
)

Size limits from RFC 1035 §2.3.4.

View Source
const (
	TypeNone  = RRType(dns.TypeNone)
	TypeA     = RRType(dns.TypeA)
	TypeNS    = RRType(dns.TypeNS)
	TypeCNAME = RRType(dns.TypeCNAME)
	TypeSOA   = RRType(dns.TypeSOA)
	TypeNULL  = RRType(dns.TypeNULL)
	TypePTR   = RRType(dns.TypePTR)
	TypeHINFO = RRType(dns.TypeHINFO)
	TypeMX    = RRType(dns.TypeMX)
	TypeTXT   = RRType(dns.TypeTXT)
	TypeAAAA  = RRType(dns.TypeAAAA)
	TypeSRV   = RRType(dns.TypeSRV)
	TypeNAPTR = RRType(dns.TypeNAPTR)
	TypeDNAME = RRType(dns.TypeDNAME)
	TypeSVCB  = RRType(dns.TypeSVCB)
	TypeHTTPS = RRType(dns.TypeHTTPS)
	TypeCAA   = RRType(dns.TypeCAA)
	TypeTLSA  = RRType(dns.TypeTLSA)
	TypeSSHFP = RRType(dns.TypeSSHFP)

	// DNSSEC types. Out of scope for v0.1, named so validation can recognise
	// and reject them rather than storing records it cannot maintain.
	TypeDS         = RRType(dns.TypeDS)
	TypeRRSIG      = RRType(dns.TypeRRSIG)
	TypeNSEC       = RRType(dns.TypeNSEC)
	TypeDNSKEY     = RRType(dns.TypeDNSKEY)
	TypeNSEC3      = RRType(dns.TypeNSEC3)
	TypeNSEC3PARAM = RRType(dns.TypeNSEC3PARAM)
	// SIG, KEY and NXT are the RFC 2535 originals that RFC 3755 replaced with
	// RRSIG, DNSKEY and NSEC. They belong to the same family.
	TypeSIG = RRType(dns.TypeSIG)
	TypeKEY = RRType(dns.TypeKEY)
	TypeNXT = RRType(dns.TypeNXT)

	// Meta types, which exist only for the lifetime of one message.
	TypeOPT  = RRType(dns.TypeOPT)
	TypeTKEY = RRType(dns.TypeTKEY)
	TypeTSIG = RRType(dns.TypeTSIG)

	// Query types, which are only meaningful in a question.
	TypeIXFR  = RRType(dns.TypeIXFR)
	TypeAXFR  = RRType(dns.TypeAXFR)
	TypeMAILB = RRType(dns.TypeMAILB)
	TypeMAILA = RRType(dns.TypeMAILA)
	TypeANY   = RRType(dns.TypeANY)
)

The record types the zone logic itself reasons about, plus the ones an operator meets day to day. Any other type is still valid and is handled by number; these exist so the code can name what it means. Values come from the IANA registry by way of the wire library, so there are no magic numbers here.

View Source
const (
	ClassIN   = Class(dns.ClassINET)
	ClassCH   = Class(dns.ClassCHAOS)
	ClassHS   = Class(dns.ClassHESIOD)
	ClassNONE = Class(dns.ClassNONE)
	ClassANY  = Class(dns.ClassANY)
)

The assigned classes.

View Source
const (

	// MaxSerialIncrement is the largest step RFC 1982 §3.1 defines for adding
	// to a serial. Adding more than this has no defined meaning, because the
	// result would not be recognisable as newer.
	MaxSerialIncrement = uint32(1<<31 - 1)
)

serialSpace is the size of the serial number space, and serialHalf the point at which RFC 1982 stops being able to tell newer from older.

Variables

View Source
var (
	// ErrInvalidName reports a name that is malformed for any reason. Every
	// other error in this group wraps it, so a caller that does not care which
	// rule was broken can test for this one alone.
	ErrInvalidName = fmt.Errorf("%w domain name", ErrInvalid)
	// ErrNameTooLong reports a name whose encoded form exceeds
	// [MaxNameWireLen].
	ErrNameTooLong = fmt.Errorf("%w: longer than %d octets", ErrInvalidName, MaxNameWireLen)
	// ErrLabelTooLong reports a label longer than [MaxLabelLen].
	ErrLabelTooLong = fmt.Errorf("%w: label longer than %d octets", ErrInvalidName, MaxLabelLen)
	// ErrEmptyLabel reports an empty label, as produced by a leading dot or by
	// two consecutive dots.
	ErrEmptyLabel = fmt.Errorf("%w: empty label", ErrInvalidName)
	// ErrBadEscape reports a backslash escape that is not a single character
	// or exactly three decimal digits in the range 0 to 255 (RFC 1035 §5.1).
	ErrBadEscape = fmt.Errorf("%w: malformed escape", ErrInvalidName)
)

Errors returned when a name cannot be parsed. They are joined to a message naming the offending input, so callers should test with errors.Is rather than by comparing strings.

View Source
var (
	// ArpaV4 is in-addr.arpa., the IPv4 reverse namespace (RFC 1035 §3.5).
	ArpaV4 = MustParseName("in-addr.arpa.")
	// ArpaV6 is ip6.arpa., the IPv6 reverse namespace in nibble form
	// (RFC 3596 §2.5).
	ArpaV6 = MustParseName("ip6.arpa.")
)

The two namespaces that hold reverse mappings.

View Source
var ErrInvalid = errors.New("invalid")

ErrInvalid is the root of every rejection this package produces. All other errors here wrap it, so a caller that only needs to distinguish "the input was bad" from "something went wrong" can test for this one alone, while a caller that wants to explain the problem can test for the specific error.

View Source
var ErrInvalidClass = fmt.Errorf("%w class", ErrInvalid)

ErrInvalidClass reports a class that is neither a known mnemonic nor the CLASS<number> form of RFC 3597 §5.

View Source
var ErrInvalidRData = fmt.Errorf("%w record data", ErrInvalid)

ErrInvalidRData reports record data that cannot be parsed or canonicalised.

View Source
var ErrInvalidRRType = fmt.Errorf("%w record type", ErrInvalid)

ErrInvalidRRType reports a record type that is neither a known mnemonic nor the TYPE<number> form of RFC 3597 §5.

View Source
var ErrInvalidTTL = fmt.Errorf("%w TTL", ErrInvalid)

ErrInvalidTTL reports a TTL that is malformed or out of range.

View Source
var ErrNotReverse = fmt.Errorf("%w: not a reverse name", ErrInvalid)

ErrNotReverse reports a name that lies outside both reverse namespaces.

View Source
var Root = Name{/* contains filtered or unexported fields */}

Root is the DNS root, ".". Every name is a subdomain of it.

Functions

func IsReverseName

func IsReverseName(n Name) bool

IsReverseName reports whether n lies in one of the reverse namespaces.

func ParseReversePrefix

func ParseReversePrefix(n Name) (netip.Prefix, error)

ParseReversePrefix returns the network a reverse zone is responsible for.

It understands the ordinary octet and nibble forms, and both spellings of the classless delegation of RFC 2317 §4: "0/25", which names the prefix length directly, and "0-127", the range form BIND setups commonly use.

func ValidateOwner

func ValidateOwner(z Zone, name Name, records []Record) error

ValidateOwner reports whether the records sharing one owner name can coexist.

func ValidateRRset

func ValidateRRset(records []Record) error

ValidateRRset reports whether a set of records forming one RRset is well formed.

An RRset is everything sharing an owner name, class and type, and DNS answers it as a unit (RFC 2181 §5). Two rules follow from that and cannot be checked on a single record:

  • every member carries the same TTL (RFC 2181 §5.2), because a resolver caches the set as one thing and a divergent TTL makes the answer depend on which copy it saw;
  • no member repeats another's data (RFC 2181 §5).

func ValidateUnderDelegation

func ValidateUnderDelegation(name Name, records []Record, delegation Name) error

ValidateUnderDelegation reports whether the records at one name may live where the zone's delegations put it.

delegation is the closest delegation point at or above name; a zero name means there is none and nothing to check. It is a parameter rather than something worked out here, because finding it means either a whole zone in memory or a walk up the database, and the two callers have one each.

func ValidateZone

func ValidateZone(z Zone, records []Record) error

ValidateZone reports whether a complete set of records forms a usable zone.

Types

type Class

type Class uint16

Class is a resource record class (RFC 1035 §3.2.4). In practice everything is ClassIN; the others exist because the protocol has them.

func ParseClass

func ParseClass(s string) (Class, error)

ParseClass parses a class given either as a mnemonic, in any casing, or in the CLASS<number> form of RFC 3597 §5.

func (Class) MarshalText

func (c Class) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler.

func (Class) Storable

func (c Class) Storable() bool

Storable reports whether records of class c may be held in a zone. NONE and ANY are QCLASSes, meaningful only inside a message (RFC 6895 §3.2).

func (Class) String

func (c Class) String() string

String returns the mnemonic for c, or the CLASS<number> form of RFC 3597 §5 when the class has no assigned mnemonic.

func (*Class) UnmarshalText

func (c *Class) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler.

type Kind

type Kind string

Kind distinguishes a forward zone from a reverse one.

const (
	// KindForward is an ordinary zone mapping names to data.
	KindForward Kind = "forward"
	// KindReverse is a zone under in-addr.arpa or ip6.arpa, mapping addresses
	// back to names.
	KindReverse Kind = "reverse"
)

type ManagedKind

type ManagedKind string

ManagedKind says why a record was generated rather than authored.

const (
	// ManagedPTR is a PTR derived from an A or AAAA record.
	ManagedPTR ManagedKind = "ptr"
	// ManagedRFC2317CNAME is a CNAME in a parent /24 delegating a single
	// address into a classless child zone (RFC 2317 §4).
	ManagedRFC2317CNAME ManagedKind = "rfc2317-cname"
)

type Name

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

Name is a fully qualified, validated domain name.

The value is held in uncompressed wire form (every label preceded by its length octet, the whole terminated by a zero octet) with US-ASCII letters lowercased. Two names are equal exactly when their encoded forms are equal, which makes Name comparable, usable as a map key, and correct with respect to the case-insensitive comparison required by RFC 4343.

Lowercasing on the way in is a storage decision, not a wire decision. A response echoes the casing of the query's QNAME (0x20 encoding), never the stored casing, so nothing observable on the wire depends on it. It also gives DNSSEC the canonical form it will need later, free (RFC 4034 §6.2).

func MustParseName

func MustParseName(s string) Name

MustParseName is ParseName for names known to be valid at compile time, such as constants and test fixtures. It panics if s is not a valid name.

func NameFromWire

func NameFromWire(b []byte) (Name, error)

NameFromWire parses an uncompressed wire-format name.

Compression pointers are rejected: a Name is a standalone value, and a pointer is only meaningful relative to the message it was read from. The caller, the query path, resolves pointers while parsing the message and passes the expanded name here.

func ParseName

func ParseName(s string) (Name, error)

ParseName parses a domain name in the presentation format of RFC 1035 §5.1.

func ReverseName

func ReverseName(addr netip.Addr) (Name, error)

ReverseName returns the name that carries the PTR record for addr: "10.2.0.192.in-addr.arpa." for 192.0.2.10, and the nibble form of RFC 3596 §2.5 for an IPv6 address.

func ReverseZoneName

func ReverseZoneName(p netip.Prefix) (Name, error)

ReverseZoneName returns the zone name responsible for a network.

A prefix on an octet boundary gets the ordinary form, "2.0.192.in-addr.arpa." for 192.0.2.0/24. A longer IPv4 prefix gets the classless form of RFC 2317 §4, "0/25.2.0.192.in-addr.arpa." for 192.0.2.0/25.

An IPv6 prefix must fall on a nibble boundary, since ip6.arpa has no finer division to offer.

func (Name) AppendWire

func (n Name) AppendWire(dst []byte) []byte

AppendWire appends the uncompressed wire form of n to dst and returns the extended buffer. It does not allocate when dst has spare capacity.

func (Name) Child

func (n Name) Child(label string) (Name, error)

Child returns n with label prepended, so Child("www") on "example.com." yields "www.example.com.".

func (Name) Compare

func (n Name) Compare(m Name) int

Compare orders n against m in the canonical name order of RFC 4034 §6.1: labels are compared from the rightmost, as unsigned octets, with a label that is a prefix of another sorting first. It returns a negative number, 0, or a positive number as n sorts before, equal to, or after m.

func (Name) Equal

func (n Name) Equal(m Name) bool

Equal reports whether n and m are the same name. Comparison is case-insensitive for US-ASCII letters, as required by RFC 4343, because both names were lowercased when they were parsed.

func (Name) FirstLabel

func (n Name) FirstLabel() (string, bool)

FirstLabel returns the leftmost label of n as raw octets, without escaping, and reports false for the root and for the zero Name.

func (Name) IsRoot

func (n Name) IsRoot() bool

IsRoot reports whether n is the root name, ".".

func (Name) IsSubDomainOf

func (n Name) IsSubDomainOf(p Name) bool

IsSubDomainOf reports whether n lies at or below p, so a name is a subdomain of itself and of the root. This matches how RFC 1034 §4.3.2 uses the term when deciding whether a query falls inside a zone.

func (Name) IsZero

func (n Name) IsZero() bool

IsZero reports whether n is the zero value, meaning no name at all. It is distinct from the root, which is a real name.

func (Name) LabelCount

func (n Name) LabelCount() int

LabelCount returns the number of labels in n, excluding the root label. The root itself has zero labels.

func (Name) Labels

func (n Name) Labels() []string

Labels returns the labels of n from left to right, as raw octets without escaping. The root has no labels.

func (Name) MarshalText

func (n Name) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler, so a Name renders as its presentation form in JSON and YAML alike.

func (Name) Parent

func (n Name) Parent() (Name, bool)

Parent returns the name one label up, reporting false for the root and for the zero Name. The parent of "www.example.com." is "example.com.", and the parent of "com." is the root.

func (Name) SortKey

func (n Name) SortKey() []byte

SortKey returns a byte string whose plain byte order equals the canonical name order of [Compare]. It is stored alongside the name so that a database ORDER BY reproduces DNS ordering without a custom collation.

The encoding reverses the labels and terminates each with two zero octets. A zero octet inside a label (legal, if vanishingly rare) is escaped to 0x00 0xFF, which keeps the encoding unambiguous and keeps a terminator sorting before any label content, exactly as a shorter name must sort before the names below it.

func (Name) String

func (n Name) String() string

String returns the name in presentation format, fully qualified with a trailing dot.

Octets that are not printable US-ASCII, and those that would otherwise be read as zonefile syntax, are escaped, so the result is always safe to write into a zonefile and always parses back to an equal Name.

func (*Name) UnmarshalText

func (n *Name) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler. Decoding validates the name, so an invalid name cannot enter the model through an API request.

func (Name) Wire

func (n Name) Wire() []byte

Wire returns a copy of the uncompressed wire form of n.

func (Name) WireLen

func (n Name) WireLen() int

WireLen returns the length of the encoded name in octets, including every length octet and the terminating zero.

type RData

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

RData is validated record data in canonical presentation format.

Canonical means that two spellings of the same data produce identical bytes: whitespace is normalised, an IPv6 address is compressed, and a domain name inside the data is fully qualified and case-folded. Equality of RData is therefore string equality, which is what lets the database catch a duplicate record with a plain unique index. See docs/adr/0001-rdata-presentation-format.md.

func MustParseRData

func MustParseRData(t RRType, c Class, s string) RData

MustParseRData is ParseRData for data known to be valid at compile time, such as test fixtures. It panics if the data is not valid.

func ParseRData

func ParseRData(t RRType, c Class, s string) (RData, error)

ParseRData parses and canonicalises record data of the given type and class.

Names inside the data must be fully qualified. Resolving a relative name needs an origin, which belongs to the zonefile parser rather than to the model, so a relative name is reported rather than silently attached to the wrong parent.

The unknown-record form of RFC 3597 §5, "\# <length> <hex>", is accepted for any type. For a type we do know, it is converted to that type's own presentation form, as RFC 3597 §5 requires.

func RDataFromCanonical

func RDataFromCanonical(text string) (RData, error)

RDataFromCanonical wraps text that ParseRData has already produced.

func RDataFromRR

func RDataFromRR(rr dns.RR) (RData, error)

RDataFromRR takes the data out of a record the wire library parsed.

It is the inverse of the trip ParseRData makes, and it exists for the zonefile reader: that reader hands whole files to the library's own RFC 1035 §5 parser rather than re-deriving the format, and needs the records it gets back in the canonical presentation form ADR 0001 asks for.

The parameter is the wire library's own type, which widens the dependency ADR 0005 admits from an internal one to an exported signature. Deliberate, and small: the alternative is a caller re-implementing the split below, and a second answer to "where does the header end" is exactly the drift the canonical form exists to prevent.

func (RData) Address

func (r RData) Address(t RRType) (netip.Addr, bool)

Address returns the IP address carried by A and AAAA data, and reports false for every other type.

func (RData) Equal

func (r RData) Equal(o RData) bool

Equal reports whether r and o hold the same data. Because both are canonical, this is an exact comparison rather than a semantic one.

func (RData) IsZero

func (r RData) IsZero() bool

IsZero reports whether r is the zero value, meaning no data at all.

func (RData) MarshalText

func (r RData) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler.

func (RData) String

func (r RData) String() string

String returns the canonical presentation form of the data.

type RRType

type RRType uint16

RRType is a resource record type (RFC 1035 §3.2.2).

The numeric value is the IANA assignment, so an unknown type carries through the system unchanged and round-trips as required by RFC 3597.

func ParseRRType

func ParseRRType(s string) (RRType, error)

ParseRRType parses a record type given either as a mnemonic, in any casing, or in the TYPE<number> form of RFC 3597 §5.

func (RRType) HasMnemonic

func (t RRType) HasMnemonic() bool

HasMnemonic reports whether t is a type with an assigned mnemonic, rather than one that can only be written in the TYPE<number> form of RFC 3597 §5.

func (RRType) IsDNSSEC

func (t RRType) IsDNSSEC() bool

IsDNSSEC reports whether t is part of the DNSSEC record set. Wegweiser does not sign zones yet, and a hand-written signature record would be actively harmful, so validation refuses these until signing exists.

func (RRType) IsMeta

func (t RRType) IsMeta() bool

IsMeta reports whether t is a meta type, carrying data that belongs to one message rather than to a zone: OPT (RFC 6891 §6.1.1), TSIG and TKEY (RFC 6895 §3.1).

func (RRType) IsQueryOnly

func (t RRType) IsQueryOnly() bool

IsQueryOnly reports whether t may appear only in a question and never in a zone: AXFR, IXFR, MAILA, MAILB and ANY are QTYPEs (RFC 6895 §3.1).

func (RRType) MarshalText

func (t RRType) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler, so a type appears as "AAAA" rather than as 28 in JSON and YAML.

func (RRType) Storable

func (t RRType) Storable() bool

Storable reports whether a record of type t may be held in a zone.

Type 0 is reserved, query and meta types belong to messages rather than to zones, and RFC 1035 §3.3.10 states outright that NULL records are not allowed in zone files.

func (RRType) String

func (t RRType) String() string

String returns the mnemonic for t, or the TYPE<number> form of RFC 3597 §5 when the type has no assigned mnemonic.

func (*RRType) UnmarshalText

func (t *RRType) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler.

type RRsetKey

type RRsetKey struct {
	Name  Name
	Class Class
	Type  RRType
}

RRsetKey identifies the set a record belongs to: everything sharing an owner name, class and type is one RRset and is answered as a unit (RFC 2181 §5).

type Record

type Record struct {
	ID     RecordID
	ZoneID ZoneID

	Name  Name
	Class Class
	Type  RRType
	TTL   TTL
	RData RData

	// ManagedBy links a generated record back to the record that caused it,
	// and is empty for an authored one.
	ManagedBy   RecordID
	ManagedKind ManagedKind

	Comment  string
	Disabled bool

	CreatedAt time.Time
	UpdatedAt time.Time
}

Record is a single resource record.

Records are stored individually rather than as RRsets, so that a comment, provenance and a stable identity can hang off each one. The RRset rules of RFC 2181 are enforced by ValidateRRset instead of by the shape of the type. See docs/adr/0003-individual-records.md.

func NewRecord

func NewRecord(zoneID ZoneID, name Name, class Class, typ RRType, ttl TTL, rdata string) (Record, error)

NewRecord builds a record from presentation-format input, canonicalising the data as it goes. It is the constructor the API, the CLI and the zonefile importer all funnel through, so nothing enters the model unvalidated.

func (Record) Address

func (r Record) Address() (netip.Addr, bool)

Address returns the IP address of an A or AAAA record, and reports false for every other type.

func (Record) Compare

func (r Record) Compare(o Record) int

Compare orders records the way a zone is exported and the way the GUI walks the name tree: by owner name in the canonical order of RFC 4034 §6.1, then by type, then by data.

func (Record) IsManaged

func (r Record) IsManaged() bool

IsManaged reports whether the record was generated from another one rather than authored. A generated record is not edited directly; its source is.

func (Record) Key

func (r Record) Key() RRsetKey

Key returns the RRset this record belongs to.

func (Record) String

func (r Record) String() string

String returns the record as one zonefile line.

func (Record) Validate

func (r Record) Validate() error

Validate reports whether the record is well formed on its own.

Rules that need the rest of the zone (the CNAME restrictions of RFC 2181 §10.1, uniform TTLs within an RRset, what a delegation permits below it) belong to ValidateRRset and [ValidateZoneRecords], because a single record cannot see them.

type RecordID

type RecordID string

RecordID identifies a record. Like ZoneID it is a ULID, so a record has a stable identity a diff line can anchor to and the API can address.

type SOA

type SOA struct {
	// NS is the MNAME field: the primary server for the zone.
	NS Name
	// Mbox is the RNAME field: the zone administrator's mailbox, written as a
	// domain name, so hostmaster@example.com is hostmaster.example.com.
	Mbox Name

	Serial Serial

	// Refresh, Retry and Expire pace a secondary's transfers. They are 32-bit
	// second counts on the wire, the same domain as a TTL, so they share the
	// type; the field names carry the meaning that the type does not.
	Refresh TTL
	Retry   TTL
	Expire  TTL
	// Minimum is the negative-caching TTL (RFC 2308 §4), not a floor on record
	// TTLs, despite what the name suggests.
	Minimum TTL

	// TTL is the time to live of the SOA record itself.
	TTL TTL
}

SOA holds the start-of-authority parameters of a zone (RFC 1035 §3.3.13).

It is modelled as fields on the zone rather than as an ordinary record. The serial belongs to the journal, not to the user: one commit advances it by exactly one, and that is the invariant IXFR replay depends on. As an editable record it would be one careless edit away from breaking every secondary. See data model §4.1.

func DefaultSOA

func DefaultSOA(primary, mbox Name) SOA

DefaultSOA supplies the timers for a newly created zone.

The values follow the recommendations in RFC 1912 §2.2: a refresh interval short enough that a secondary notices a change within an hour even if NOTIFY is lost, a retry well below it, an expiry long enough to survive a weekend outage of the primary, and a negative-cache TTL kept short so a mistake can be corrected quickly.

func ParseSOAData

func ParseSOAData(s string) (SOA, error)

ParseSOAData reads the presentation form of an SOA's record data back into the parameters it came from.

func (SOA) NegativeTTL

func (s SOA) NegativeTTL() TTL

NegativeTTL returns the TTL to put on a negative answer for this zone.

RFC 2308 §3 and §5 make it the lesser of the SOA record's own TTL and the SOA MINIMUM field, so that shortening either one takes effect.

func (SOA) RData

func (s SOA) RData() string

RData returns the SOA in the presentation form of a record's data, which is what a zonefile writes and what the snapshot builder hands to the wire.

func (SOA) Validate

func (s SOA) Validate() error

Validate reports whether the SOA parameters are usable.

It enforces the rules that would produce a broken zone, not the style recommendations of RFC 1912: a secondary that cannot refresh, or an expiry that fires before a retry has had a chance, is a fault rather than a matter of taste.

type Serial

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

Serial is a DNS zone serial number, with the arithmetic of RFC 1982.

It is a struct rather than a bare uint32 so that "<" and ">" do not compile. Serials wrap: 4294967295 is older than 0, and comparing them as plain integers is the classic way to make a secondary refuse a transfer forever. Use Serial.After, Serial.Before or Serial.Compare. Equality with "==" is fine and is exactly what RFC 1982 §3.2 defines it to be.

func NewSerial

func NewSerial(v uint32) Serial

NewSerial returns the serial with the given numeric value.

func (Serial) Add

func (s Serial) Add(n uint32) (Serial, error)

Add returns the serial n steps on, wrapping past the end of the space.

RFC 1982 §3.1 defines addition only for n up to MaxSerialIncrement; adding more would produce a value that is not recognisably newer, so it is refused rather than silently wrapping into the past.

func (Serial) After

func (s Serial) After(o Serial) bool

After reports whether s is newer than o.

func (Serial) Before

func (s Serial) Before(o Serial) bool

Before reports whether s is older than o.

func (Serial) Comparable

func (s Serial) Comparable(o Serial) bool

Comparable reports whether RFC 1982 §3.2 defines an ordering for s and o.

func (Serial) Compare

func (s Serial) Compare(o Serial) int

Compare orders s against o by the rules of RFC 1982 §3.2, returning a negative number, zero, or a positive number as s is older than, the same as, or newer than o.

When the two are exactly half the space apart, RFC 1982 leaves the relation undefined. Compare still answers, falling back to the raw numeric order so that it stays antisymmetric and callers get a stable answer rather than one that depends on argument order. Serial.Comparable reports that case, and a caller that must not guess should ask first.

func (Serial) IsZero

func (s Serial) IsZero() bool

IsZero reports whether s is the zero serial. Zero is a legal serial value, so this is only useful for spotting a field that was never set.

func (Serial) MarshalJSON

func (s Serial) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, writing the serial as a number.

func (Serial) Next

func (s Serial) Next() Serial

Next returns the following serial, wrapping past the end of the space.

This is the only increment the journal uses: one commit advances a zone by exactly one, which is what lets IXFR replay commits directly. See docs/decisions.md, D2.

func (Serial) String

func (s Serial) String() string

String returns the serial in decimal, as a zonefile writes it.

func (Serial) Uint32

func (s Serial) Uint32() uint32

Uint32 returns the numeric value of the serial, for storage and the wire.

func (*Serial) UnmarshalJSON

func (s *Serial) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler.

type TTL

type TTL uint32

TTL is a resource record time to live, in seconds.

It is seconds rather than a time.Duration because DNS TTLs are integral seconds on the wire and in a zonefile. A Duration would admit values such as 1500ms that have no representation, and would have to be rounded somewhere — silently, and in more than one place.

const MaxTTL TTL = 1<<31 - 1

MaxTTL is the largest permitted TTL.

RFC 2181 §8 defines the TTL field as an unsigned 32-bit number whose top bit must be zero, and requires a receiver to treat a value with that bit set as zero. Accepting such a value on input would therefore store something that resolvers read as "do not cache", so it is refused instead.

func ParseTTL

func ParseTTL(s string) (TTL, error)

ParseTTL parses a TTL given either as a plain number of seconds or in the suffixed form BIND uses, so a value pasted out of an existing zonefile is accepted as typed.

func (TTL) Duration

func (t TTL) Duration() time.Duration

Duration returns the TTL as a time.Duration, for arithmetic against clocks and timers.

func (TTL) MarshalJSON

func (t TTL) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler, writing the TTL as a number so that a client receives 3600 rather than "3600".

func (TTL) String

func (t TTL) String() string

String returns the TTL as a plain number of seconds, which is the form a zonefile and the wire both use.

func (*TTL) UnmarshalJSON

func (t *TTL) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler. It accepts both a number of seconds and a suffixed string such as "1h", so a hand-written YAML file can use whichever is clearer.

func (TTL) Valid

func (t TTL) Valid() bool

Valid reports whether t is within the range RFC 2181 §8 permits.

type Zone

type Zone struct {
	ID   ZoneID
	Name Name

	Kind Kind
	// Prefix is the network a reverse zone answers for, derived from its name
	// when the zone is created. It is the zero value for a forward zone.
	Prefix netip.Prefix

	SOA SOA

	// DefaultTTL is applied to a record added without one.
	DefaultTTL TTL
	// AutoReverse enables PTR generation for records in this zone. Nil inherits
	// the global setting.
	AutoReverse *bool

	Disabled bool
	Comment  string

	CreatedAt time.Time
	UpdatedAt time.Time
}

Zone is a namespace this server is authoritative for.

func NewZone

func NewZone(name Name, soa SOA) (Zone, error)

NewZone builds a zone for the given apex, deriving its kind and, for a reverse zone, the network it is responsible for.

func (Zone) Contains

func (z Zone) Contains(n Name) bool

Contains reports whether n lies at or below the zone apex. It does not account for delegations: a name below an NS record inside this zone is still within the zone's namespace, just not answered from it.

func (Zone) Covers

func (z Zone) Covers(addr netip.Addr) bool

Covers reports whether addr falls inside the network a reverse zone answers for. It is always false for a forward zone.

func (Zone) IsApex

func (z Zone) IsApex(n Name) bool

IsApex reports whether n is the zone apex, where the SOA and the zone's own NS records live.

func (Zone) ReverseOwner

func (z Zone) ReverseOwner(addr netip.Addr) (Name, error)

ReverseOwner returns the owner name a PTR for addr takes inside z.

Usually that is simply the reverse name of the address, because a reverse zone on an octet or nibble boundary is an ancestor of every name below it. The exception is RFC 2317: a classless child such as "0/25.2.0.192.in-addr.arpa." is *not* an ancestor of "10.2.0.192.in-addr.arpa.", even though it answers for that address. The host part is re-attached under the child's own apex instead, giving "10.0/25.2.0.192.in-addr.arpa.", which is the name the parent zone's generated CNAME points at.

func (Zone) Validate

func (z Zone) Validate() error

Validate reports whether the zone itself is well formed. It says nothing about the records in it; that is ValidateRRset and the applier's job.

type ZoneID

type ZoneID string //nolint:revive // "zone.ZoneID" reads better at call sites than "zone.ID"

ZoneID identifies a zone. It is a ULID: assignable before the transaction that stores it, safe to merge across nodes once the cluster exists, and ordered by time so it keeps index locality.

Jump to

Keyboard shortcuts

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