Each view's match-clients starts with "key jarl-view-NAME" (after
"!key" for the views listed after it), so a query signed with a view's
key lands in that view from any address. The secrets are derived from a
random seed in Jarl's database. The delegation check signs its queries
to this server's own addresses with the key of the zone's view: before,
they got whichever view matched the server's address, usually internal.
jarl view key / GET /views/{id}/key show a key for dig -k (tsig:write,
audited). jarl-view- key names are reserved; the importer drops view
keys from a configuration Jarl wrote.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
120 lines
3.9 KiB
Go
120 lines
3.9 KiB
Go
package render
|
|
|
|
import (
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"jarl/internal/model"
|
|
"jarl/internal/namedconf"
|
|
)
|
|
|
|
// Extras are additions to named's configuration that come from jarld's
|
|
// own configuration (jarl.yaml) rather than from the database.
|
|
type Extras struct {
|
|
// Stats, when set, makes named publish what Jarl's statistics
|
|
// collector reads.
|
|
Stats *StatsHooks
|
|
// ViewKeySeed, when set, gives every view a TSIG key that selects it
|
|
// (viewkeys.go).
|
|
ViewKeySeed []byte
|
|
}
|
|
|
|
// StatsHooks describe the statistics channel and response log Jarl needs.
|
|
type StatsHooks struct {
|
|
Channel netip.AddrPort // statistics-channels address (loopback)
|
|
LogFile string // response log; "" disables response logging
|
|
LogVersions int
|
|
LogSize string
|
|
}
|
|
|
|
// StatsChannelName is the logging channel Jarl adds for responses.
|
|
const StatsChannelName = "jarl_responses"
|
|
|
|
const statsComment = "Added by Jarl for query statistics (stats in /etc/jarl/jarl.yaml)."
|
|
|
|
// statsInet is the statistics-channels clause for the collector.
|
|
func (h *StatsHooks) statsInet() *namedconf.Stmt {
|
|
addr := h.Channel.Addr().String()
|
|
return namedconf.NewStmt("inet", addr, "port", strconv.Itoa(int(h.Channel.Port())),
|
|
"allow", namedconf.NewBlock(namedconf.NewStmt(addr)))
|
|
}
|
|
|
|
// logChannel is the logging channel the response log is written to.
|
|
func (h *StatsHooks) logChannel() *namedconf.Stmt {
|
|
return namedconf.NewStmt("channel", StatsChannelName, namedconf.NewBlock(
|
|
namedconf.NewStmt("file", namedconf.Quoted(h.LogFile), "versions", strconv.Itoa(h.LogVersions), "size", h.LogSize),
|
|
namedconf.NewStmt("print-time", "yes"),
|
|
namedconf.NewStmt("severity", "info"),
|
|
))
|
|
}
|
|
|
|
// statsStatement returns the statistics-channels statement, merged into the
|
|
// user's own if there is one.
|
|
func (h *StatsHooks) statsStatement(user *model.Option) string {
|
|
if user != nil {
|
|
if st, err := namedconf.ParseOptionValue(user.Name, user.Value); err == nil && st.FirstBlock() != nil {
|
|
b := st.FirstBlock()
|
|
want := namedconf.CompactStmt(h.statsInet())
|
|
for _, s := range b.Stmts {
|
|
if namedconf.CompactStmt(s) == want {
|
|
return optionText(user, 0)
|
|
}
|
|
}
|
|
b.Stmts = append(b.Stmts, h.statsInet())
|
|
return commentLines(user.Comment) + commentBlock("//", statsComment) + namedconf.FormatStmt(st, 0)
|
|
}
|
|
// Unparsable: validation reports it; leave it alone.
|
|
return optionText(user, 0)
|
|
}
|
|
st := namedconf.NewStmt("statistics-channels", namedconf.NewBlock(h.statsInet()))
|
|
return commentBlock("//", statsComment) + namedconf.FormatStmt(st, 0)
|
|
}
|
|
|
|
// loggingStatement returns the logging statement with Jarl's channel and
|
|
// the responses category (and rpz, for blocking's rewrites), merged into
|
|
// the user's own if there is one.
|
|
func (h *StatsHooks) loggingStatement(user *model.Option, rpz bool) string {
|
|
var st *namedconf.Stmt
|
|
if user != nil {
|
|
var err error
|
|
st, err = namedconf.ParseOptionValue(user.Name, user.Value)
|
|
if err != nil || st.FirstBlock() == nil {
|
|
return optionText(user, 0)
|
|
}
|
|
} else {
|
|
st = namedconf.NewStmt("logging", namedconf.NewBlock())
|
|
}
|
|
b := st.FirstBlock()
|
|
// Channels must be defined before the categories that use them.
|
|
b.Stmts = append([]*namedconf.Stmt{h.logChannel()}, b.Stmts...)
|
|
categories := []string{"responses"}
|
|
if rpz {
|
|
categories = append(categories, "rpz")
|
|
}
|
|
for _, cat := range categories {
|
|
merged := false
|
|
for _, s := range b.Stmts {
|
|
if s.Keyword() == "category" && strings.EqualFold(s.Arg(0), cat) && s.FirstBlock() != nil {
|
|
s.FirstBlock().Stmts = append(s.FirstBlock().Stmts, namedconf.NewStmt(StatsChannelName))
|
|
merged = true
|
|
}
|
|
}
|
|
if !merged {
|
|
b.Stmts = append(b.Stmts, namedconf.NewStmt("category", cat, namedconf.NewBlock(namedconf.NewStmt(StatsChannelName))))
|
|
}
|
|
}
|
|
prefix := ""
|
|
if user != nil {
|
|
prefix = commentLines(user.Comment)
|
|
}
|
|
return prefix + commentBlock("//", statsComment) + namedconf.FormatStmt(st, 0)
|
|
}
|
|
|
|
func commentLines(c string) string {
|
|
if c == "" {
|
|
return ""
|
|
}
|
|
return commentBlock("//", c)
|
|
}
|