Documentation
¶
Overview ¶
Package apply is the write path, and the only thing that changes zone data.
A client submits a Command. The applier works out what it amounts to against the zone as it stands, writes it, and records it as one journal.Commit in the same transaction. Nothing else advances a zone serial, and no record is written without a commit explaining it (invariant 4).
Three cases are returned to the caller rather than logged:
- the address already answers with another name (D3). The existing entry stays and Conflict reports it.
- no zone exists to hold the entry (D6). MissingZone names the zone that would be needed, since creating one asserts authority over a namespace.
- a different entry is wanted (D4). Editing a generated record is refused; ActionDetach hands it over instead.
RFC 2317 is handled on both sides. An entry for an address inside a classless child goes under that child's apex, and where this server also holds the parent, the CNAME pointing there is written too (D7). Provenance is a chain, delegation to entry to address record, and removals are ordered from the links rather than assumed.
A reverse zone created for a network already in use has no change to react to. Applier.Reconcile fills it, and moves what the arrival of a more specific zone has overtaken. It only adds: obsolete entries were removed by the change that obsoleted them, and a detached entry (D4) is not the automation's to touch.
Index ¶
- Constants
- func NotifyTargetsText(targets []NotifyTarget) []string
- func TransferAllowText(allow TransferAllow) []string
- func WriteSnapshot(w io.Writer, h SnapshotHeader, items iter.Seq2[*store.Replicated, error]) error
- type Applied
- type AppliedZone
- type Applier
- func (a *Applier) Apply(ctx context.Context, cmd Command) (*Result, error)
- func (a *Applier) ApplyBatch(ctx context.Context, b *Batch) error
- func (a *Applier) ApplyBatchAt(ctx context.Context, b *Batch, at Index) error
- func (a *Applier) CheckReverse(ctx context.Context, zid zone.ZoneID) ([]zone.Finding, error)
- func (a *Applier) CreateKey(ctx context.Context, name zone.Name, alg zone.TSIGAlgorithm, secret []byte) (*store.TSIGKey, error)
- func (a *Applier) CreateToken(ctx context.Context, tok *store.Token) error
- func (a *Applier) CreateZone(ctx context.Context, z *zone.Zone, records []zone.Record, meta Meta) (*Result, error)
- func (a *Applier) DeleteZone(ctx context.Context, zid zone.ZoneID, meta Meta) (*Result, error)
- func (a *Applier) Exclusive(ctx context.Context, fn func(context.Context) error) error
- func (a *Applier) Import(ctx context.Context, in Import, meta Meta) (*Result, error)
- func (a *Applier) Plan(ctx context.Context, cmd Command) (*Batch, *Result, error)
- func (a *Applier) PlanCreateKey(name zone.Name, alg zone.TSIGAlgorithm, secret []byte) (*Batch, *store.TSIGKey, error)
- func (a *Applier) PlanCreateToken(tok *store.Token) (*Batch, error)
- func (a *Applier) PlanCreateZone(ctx context.Context, z *zone.Zone, records []zone.Record, meta Meta) (*Batch, *Result, error)
- func (a *Applier) PlanDeleteZone(ctx context.Context, zid zone.ZoneID, meta Meta) (*Batch, *Result, error)
- func (a *Applier) PlanReconcile(ctx context.Context, zid zone.ZoneID, meta Meta) (*Batch, *Result, error)
- func (a *Applier) PlanRevokeKey(kid store.TSIGKeyID) (*Batch, error)
- func (a *Applier) PlanRevokeToken(ctx context.Context, tid store.TokenID, guard TokenGuard) (*Batch, error)
- func (a *Applier) PlanRollback(ctx context.Context, zid zone.ZoneID, target zone.Serial, meta Meta) (*Batch, *Result, error)
- func (a *Applier) PlanSettings(changes []SettingChange) (*Batch, error)
- func (a *Applier) PlanUpdateZone(ctx context.Context, z *zone.Zone, meta Meta) (*Batch, *Result, error)
- func (a *Applier) Policy(ctx context.Context, r store.Reader) (Policy, error)
- func (a *Applier) Reconcile(ctx context.Context, zid zone.ZoneID, meta Meta) (*Result, error)
- func (a *Applier) RevokeKey(ctx context.Context, kid store.TSIGKeyID) error
- func (a *Applier) RevokeToken(ctx context.Context, tid store.TokenID, guard TokenGuard) error
- func (a *Applier) Rollback(ctx context.Context, zid zone.ZoneID, target zone.Serial, meta Meta) (*Result, error)
- func (a *Applier) RotateCookieSecrets(ctx context.Context) (CookieSecrets, bool, error)
- func (a *Applier) SetSettings(ctx context.Context, changes []SettingChange) error
- func (a *Applier) Since(ctx context.Context, apex zone.Name, from, to zone.Serial) ([]*journal.Commit, bool, error)
- func (a *Applier) UpdateZone(ctx context.Context, z *zone.Zone, meta Meta) (*Result, error)
- type Batch
- type Command
- type Conflict
- type CookieSecret
- type CookieSecrets
- type Import
- type Index
- type KeyOp
- type KeyOpKind
- type Meta
- type MissingZone
- type NotifyTarget
- type OpAction
- type Options
- type Policy
- type RecordOp
- type Replication
- type Result
- type SettingChange
- type Skipped
- type SnapshotHeader
- type SnapshotWriter
- type TokenGuard
- type TokenOp
- type TokenOpKind
- type TransferAllow
- type ZoneOp
- type ZoneOpKind
Constants ¶
const CookieRotation = time.Hour
CookieRotation is how often the secret changes.
It is the hour a Server Cookie is honoured for (RFC 9018 §4.3) rather than a period somebody picked, and the two are the same number for a reason: an hour is the shortest rotation that never takes a cookie away from a client still entitled to use it. The secret a cookie was issued under is the current one for an hour and the predecessor for an hour after that, by which time every cookie built under it has expired on its own.
const CookieSecretLen = 16
CookieSecretLen is the length of one secret, in octets. SipHash-2-4 takes a 128-bit key, and RFC 9018 §6 leaves no second choice of hash.
const CookieSecretSetting = "cookie_secrets"
CookieSecretSetting is the key the DNS cookie secrets are stored under.
A Server Cookie is a hash under this secret (RFC 9018 §4.4), so it decides which cookies this server recognises as its own. It is a stored setting rather than a file beside the database because every node answering for the same addresses has to hold the same one, and settings are what a batch already carries between nodes (D32).
const NotifySetting = "notify_targets"
NotifySetting is the key the list of secondaries told about a change is stored under. Empty means nobody, which is where a server starts (docs/decisions/d27-notify.md).
const PolicySetting = "reverse_policy"
PolicySetting is the key the server-wide reverse conflict policy is stored under.
It lives in the database rather than in the configuration file because an operator changes it and every client has to be able to reach it (docs/decisions/ D11). Reading it inside the write transaction is what makes a change take effect on the next write instead of the next restart.
const TransferSetting = "transfer_allow"
TransferSetting is the key the list of clients allowed to pull a whole zone is stored under. Empty means nobody, which is where a server starts (docs/decisions/d26-outbound-zone-transfer.md).
Variables ¶
This section is empty.
Functions ¶
func NotifyTargetsText ¶ added in v0.2.0
func NotifyTargetsText(targets []NotifyTarget) []string
NotifyTargetsText renders the list the way a client sends it, leaving the port off where it is the one every resolver would have assumed.
func TransferAllowText ¶ added in v0.2.0
func TransferAllowText(allow TransferAllow) []string
TransferAllowText renders the list the way a client sends it, addresses first.
func WriteSnapshot ¶ added in v0.5.0
func WriteSnapshot(w io.Writer, h SnapshotHeader, items iter.Seq2[*store.Replicated, error]) error
WriteSnapshot writes a log snapshot: the header, one line per item, and a mark saying the content is complete. Items may be nil, which is what a witness writes.
Types ¶
type Applied ¶ added in v0.5.0
type Applied struct {
// Zones are the zones the batch changed, each once. A deleted zone is among
// them: the store no longer holds it, and that is how a reader learns to
// drop it.
Zones []AppliedZone
// Keys is whether the transfer keys changed.
Keys bool
// Settings are the keys of the server settings the batch wrote.
Settings []string
}
Applied says what a batch changed, for anything that keeps a copy of what the store holds somewhere else.
It names what was touched rather than carrying it, so the copy is rebuilt from the store. Two of these handled out of order then still end at the latest state, because by the time either is handled the store holds it.
type AppliedZone ¶ added in v0.5.0
AppliedZone names one zone a batch changed. The name travels beside the identifier because a zone that was deleted can no longer be looked up by it.
type Applier ¶
type Applier struct {
// contains filtered or unexported fields
}
Applier is the only thing that changes what this server stores.
Every change to a zone is recorded as a commit in the same transaction (architecture invariant 4), and nothing else advances a zone serial. That is what lets the audit log, the diff view, rollback and incremental transfer all read from one structure instead of four.
Tokens, transfer keys and server settings go through here as well and carry no commit, because they belong to no zone and step no serial. What they get from this path is the other half of it: one place where a change is decided, and one shape it travels in.
func (*Applier) ApplyBatch ¶ added in v0.2.0
ApplyBatch carries out a batch this node planned itself, which is every batch until there is a cluster.
It does not take the zone's write lock; see Applier.Plan.
func (*Applier) ApplyBatchAt ¶ added in v0.2.0
ApplyBatchAt carries out a batch that arrived from the replicated log and records the position it arrived from, both in the same transaction. A node that wrote the index separately would come back from a power cut having applied an entry it does not remember, or remembering one it never applied (D24).
It does nothing if that entry has already been carried out here. Replaying the log is what a node does after a restart, and every entry it replays is one it may well have applied before.
It does not take the zone's write lock; see Applier.Plan.
func (*Applier) CheckReverse ¶ added in v0.2.0
CheckReverse reports the reverse entries this zone's records imply and does not have.
It plans exactly what Applier.Reconcile would carry out and reports it instead of writing it, so the rules deciding what an address record generates run in one place rather than two that drift (D21).
The inverse question, an entry that is here and would not be generated today, is deliberately not asked. Reverse automation only ever adds: an entry made obsolete is taken away by the change that obsoleted it, and one somebody detached is theirs to keep (D4). Reporting those would need a rule for what may legitimately remain, which D4 leaves to the person.
func (*Applier) CreateKey ¶ added in v0.2.0
func (a *Applier) CreateKey( ctx context.Context, name zone.Name, alg zone.TSIGAlgorithm, secret []byte, ) (*store.TSIGKey, error)
CreateKey brings a transfer key into being and returns it.
The secret is the caller's: generated or pasted in on the node that accepted the request, which is where D24 wants it settled. What this adds is the identifier and the moment, so that two nodes cannot hold one key under two identities or two ages.
func (*Applier) CreateToken ¶ added in v0.2.0
CreateToken writes a token.
The token is minted by the caller, on the node that accepted the request, because the secret is shown to whoever asked and never again. Everything stored about it travels.
func (*Applier) CreateZone ¶
func (a *Applier) CreateZone( ctx context.Context, z *zone.Zone, records []zone.Record, meta Meta, ) (*Result, error)
CreateZone brings a zone into existence together with its first records, and records it as one commit.
The zone has to be usable when the call returns: at least one NS record at the apex (RFC 1034 §4.2.1). A zone with none names no authority for itself and no parent can delegate to it, and refusing it here means every zone in the database is one that can actually be answered from. Supplying a sensible default, usually an NS pointing at the SOA's primary, is the job of whatever is talking to the person, not of the write path.
The zone's own serial is where the journal starts counting. An import therefore keeps the serial the zone already had elsewhere, which its secondaries have seen; see docs/decisions/ D2.
func (*Applier) DeleteZone ¶
DeleteZone removes a zone and everything in it, and records that it happened.
func (*Applier) Exclusive ¶ added in v0.5.0
Exclusive runs fn with no write of this applier in flight, and none starting until it returns. Making a node the first member of a cluster runs this way, so that no write lands in the store between the moment the log begins and the moment its first snapshot is taken (docs/decisions/d44-starting-and-joining.md).
func (*Applier) Import ¶
Import brings a whole zone in at once.
The zone must not already exist. A file is the complete contents of a zone, so importing into a zone that already holds records is a replacement rather than an import, and doing that silently would be the difference between gaining a zone and losing one.
func (*Applier) Plan ¶ added in v0.2.0
Plan works out what a command amounts to and leaves the store as it found it. It mints every identifier and timestamp, and checks its work by writing inside a transaction it throws away.
The Result carries what the command could not do, which the planner reports and the batch does not carry.
It takes no lock. Applier.Apply holds the zone's across both halves, and so must anything else that sequences the two.
func (*Applier) PlanCreateKey ¶ added in v0.2.0
func (a *Applier) PlanCreateKey( name zone.Name, alg zone.TSIGAlgorithm, secret []byte, ) (*Batch, *store.TSIGKey, error)
PlanCreateKey works out the batch that writes a key, and mints what the key needs beyond what the caller gave.
func (*Applier) PlanCreateToken ¶ added in v0.2.0
PlanCreateToken works out the batch that writes a token.
func (*Applier) PlanCreateZone ¶ added in v0.2.0
func (a *Applier) PlanCreateZone( ctx context.Context, z *zone.Zone, records []zone.Record, meta Meta, ) (*Batch, *Result, error)
PlanCreateZone works out what bringing a zone into being would amount to, and leaves the store as it found it. See Applier.Plan.
func (*Applier) PlanDeleteZone ¶ added in v0.2.0
func (a *Applier) PlanDeleteZone( ctx context.Context, zid zone.ZoneID, meta Meta, ) (*Batch, *Result, error)
PlanDeleteZone works out what removing a zone would amount to, and leaves the store as it found it. See Applier.Plan.
func (*Applier) PlanReconcile ¶ added in v0.2.0
func (a *Applier) PlanReconcile( ctx context.Context, zid zone.ZoneID, meta Meta, ) (*Batch, *Result, error)
PlanReconcile works out what filling a zone would amount to, and leaves the store as it found it. See Applier.Plan for what a plan is and is not.
func (*Applier) PlanRevokeKey ¶ added in v0.2.0
PlanRevokeKey works out the batch that ends a key. The moment is settled here rather than while writing, so every node records the same one.
func (*Applier) PlanRevokeToken ¶ added in v0.2.0
func (a *Applier) PlanRevokeToken( ctx context.Context, tid store.TokenID, guard TokenGuard, ) (*Batch, error)
PlanRevokeToken works out the batch that ends a token, and refuses here rather than while writing: a follower must not be in a position to disagree (D24).
func (*Applier) PlanRollback ¶ added in v0.2.0
func (a *Applier) PlanRollback( ctx context.Context, zid zone.ZoneID, target zone.Serial, meta Meta, ) (*Batch, *Result, error)
PlanRollback works out what restoring an earlier state would amount to, and leaves the store as it found it. See Applier.Plan.
func (*Applier) PlanSettings ¶ added in v0.2.0
func (a *Applier) PlanSettings(changes []SettingChange) (*Batch, error)
PlanSettings turns settings changes into the batch that carries them out.
Nothing here reads the store: a setting is given rather than worked out, so there is no state for the plan to resolve against. It is still a plan, because what reaches every node has to be the resolved value and not the request for one.
func (*Applier) PlanUpdateZone ¶ added in v0.2.0
func (a *Applier) PlanUpdateZone( ctx context.Context, z *zone.Zone, meta Meta, ) (*Batch, *Result, error)
PlanUpdateZone works out what changing a zone's settings would amount to, and leaves the store as it found it. See Applier.Plan.
func (*Applier) Policy ¶
Policy returns the reverse conflict policy in force, which is what a client asking to see the settings is told.
func (*Applier) Reconcile ¶
Reconcile writes the reverse entries a zone's records imply but does not yet have.
It exists because reverse automation reacts to changes, and a zone that arrives after the records is a case with no change to react to. Creating a reverse zone for a network whose addresses are already in use is exactly that: the entries would appear one at a time as each address record happened to be edited, which is no answer at all. So the zone is created (never as a side effect, always because somebody asked (D6)) and then filled.
It only adds. A generated entry that should no longer exist is already taken away by the change that made it obsolete, and one that somebody detached is theirs to keep (D4): removing it here would make detaching mean nothing.
func (*Applier) RevokeToken ¶ added in v0.2.0
RevokeToken withdraws a token, if guard allows it.
The read the guard judges and the write it permits are one transaction and one lock, so two revocations racing cannot each see the other's token and both go ahead.
func (*Applier) Rollback ¶
func (a *Applier) Rollback( ctx context.Context, zid zone.ZoneID, target zone.Serial, meta Meta, ) (*Result, error)
Rollback restores a zone to the state it had at a serial.
It moves forward to that state rather than rewinding to it: the difference between the zone as it now stands and the zone as it stood then is written as a new commit, marked as a rollback and naming the serial it restores. History stays append-only, which is not tidiness but correctness: a secondary that has already seen serial 90 will never accept a jump back to 42, because RFC 1982 arithmetic makes 42 older and RFC 1995 has no way to express going backwards. See data model §3.7.
Records the server generated are not restored. They are derived from other records, and the automation puts them back from whatever those now say; a rollback that also wrote the derived copies would be racing the thing that owns them.
func (*Applier) RotateCookieSecrets ¶ added in v0.4.0
RotateCookieSecrets mints the next secret where the stored pair is due, and returns the pair in force afterwards along with whether it changed.
In a cluster this is the leader's to do, for the reason D36 gives about probing a secondary: every node rotating on its own schedule would publish several answers to one fact, and a client would hold a cookie that half the nodes recognise.
func (*Applier) SetSettings ¶ added in v0.2.0
func (a *Applier) SetSettings(ctx context.Context, changes []SettingChange) error
SetSettings writes server settings, all of them or none.
It takes no zone lock, because a setting belongs to no zone. What orders it against a write in flight is the store, which serializes writers: the planning transaction reading the policy either sees this change or does not, and never half of it.
func (*Applier) Since ¶ added in v0.2.0
func (a *Applier) Since( ctx context.Context, apex zone.Name, from, to zone.Serial, ) ([]*journal.Commit, bool, error)
Since returns the commits that took a zone from one serial to another, oldest first, so that an incremental transfer can replay them (RFC 1995 §4).
The upper end is the caller's rather than the newest the journal holds. A transfer is served from the snapshot, which lags the database for as long as a write takes to publish, and a commit past it has not been answered from yet.
It reports false where the range is not covered, which is the ordinary answer to a client that has been away longer than the retention of docs/decisions/ D8, or that names a serial the zone never had.
func (*Applier) UpdateZone ¶
UpdateZone replaces a zone's own settings, leaving its records alone.
It returns nil when nothing changed. Where something did, the serial advances like any other change: a zone's settings include the SOA timers, which are served, and whether the zone answers at all.
type Batch ¶ added in v0.2.0
type Batch struct {
// Commits explain the work, one per zone the change reached.
Commits []*journal.Commit
// Zones are the changes to the zones themselves. They bracket the record
// changes: a record needs its zone to exist, and a zone outlives the
// journal entry that removes it.
Zones []ZoneOp
// Settings are the server settings this change writes. They are not zone
// data and produce no commit, so a batch can carry these and nothing else
// (D32).
Settings []SettingChange
// Keys are the changes to the keys a secondary signs with, for the same
// reason and with the same consequence.
Keys []KeyOp
// Tokens are the changes to the tokens this server accepts, likewise.
Tokens []TokenOp
// contains filtered or unexported fields
}
Batch is one command resolved against the zones as they stand, ready to be carried out without deciding anything further. A cluster replicates this rather than the command; see docs/decisions/d24-what-the-cluster-replicates.md.
func (*Batch) Empty ¶ added in v0.2.0
Empty reports whether the command changed nothing.
A commit is not the test on its own. Everything that touches a zone produces one, and a setting touches no zone.
func (*Batch) MarshalJSON ¶ added in v0.2.0
MarshalJSON implements json.Marshaler.
func (*Batch) UnmarshalJSON ¶ added in v0.2.0
UnmarshalJSON implements json.Unmarshaler. Everything is checked again rather than trusted: what comes out the far end of this is a DNS answer.
type Command ¶
type Command struct {
ZoneID zone.ZoneID
// Ops are the authored operations, applied in order.
Ops []RecordOp
// ExpectedSerial enables optimistic concurrency: the command is refused if
// the zone has moved on since it was read. Nil skips the check. It is a
// pointer because serial 0 is a legal serial and could not otherwise be
// told apart from "do not check".
ExpectedSerial *zone.Serial
Kind journal.Kind
Source journal.Source
Actor string
Comment string
}
Command is the intent a client submits, before validation against the zone and before reverse automation expands it.
It is the unit Raft will replicate; see docs/decisions/d19-journal-as-command-log.md. That is why the applier fills in everything undetermined, identifiers above all, before it starts, rather than while applying: a command has to produce the same result on every node that applies it.
type Conflict ¶
type Conflict struct {
// Source is the address record whose reverse entry was not created.
Source zone.RecordID
// SourceName is the name that record sits at, and Address what it points at.
SourceName zone.Name
Address netip.Addr
// ReverseZone and ReverseName are where the entry would have gone.
ReverseZone zone.ZoneID
ReverseName zone.Name
// Existing is the name the address currently reverses to, and Generated
// says whether the record holding it is one the server made. An entry
// someone wrote by hand is never taken away from them.
Existing zone.Name
Generated bool
Policy Policy
}
Conflict is a reverse entry that was not created because the address already answers with a different name.
It is returned to the caller rather than logged: several names pointing at one address is the normal case (virtual hosts, a load balancer, a service alias) and a conflict only visible in the server log is the same as no conflict detection at all (D3).
type CookieSecret ¶ added in v0.4.0
type CookieSecret [CookieSecretLen]byte
CookieSecret is one key Server Cookies are computed under.
It is minted here and never shown to anybody: unlike a token or a TSIG key, no operator ever types it in, and there is nothing on the other end of it to configure. That is what makes rotating it a decision this server can take on its own.
func NewCookieSecret ¶ added in v0.4.0
func NewCookieSecret() (CookieSecret, error)
NewCookieSecret mints a secret from the system's randomness.
D24's rule about minting holds here: the secret is settled on the node that decided to rotate and travels with the entry, because two nodes inventing two secrets for one rotation would hand out cookies neither recognises.
func (CookieSecret) IsZero ¶ added in v0.4.0
func (s CookieSecret) IsZero() bool
IsZero reports whether the secret is unset. A minted one is never zero in any run anybody will see.
func (CookieSecret) MarshalJSON ¶ added in v0.4.0
func (s CookieSecret) MarshalJSON() ([]byte, error)
MarshalJSON writes the secret as hex, which is how RFC 9018's own test vectors write one and the only form it is ever read in.
func (*CookieSecret) UnmarshalJSON ¶ added in v0.4.0
func (s *CookieSecret) UnmarshalJSON(b []byte) error
UnmarshalJSON reads a secret written as hex, and refuses one of the wrong length rather than padding it into something that hashes.
type CookieSecrets ¶ added in v0.4.0
type CookieSecrets struct {
// Current is what a cookie handed out now is computed under.
Current CookieSecret `json:"current"`
// Previous is the secret before it, still accepted. It is zero on a server
// that has never rotated.
Previous CookieSecret `json:"previous"`
// RotatedAt is when Current was minted, and is what says whether the next
// rotation is due.
RotatedAt time.Time `json:"rotated_at"`
}
CookieSecrets are the secrets this server computes and checks Server Cookies under.
There are two of them because a rotation cannot be instantaneous. A client holds a Server Cookie for up to an hour (RFC 9018 §4.3), and a rotation that forgot the secret those were built under would refuse every one of them in the same second, which is the round trip D35 is trying to charge once.
func StoredCookieSecrets ¶ added in v0.4.0
StoredCookieSecrets returns the secrets the database holds, or the zero pair when none have been minted.
func (CookieSecrets) Due ¶ added in v0.4.0
func (s CookieSecrets) Due(now time.Time) bool
Due reports whether the secrets are old enough to be rotated at now. A pair that was never minted is due immediately, which is how the first one comes into existence.
func (CookieSecrets) IsZero ¶ added in v0.4.0
func (s CookieSecrets) IsZero() bool
IsZero reports whether no secret has been minted yet, which is true exactly once in a server's life.
func (CookieSecrets) Rotate ¶ added in v0.4.0
func (s CookieSecrets) Rotate(now time.Time) (CookieSecrets, error)
Rotate returns the pair that follows this one: a freshly minted secret, with the one it replaces kept as the predecessor.
Rotating the zero pair is how the first secret comes into existence, and it leaves no predecessor because there is none to keep.
type Import ¶
type Import struct {
// Name is the zone apex.
Name zone.Name
// SOA is the zone's own settings, serial included. The serial is the one
// the zone had wherever it came from, and it is kept: starting a migrated
// zone at 1 would make every existing secondary consider our copy older
// than what it already has (RFC 1982 §3.2) and refuse to transfer it. See
// docs/decisions/ D2.
SOA zone.SOA
// Records is everything the zone holds, apart from the SOA.
Records []zone.Record
// DefaultTTL is what a record added later gets when it names no TTL. Zero
// takes the SOA's own TTL, which for a file is what its $TTL directive
// most likely said.
DefaultTTL zone.TTL
}
Import is a whole zone arriving at once.
It is deliberately not a zonefile. A file is one way a zone arrives and a declarative configuration is another, and neither belongs in the write path: zonefiles are an import and export format, not a storage format (architecture invariant 5). What arrives here is a name, the zone's own settings and its records.
type Index ¶ added in v0.2.0
type Index uint64
Index is a position in the replicated log. Zero is no position at all: a batch this node planned and carried out on its own never travelled, so there is nothing about it to remember.
type KeyOp ¶ added in v0.2.0
type KeyOp struct {
Kind KeyOpKind
// Key is the whole key a create writes, secret included. It has to travel:
// verifying a signature means recomputing the MAC, so a node without the
// material cannot answer a signed request at all (D28). What keeps that
// off the wire in the open is the transport, which D24 authenticates.
Key *store.TSIGKey
// KeyID and At are the revocation.
KeyID store.TSIGKeyID
At time.Time
}
KeyOp is one change to the keys a secondary signs with.
A key is not zone data and produces no commit, so a batch can carry one of these and nothing else (D32).
type KeyOpKind ¶ added in v0.2.0
type KeyOpKind string
KeyOpKind is what a batch does to a transfer key.
type Meta ¶
Meta is who asked for a change and why. Every commit carries it, because an audit log that cannot say who did something is a list of mysteries.
type MissingZone ¶
type MissingZone struct {
Source zone.RecordID
SourceName zone.Name
Address netip.Addr
// Suggested is the reverse zone that would cover the address, at the
// boundary such zones are conventionally delegated on: a /24 for IPv4 and a
// /64 for IPv6.
Suggested zone.Name
Prefix netip.Prefix
}
MissingZone reports that an address has no reverse zone to put an entry in.
Creating one is an assertion of authority over a namespace, and doing that as a side effect of adding a record would be a surprise: for public address space it would be wrong. So the caller is told which zone would be needed and decides (D6).
type NotifyTarget ¶ added in v0.2.0
type NotifyTarget struct {
Addr netip.AddrPort
// Key is the TSIG key the notification is signed with, and is the zero
// name for one that goes out unsigned.
Key zone.Name
}
NotifyTarget is one secondary told when a zone changes.
func ParseNotifyTargets ¶ added in v0.2.0
func ParseNotifyTargets(in []string) ([]NotifyTarget, error)
ParseNotifyTargets turns what a client sent into addresses to send to.
A port may be given, because a secondary does not have to listen on 53. Where none is, the port RFC 1035 §4.2 assigns is meant. Unlike the transfer list this takes no prefixes: that list decides who may ask, and this one has to name somewhere a datagram can arrive.
func StoredNotifyTargets ¶ added in v0.2.0
StoredNotifyTargets returns the secondaries a notification is sent to.
type OpAction ¶
type OpAction string
OpAction is what one step of a Command asks for.
const ( // ActionAdd adds one record. ActionAdd OpAction = "add" // ActionUpdate replaces one record's content, keeping its identity so that // its comment, its provenance and the diff line pointing at it survive. ActionUpdate OpAction = "update" // ActionDelete removes one record. ActionDelete OpAction = "delete" // ActionReplaceRRset makes an RRset exactly the given set of records. An // empty set removes it. This is what a record editor submits, because it // is the unit DNS answers with (RFC 2181 §5). ActionReplaceRRset OpAction = "replace_rrset" // ActionMakeCanonical takes the reverse entry for an address away from the // name holding it and generates it from this record instead. It is the // answer to a conflict rather than a way to write one (D3, D33), and it // changes nothing about the record it names. ActionMakeCanonical OpAction = "make_canonical" // ActionDetach turns a generated record into an authored one. It keeps its // data and its identity, loses its link to the record it came from, and the // automation stops touching it. See docs/decisions/ D4. ActionDetach OpAction = "detach" )
type Options ¶
type Options struct {
// Now supplies the time a commit is stamped with. Nil uses [time.Now];
// tests pass their own so that the stamp is theirs to predict.
Now func() time.Time
// AutoReverse is whether reverse entries are generated for zones that do
// not say either way. Nil means on, so a caller that does not mention the
// automation gets it: it is the product's headline feature, and a zero
// value that switched it off would turn every forgotten option into a
// silently half-working server. Only a caller that means to switch it off
// writes it down. A zone overrides this on itself.
AutoReverse *bool
// Policy is what to do when an address already answers a reverse lookup
// with another name. Empty means [PolicyFirstWins].
Policy Policy
// OnApplied is told what each batch changed, once the change is committed
// and before the call that carried it out returns. Whatever keeps the
// query path in step with the store hangs here rather than behind the
// HTTP handlers, because a node applying a batch from the replicated log
// has no handler (docs/decisions/d41-what-follows-applying-a-batch.md).
//
// It runs on the goroutine that applied the batch, so it may be called
// from several at once, and it cannot fail the write: that is committed by
// the time it hears of it. Nil tells nobody.
OnApplied func(context.Context, Applied)
// Replication routes the batches this node plans through a cluster's log.
// Nil is a single node, which carries every batch out itself.
Replication Replication
}
Options configure an applier.
type Policy ¶
type Policy string
Policy says what to do when an address already answers a reverse lookup with some other name. See docs/decisions/ D3.
const ( // PolicyFirstWins keeps the reverse entry that is already there and reports // a conflict. It is the default, and the only setting that never changes an // answer nobody asked to change. PolicyFirstWins Policy = "first-wins" // PolicyLastWins replaces a generated entry with the new one, still // reporting the conflict. An entry someone wrote by hand is never replaced. PolicyLastWins Policy = "last-wins" // PolicyMulti keeps both. It is the most literal reading of "generate the // reverse entry", and it turns a routine change into a multi-record PTR set // that reverse-lookup checks are not built for. PolicyMulti Policy = "multi" // PolicyReject refuses the whole change. PolicyReject Policy = "reject" )
func StoredPolicy ¶
StoredPolicy returns the policy the database holds, or the empty policy when nothing has been set.
An unreadable or unknown value is an error rather than a silent fallback: a server that quietly ignores the policy an operator asked for is worse than one that refuses to write until the value is fixed.
type RecordOp ¶
type RecordOp struct {
Action OpAction
// RecordID addresses the record for [ActionUpdate] and [ActionDelete].
RecordID zone.RecordID
// Record is the content for [ActionAdd] and [ActionUpdate].
Record *zone.Record
// Key names the RRset for [ActionReplaceRRset]. It is given separately from
// Records because removing an RRset entirely leaves no record to read it
// from.
Key zone.RRsetKey
// Records is what the RRset should hold after [ActionReplaceRRset]. Empty
// removes the set.
Records []zone.Record
}
RecordOp is one authored change inside a Command.
type Replication ¶ added in v0.5.0
type Replication interface {
// Replicating reports whether writes go through the replicated log. A node
// that has not started or joined a cluster yet writes to its own store.
Replicating() bool
// Settle returns once this node may plan: it leads, and everything
// committed before now is applied here.
Settle(ctx context.Context) error
// Propose puts a planned batch into the log and returns once it is applied
// here.
Propose(ctx context.Context, b *Batch) error
}
Replication is what a cluster puts between planning a batch and carrying it out (docs/decisions/d24-what-the-cluster-replicates.md). A *cluster.Replication is one.
type Result ¶
type Result struct {
// Commits are the commits written, one per zone the change reached, in the
// order the zones were touched. It is empty when nothing changed.
Commits []*journal.Commit
Conflicts []Conflict
MissingZones []MissingZone
// Skipped are records an import left out because the zone could never
// answer with them. Empty for every other kind of change.
Skipped []Skipped
}
Result is what a change produced.
Conflicts and missing zones are returned rather than logged, because both are things a person has to decide about and neither is an error: a conflict means the address already answers with another name, and a missing zone means there is nowhere to put a reverse entry. See docs/decisions/ D3 and D6.
type SettingChange ¶ added in v0.2.0
SettingChange is one server setting, resolved to the bytes that will be stored.
It travels in a batch like everything else this path writes: a setting is read while a change is planned, so two nodes holding different ones would resolve the same command into different records (D32).
func CookieSecretsChange ¶ added in v0.4.0
func CookieSecretsChange(s CookieSecrets) (SettingChange, error)
CookieSecretsChange is the change that records a mint or a rotation.
func NotifyTargetsChange ¶ added in v0.2.0
func NotifyTargetsChange(targets []NotifyTarget) (SettingChange, error)
NotifyTargetsChange is the change that records who is told when a zone changes.
func PolicyChange ¶ added in v0.2.0
func PolicyChange(p Policy) (SettingChange, error)
PolicyChange is the change that records the policy every zone saying nothing about itself inherits.
func TransferAllowChange ¶ added in v0.2.0
func TransferAllowChange(allow TransferAllow) (SettingChange, error)
TransferAllowChange is the change that records who may pull a zone.
type Skipped ¶
Skipped is a record an import did not write, and the reason.
It travels as data rather than as an error, for the reason docs/decisions/ D3 and D6 give about conflicts and missing reverse zones: this is something a person has to decide about, and refusing the whole file over it would mean nobody can migrate a zone that has an oddity in it.
type SnapshotHeader ¶ added in v0.5.0
type SnapshotHeader struct {
// Index is the position in the replicated log the content is current as
// of. It travels inside the snapshot because a restore is handed the bytes
// and nothing else.
Index uint64
// Writer is what kind of member wrote it.
Writer SnapshotWriter
}
SnapshotHeader is what a log snapshot says about itself ahead of its content (docs/decisions/d30-what-a-log-snapshot-contains.md).
func ReadSnapshot ¶ added in v0.5.0
func ReadSnapshot(r io.Reader) (SnapshotHeader, iter.Seq2[*store.Replicated, error], error)
ReadSnapshot reads a log snapshot's header, and returns it with the content still to come. The header is read at once so that a caller can refuse the snapshot before touching anything; the content can be ranged over once.
Everything is checked again rather than trusted, as a batch is: what comes out of this ends up as a DNS answer.
type SnapshotWriter ¶ added in v0.5.0
type SnapshotWriter string
SnapshotWriter says what kind of member wrote a log snapshot.
const ( // WriterStore is a member that holds a store, which is every member this // server can be. WriterStore SnapshotWriter = "store" // WriterWitness is a member that keeps the log and applies none of it // (docs/decisions/d39-the-witness.md). Its snapshot has no content, and a // node that restored one would empty itself. WriterWitness SnapshotWriter = "witness" )
type TokenGuard ¶ added in v0.2.0
TokenGuard is asked, inside the planning transaction, whether a revocation may go ahead. What makes a token an administrator's is the API's vocabulary rather than the store's, so the rule lives with the caller and the atomicity lives here.
type TokenOp ¶ added in v0.2.0
type TokenOp struct {
Kind TokenOpKind
// Token is the whole token a create writes.
Token *store.Token
// TokenID and At are the revocation.
TokenID store.TokenID
At time.Time
}
TokenOp is one change to the tokens this server accepts.
What travels is the hash, never the secret: D5 stores a token as the SHA-256 of what the client holds, so there is nothing here that authenticates anybody even if it is read.
type TokenOpKind ¶ added in v0.2.0
type TokenOpKind string
TokenOpKind is what a batch does to an API token.
const ( // TokenCreate brings a token into being. TokenCreate TokenOpKind = "create" // TokenRevoke ends one. The row stays, so the history keeps naming the // token behind each change (D5). TokenRevoke TokenOpKind = "revoke" )
type TransferAllow ¶ added in v0.2.0
TransferAllow is who may pull a zone off this server: client addresses, and the TSIG keys a request may sign itself with. One entry matching is enough (docs/decisions/d28-tsig.md).
func ParseTransferAllow ¶ added in v0.2.0
func ParseTransferAllow(in []string) (TransferAllow, error)
ParseTransferAllow turns what a client sent into addresses and key names.
An entry beginning with "key:" names a TSIG key, which grants a transfer from any address at all; that is what a key is for. Everything else is an address.
A bare address is the single-host prefix it means, so that an operator naming one secondary does not have to write /32 to be understood. A prefix with host bits set is refused rather than masked: 192.0.2.7/24 is either a typo for the host or for the network, and guessing which would sometimes hand a zone to a network nobody meant to name.
func StoredTransferAllow ¶ added in v0.2.0
StoredTransferAllow returns who a zone transfer is served to.
func (TransferAllow) Empty ¶ added in v0.2.0
func (a TransferAllow) Empty() bool
Empty reports whether the list permits nobody, which is where a server starts.
type ZoneOp ¶ added in v0.2.0
type ZoneOp struct {
Kind ZoneOpKind
ZoneID zone.ZoneID
// Zone is what the zone should become. Nil for a deletion.
Zone *zone.Zone
}
ZoneOp is one change to a zone itself.
type ZoneOpKind ¶ added in v0.2.0
type ZoneOpKind string
ZoneOpKind is what a batch does to a zone itself, as opposed to what it holds.
const ( // ZoneCreate brings a zone into being. ZoneCreate ZoneOpKind = "create" // ZoneUpdate replaces a zone's own settings. ZoneUpdate ZoneOpKind = "update" // ZoneDelete removes a zone, and its records with it. ZoneDelete ZoneOpKind = "delete" )