The setup takes several host names (one certificate for all; the health check says when the served certificate misses one), stored in the settings. Each listen address gets its own views for the host names' A/AAAA records (--address ADDR=VIEW,..., a view checkbox per address in the form), always local to the view, with a warning when a public view would get a private address; a view no longer given loses the address, other records of the names stay, and the form starts from where the records are now. Certificates (page and jarl cert show) say what named uses each for: DNS over TLS or HTTPS, on which addresses and ports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
384 lines
16 KiB
Go
384 lines
16 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"
|
|
// Dismissing a reverse zone suggestion, and taking that back, become
|
|
// view.update or settings.update operations.
|
|
OpReverseDismiss = "reverse.dismiss"
|
|
OpReverseUndismiss = "reverse.undismiss"
|
|
)
|
|
|
|
// DismissInput dismisses a reverse zone suggestion: a network in a view
|
|
// (by ID or name), or the server's (neither given).
|
|
type DismissInput struct {
|
|
ViewID int64 `json:"view_id,omitempty"`
|
|
View string `json:"view,omitempty"`
|
|
Prefix string `json:"prefix,omitempty"`
|
|
// Prefixes dismisses (or brings back) several at once.
|
|
Prefixes []string `json:"prefixes,omitempty"`
|
|
}
|
|
|
|
// 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"`
|
|
// ReverseSuggestions turns the reverse zone suggestions for the
|
|
// view's addresses on or off (docs/reverse.md).
|
|
ReverseSuggestions *bool `json:"reverse_suggestions,omitempty"`
|
|
// DismissedReverse are the networks whose suggestion was dismissed.
|
|
DismissedReverse *[]string `json:"dismissed_reverse,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"`
|
|
// DismissedReverse are dismissed reverse zone suggestions (without
|
|
// views).
|
|
DismissedReverse *[]string `json:"dismissed_reverse,omitempty"`
|
|
// EncryptedDNSHosts are the encrypted DNS setup's host names.
|
|
EncryptedDNSHosts *[]string `json:"encrypted_dns_hosts,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
|