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) }