package model import ( "fmt" "net/netip" "slices" "sort" "strings" "time" ) // Blocklist is a list of domains to block, downloaded from URL and kept up // to date by jarld (docs/blocking.md). type Blocklist struct { ID int64 `json:"id"` Name string `json:"name"` // a DNS label: it names the list's policy zone URL string `json:"url"` // ViewIDs are the views the list applies to; empty means every view // that offers recursion (see BlockingViews). ViewIDs []int64 `json:"view_ids"` Disabled bool `json:"disabled,omitempty"` // StepID is the blocking step the list starts at: StepNormal for // every client, or a BlockStep for the clients of groups at that step // or above. StepID int64 `json:"step_id"` Comment string `json:"comment"` Meta } // Built-in blocking steps (BlockStep IDs are positive). const ( StepOff int64 = -1 // no blocklists (rules still apply) StepNormal int64 = 0 // the default: every client not in a group ) // BlockStep is a blocking step above Normal. Steps are ordered: each // includes the lists of the steps below it, plus its own (docs/blocking.md). type BlockStep struct { ID int64 `json:"id"` Name string `json:"name"` Position int `json:"position"` Comment string `json:"comment"` Meta } // ClientGroup gives the clients with these addresses a blocking step. type ClientGroup struct { ID int64 `json:"id"` Name string `json:"name"` // Members are addresses, prefixes, or names of ACLs made of them. Members []string `json:"members"` StepID int64 `json:"step_id"` // StepOff, StepNormal or a BlockStep ViewIDs []int64 `json:"view_ids"` // as for Blocklist Comment string `json:"comment"` Meta } // OrderedBlockSteps returns the steps above Normal, lowest first. func (s *State) OrderedBlockSteps() []*BlockStep { out := make([]*BlockStep, 0, len(s.BlockSteps)) for _, st := range s.BlockSteps { out = append(out, st) } sort.Slice(out, func(i, j int) bool { if out[i].Position != out[j].Position { return out[i].Position < out[j].Position } return out[i].ID < out[j].ID }) return out } // StepRank orders steps: -1 for Off, 0 for Normal, 1, 2, ... for the // steps above it. ok is false for a step that doesn't exist. func (s *State) StepRank(id int64) (rank int, ok bool) { switch id { case StepOff: return -1, true case StepNormal: return 0, true } for i, st := range s.OrderedBlockSteps() { if st.ID == id { return i + 1, true } } return 0, false } // StepName returns a step's name ("off" and "normal" for the built-in // ones). func (s *State) StepName(id int64) string { switch id { case StepOff: return "off" case StepNormal: return "normal" } if st := s.BlockSteps[id]; st != nil { return st.Name } return fmt.Sprintf("step %d", id) } // StepByName finds a step by name, the built-in ones included. func (s *State) StepByName(name string) (int64, bool) { name = strings.ToLower(strings.TrimSpace(name)) switch name { case "off": return StepOff, true case "normal": return StepNormal, true } for _, st := range s.BlockSteps { if strings.EqualFold(st.Name, name) { return st.ID, true } } return 0, false } // ClientGroupByName finds a client group by name. func (s *State) ClientGroupByName(name string) *ClientGroup { for _, g := range s.ClientGroups { if strings.EqualFold(g.Name, name) { return g } } return nil } // SortedClientGroups returns the client groups ordered by name. func (s *State) SortedClientGroups() []*ClientGroup { out := make([]*ClientGroup, 0, len(s.ClientGroups)) for _, g := range s.ClientGroups { out = append(out, g) } sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) return out } // Block rule actions. const ( BlockActionBlock = "block" BlockActionAllow = "allow" ) // BlockRule blocks or allows a domain, and unless Exact the names below // it. Rules take precedence over blocklists. type BlockRule struct { ID int64 `json:"id"` Name string `json:"name"` // lowercase, without the final dot Action string `json:"action"` // block or allow Exact bool `json:"exact,omitempty"` ViewIDs []int64 `json:"view_ids"` // as for Blocklist Comment string `json:"comment"` Meta } // BlocklistStatus is what jarld knows about a list's downloads. It is not // configuration: it is kept outside changesets. type BlocklistStatus struct { ID int64 `json:"id"` URL string `json:"url"` // the URL last tried ETag string `json:"-"` LastModified string `json:"-"` CheckedAt *time.Time `json:"checked_at,omitempty"` // last attempt UpdatedAt *time.Time `json:"updated_at,omitempty"` // last time the content changed Entries int `json:"entries"` // domains Records int `json:"records"` // policy zone records Skipped int `json:"skipped"` // lines that weren't usable SHA256 string `json:"-"` Error string `json:"error,omitempty"` // the last attempt failed } // BlocklistByName finds a blocklist by name. func (s *State) BlocklistByName(name string) *Blocklist { for _, b := range s.Blocklists { if strings.EqualFold(b.Name, name) { return b } } return nil } // SortedBlocklists returns the blocklists ordered by name. func (s *State) SortedBlocklists() []*Blocklist { out := make([]*Blocklist, 0, len(s.Blocklists)) for _, b := range s.Blocklists { out = append(out, b) } sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) return out } // SortedBlockRules returns the rules ordered by domain, parents first. func (s *State) SortedBlockRules() []*BlockRule { out := make([]*BlockRule, 0, len(s.BlockRules)) for _, r := range s.BlockRules { out = append(out, r) } sort.Slice(out, func(i, j int) bool { a, b := out[i], out[j] if a.Name != b.Name { return CanonicalLess(a.Name+".", b.Name+".") } if a.Exact != b.Exact { return !a.Exact } return a.ID < b.ID }) return out } // RecursionOn reports whether a view (0: the server without views) offers // recursion: its recursion option, else the server's, else BIND's default // (yes). func (s *State) RecursionOn(viewID int64) bool { o := s.FindOption(ScopeView, viewID, "recursion") if viewID == 0 || o == nil { if g := s.FindOption(ScopeOptions, 0, "recursion"); o == nil { o = g } } if o == nil { return true } switch strings.ToLower(strings.Trim(strings.TrimSpace(o.Value), `";`)) { case "no", "false", "0": return false } return true } // BlockingViews returns the views a blocklist or rule with viewIDs applies // to, in view order: the given views, or when none are given every view // that offers recursion (blocking only affects recursive queries). Without // views it returns []int64{0}, the whole server, if it offers recursion. func (s *State) BlockingViews(viewIDs []int64) []int64 { views := s.OrderedViews() if len(views) == 0 { if !s.RecursionOn(0) { return nil } return []int64{0} } var out []int64 for _, v := range views { if len(viewIDs) == 0 && s.RecursionOn(v.ID) || slices.Contains(viewIDs, v.ID) { out = append(out, v.ID) } } return out } // GroupPrefixes returns the addresses of a client group as prefixes: its // own, and those of the ACLs it names (and the ACLs they name). Elements // that aren't addresses are left out (validation reports them). func (s *State) GroupPrefixes(g *ClientGroup) []netip.Prefix { var out []netip.Prefix seen := map[string]bool{} var add func(elems []string) add = func(elems []string) { for _, e := range elems { e = strings.TrimSpace(e) if a, err := netip.ParseAddr(e); err == nil { a = a.Unmap() out = append(out, netip.PrefixFrom(a, a.BitLen())) } else if p, err := netip.ParsePrefix(e); err == nil { out = append(out, p.Masked()) } else if acl := s.ACLByName(e); acl != nil && !seen[acl.Name] { seen[acl.Name] = true add(acl.Elements) } } } add(g.Members) return out }