ACLs, views' match lists, options with address match lists and blocking client groups take ranges such as 10.0.10.101-10.0.10.199 or 2001:db8::100-2001:db8::1ff (negated, or with spaces around the dash, too). Jarl stores the range as written and writes the fewest prefixes that cover exactly that range to named.conf, with the range as a comment. New package internal/iprange; namedconf gets an AMRange element (a range used to parse as an ACL name), ExpandRanges and statement comments. Mistakes are refused when entered, validation sees what ranges cover, and IPv6 ranges in the IPv4-mapped space get a warning. The web UI shows what a range becomes while typing (GET /catalog/ranges). 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) + 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) + 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) + formatStmt(st, 0)
|
|
}
|
|
|
|
func commentLines(c string) string {
|
|
if c == "" {
|
|
return ""
|
|
}
|
|
return commentBlock("//", c)
|
|
}
|