all: rewrites enabled

This commit is contained in:
Stanislav Chzhen 2025-10-03 14:21:53 +03:00
parent 6dbf114ff9
commit f1bf45aa42
19 changed files with 811 additions and 115 deletions

View File

@ -19,8 +19,40 @@ NOTE: Add new changes BELOW THIS COMMENT.
-->
### Added
- New DNS rewrite settings endpoints `GET /control/rewrite/settings` and `PUT /control/rewrite/settings/update`. See `openapi/openapi.yaml` for details.
- New fields `"groups"` and `"group_id"` added to the HTTP API (`GET /control/blocked_services/all`). See `openapi/openapi.yaml` for the full description.
### Changed
- `POST /control/rewrite/add` and `PUT /control/rewrite/update` now accept the optional field "enabled". See `openapi/openapi.yaml` for details.
#### Configuration changes
In this release, the schema version has changed from 30 to 31.
- Added a new boolean field `filtering.rewrites_enabled` to globally enable/disable DNS rewrites.
- Added a new boolean field `enabled` for each entry in `filtering.rewrites` to toggle individual rewrites.
```yaml
# BEFORE:
'filtering':
'rewrites':
- 'domain': test.example
'answer': 192.0.2.0
# …
# AFTER:
'filtering':
'rewrites_enabled': true
'rewrites':
- 'domain': test.example
'answer': 192.0.2.0
'enabled': true
# …
```
To roll back this change, set `schema_version` back to `30`.
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->

View File

@ -2,4 +2,4 @@
package configmigrate
// LastSchemaVersion is the most recent schema version.
const LastSchemaVersion uint = 30
const LastSchemaVersion uint = 31

View File

@ -126,6 +126,7 @@ func (m *Migrator) upgradeConfigSchema(current, target uint, diskConf yobj) (err
27: migrateTo28,
28: m.migrateTo29,
29: m.migrateTo30,
30: m.migrateTo31,
}
for i, migrate := range upgrades[current:target] {

View File

@ -199,6 +199,10 @@ func TestMigrateConfig_Migrate(t *testing.T) {
yamlEqFunc: require.YAMLEq,
name: "v30",
targetVersion: 30,
}, {
yamlEqFunc: require.YAMLEq,
name: "v31",
targetVersion: 31,
}}
for _, tc := range testCases {

View File

@ -0,0 +1,122 @@
http:
address: 127.0.0.1:3000
session_ttl: 3h
pprof:
enabled: true
port: 6060
users:
- name: testuser
password: testpassword
dns:
bind_hosts:
- 127.0.0.1
port: 53
parental_sensitivity: 0
upstream_dns:
- tls://1.1.1.1
- tls://1.0.0.1
- quic://8.8.8.8:784
bootstrap_dns:
- 8.8.8.8:53
cache_size: 4194304
edns_client_subnet:
enabled: true
use_custom: false
custom_ip: ""
filtering:
filtering_enabled: true
parental_enabled: false
safebrowsing_enabled: false
rewrites:
- domain: test.example
answer: 192.0.2.0
safe_fs_patterns: []
safe_search:
enabled: false
bing: true
duckduckgo: true
google: true
pixabay: true
yandex: true
youtube: true
protection_enabled: true
blocked_services:
schedule:
time_zone: Local
ids:
- 500px
blocked_response_ttl: 10
filters:
- url: https://adaway.org/hosts.txt
name: AdAway
enabled: false
- url: FILEPATH
name: Local Filter
enabled: false
clients:
persistent:
- name: localhost
ids:
- 127.0.0.1
- aa:aa:aa:aa:aa:aa
use_global_settings: true
use_global_blocked_services: true
filtering_enabled: false
parental_enabled: false
safebrowsing_enabled: false
safe_search:
enabled: true
bing: true
duckduckgo: true
google: true
pixabay: true
yandex: true
youtube: true
blocked_services:
schedule:
time_zone: Local
ids:
- 500px
runtime_sources:
whois: true
arp: true
rdns: true
dhcp: true
hosts: true
dhcp:
enabled: false
interface_name: vboxnet0
local_domain_name: local
dhcpv4:
gateway_ip: 192.168.0.1
subnet_mask: 255.255.255.0
range_start: 192.168.0.10
range_end: 192.168.0.250
lease_duration: 1234
icmp_timeout_msec: 10
schema_version: 29
user_rules: []
querylog:
enabled: true
file_enabled: true
interval: 720h
size_memory: 1000
ignored:
- '|.^'
statistics:
enabled: true
interval: 240h
ignored:
- '|.^'
os:
group: ''
rlimit_nofile: 123
user: ''
log:
file: ""
max_backups: 0
max_size: 100
max_age: 3
compress: true
local_time: false
verbose: true

View File

@ -0,0 +1,124 @@
http:
address: 127.0.0.1:3000
session_ttl: 3h
pprof:
enabled: true
port: 6060
users:
- name: testuser
password: testpassword
dns:
bind_hosts:
- 127.0.0.1
port: 53
parental_sensitivity: 0
upstream_dns:
- tls://1.1.1.1
- tls://1.0.0.1
- quic://8.8.8.8:784
bootstrap_dns:
- 8.8.8.8:53
cache_enabled: true
cache_size: 4194304
edns_client_subnet:
enabled: true
use_custom: false
custom_ip: ""
filtering:
filtering_enabled: true
parental_enabled: false
safebrowsing_enabled: false
rewrites:
- domain: test.example
answer: 192.0.2.0
enabled: true
safe_fs_patterns: []
safe_search:
enabled: false
bing: true
duckduckgo: true
google: true
pixabay: true
yandex: true
youtube: true
protection_enabled: true
blocked_services:
schedule:
time_zone: Local
ids:
- 500px
blocked_response_ttl: 10
filters:
- url: https://adaway.org/hosts.txt
name: AdAway
enabled: false
- url: FILEPATH
name: Local Filter
enabled: false
clients:
persistent:
- name: localhost
ids:
- 127.0.0.1
- aa:aa:aa:aa:aa:aa
use_global_settings: true
use_global_blocked_services: true
filtering_enabled: false
parental_enabled: false
safebrowsing_enabled: false
safe_search:
enabled: true
bing: true
duckduckgo: true
google: true
pixabay: true
yandex: true
youtube: true
blocked_services:
schedule:
time_zone: Local
ids:
- 500px
runtime_sources:
whois: true
arp: true
rdns: true
dhcp: true
hosts: true
dhcp:
enabled: false
interface_name: vboxnet0
local_domain_name: local
dhcpv4:
gateway_ip: 192.168.0.1
subnet_mask: 255.255.255.0
range_start: 192.168.0.10
range_end: 192.168.0.250
lease_duration: 1234
icmp_timeout_msec: 10
schema_version: 31
user_rules: []
querylog:
enabled: true
file_enabled: true
interval: 720h
size_memory: 1000
ignored:
- '|.^'
statistics:
enabled: true
interval: 240h
ignored:
- '|.^'
os:
group: ''
rlimit_nofile: 123
user: ''
log:
file: ""
max_backups: 0
max_size: 100
max_age: 3
compress: true
local_time: false
verbose: true

View File

@ -0,0 +1,41 @@
package configmigrate
// migrateTo31 performs the following changes:
//
// # BEFORE:
// 'filtering':
// 'rewrites':
// - 'domain': test.example
// 'answer': 192.0.2.0
// # …
// # …
//
// # AFTER:
// 'filtering':
// 'rewrites':
// - 'domain': test.example
// 'answer': 192.0.2.0
// 'enabled': true
// # …
// # …
func (m Migrator) migrateTo31(diskConf yobj) (err error) {
diskConf["schema_version"] = 31
fltConf, ok, err := fieldVal[yobj](diskConf, "filtering")
if !ok {
return err
}
rewrites, ok, err := fieldVal[yarr](fltConf, "rewrites")
if !ok {
return err
}
for i := range rewrites {
if r, isYobj := rewrites[i].(yobj); isYobj {
r["enabled"] = true
}
}
return nil
}

View File

@ -1247,18 +1247,22 @@ func TestRewrite(t *testing.T) {
BlockedServices: emptyFilteringBlockedServices(),
BlockingMode: filtering.BlockingModeDefault,
Rewrites: []*filtering.LegacyRewrite{{
Domain: "test.com",
Answer: "1.2.3.4",
Type: dns.TypeA,
Domain: "test.com",
Answer: "1.2.3.4",
Type: dns.TypeA,
Enabled: true,
}, {
Domain: "alias.test.com",
Answer: "test.com",
Type: dns.TypeCNAME,
Domain: "alias.test.com",
Answer: "test.com",
Type: dns.TypeCNAME,
Enabled: true,
}, {
Domain: "my.alias.example.org",
Answer: "example.org",
Type: dns.TypeCNAME,
Domain: "my.alias.example.org",
Answer: "example.org",
Type: dns.TypeCNAME,
Enabled: true,
}},
RewritesEnabled: true,
}
f, err := filtering.New(c, nil)
require.NoError(t, err)

View File

@ -138,6 +138,7 @@ type Config struct {
// to DNS requests blocked by safe-browsing.
SafeBrowsingBlockHost string `yaml:"safebrowsing_block_host"`
// Rewrites is a list of legacy DNS rewrite records.
Rewrites []*LegacyRewrite `yaml:"rewrites"`
// Filters are the blocking filter lists.
@ -177,6 +178,9 @@ type Config struct {
// FilteringEnabled indicates whether or not use filter lists.
FilteringEnabled bool `yaml:"filtering_enabled"`
// RewritesEnabled indicates whether legacy rewrites are applied.
RewritesEnabled bool `yaml:"rewrites_enabled"`
ParentalEnabled bool `yaml:"parental_enabled"`
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
@ -542,6 +546,10 @@ func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
ctx := context.TODO()
if !d.conf.RewritesEnabled {
return Result{}
}
rewrites, matched := findRewrites(d.conf.Rewrites, host, qtype)
if !matched {
return Result{}
@ -549,8 +557,22 @@ func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
res.Reason = Rewritten
return d.handleRewriteLoop(ctx, host, qtype, rewrites, matched, &res)
}
// handleRewriteLoop performs filtering rewrite processing based on the legacy
// rewrite records. res must not be nil.
func (d *DNSFilter) handleRewriteLoop(
ctx context.Context,
host string,
qtype uint16,
rewrites []*LegacyRewrite,
matched bool,
res *Result,
) (resResult Result) {
cnames := container.NewMapSet[string]()
origHost := host
for matched && len(rewrites) > 0 && rewrites[0].Type == dns.TypeCNAME {
rw := rewrites[0]
rwPat := rw.Domain
@ -577,7 +599,7 @@ func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
if cnames.Has(host) {
d.logger.InfoContext(ctx, "cname loop", "host", host, "original", origHost)
return res
return *res
}
cnames.Add(host)
@ -585,9 +607,9 @@ func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
rewrites, matched = findRewrites(d.conf.Rewrites, host, qtype)
}
d.setRewriteResult(ctx, &res, host, rewrites, qtype)
d.setRewriteResult(ctx, res, host, rewrites, qtype)
return res
return *res
}
// matchBlockedServicesRules checks the host against the blocked services rules

View File

@ -45,7 +45,8 @@ func newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts
} else {
// It must not be nil.
c = &Config{
Logger: testLogger,
Logger: testLogger,
RewritesEnabled: true,
}
}
f, err := New(c, filters)

View File

@ -645,9 +645,11 @@ func (d *DNSFilter) RegisterFilteringHandlers() {
registerHTTP(http.MethodPut, "/control/safesearch/settings", d.handleSafeSearchSettings)
registerHTTP(http.MethodGet, "/control/rewrite/list", d.handleRewriteList)
registerHTTP(http.MethodGet, "/control/rewrite/settings", d.handleRewriteSettings)
registerHTTP(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd)
registerHTTP(http.MethodPut, "/control/rewrite/update", d.handleRewriteUpdate)
registerHTTP(http.MethodPost, "/control/rewrite/delete", d.handleRewriteDelete)
registerHTTP(http.MethodPut, "/control/rewrite/settings/update", d.handleRewriteSettingsUpdate)
registerHTTP(http.MethodPut, "/control/rewrite/update", d.handleRewriteUpdate)
registerHTTP(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesIDs)
registerHTTP(http.MethodGet, "/control/blocked_services/all", d.handleBlockedServicesAll)

View File

@ -9,6 +9,8 @@ import (
)
// Item is a single DNS rewrite record.
//
// TODO(s.chzhen): Add "Enabled" property.
type Item struct {
// Domain is the domain pattern for which this rewrite should work.
Domain string `yaml:"domain"`

View File

@ -5,13 +5,26 @@ import (
"net/http"
"slices"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
)
// rewriteEntryJSON is a single entry of the DNS rewrite.
//
// TODO(d.kolyshev): Use [rewrite.Item] instead.
type rewriteEntryJSON struct {
Domain string `json:"domain"`
Answer string `json:"answer"`
Domain string `json:"domain"`
Answer string `json:"answer"`
Enabled aghalg.NullBool `json:"enabled"`
}
// rewriteSettings contains DNS rewrite settings.
type rewriteSettings struct {
// Enabled indicates whether legacy rewrites are applied.
//
// TODO(s.chzhen): Consider using [aghalg.NullBool] so "{}" won't
// accidentally disable rewrites on decode.
Enabled bool `json:"enabled"`
}
// handleRewriteList is the handler for the GET /control/rewrite/list HTTP API.
@ -24,8 +37,9 @@ func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
for _, ent := range d.conf.Rewrites {
jsonEnt := rewriteEntryJSON{
Domain: ent.Domain,
Answer: ent.Answer,
Domain: ent.Domain,
Answer: ent.Answer,
Enabled: aghalg.BoolToNullBool(ent.Enabled),
}
arr = append(arr, &jsonEnt)
}
@ -46,9 +60,15 @@ func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
return
}
enabled := true
if rwJSON.Enabled != aghalg.NBNull {
enabled = rwJSON.Enabled == aghalg.NBTrue
}
rw := &LegacyRewrite{
Domain: rwJSON.Domain,
Answer: rwJSON.Answer,
Domain: rwJSON.Domain,
Answer: rwJSON.Answer,
Enabled: enabled,
}
err = rw.normalize(ctx, d.logger)
@ -174,6 +194,12 @@ func (d *DNSFilter) handleRewriteUpdate(w http.ResponseWriter, r *http.Request)
return
}
if updateJSON.Update.Enabled == aghalg.NBNull {
rwAdd.Enabled = d.conf.Rewrites[index].Enabled
} else {
rwAdd.Enabled = updateJSON.Update.Enabled == aghalg.NBTrue
}
d.conf.Rewrites = slices.Replace(d.conf.Rewrites, index, index+1, rwAdd)
d.logger.DebugContext(
@ -189,3 +215,28 @@ func (d *DNSFilter) handleRewriteUpdate(w http.ResponseWriter, r *http.Request)
"answer", rwAdd.Answer,
)
}
// handleRewriteSettings is the handler for the GET /control/rewrite/settings
// HTTP API.
func (d *DNSFilter) handleRewriteSettings(w http.ResponseWriter, r *http.Request) {
resp := &rewriteSettings{
Enabled: protectedBool(d.confMu, &d.conf.RewritesEnabled),
}
aghhttp.WriteJSONResponseOK(w, r, resp)
}
// handleRewriteSettingsUpdate is the handler for the PUT
// /control/rewrite/settings/update HTTP API.
func (d *DNSFilter) handleRewriteSettingsUpdate(w http.ResponseWriter, r *http.Request) {
req := &rewriteSettings{}
err := json.NewDecoder(r.Body).Decode(req)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
setProtectedBool(d.confMu, &d.conf.RewritesEnabled, req.Enabled)
d.conf.ConfModifier.Apply(r.Context())
}

View File

@ -4,12 +4,14 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/logutil/slogutil"
@ -20,8 +22,18 @@ import (
// TODO(d.kolyshev): Use [rewrite.Item] instead.
type rewriteJSON struct {
Domain string `json:"domain"`
Answer string `json:"answer"`
Domain string `json:"domain"`
Answer string `json:"answer"`
Enabled aghalg.NullBool `json:"enabled"`
}
// newRewriteJSON returns a freshly initialized *rewriteJSON.
func newRewriteJSON(domain, answer string, enabled aghalg.NullBool) (rw *rewriteJSON) {
return &rewriteJSON{
Domain: domain,
Answer: answer,
Enabled: enabled,
}
}
type rewriteUpdateJSON struct {
@ -38,16 +50,33 @@ const (
deleteURL = "/control/rewrite/delete"
updateURL = "/control/rewrite/update"
decodeErrorMsg = "json.Decode: json: cannot unmarshal string into Go value of type" +
" filtering.rewriteEntryJSON\n"
decodeMsg = "json.Decode: json: cannot unmarshal string into Go value of type"
decodeErrorMsg = decodeMsg + " filtering.rewriteEntryJSON\n"
decodeUpdateErrorMsg = decodeMsg + " filtering.rewriteUpdateJSON\n"
)
func TestDNSFilter_handleRewriteHTTP(t *testing.T) {
confModCh := make(chan struct{})
reqCh := make(chan struct{})
func TestDNSFilter_HandleRewriteHTTP(t *testing.T) {
t.Parallel()
const (
exampleDomain = "example.local"
exampleAnswer = "example.rewrite"
oneDomain = "one.local"
oneAnswer = "one.rewrite"
disabledDomain = "disabled.local"
disabledAnswer = "disabled.rewrite"
addDomain = "add.local"
addAnswer = "add.rewrite"
updDomain = "upd.local"
updAnswer = "upd.rewrite"
invDomain = "inv.local"
invAnswer = "inv.rewrite"
)
testRewrites := []*rewriteJSON{
{Domain: "example.local", Answer: "example.rewrite"},
{Domain: "one.local", Answer: "one.rewrite"},
newRewriteJSON(exampleDomain, exampleAnswer, aghalg.NBTrue),
newRewriteJSON(oneDomain, oneAnswer, aghalg.NBTrue),
newRewriteJSON(disabledDomain, disabledAnswer, aghalg.NBFalse),
}
testRewritesJSON, mErr := json.Marshal(testRewrites)
@ -72,16 +101,48 @@ func TestDNSFilter_handleRewriteHTTP(t *testing.T) {
wantBody: string(testRewritesJSON) + "\n",
wantList: testRewrites,
}, {
name: "add",
name: "add_enabled_null",
url: addURL,
method: http.MethodPost,
reqData: rewriteJSON{Domain: "add.local", Answer: "add.rewrite"},
reqData: rewriteJSON{Domain: addDomain, Answer: addAnswer},
wantConfMod: true,
wantStatus: http.StatusOK,
wantBody: "",
wantList: append(
testRewrites,
&rewriteJSON{Domain: "add.local", Answer: "add.rewrite"},
newRewriteJSON(addDomain, addAnswer, aghalg.NBTrue),
),
}, {
name: "add_enabled_false",
url: addURL,
method: http.MethodPost,
reqData: rewriteJSON{
Domain: addDomain,
Answer: addAnswer,
Enabled: aghalg.NBFalse,
},
wantConfMod: true,
wantStatus: http.StatusOK,
wantBody: "",
wantList: append(
testRewrites,
newRewriteJSON(addDomain, addAnswer, aghalg.NBFalse),
),
}, {
name: "add_enabled_true",
url: addURL,
method: http.MethodPost,
reqData: rewriteJSON{
Domain: addDomain,
Answer: addAnswer,
Enabled: aghalg.NBTrue,
},
wantConfMod: true,
wantStatus: http.StatusOK,
wantBody: "",
wantList: append(
testRewrites,
newRewriteJSON(addDomain, addAnswer, aghalg.NBTrue),
),
}, {
name: "add_error",
@ -96,11 +157,14 @@ func TestDNSFilter_handleRewriteHTTP(t *testing.T) {
name: "delete",
url: deleteURL,
method: http.MethodPost,
reqData: rewriteJSON{Domain: "one.local", Answer: "one.rewrite"},
reqData: rewriteJSON{Domain: oneDomain, Answer: oneAnswer},
wantConfMod: true,
wantStatus: http.StatusOK,
wantBody: "",
wantList: []*rewriteJSON{{Domain: "example.local", Answer: "example.rewrite"}},
wantList: []*rewriteJSON{
newRewriteJSON(exampleDomain, exampleAnswer, aghalg.NBTrue),
newRewriteJSON(disabledDomain, disabledAnswer, aghalg.NBFalse),
},
}, {
name: "delete_error",
url: deleteURL,
@ -111,19 +175,56 @@ func TestDNSFilter_handleRewriteHTTP(t *testing.T) {
wantBody: decodeErrorMsg,
wantList: testRewrites,
}, {
name: "update",
name: "update_enabled_null",
url: updateURL,
method: http.MethodPut,
reqData: rewriteUpdateJSON{
Target: rewriteJSON{Domain: "one.local", Answer: "one.rewrite"},
Update: rewriteJSON{Domain: "upd.local", Answer: "upd.rewrite"},
Target: rewriteJSON{Domain: oneDomain, Answer: oneAnswer},
Update: rewriteJSON{Domain: updDomain, Answer: updAnswer},
},
wantConfMod: true,
wantStatus: http.StatusOK,
wantBody: "",
wantList: []*rewriteJSON{
{Domain: "example.local", Answer: "example.rewrite"},
{Domain: "upd.local", Answer: "upd.rewrite"},
newRewriteJSON(exampleDomain, exampleAnswer, aghalg.NBTrue),
newRewriteJSON(updDomain, updAnswer, aghalg.NBTrue),
newRewriteJSON(disabledDomain, disabledAnswer, aghalg.NBFalse),
},
}, {
name: "update_enabled_false",
url: updateURL,
method: http.MethodPut,
reqData: rewriteUpdateJSON{
Target: rewriteJSON{Domain: oneDomain, Answer: oneAnswer},
Update: rewriteJSON{
Domain: updDomain,
Answer: updAnswer,
Enabled: aghalg.NBFalse,
},
},
wantConfMod: true,
wantStatus: http.StatusOK,
wantBody: "",
wantList: []*rewriteJSON{
newRewriteJSON(exampleDomain, exampleAnswer, aghalg.NBTrue),
newRewriteJSON(updDomain, updAnswer, aghalg.NBFalse),
newRewriteJSON(disabledDomain, disabledAnswer, aghalg.NBFalse),
},
}, {
name: "update_enabled_true",
url: updateURL,
method: http.MethodPut,
reqData: rewriteUpdateJSON{
Target: rewriteJSON{Domain: oneDomain, Answer: oneAnswer},
Update: rewriteJSON{Domain: updDomain, Answer: updAnswer, Enabled: aghalg.NBTrue},
},
wantConfMod: true,
wantStatus: http.StatusOK,
wantBody: "",
wantList: []*rewriteJSON{
newRewriteJSON(exampleDomain, exampleAnswer, aghalg.NBTrue),
newRewriteJSON(updDomain, updAnswer, aghalg.NBTrue),
newRewriteJSON(disabledDomain, disabledAnswer, aghalg.NBFalse),
},
}, {
name: "update_error",
@ -132,16 +233,15 @@ func TestDNSFilter_handleRewriteHTTP(t *testing.T) {
reqData: "invalid_json",
wantConfMod: false,
wantStatus: http.StatusBadRequest,
wantBody: "json.Decode: json: cannot unmarshal string into Go value of type" +
" filtering.rewriteUpdateJSON\n",
wantList: testRewrites,
wantBody: decodeUpdateErrorMsg,
wantList: testRewrites,
}, {
name: "update_error_target",
url: updateURL,
method: http.MethodPut,
reqData: rewriteUpdateJSON{
Target: rewriteJSON{Domain: "inv.local", Answer: "inv.rewrite"},
Update: rewriteJSON{Domain: "upd.local", Answer: "upd.rewrite"},
Target: rewriteJSON{Domain: invDomain, Answer: invAnswer},
Update: rewriteJSON{Domain: updDomain, Answer: updAnswer},
},
wantConfMod: false,
wantStatus: http.StatusBadRequest,
@ -151,6 +251,11 @@ func TestDNSFilter_handleRewriteHTTP(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
confModCh := make(chan struct{})
reqCh := make(chan struct{})
handlers := make(map[string]http.Handler)
confModifier := &aghtest.ConfigModifier{}
confModifier.OnApply = func(_ context.Context) {
@ -229,10 +334,74 @@ func assertRewritesList(tb testing.TB, handler http.Handler, wantList []*rewrite
func rewriteEntriesToLegacyRewrites(entries []*rewriteJSON) (rw []*filtering.LegacyRewrite) {
for _, entry := range entries {
rw = append(rw, &filtering.LegacyRewrite{
Domain: entry.Domain,
Answer: entry.Answer,
Domain: entry.Domain,
Answer: entry.Answer,
Enabled: entry.Enabled == aghalg.NBTrue,
})
}
return rw
}
func TestDNSFilter_HandleRewriteSettings(t *testing.T) {
const (
enabled = "enabled"
path = "/control/rewrite/settings"
pathUpdate = path + "/update"
)
var (
wantEnabled = fmt.Sprintf("{%q:%s}", enabled, "true")
wantDisabled = fmt.Sprintf("{%q:%s}", enabled, "false")
)
confUpdated := false
confModifier := &aghtest.ConfigModifier{
OnApply: func(_ context.Context) {
confUpdated = true
},
}
handlers := make(map[string]http.Handler)
d, err := filtering.New(&filtering.Config{
Logger: slogutil.NewDiscardLogger(),
ConfModifier: confModifier,
HTTPRegister: func(_, url string, handler http.HandlerFunc) {
handlers[url] = handler
},
RewritesEnabled: false,
}, nil)
require.NoError(t, err)
t.Cleanup(d.Close)
require.True(t, t.Run("register", func(t *testing.T) {
d.RegisterFilteringHandlers()
require.NotEmpty(t, handlers)
require.Contains(t, handlers, path)
require.Contains(t, handlers, pathUpdate)
r := httptest.NewRequest(http.MethodGet, path, nil)
w := httptest.NewRecorder()
handlers[path].ServeHTTP(w, r)
require.Equal(t, http.StatusOK, w.Code)
assert.JSONEq(t, wantDisabled, w.Body.String())
}))
require.True(t, t.Run("update", func(t *testing.T) {
r := httptest.NewRequest(http.MethodPut, path, bytes.NewReader([]byte(wantEnabled)))
w := httptest.NewRecorder()
handlers[pathUpdate].ServeHTTP(w, r)
require.Equal(t, http.StatusOK, w.Code)
r = httptest.NewRequest(http.MethodGet, path, nil)
w = httptest.NewRecorder()
handlers[path].ServeHTTP(w, r)
require.Equal(t, http.StatusOK, w.Code)
assert.True(t, confUpdated)
assert.JSONEq(t, wantEnabled, w.Body.String())
}))
}

View File

@ -32,6 +32,9 @@ type LegacyRewrite struct {
// Type is the DNS record type: A, AAAA, or CNAME.
Type uint16 `yaml:"-"`
// Enabled indicates whether this rewrite is active.
Enabled bool `yaml:"enabled"`
}
// equal returns true if the rw is equal to the other.
@ -162,6 +165,10 @@ func findRewrites(
qtype uint16,
) (rewrites []*LegacyRewrite, matched bool) {
for _, e := range entries {
if !e.Enabled {
continue
}
if e.Domain != host && !matchDomainWildcard(host, e.Domain) {
continue
}
@ -176,6 +183,11 @@ func findRewrites(
return nil, matched
}
return finalizeRewrites(rewrites), matched
}
// finalizeRewrites sorts rewrites and truncates wildcard ones.
func finalizeRewrites(rewrites []*LegacyRewrite) (resRewrites []*LegacyRewrite) {
slices.SortFunc(rewrites, (*LegacyRewrite).Compare)
for i, r := range rewrites {
@ -188,7 +200,7 @@ func findRewrites(
}
}
return rewrites, matched
return rewrites
}
// setRewriteResult sets the Reason or IPList of res if necessary. res must not

View File

@ -29,64 +29,86 @@ func TestRewrites(t *testing.T) {
d.conf.Rewrites = []*LegacyRewrite{{
// This one and below are about CNAME, A and AAAA.
Domain: "somecname",
Answer: "somehost.com",
Domain: "somecname",
Answer: "somehost.com",
Enabled: true,
}, {
Domain: "somehost.com",
Answer: netip.IPv4Unspecified().String(),
Domain: "somehost.com",
Answer: netip.IPv4Unspecified().String(),
Enabled: true,
}, {
Domain: "host.com",
Answer: addr1v4.String(),
Domain: "host.com",
Answer: addr1v4.String(),
Enabled: true,
}, {
Domain: "host.com",
Answer: addr2v4.String(),
Domain: "host.com",
Answer: addr2v4.String(),
Enabled: true,
}, {
Domain: "host.com",
Answer: addr1v6.String(),
Domain: "host.com",
Answer: addr1v6.String(),
Enabled: true,
}, {
Domain: "www.host.com",
Answer: "host.com",
Domain: "www.host.com",
Answer: "host.com",
Enabled: true,
}, {
// This one is a wildcard.
Domain: "*.host.com",
Answer: addr2v4.String(),
Domain: "*.host.com",
Answer: addr2v4.String(),
Enabled: true,
}, {
// This one and below are about wildcard overriding.
Domain: "a.host.com",
Answer: addr1v4.String(),
Domain: "a.host.com",
Answer: addr1v4.String(),
Enabled: true,
}, {
// This one is about CNAME and wildcard interacting.
Domain: "*.host2.com",
Answer: "host.com",
Domain: "*.host2.com",
Answer: "host.com",
Enabled: true,
}, {
// This one and below are about 2 level CNAME.
Domain: "b.host.com",
Answer: "somecname",
Domain: "b.host.com",
Answer: "somecname",
Enabled: true,
}, {
// This one and below are about 2 level CNAME and wildcard.
Domain: "b.host3.com",
Answer: "a.host3.com",
Domain: "b.host3.com",
Answer: "a.host3.com",
Enabled: true,
}, {
Domain: "a.host3.com",
Answer: "x.host.com",
Domain: "a.host3.com",
Answer: "x.host.com",
Enabled: true,
}, {
Domain: "*.hostboth.com",
Answer: addr3v4.String(),
Domain: "*.hostboth.com",
Answer: addr3v4.String(),
Enabled: true,
}, {
Domain: "*.hostboth.com",
Answer: addr2v6.String(),
Domain: "*.hostboth.com",
Answer: addr2v6.String(),
Enabled: true,
}, {
Domain: "BIGHOST.COM",
Answer: addr4v4.String(),
Domain: "BIGHOST.COM",
Answer: addr4v4.String(),
Enabled: true,
}, {
Domain: "*.issue4016.com",
Answer: "sub.issue4016.com",
Domain: "*.issue4016.com",
Answer: "sub.issue4016.com",
Enabled: true,
}, {
Domain: "*.sub.issue6226.com",
Answer: addr2v4.String(),
Domain: "*.sub.issue6226.com",
Answer: addr2v4.String(),
Enabled: true,
}, {
Domain: "*.issue6226.com",
Answer: addr1v4.String(),
Domain: "*.issue6226.com",
Answer: addr1v4.String(),
Enabled: true,
}, {
Domain: "disabled.rewrite.test",
Answer: addr1v4.String(),
Enabled: false,
}}
ctx := testutil.ContextWithTimeout(t, testTimeout)
@ -204,6 +226,13 @@ func TestRewrites(t *testing.T) {
wantIPs: []netip.Addr{addr2v4},
wantReason: Rewritten,
dtyp: dns.TypeA,
}, {
name: "not_filtered_disabled_rewrite",
host: "disabled.rewrite.test",
wantCName: "",
wantIPs: nil,
wantReason: NotFilteredNotFound,
dtyp: dns.TypeA,
}}
for _, tc := range testCases {
@ -225,17 +254,20 @@ func TestRewritesLevels(t *testing.T) {
t.Cleanup(d.Close)
// Exact host, wildcard L2, wildcard L3.
d.conf.Rewrites = []*LegacyRewrite{{
Domain: "host.com",
Answer: "1.1.1.1",
Type: dns.TypeA,
Domain: "host.com",
Answer: "1.1.1.1",
Type: dns.TypeA,
Enabled: true,
}, {
Domain: "*.host.com",
Answer: "2.2.2.2",
Type: dns.TypeA,
Domain: "*.host.com",
Answer: "2.2.2.2",
Type: dns.TypeA,
Enabled: true,
}, {
Domain: "*.sub.host.com",
Answer: "3.3.3.3",
Type: dns.TypeA,
Domain: "*.sub.host.com",
Answer: "3.3.3.3",
Type: dns.TypeA,
Enabled: true,
}}
ctx := testutil.ContextWithTimeout(t, testTimeout)
@ -273,14 +305,17 @@ func TestRewritesExceptionCNAME(t *testing.T) {
t.Cleanup(d.Close)
// Wildcard and exception for a sub-domain.
d.conf.Rewrites = []*LegacyRewrite{{
Domain: "*.host.com",
Answer: "2.2.2.2",
Domain: "*.host.com",
Answer: "2.2.2.2",
Enabled: true,
}, {
Domain: "sub.host.com",
Answer: "sub.host.com",
Domain: "sub.host.com",
Answer: "sub.host.com",
Enabled: true,
}, {
Domain: "*.sub.host.com",
Answer: "*.sub.host.com",
Domain: "*.sub.host.com",
Answer: "*.sub.host.com",
Enabled: true,
}}
ctx := testutil.ContextWithTimeout(t, testTimeout)
@ -325,25 +360,30 @@ func TestRewritesExceptionIP(t *testing.T) {
t.Cleanup(d.Close)
// Exception for AAAA record.
d.conf.Rewrites = []*LegacyRewrite{{
Domain: "host.com",
Answer: "1.2.3.4",
Type: dns.TypeA,
Domain: "host.com",
Answer: "1.2.3.4",
Type: dns.TypeA,
Enabled: true,
}, {
Domain: "host.com",
Answer: "AAAA",
Type: dns.TypeAAAA,
Domain: "host.com",
Answer: "AAAA",
Type: dns.TypeAAAA,
Enabled: true,
}, {
Domain: "host2.com",
Answer: "::1",
Type: dns.TypeAAAA,
Domain: "host2.com",
Answer: "::1",
Type: dns.TypeAAAA,
Enabled: true,
}, {
Domain: "host2.com",
Answer: "A",
Type: dns.TypeA,
Domain: "host2.com",
Answer: "A",
Type: dns.TypeA,
Enabled: true,
}, {
Domain: "host3.com",
Answer: "A",
Type: dns.TypeA,
Domain: "host3.com",
Answer: "A",
Type: dns.TypeA,
Enabled: true,
}}
ctx := testutil.ContextWithTimeout(t, testTimeout)

View File

@ -530,6 +530,8 @@ var config = &configuration{
FilteringEnabled: true,
FiltersUpdateIntervalHours: 24,
RewritesEnabled: true,
ParentalEnabled: false,
SafeBrowsingEnabled: false,

View File

@ -2,7 +2,23 @@
<!-- TODO(a.garipov): Reformat in accordance with the KeepAChangelog spec. -->
## v0.107.67: API changes
## v0.107.68: API changes
### New HTTP APIs 'GET /control/rewrite/settings' and 'PUT /control/rewrite/settings/update'
- New HTTP APIs to manage global DNS rewrites.
```json
{
"enabled": true
}
```
### New `"enabled"` field in 'POST /control/rewrite/add' and 'PUT /control/rewrite/update'
- New optional field `"enabled"` indicates whether the rewrite is active.
### The blocked services groups
- The new field `"groups"` in `GET /control/blocked_services/all` is a list of service group. Groups make it possible to block multiple services with equal `"group_id"` at once.

View File

@ -16,6 +16,8 @@
- 'basicAuth': []
'tags':
- 'name': 'blocked_services'
'description': 'Blocked services controls'
- 'name': 'clients'
'description': 'Clients list operations'
- 'name': 'dhcp'
@ -34,6 +36,8 @@
'description': 'Apple .mobileconfig'
- 'name': 'parental'
'description': 'Blocking adult and explicit materials'
- 'name': 'rewrite'
'description': 'DNS rewrites'
- 'name': 'safebrowsing'
'description': 'Blocking malware/phishing sites'
- 'name': 'safesearch'
@ -1153,6 +1157,30 @@
'responses':
'200':
'description': 'OK.'
'/rewrite/settings':
'get':
'tags':
- 'rewrite'
'operationId': 'rewriteSettingsGet'
'summary': 'Get rewrite settings'
'responses':
'200':
'description': 'OK.'
'content':
'application/json':
'schema':
'$ref': '#/components/schemas/RewriteSettings'
'/rewrite/settings/update':
'put':
'tags':
- 'rewrite'
'operationId': 'rewriteSettingsUpdate'
'summary': 'Update rewrite settings'
'requestBody':
'$ref': '#/components/requestBodies/RewriteSettings'
'responses':
'200':
'description': 'OK.'
'/rewrite/update':
'put':
'tags':
@ -1414,6 +1442,12 @@
'schema':
'$ref': '#/components/schemas/RewriteEntry'
'required': true
'RewriteSettings':
'content':
'application/json':
'schema':
'$ref': '#/components/schemas/RewriteSettings'
'required': true
'RewriteUpdate':
'content':
'application/json':
@ -3002,6 +3036,23 @@
'type': 'string'
'description': 'value of A, AAAA or CNAME DNS record'
'example': '127.0.0.1'
'enabled':
'type': 'boolean'
'description': >
Optional. If omitted on add, defaults to `true`. On update, omitted
preserves previous value.
'example': true
'default': true
'RewriteSettings':
'type': 'object'
'description': 'DNS rewrite settings'
'required':
- 'enabled'
'properties':
'enabled':
'type': 'boolean'
'description': 'indicates whether rewrites are applied'
'example': true
'BlockedServicesArray':
'type': 'array'
'items':