Missing reverse zones are suggested for the private views' A and AAAA records (/24 for private IPv4, /32 for a public address, /48 for IPv6) and created filled with PTR records (jarl zone suggest-reverse, the Zones page). zone create --fill and zone fill-ptr add the PTRs a reverse zone lacks; a reverse zone created without a view goes to the private view using its addresses. Per zone, auto_ptr keeps the PTR records of A and AAAA records in step on every change (database version 12). Zone commands take a network. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
360 lines
14 KiB
Go
360 lines
14 KiB
Go
package engine
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"jarl/internal/model"
|
|
)
|
|
|
|
// Operation kinds.
|
|
const (
|
|
OpZoneCreate = "zone.create"
|
|
OpZoneUpdate = "zone.update"
|
|
OpZoneDelete = "zone.delete"
|
|
OpRecordCreate = "record.create"
|
|
OpRecordUpdate = "record.update"
|
|
OpRecordDelete = "record.delete"
|
|
OpRRSetReplace = "rrset.replace"
|
|
OpViewCreate = "view.create"
|
|
OpViewUpdate = "view.update"
|
|
OpViewDelete = "view.delete"
|
|
OpViewReorder = "view.reorder"
|
|
OpACLCreate = "acl.create"
|
|
OpACLUpdate = "acl.update"
|
|
OpACLDelete = "acl.delete"
|
|
OpTSIGCreate = "tsig.create"
|
|
OpTSIGUpdate = "tsig.update"
|
|
OpTSIGDelete = "tsig.delete"
|
|
OpOptionCreate = "option.create"
|
|
OpOptionUpdate = "option.update"
|
|
OpOptionDelete = "option.delete"
|
|
OpSettingsUpdate = "settings.update"
|
|
|
|
OpBlocklistCreate = "blocklist.create"
|
|
OpBlocklistUpdate = "blocklist.update"
|
|
OpBlocklistDelete = "blocklist.delete"
|
|
OpBlockRuleCreate = "blockrule.create"
|
|
OpBlockRuleUpdate = "blockrule.update"
|
|
OpBlockRuleDelete = "blockrule.delete"
|
|
OpBlockingPause = "blocking.pause"
|
|
|
|
OpBlockStepCreate = "blockstep.create"
|
|
OpBlockStepUpdate = "blockstep.update"
|
|
OpBlockStepDelete = "blockstep.delete"
|
|
OpClientGroupCreate = "clientgroup.create"
|
|
OpClientGroupUpdate = "clientgroup.update"
|
|
OpClientGroupDelete = "clientgroup.delete"
|
|
|
|
// Requests only: disabling and enabling records, by ID or by match
|
|
// (zone, name, type and optionally data), become record.update
|
|
// operations.
|
|
OpRecordDisable = "record.disable"
|
|
OpRecordEnable = "record.enable"
|
|
// Acknowledging an exposure finding (docs/exposure.md), and taking it
|
|
// back, become view.update or settings.update operations.
|
|
OpExposureAck = "exposure.ack"
|
|
OpExposureUnack = "exposure.unack"
|
|
// Linked zones (docs/views.md#linked-zones): requests that expand
|
|
// into zone and record updates.
|
|
OpZoneLink = "zone.link"
|
|
OpZoneUnlink = "zone.unlink"
|
|
// Filling a reverse zone with the PTR records of its view's A and
|
|
// AAAA records (docs/reverse.md) becomes record.create operations.
|
|
OpZoneFillPTR = "zone.fill_ptr"
|
|
)
|
|
|
|
// AckInput acknowledges (or un-acknowledges) an exposure finding of a view
|
|
// (by ID or name), or of the server (neither given).
|
|
type AckInput struct {
|
|
ViewID int64 `json:"view_id,omitempty"`
|
|
View string `json:"view,omitempty"`
|
|
Code string `json:"code"`
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
|
|
// OpKinds lists all operation kinds.
|
|
var OpKinds = []string{OpZoneCreate, OpZoneUpdate, OpZoneDelete, OpRecordCreate, OpRecordUpdate, OpRecordDelete, OpRRSetReplace,
|
|
OpViewCreate, OpViewUpdate, OpViewDelete, OpViewReorder, OpACLCreate, OpACLUpdate, OpACLDelete,
|
|
OpTSIGCreate, OpTSIGUpdate, OpTSIGDelete, OpOptionCreate, OpOptionUpdate, OpOptionDelete, OpSettingsUpdate,
|
|
OpBlocklistCreate, OpBlocklistUpdate, OpBlocklistDelete, OpBlockRuleCreate, OpBlockRuleUpdate, OpBlockRuleDelete, OpBlockingPause,
|
|
OpBlockStepCreate, OpBlockStepUpdate, OpBlockStepDelete, OpClientGroupCreate, OpClientGroupUpdate, OpClientGroupDelete}
|
|
|
|
// Request is an operation as submitted by a client. Requests are expanded
|
|
// into one or more stored operations (for example an A record with "ptr"
|
|
// also creates the matching PTR record).
|
|
type Request struct {
|
|
Op string `json:"op"`
|
|
// ID is the object to update or delete.
|
|
ID int64 `json:"id,omitempty"`
|
|
// BaseVersion, if set, makes the request fail when the object's
|
|
// version differs (optimistic locking). Otherwise the version at
|
|
// staging time is recorded.
|
|
BaseVersion int `json:"base_version,omitempty"`
|
|
Data json.RawMessage `json:"data,omitempty"`
|
|
}
|
|
|
|
// NewRequest builds a request with data marshaled from an input struct.
|
|
func NewRequest(op string, id int64, data any) Request {
|
|
r := Request{Op: op, ID: id}
|
|
if data != nil {
|
|
b, err := json.Marshal(data)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
r.Data = b
|
|
}
|
|
return r
|
|
}
|
|
|
|
// SOAInput sets SOA fields of a primary zone.
|
|
type SOAInput struct {
|
|
MName *string `json:"mname,omitempty"`
|
|
RName *string `json:"rname,omitempty"` // mailbox name or email address
|
|
Serial *uint32 `json:"serial,omitempty"`
|
|
Refresh *uint32 `json:"refresh,omitempty"`
|
|
Retry *uint32 `json:"retry,omitempty"`
|
|
Expire *uint32 `json:"expire,omitempty"`
|
|
Minimum *uint32 `json:"minimum,omitempty"`
|
|
}
|
|
|
|
// ZoneInput creates or updates a zone.
|
|
type ZoneInput struct {
|
|
// Name is the zone name. For create, a CIDR prefix such as
|
|
// "192.168.1.0/24" creates the matching reverse zone.
|
|
Name *string `json:"name,omitempty"`
|
|
ViewID *int64 `json:"view_id,omitempty"`
|
|
View *string `json:"view,omitempty"` // view name, alternative to view_id
|
|
Type *string `json:"type,omitempty"`
|
|
InView *string `json:"in_view,omitempty"`
|
|
File *string `json:"file,omitempty"`
|
|
SOA *SOAInput `json:"soa,omitempty"`
|
|
DefaultTTL *uint32 `json:"default_ttl,omitempty"`
|
|
SerialPolicy *string `json:"serial_policy,omitempty"`
|
|
// DNSSECPolicy turns signing on ("default" or a custom policy's
|
|
// name), starts turning it off ("insecure") or, once that has
|
|
// finished, off ("").
|
|
DNSSECPolicy *string `json:"dnssec_policy,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
// Disabled keeps the zone in Jarl but out of named's configuration.
|
|
Disabled *bool `json:"disabled,omitempty"`
|
|
// LinkGroup is the zone's link group (0: not linked). Set by zone.link
|
|
// and zone.unlink; requests use those.
|
|
LinkGroup *int64 `json:"link_group,omitempty"`
|
|
// LinkWith (create only) links the new zone with the zone of the same
|
|
// name in this view, copying its shared records (or all of its
|
|
// records, when it isn't linked yet).
|
|
LinkWith string `json:"link_with,omitempty"`
|
|
// AutoPTR keeps the PTR records of the zone's A and AAAA records in
|
|
// step (docs/reverse.md).
|
|
AutoPTR *bool `json:"auto_ptr,omitempty"`
|
|
// Fill (create only, reverse zones) adds a PTR record for every
|
|
// address of the view's A and AAAA records in the zone's range.
|
|
Fill bool `json:"fill,omitempty"`
|
|
|
|
// Create only: name servers for the apex NS records (default: the
|
|
// configured default name servers), additional records, and a zone to
|
|
// copy records from.
|
|
Nameservers []string `json:"nameservers,omitempty"`
|
|
Records []RecordInput `json:"records,omitempty"`
|
|
CloneFrom int64 `json:"clone_from,omitempty"`
|
|
// Options (create only) are zone options to set, e.g. primaries for a
|
|
// secondary zone or forwarders for a forward zone.
|
|
Options []ZoneOption `json:"options,omitempty"`
|
|
}
|
|
|
|
// ZoneOption is an option given when creating a zone.
|
|
type ZoneOption struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
Comment string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// RecordInput creates, updates or (by match) deletes a record.
|
|
type RecordInput struct {
|
|
ZoneID int64 `json:"zone_id,omitempty"`
|
|
Zone string `json:"zone,omitempty"` // zone name, alternative to zone_id
|
|
View string `json:"view,omitempty"` // view of Zone
|
|
Name *string `json:"name,omitempty"`
|
|
Type *string `json:"type,omitempty"`
|
|
TTL model.Opt[uint32] `json:"ttl,omitzero"` // null: use the zone default
|
|
Data *string `json:"data,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
// Disabled keeps the record in Jarl but out of the zone.
|
|
Disabled *bool `json:"disabled,omitempty"`
|
|
// PTR also creates (or deletes, disables or enables) the matching PTR
|
|
// record in the reverse zone, for A and AAAA records. On an update that
|
|
// changes nothing else it adds the PTR record if it is missing. In a
|
|
// linked zone, every linked view's reverse zone counts.
|
|
PTR bool `json:"ptr,omitempty"`
|
|
// MissingOK makes a delete-by-match succeed when nothing matches.
|
|
MissingOK bool `json:"missing_ok,omitempty"`
|
|
// Local (requests, linked zones): true keeps the record in this view
|
|
// only (an override: the whole name and type becomes local here);
|
|
// false shares it with the linked zones. Unset: like the records of
|
|
// the same name and type, shared when there are none.
|
|
Local *bool `json:"local,omitempty"`
|
|
// SharedID (stored operations) ties the copies of a shared record
|
|
// together; 0 makes it local.
|
|
SharedID *int64 `json:"shared_id,omitempty"`
|
|
}
|
|
|
|
// LinkInput links zones of the same name in several views, or (unlink)
|
|
// takes one out of its link.
|
|
type LinkInput struct {
|
|
ZoneID int64 `json:"zone_id,omitempty"`
|
|
Zone string `json:"zone,omitempty"`
|
|
View string `json:"view,omitempty"`
|
|
// With are the views whose zone of the same name to link with (link
|
|
// only; default: every view that has one).
|
|
With []string `json:"with,omitempty"`
|
|
}
|
|
|
|
// RRSetInput replaces all records of one name and type.
|
|
type RRSetInput struct {
|
|
ZoneID int64 `json:"zone_id,omitempty"`
|
|
Zone string `json:"zone,omitempty"`
|
|
View string `json:"view,omitempty"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
TTL model.Opt[uint32] `json:"ttl,omitzero"`
|
|
Data []string `json:"data"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
// NewIDs are allocated for records that need to be created.
|
|
NewIDs []int64 `json:"new_ids,omitempty"`
|
|
// Local (requests): like RecordInput.Local, for the whole set.
|
|
Local *bool `json:"local,omitempty"`
|
|
// Shared (stored operations, linked zones): the records are shared
|
|
// (true) or local (false).
|
|
Shared *bool `json:"shared,omitempty"`
|
|
}
|
|
|
|
// ViewInput creates or updates a view.
|
|
type ViewInput struct {
|
|
Name *string `json:"name,omitempty"`
|
|
MatchClients *[]string `json:"match_clients,omitempty"`
|
|
MatchDestinations *[]string `json:"match_destinations,omitempty"`
|
|
MatchRecursiveOnly model.Opt[bool] `json:"match_recursive_only,omitzero"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
Position *int `json:"position,omitempty"`
|
|
// AdoptZones (create) moves all zones that are not in a view into the
|
|
// new view; named requires every zone to be in a view once views
|
|
// are used.
|
|
AdoptZones bool `json:"adopt_zones,omitempty"`
|
|
// Cascade (delete) also deletes the view's zones.
|
|
Cascade bool `json:"cascade,omitempty"`
|
|
// Exposure is "auto" (or ""), "public" or "private" (docs/exposure.md).
|
|
Exposure *string `json:"exposure,omitempty"`
|
|
Acknowledged *[]model.Acknowledgement `json:"acknowledged,omitempty"`
|
|
}
|
|
|
|
// ReorderInput sets the order of views.
|
|
type ReorderInput struct {
|
|
IDs []int64 `json:"ids"`
|
|
}
|
|
|
|
// ACLInput creates or updates an ACL.
|
|
type ACLInput struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Elements *[]string `json:"elements,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// TSIGInput creates or updates a TSIG key. A missing secret on create is
|
|
// generated.
|
|
type TSIGInput struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Algorithm *string `json:"algorithm,omitempty"`
|
|
Secret *string `json:"secret,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// OptionInput creates or updates an option.
|
|
type OptionInput struct {
|
|
Scope *string `json:"scope,omitempty"` // options, view, zone, toplevel
|
|
ScopeID *int64 `json:"scope_id,omitempty"` // view or zone ID
|
|
Name *string `json:"name,omitempty"`
|
|
Value *string `json:"value,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
Position *int `json:"position,omitempty"`
|
|
}
|
|
|
|
// SettingsInput updates Jarl's settings.
|
|
type SettingsInput struct {
|
|
DefaultTTL *uint32 `json:"default_ttl,omitempty"`
|
|
Nameservers *[]string `json:"nameservers,omitempty"`
|
|
RName *string `json:"rname,omitempty"`
|
|
Refresh *uint32 `json:"refresh,omitempty"`
|
|
Retry *uint32 `json:"retry,omitempty"`
|
|
Expire *uint32 `json:"expire,omitempty"`
|
|
NegativeTTL *uint32 `json:"negative_ttl,omitempty"`
|
|
SerialPolicy *string `json:"serial_policy,omitempty"`
|
|
// Exposure and Acknowledged are the server's (docs/exposure.md).
|
|
Exposure *string `json:"exposure,omitempty"`
|
|
Acknowledged *[]model.Acknowledgement `json:"acknowledged,omitempty"`
|
|
}
|
|
|
|
// DeleteInput carries options for delete operations.
|
|
type DeleteInput struct {
|
|
Cascade bool `json:"cascade,omitempty"`
|
|
}
|
|
|
|
// BlocklistInput creates or updates a blocklist. Views are given by name
|
|
// (Views) or ID (ViewIDs); an empty list means every view that offers
|
|
// recursion.
|
|
type BlocklistInput struct {
|
|
Name *string `json:"name,omitempty"`
|
|
URL *string `json:"url,omitempty"`
|
|
Views *[]string `json:"views,omitempty"`
|
|
ViewIDs *[]int64 `json:"view_ids,omitempty"`
|
|
Disabled *bool `json:"disabled,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
// Step is the step the list starts at, by name ("normal" or a step
|
|
// above it) or ID (StepID; 0 is normal).
|
|
Step *string `json:"step,omitempty"`
|
|
StepID *int64 `json:"step_id,omitempty"`
|
|
}
|
|
|
|
// BlockStepInput creates or updates a blocking step above Normal.
|
|
// Position orders the steps (new steps go on top).
|
|
type BlockStepInput struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Position *int `json:"position,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// ClientGroupInput creates or updates a client group. Members are
|
|
// addresses, prefixes or ACL names; the step is given by name ("off",
|
|
// "normal" or a step) or ID; views as for BlocklistInput.
|
|
type ClientGroupInput struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Members *[]string `json:"members,omitempty"`
|
|
Step *string `json:"step,omitempty"`
|
|
StepID *int64 `json:"step_id,omitempty"`
|
|
Views *[]string `json:"views,omitempty"`
|
|
ViewIDs *[]int64 `json:"view_ids,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// BlockRuleInput creates or updates a block or allow rule. Views as for
|
|
// BlocklistInput.
|
|
type BlockRuleInput struct {
|
|
Name *string `json:"name,omitempty"`
|
|
Action *string `json:"action,omitempty"` // block (default) or allow
|
|
Exact *bool `json:"exact,omitempty"` // only the name, not the names below it
|
|
Views *[]string `json:"views,omitempty"`
|
|
ViewIDs *[]int64 `json:"view_ids,omitempty"`
|
|
Comment *string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// BlockingPauseInput pauses blocking until a time, or resumes it (null).
|
|
type BlockingPauseInput struct {
|
|
Until *time.Time `json:"until"`
|
|
// Indefinitely pauses until someone resumes blocking (Until is nil).
|
|
Indefinitely bool `json:"indefinitely,omitempty"`
|
|
}
|
|
|
|
// MaxBlockingPause is the longest pause: blocking shouldn't stay off by
|
|
// mistake (disable lists to stop using them).
|
|
const MaxBlockingPause = 24 * time.Hour
|