Added:
- Server statistics: named's memory and CPU (from /proc), cache size,
sockets and client counts against their limits (statistics channel,
rndc status), host memory and load, and free disk space. Memory and
CPU are kept over time as gauges (sum and sample count, averaged in
timelines). Statistics page Server section, jarl stats server,
GET /stats/server.
- jarl record add|set|delete|disable|enable --view a,b | all: the same
change in each view, applied as one changeset.
- Blocking can be paused until resumed (jarl block pause indefinitely,
"Until I resume", {"indefinitely": true}).
Changed:
- List options (allow-transfer, forwarders, ...) get their braces added
when given without them.
- The dashboard lists named-checkconf's warnings about the running
configuration; the CLI prints them after an apply.
- Stats page: tile and card rows fill their width, and the queries and
failed answers charts share margins so their time axes line up.
- Dashboard: options have their own tile.
- 24-hour times in the web UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
262 lines
9.9 KiB
Go
262 lines
9.9 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"
|
|
|
|
// 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"
|
|
)
|
|
|
|
// 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}
|
|
|
|
// 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"`
|
|
|
|
// 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.
|
|
PTR bool `json:"ptr,omitempty"`
|
|
// MissingOK makes a delete-by-match succeed when nothing matches.
|
|
MissingOK bool `json:"missing_ok,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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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
|