package validate import ( "net/netip" "strings" "jarl/internal/model" "jarl/internal/namedconf" ) // checkBlocking checks blocklists and block/allow rules (docs/blocking.md). func (c *checker) checkBlocking() { viewNames := func(ids []int64) string { var names []string for _, id := range ids { if v := c.s.Views[id]; v != nil { names = append(names, v.Name) } } return strings.Join(names, ", ") } used := map[int64]bool{} // views (0: the server) with blocking // what describes where something applies, and warns when it can't // have any effect. effective := func(objType string, id int64, label string, ids []int64) []int64 { for _, vid := range ids { if c.s.Views[vid] == nil { c.errf("blocking_view_missing", objType, id, "%s names view %d, which doesn't exist", label, vid) } } views := c.s.BlockingViews(ids) if len(views) == 0 { c.warnf("blocking_no_view", objType, id, "%s has no effect: no view offers recursion, and blocking only changes answers to recursive queries", label) } var idle []int64 for _, vid := range views { if !c.s.RecursionOn(vid) { idle = append(idle, vid) } } if len(idle) > 0 { c.warnf("blocking_no_recursion", objType, id, "%s has no effect in view %s: it doesn't offer recursion, and blocking only changes answers to recursive queries", label, viewNames(idle)) } for _, vid := range views { used[vid] = true } return views } for _, b := range c.s.SortedBlocklists() { if !b.Disabled { effective("blocklist", b.ID, "blocklist "+b.Name, b.ViewIDs) } } // One rule per name and view: two would compete for the same entries // of the policy zone. seen := map[string]*model.BlockRule{} // view|name -> first rule for _, r := range c.s.SortedBlockRules() { label := "the rule for " + r.Name for _, vid := range effective("blockrule", r.ID, label, r.ViewIDs) { key := c.blockingScope(vid) + "|" + r.Name if first := seen[key]; first != nil && first.ID != r.ID { where := "" if v := c.s.Views[vid]; v != nil { where = " in view " + v.Name } c.errf("blocking_rule_duplicate", "blockrule", r.ID, "there are two rules for %s%s (%s and %s); keep one", r.Name, where, first.Action, r.Action) continue } seen[key] = r } } c.checkClientGroups(effective) // Jarl writes the response-policy statement where blocking is used; // named allows one per view (or for the server). for vid := range used { if o := c.s.FindOption(model.ScopeView, vid, "response-policy"); vid != 0 && o != nil { p := c.errf("blocking_response_policy", "option", o.ID, "view %s has a response-policy option, but Jarl's blocking writes that statement there; remove the option (or the blocking that applies to this view)", c.s.Views[vid].Name) p.View = c.s.Views[vid].Name } } if o := c.s.FindOption(model.ScopeOptions, 0, "response-policy"); o != nil && len(used) > 0 { c.errf("blocking_response_policy", "option", o.ID, "the server has a response-policy option, which Jarl's blocking replaces wherever it is used; remove the option, or set up your policy zones with Jarl's blocking instead") } for _, z := range c.s.Zones { if n := strings.TrimSuffix(z.Name, "."); n == "jarl.invalid" || strings.HasSuffix(n, ".jarl.invalid") { c.zoneProblem(z, Error, "zone_name_reserved", "zone names under jarl.invalid are reserved for Jarl's blocking") } } } func (c *checker) blockingScope(viewID int64) string { if v := c.s.Views[viewID]; v != nil { return v.Name } return "" } // checkClientGroups checks blocking steps and client groups: steps exist, // members are addresses (directly or through ACLs), and no address is in // two groups of the same view. func (c *checker) checkClientGroups(effective func(objType string, id int64, label string, ids []int64) []int64) { for _, b := range c.s.SortedBlocklists() { if _, ok := c.s.StepRank(b.StepID); !ok || b.StepID == model.StepOff { c.errf("blocking_step_missing", "blocklist", b.ID, "blocklist %s is at step %d, which doesn't exist", b.Name, b.StepID) } } // The highest step any group reaches, per view: lists above it block // nothing. top := map[int64]int{} seen := map[string]*model.ClientGroup{} // view|prefix -> first group for _, g := range c.s.SortedClientGroups() { label := "client group " + g.Name rank, ok := c.s.StepRank(g.StepID) if !ok { c.errf("blocking_step_missing", "clientgroup", g.ID, "%s is at step %d, which doesn't exist", label, g.StepID) } if len(g.Members) == 0 { c.warnf("client_group_empty", "clientgroup", g.ID, "%s has no members", label) } for _, m := range g.Members { c.checkMember(g, m, map[string]bool{}) } views := effective("clientgroup", g.ID, label, g.ViewIDs) prefixes := c.s.GroupPrefixes(g) for _, vid := range views { if len(prefixes) > 0 { top[vid] = max(top[vid], rank) } for _, p := range prefixes { key := c.blockingScope(vid) + "|" + p.String() if first := seen[key]; first != nil && first.ID != g.ID { c.errf("client_group_overlap", "clientgroup", g.ID, "%s is in client groups %s and %s; a client can only be in one (a more specific prefix in another group is fine)", p, first.Name, g.Name) continue } seen[key] = g } } } for _, b := range c.s.SortedBlocklists() { rank, ok := c.s.StepRank(b.StepID) if !ok || rank == 0 || b.Disabled { continue } reached := false for _, vid := range c.s.BlockingViews(b.ViewIDs) { if top[vid] >= rank { reached = true } } if !reached { c.warnf("blocklist_step_unused", "blocklist", b.ID, "blocklist %s blocks nothing: it is at step %s, and no client group is at that step or above", b.Name, c.s.StepName(b.StepID)) } } } // checkMember checks one member of a client group: an address, a prefix, // or an ACL whose elements are (recursively) addresses and prefixes. func (c *checker) checkMember(g *model.ClientGroup, m string, seen map[string]bool) { if _, err := netip.ParseAddr(m); err == nil { return } if _, err := netip.ParsePrefix(m); err == nil { return } acl := c.s.ACLByName(m) if acl == nil { c.errf("client_group_member_invalid", "clientgroup", g.ID, "client group %s: %q is not an address, a prefix or an ACL", g.Name, m) return } if seen[acl.Name] { return } seen[acl.Name] = true for _, e := range acl.Elements { el, err := namedconf.ParseAMElement(e) if err != nil { continue // reported for the ACL } if el.Negated || el.Kind == namedconf.AMKey || el.Kind == namedconf.AMNested || el.Kind == namedconf.AMName && el.IsBuiltin() { c.errf("client_group_member_invalid", "clientgroup", g.ID, "client group %s uses ACL %s, whose element %q isn't an address or a prefix: blocking matches clients by address only", g.Name, acl.Name, e) continue } if el.Kind == namedconf.AMName { c.checkMember(g, el.Name, seen) } } }