package render import ( "fmt" "path/filepath" "slices" "strings" "jarl/internal/blocking" "jarl/internal/model" "jarl/internal/namedconf" ) // KindPolicy is the kind of a rendered rules policy zone file. const KindPolicy = "policy" // PolicyZone is a response policy zone in named's configuration // (docs/blocking.md). type PolicyZone struct { Name string // without the final dot ViewID int64 // 0 without views View string // "" without views File string ListID int64 // the blocklist; 0 for the rules zone } // BlocklistsDir holds the zone files of downloaded blocklists, one per // list, shared by the views that use it. func (p Paths) BlocklistsDir() string { return filepath.Join(p.CacheDir, "blocklists") } // BlocklistFile returns the zone file of a blocklist. It is named after // the list's ID, so renaming the list doesn't move it. func (p Paths) BlocklistFile(id int64) string { return filepath.Join(p.BlocklistsDir(), fmt.Sprintf("list-%d.rpz", id)) } // rulesFile returns a view's rules zone file. func (p Paths) rulesFile(view string) string { return filepath.Join(p.ZoneDir, ViewDir(view), blocking.RulesZone+".rpz") } // PolicyZones returns the policy zones named loads, per view in view // order, each view's in the order named checks them: the rules zone first // (rules win over lists), then the enabled lists by name. func PolicyZones(s *model.State, p Paths) []PolicyZone { var out []PolicyZone rules, lists := s.SortedBlockRules(), s.SortedBlocklists() scopes := []int64{0} if len(s.Views) > 0 { scopes = nil for _, v := range s.OrderedViews() { scopes = append(scopes, v.ID) } } for _, vid := range scopes { view := "" if v := s.Views[vid]; v != nil { view = v.Name } for _, r := range rules { if slices.Contains(s.BlockingViews(r.ViewIDs), vid) { out = append(out, PolicyZone{Name: blocking.RulesZone, ViewID: vid, View: view, File: p.rulesFile(view)}) break } } for _, b := range lists { if !b.Disabled && slices.Contains(s.BlockingViews(b.ViewIDs), vid) { out = append(out, PolicyZone{Name: blocking.ListZone(b.Name), ViewID: vid, View: view, File: p.BlocklistFile(b.ID), ListID: b.ID}) } } } return out } // ViewRules returns the rules that apply in a view (0 without views). func ViewRules(s *model.State, viewID int64) []blocking.Rule { var out []blocking.Rule for _, r := range s.SortedBlockRules() { if slices.Contains(s.BlockingViews(r.ViewIDs), viewID) { out = append(out, blocking.Rule{Name: r.Name, Action: r.Action, Exact: r.Exact}) } } return out } // writeResponsePolicy writes the response-policy statement for a view's // policy zones (or the server's, without views). Rewrites are logged to // Jarl's response log when statistics read it, and not at all otherwise // (they would go to named's default log). func writeResponsePolicy(w *confWriter, zones []PolicyZone, logRewrites bool) { if len(zones) == 0 { return } w.begin("blocking", 0, "blocking (rules and blocklists)") w.write("\t// Blocking (docs/blocking.md): rules first, then blocklists.\n\tresponse-policy {\n") for _, z := range zones { // Apply a new version at once: Jarl reloads a list only when it // changed (at most every blocking.refresh), so named's default // delay for batching frequent updates only adds a wait. opts := []string{"min-update-interval 0"} if !logRewrites { opts = append(opts, "log no") } w.write(fmt.Sprintf("\t\tzone %s%s;\n", namedconf.QuoteString(z.Name), prefixSpace(strings.Join(opts, " ")))) } // Block without waiting for the real answer, and also for clients // that ask for DNSSEC data. w.write("\t} qname-wait-recurse no break-dnssec yes;\n") w.end() } func prefixSpace(s string) string { if s == "" { return "" } return " " + s } // writePolicyZones writes the zone statements of policy zones and adds // their files to the output: rules zone files are rendered, blocklist // files only listed (jarld downloads them; see Output.Blocklists). func writePolicyZones(w *confWriter, s *model.State, zones []PolicyZone, out *Output, indent int) { tabs := strings.Repeat("\t", indent) for _, z := range zones { w.write("\n") label := "blocking: your rules" if z.ListID != 0 { label = "blocking: blocklist " + s.Blocklists[z.ListID].Name } w.begin("blocking", z.ListID, label) w.write(fmt.Sprintf("%szone %s {\n%s\ttype primary;\n%s\tfile %s;\n%s\tallow-query { none; };\n%s\tallow-transfer { none; };\n%s\tnotify no;\n%s};\n", tabs, namedconf.QuoteString(z.Name), tabs, tabs, namedconf.QuoteString(z.File), tabs, tabs, tabs, tabs)) w.end() if z.ListID != 0 { out.Blocklists = appendUnique(out.Blocklists, z.File) continue } title := "Blocking rules" if z.View != "" { title += " for view " + z.View } out.Files = append(out.Files, &File{Path: z.File, Content: []byte(blocking.RulesZoneFile(title, ViewRules(s, z.ViewID))), Mode: 0o640, Kind: KindPolicy}) } } // scopeZones returns the policy zones of one view (0 without views). func scopeZones(all []PolicyZone, viewID int64) []PolicyZone { var out []PolicyZone for _, z := range all { if z.ViewID == viewID { out = append(out, z) } } return out }