Jarl works out which views are reachable from the internet (match-clients
ending in a catch-all, not private-looking addresses: an internal view of a
global IPv6 prefix is still yours; a server without views: open allow-query
and a non-private listen address), with a per-view override (schema 9), and
checks public ones: rate limiting, open resolver, minimal-any, cache
snooping, open NOTIFY and transfers, version and host name, management
interfaces. Fixes are staged like any change, including rate limiting from
ISC's example in log-only mode with the secondaries exempt, then enforcing.
named logs what rate limiting limits (or would, in log-only mode, where its
counters don't move) as query-errors; Jarl routes those lines to its log
and counts them (rrl breakdown, ratelimited top lists, Prometheus
jarl_ratelimit_logged_total), shown on the new Exposure page.
A change that newly opens a public view up must be confirmed (accept_risk,
--accept-risk, a checkbox on the Changes page; 409 confirm_risk
otherwise). Findings can be acknowledged with a reason and stay visible as
info. GET /exposure, POST /exposure/fix|ack|unack, jarl exposure,
docs/exposure.md.
Also fixes query statistics skipping TSIG-signed queries ("IP#port/key
NAME" in named's log).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
2.3 KiB
Go
69 lines
2.3 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"jarl/internal/bind"
|
|
"jarl/internal/validate"
|
|
)
|
|
|
|
// ErrorKind classifies engine errors (the API maps them to HTTP status
|
|
// codes).
|
|
type ErrorKind string
|
|
|
|
const (
|
|
KindBadRequest ErrorKind = "bad_request" // malformed or invalid input
|
|
KindForbidden ErrorKind = "forbidden" // permission denied
|
|
KindNotFound ErrorKind = "not_found" // object does not exist
|
|
KindConflict ErrorKind = "conflict" // concurrent change, wrong state
|
|
KindValidation ErrorKind = "validation_failed" // the resulting config has errors
|
|
KindBIND ErrorKind = "bind_failed" // named-checkconf or rndc failed
|
|
KindDrift ErrorKind = "drift" // files were changed outside Jarl
|
|
KindNotAdopted ErrorKind = "not_adopted" // named does not load Jarl's config yet
|
|
KindRisk ErrorKind = "confirm_risk" // the change opens the server up and needs confirming
|
|
)
|
|
|
|
// Error is an engine error with details for the caller.
|
|
type Error struct {
|
|
Kind ErrorKind `json:"code"`
|
|
Message string `json:"message"`
|
|
OpSeq int `json:"op,omitempty"` // the operation that failed
|
|
// OpSummary describes that operation, so people can find it in the
|
|
// changeset (record IDs mean little to them).
|
|
OpSummary string `json:"op_summary,omitempty"`
|
|
Problems []validate.Problem `json:"problems,omitempty"`
|
|
Check []CheckMessage `json:"check,omitempty"`
|
|
Diff string `json:"diff,omitempty"`
|
|
}
|
|
|
|
func (e *Error) Error() string {
|
|
if e.OpSeq > 0 && e.OpSummary != "" {
|
|
return fmt.Sprintf("operation %d (%s): %s", e.OpSeq, e.OpSummary, e.Message)
|
|
}
|
|
if e.OpSeq > 0 {
|
|
return fmt.Sprintf("operation %d: %s", e.OpSeq, e.Message)
|
|
}
|
|
return e.Message
|
|
}
|
|
|
|
func errf(kind ErrorKind, format string, args ...any) *Error {
|
|
return &Error{Kind: kind, Message: fmt.Sprintf(format, args...)}
|
|
}
|
|
|
|
// AsError returns err as *Error, if it is one.
|
|
func AsError(err error) (*Error, bool) {
|
|
var e *Error
|
|
ok := errors.As(err, &e)
|
|
return e, ok
|
|
}
|
|
|
|
// CheckMessage is a named-checkconf message mapped back to the Jarl object
|
|
// that produced the offending line.
|
|
type CheckMessage struct {
|
|
bind.Message
|
|
ObjectType string `json:"object_type,omitempty"`
|
|
ObjectID int64 `json:"object_id,omitempty"`
|
|
Object string `json:"object,omitempty"`
|
|
}
|