filtering: imp code, docs

This commit is contained in:
Ainar Garipov 2025-09-24 17:26:29 +03:00
parent a7d2af91f4
commit c4a416ee59
7 changed files with 178 additions and 166 deletions

View File

@ -73,7 +73,7 @@ func (m *SortedMap[K, V]) Clear() {
return
}
m.keys = nil
m.keys = m.keys[:0]
clear(m.vals)
}

View File

@ -1,7 +1,6 @@
package filtering
import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
@ -30,12 +29,9 @@ func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) {
dr := nr.DNSRewrite
if dr.NewCNAME != "" {
// NewCNAME rules have a higher priority than other rules.
rules = []*ResultRule{{
// #nosec G115 -- The overflow is required for backwards
// compatibility.
FilterListID: rulelist.APIID(nr.GetFilterListID()),
Text: nr.Text(),
}}
rules = []*ResultRule{
NewResultRule(nr),
}
return Result{
Rules: rules,
@ -48,21 +44,13 @@ func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) {
case dns.RcodeSuccess:
dnsrr.RCode = dr.RCode
dnsrr.Response[dr.RRType] = append(dnsrr.Response[dr.RRType], dr.Value)
rules = append(rules, &ResultRule{
// #nosec G115 -- The overflow is required for backwards
// compatibility.
FilterListID: rulelist.APIID(nr.GetFilterListID()),
Text: nr.Text(),
})
rules = append(rules, NewResultRule(nr))
default:
// RcodeRefused and other such codes have higher priority. Return
// immediately.
rules = []*ResultRule{{
// #nosec G115 -- The overflow is required for backwards
// compatibility.
FilterListID: rulelist.APIID(nr.GetFilterListID()),
Text: nr.Text(),
}}
rules = []*ResultRule{
NewResultRule(nr),
}
dnsrr = &DNSRewriteResult{
RCode: dr.RCode,
}

View File

@ -306,82 +306,6 @@ type Filter struct {
ID rules.ListID `yaml:"id"`
}
// Reason holds an enum detailing why it was filtered or not filtered
type Reason int
const (
// reasons for not filtering
// NotFilteredNotFound - host was not find in any checks, default value for result
NotFilteredNotFound Reason = iota
// NotFilteredAllowList - the host is explicitly allowed
NotFilteredAllowList
// NotFilteredError is returned when there was an error during
// checking. Reserved, currently unused.
NotFilteredError
// reasons for filtering
// FilteredBlockList - the host was matched to be advertising host
FilteredBlockList
// FilteredSafeBrowsing - the host was matched to be malicious/phishing
FilteredSafeBrowsing
// FilteredParental - the host was matched to be outside of parental control settings
FilteredParental
// FilteredInvalid - the request was invalid and was not processed
FilteredInvalid
// FilteredSafeSearch - the host was replaced with safesearch variant
FilteredSafeSearch
// FilteredBlockedService - the host is blocked by "blocked services" settings
FilteredBlockedService
// Rewritten is returned when there was a rewrite by a legacy DNS rewrite
// rule.
Rewritten
// RewrittenAutoHosts is returned when there was a rewrite by autohosts
// rules (/etc/hosts and so on).
RewrittenAutoHosts
// RewrittenRule is returned when a $dnsrewrite filter rule was applied.
//
// TODO(a.garipov): Remove Rewritten and RewrittenAutoHosts by merging their
// functionality into RewrittenRule.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2499.
RewrittenRule
)
// TODO(a.garipov): Resync with actual code names or replace completely
// in HTTP API v1.
var reasonNames = []string{
NotFilteredNotFound: "NotFilteredNotFound",
NotFilteredAllowList: "NotFilteredWhiteList",
NotFilteredError: "NotFilteredError",
FilteredBlockList: "FilteredBlackList",
FilteredSafeBrowsing: "FilteredSafeBrowsing",
FilteredParental: "FilteredParental",
FilteredInvalid: "FilteredInvalid",
FilteredSafeSearch: "FilteredSafeSearch",
FilteredBlockedService: "FilteredBlockedService",
Rewritten: "Rewrite",
RewrittenAutoHosts: "RewriteEtcHosts",
RewrittenRule: "RewriteRule",
}
func (r Reason) String() string {
if r < 0 || int(r) >= len(reasonNames) {
return ""
}
return reasonNames[r]
}
// In returns true if reasons include r.
func (r Reason) In(reasons ...Reason) (ok bool) { return slices.Contains(reasons, r) }
// SetEnabled sets the status of the *DNSFilter.
func (d *DNSFilter) SetEnabled(enabled bool) {
atomic.StoreUint32(&d.conf.enabled, mathutil.BoolToNumber[uint32](enabled))
@ -556,54 +480,6 @@ func (d *DNSFilter) ParentalBlockHost() (host string) {
return d.conf.ParentalBlockHost
}
// ResultRule contains information about applied rules.
type ResultRule struct {
// Text is the text of the rule.
Text string `json:",omitempty"`
// IP is the host IP. It is nil unless the rule uses the
// /etc/hosts syntax or the reason is FilteredSafeSearch.
IP netip.Addr `json:",omitempty"`
// FilterListID is the ID of the rule's filter list.
FilterListID rulelist.APIID `json:",omitempty"`
}
// Result contains the result of a request check.
//
// All fields transitively have omitempty tags so that the query log doesn't
// become too large.
//
// TODO(a.garipov): Clarify relationships between fields. Perhaps replace with
// a sum type or an interface?
type Result struct {
// DNSRewriteResult is the $dnsrewrite filter rule result.
DNSRewriteResult *DNSRewriteResult `json:",omitempty"`
// CanonName is the CNAME value from the lookup rewrite result. It is empty
// unless Reason is set to Rewritten or RewrittenRule.
CanonName string `json:",omitempty"`
// ServiceName is the name of the blocked service. It is empty unless
// Reason is set to FilteredBlockedService.
ServiceName string `json:",omitempty"`
// IPList is the lookup rewrite result. It is empty unless Reason is set to
// Rewritten.
IPList []netip.Addr `json:",omitempty"`
// Rules are applied rules. If Rules are not empty, each rule is not nil.
Rules []*ResultRule `json:",omitempty"`
// Reason is the reason for blocking or unblocking the request.
Reason Reason `json:",omitempty"`
// IsFiltered is true if the request is filtered.
//
// TODO(d.kolyshev): Get rid of this flag.
IsFiltered bool `json:",omitempty"`
}
// Matched returns true if any match at all was found regardless of
// whether it was filtered or not.
func (r Reason) Matched() bool {
@ -1048,12 +924,7 @@ func (d *DNSFilter) matchHost(
func makeResult(matchedRules []rules.Rule, reason Reason) (res Result) {
resRules := make([]*ResultRule, len(matchedRules))
for i, mr := range matchedRules {
resRules[i] = &ResultRule{
// #nosec G115 -- The overflow is required for backwards
// compatibility.
FilterListID: rulelist.APIID(mr.GetFilterListID()),
Text: mr.Text(),
}
resRules[i] = NewResultRule(mr)
}
return Result{

View File

@ -0,0 +1,152 @@
package filtering
import (
"fmt"
"net/netip"
"slices"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
"github.com/AdguardTeam/urlfilter/rules"
)
// Result contains the result of a request check. All fields transitively have
// omitempty tags so that the query log doesn't become too large.
//
// TODO(a.garipov): Clarify relationships between fields. Perhaps replace with
// a sum type or an interface?
type Result struct {
// DNSRewriteResult is the $dnsrewrite filter rule result.
DNSRewriteResult *DNSRewriteResult `json:",omitempty"`
// CanonName is the CNAME value from the lookup rewrite result. It is empty
// unless Reason is set to Rewritten or RewrittenRule.
CanonName string `json:",omitempty"`
// ServiceName is the name of the blocked service. It is empty unless
// Reason is set to FilteredBlockedService.
ServiceName string `json:",omitempty"`
// IPList is the lookup rewrite result. It is empty unless Reason is set to
// Rewritten.
IPList []netip.Addr `json:",omitempty"`
// Rules are applied rules. If Rules are not empty, each rule is not nil.
Rules []*ResultRule `json:",omitempty"`
// Reason is the reason for blocking or unblocking the request.
Reason Reason `json:",omitempty"`
// IsFiltered is true if the request is filtered.
//
// TODO(d.kolyshev): Get rid of this flag.
IsFiltered bool `json:",omitempty"`
}
// ResultRule contains information about applied rules.
type ResultRule struct {
// Text is the text of the rule.
Text string `json:",omitempty"`
// IP is the host IP. It is nil unless the rule uses the /etc/hosts syntax
// or the reason is [FilteredSafeSearch].
IP netip.Addr `json:",omitzero"`
// FilterListID is the ID of the rule's filter list.
FilterListID rulelist.APIID `json:",omitempty"`
}
// NewResultRule converts an URLFilter rule into a *ResultRule. nr must not be
// nil.
func NewResultRule(r rules.Rule) (rr *ResultRule) {
return &ResultRule{
// #nosec G115 -- The overflow is required for backwards
// compatibility.
FilterListID: rulelist.APIID(r.GetFilterListID()),
Text: r.Text(),
}
}
// Reason holds an enum detailing why it was filtered or not filtered
type Reason int
const (
// NotFilteredNotFound: the host was not find in any checks, default value
// for results.
NotFilteredNotFound Reason = iota
// NotFilteredAllowList: the host is explicitly allowed.
NotFilteredAllowList
// NotFilteredError is returned when there was an error during checking.
// Reserved, currently unused.
NotFilteredError
// FilteredBlockList: the host was matched to be advertising host.
FilteredBlockList
// FilteredSafeBrowsing: the host was matched to be malicious/phishing.
FilteredSafeBrowsing
// FilteredParental: the host was matched to be outside of parental control
// settings.
FilteredParental
// FilteredInvalid: the request was invalid and was not processed.
FilteredInvalid
// FilteredSafeSearch: the host was replaced with safesearch variant.
FilteredSafeSearch
// FilteredBlockedService: the host is blocked by the blocked services
// feature.
FilteredBlockedService
// Rewritten is returned when there was a rewrite by a legacy DNS rewrite
// rule.
Rewritten
// RewrittenAutoHosts is returned when there was a rewrite by /etc/hosts.
RewrittenAutoHosts
// RewrittenRule is returned when a $dnsrewrite filter rule was applied.
//
// TODO(a.garipov): Remove [Rewritten] and [RewrittenAutoHosts] by merging
// their functionality into RewrittenRule.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2499.
RewrittenRule
)
// TODO(a.garipov): Resync with actual code names or replace completely in HTTP
// API v1.
var reasonNames = []string{
NotFilteredNotFound: "NotFilteredNotFound",
NotFilteredAllowList: "NotFilteredWhiteList",
NotFilteredError: "NotFilteredError",
FilteredBlockList: "FilteredBlackList",
FilteredSafeBrowsing: "FilteredSafeBrowsing",
FilteredParental: "FilteredParental",
FilteredInvalid: "FilteredInvalid",
FilteredSafeSearch: "FilteredSafeSearch",
FilteredBlockedService: "FilteredBlockedService",
Rewritten: "Rewrite",
RewrittenAutoHosts: "RewriteEtcHosts",
RewrittenRule: "RewriteRule",
}
// type check
var _ fmt.Stringer = NotFilteredNotFound
// String implements the [fmt.Stringer] interface for Reason.
func (r Reason) String() (s string) {
if r < 0 || int(r) >= len(reasonNames) {
return ""
}
return reasonNames[r]
}
// In returns true if reasons include r.
func (r Reason) In(reasons ...Reason) (ok bool) { return slices.Contains(reasons, r) }

View File

@ -155,16 +155,15 @@ func (f *Filter) setFromHTTP(
) (parseRes *ParseResult, err error) {
defer func() { err = errors.Annotate(err, "setting from http: %w") }()
text, parseRes, err := f.readFromHTTP(ctx, parseBuf, cli, cachePath, maxSize)
data, parseRes, err := f.readFromHTTP(ctx, parseBuf, cli, cachePath, maxSize)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
// TODO(a.garipov): Add filterlist.BytesRuleList.
f.ruleList = filterlist.NewString(&filterlist.StringConfig{
f.ruleList = filterlist.NewBytes(&filterlist.BytesConfig{
ID: f.urlFilterID,
RulesText: text,
RulesText: data,
IgnoreCosmetic: true,
})
@ -180,27 +179,27 @@ func (f *Filter) readFromHTTP(
cli *http.Client,
cachePath string,
maxSize uint64,
) (text string, parseRes *ParseResult, err error) {
) (data []byte, parseRes *ParseResult, err error) {
urlStr := f.url.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return "", nil, fmt.Errorf("making request for http url %q: %w", urlStr, err)
return nil, nil, fmt.Errorf("making request for http url %q: %w", urlStr, err)
}
resp, err := cli.Do(req)
if err != nil {
return "", nil, fmt.Errorf("requesting from http url: %w", err)
return nil, nil, fmt.Errorf("requesting from http url: %w", err)
}
defer func() { err = errors.WithDeferred(err, resp.Body.Close()) }()
// TODO(a.garipov): Use [agdhttp.CheckStatus] when it's moved to golibs.
if resp.StatusCode != http.StatusOK {
return "", nil, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
return nil, nil, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
}
fltFile, err := aghrenameio.NewPendingFile(cachePath, aghos.DefaultPermFile)
if err != nil {
return "", nil, fmt.Errorf("creating temp file: %w", err)
return nil, nil, fmt.Errorf("creating temp file: %w", err)
}
defer func() { err = aghrenameio.WithDeferredCleanup(err, fltFile) }()
@ -211,10 +210,10 @@ func (f *Filter) readFromHTTP(
httpBody := ioutil.LimitReader(resp.Body, maxSize)
parseRes, err = parser.Parse(mw, httpBody, parseBuf)
if err != nil {
return "", nil, fmt.Errorf("parsing response from http url %q: %w", urlStr, err)
return nil, nil, fmt.Errorf("parsing response from http url %q: %w", urlStr, err)
}
return buf.String(), parseRes, nil
return buf.Bytes(), parseRes, nil
}
// setName sets the title using either the already-present name, the given title

View File

@ -47,9 +47,9 @@ const (
//
// TODO(d.kolyshev): Add URLFilterIDLegacyRewrite here and to the UI.
const (
IDCustom rules.ListID = 0
IDBlockedService rules.ListID = math.MaxUint64 - 2 + 1
IDSafeSearch rules.ListID = math.MaxUint64 - 5 + 1
IDCustom rules.ListID = rules.ListID(APIIDCustom)
IDBlockedService rules.ListID = math.MaxUint64 - rules.ListID(-APIIDBlockedService) + 1
IDSafeSearch rules.ListID = math.MaxUint64 - rules.ListID(-APIIDSafeSearch) + 1
)
// UID is the type for the unique IDs of filtering-rule lists.

View File

@ -40,11 +40,13 @@ type StorageConfig struct {
CacheDir string
// AllowFilters are the filtering-rule lists used to exclude domain names
// from the filtering. Each item must not be nil.
// from the filtering. Each item must not be nil and must have unique IDs
// between AllowFilters and BlockFilters.
AllowFilters []*Filter
// BlockFilters are the filtering-rule lists used to block domain names.
// Each item must not be nil.
// Each item must not be nil and must have unique IDs between AllowFilters
// and BlockFilters.
BlockFilters []*Filter
// CustomRules contains custom rules of the user. They have priority over