Pull request 2493: ADG-10852-rewrites-enabled

Squashed commit of the following:

commit 8ce89d6dab8031dadac7698e71a489edfffe29f8
Merge: 7b0052d69 b76d10040
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Oct 16 16:58:28 2025 +0300

    Merge branch 'master' into ADG-10852-rewrites-enabled

commit 7b0052d695
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 15 18:09:34 2025 +0300

    client: fix i18n

commit 6ac47b30bb
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Wed Oct 15 07:56:16 2025 +0300

    fix eslint

commit 5e38412748
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Mon Oct 13 18:20:08 2025 +0300

    add notify

commit b7018efe07
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Mon Oct 13 18:06:35 2025 +0300

    update ux

commit 89fa121be1
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Mon Oct 13 15:39:49 2025 +0300

    update ux for rewrites page

commit 2ed3a128f2
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Fri Oct 10 16:11:06 2025 +0300

    update frontend

commit bb279f6b2e
Merge: 8ddc0a7af 497441d59
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Fri Oct 10 14:01:57 2025 +0300

    merge

commit 8ddc0a7afb
Author: Eugene Miroshkin <e.miroshkin@adguard.com>
Date:   Fri Oct 10 14:01:37 2025 +0300

    add rewrites toggle

commit 497441d595
Merge: 50a76760d 2f810068a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Oct 9 18:44:18 2025 +0300

    Merge branch 'master' into ADG-10852-rewrites-enabled

commit 50a76760db
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Oct 9 18:25:52 2025 +0300

    filtering: fix config write

commit f1bf45aa42
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Oct 3 14:21:53 2025 +0300

    all: rewrites enabled
This commit is contained in:
Stanislav Chzhen 2025-10-16 17:34:07 +03:00
parent b76d100406
commit 83feced4c8
30 changed files with 1021 additions and 142 deletions

View File

@ -24,8 +24,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`.
[go-1.25.3]: https://groups.google.com/g/golang-announce/c/YEyj6FUNbik
<!--

View File

@ -514,6 +514,7 @@
"rewrite_added": "DNS rewrite for \"{{key}}\" successfully added",
"rewrite_deleted": "DNS rewrite for \"{{key}}\" successfully deleted",
"rewrite_updated": "DNS rewrite successfully updated",
"rewrite_settings_updated": "DNS rewrite settings successfully updated",
"rewrite_add": "Add DNS rewrite",
"rewrite_edit": "Edit DNS rewrite",
"rewrite_not_found": "No DNS rewrites found",
@ -522,6 +523,10 @@
"rewrite_applied": "Rewrite rule is applied",
"rewrite_hosts_applied": "Rewritten by the hosts file rule",
"dns_rewrites": "DNS rewrites",
"rewrites_enabled_table_header": "Rewrites are enabled",
"rewrites_disabled_table_header": "Rewrites are disabled",
"enable_rewrites": "Enable rewrite rules",
"disable_rewrites": "Disable rewrite rules",
"form_domain": "Enter domain name or wildcard",
"form_answer": "Enter IP address or domain name",
"form_error_domain_format": "Invalid domain format",

View File

@ -2,6 +2,7 @@ import { createAction } from 'redux-actions';
import i18next from 'i18next';
import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './toasts';
import type { RootState } from '../initialState';
export const toggleRewritesModal = createAction('TOGGLE_REWRITES_MODAL');
@ -47,12 +48,15 @@ export const updateRewriteSuccess = createAction('UPDATE_REWRITE_SUCCESS');
* @param {string} config.target - current DNS rewrite value
* @param {string} config.update - updated DNS rewrite value
*/
export const updateRewrite = (config: any) => async (dispatch: any) => {
export const updateRewrite = (config: any) => async (dispatch: any, getState: () => RootState) => {
dispatch(updateRewriteRequest());
try {
await apiClient.updateRewrite(config);
dispatch(updateRewriteSuccess());
dispatch(toggleRewritesModal());
const state = getState();
if (state?.rewrites?.isModalOpen) {
dispatch(toggleRewritesModal());
}
dispatch(getRewritesList());
dispatch(addSuccessToast(i18next.t('rewrite_updated', { key: config.domain })));
} catch (error) {
@ -77,3 +81,35 @@ export const deleteRewrite = (config: any) => async (dispatch: any) => {
dispatch(deleteRewriteFailure());
}
};
export const getRewriteSettingsRequest = createAction('GET_REWRITE_SETTINGS_REQUEST');
export const getRewriteSettingsFailure = createAction('GET_REWRITE_SETTINGS_FAILURE');
export const getRewriteSettingsSuccess = createAction('GET_REWRITE_SETTINGS_SUCCESS');
export const getRewriteSettings = () => async (dispatch: any) => {
dispatch(getRewriteSettingsRequest());
try {
const data = await apiClient.getRewriteSettings();
dispatch(getRewriteSettingsSuccess(data));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getRewriteSettingsFailure());
}
};
export const updateRewriteSettingsRequest = createAction('UPDATE_REWRITE_SETTINGS_REQUEST');
export const updateRewriteSettingsFailure = createAction('UPDATE_REWRITE_SETTINGS_FAILURE');
export const updateRewriteSettingsSuccess = createAction('UPDATE_REWRITE_SETTINGS_SUCCESS');
export const updateRewriteSettings = (config: any) => async (dispatch: any) => {
dispatch(updateRewriteSettingsRequest());
try {
await apiClient.updateRewriteSettings(config);
dispatch(updateRewriteSettingsSuccess(config));
dispatch(getRewriteSettings());
dispatch(addSuccessToast(i18next.t('rewrite_settings_updated')));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(updateRewriteSettingsFailure());
}
};

View File

@ -489,6 +489,10 @@ class Api {
REWRITE_DELETE = { path: 'rewrite/delete', method: 'POST' };
REWRITE_SETTINGS = { path: 'rewrite/settings', method: 'GET' };
REWRITE_SETTINGS_UPDATE = { path: 'rewrite/settings/update', method: 'PUT' };
getRewritesList() {
const { path, method } = this.REWRITES_LIST;
@ -511,6 +515,14 @@ class Api {
return this.makeRequest(path, method, parameters);
}
updateRewriteSettings(config: any) {
const { path, method } = this.REWRITE_SETTINGS_UPDATE;
const parameters = {
data: config,
};
return this.makeRequest(path, method, parameters);
}
deleteRewrite(config: any) {
const { path, method } = this.REWRITE_DELETE;
const parameters = {
@ -519,6 +531,12 @@ class Api {
return this.makeRequest(path, method, parameters);
}
getRewriteSettings() {
const { path, method } = this.REWRITE_SETTINGS;
return this.makeRequest(path, method);
}
// Blocked services
BLOCKED_SERVICES_GET = { path: 'blocked_services/get', method: 'GET' };

View File

@ -15,8 +15,10 @@ interface TableProps {
processingAdd: boolean;
processingDelete: boolean;
processingUpdate: boolean;
settings: Record<string, boolean>;
handleDelete: (...args: unknown[]) => unknown;
toggleRewritesModal: (...args: unknown[]) => unknown;
toggleRewrite: (...args: unknown[]) => unknown;
}
class Table extends Component<TableProps> {
@ -43,24 +45,35 @@ class Table extends Component<TableProps> {
{
Header: this.props.t('actions_table_header'),
accessor: 'actions',
maxWidth: 100,
maxWidth: 150,
sortable: false,
resizable: false,
Cell: (value: any) => {
const currentRewrite = {
answer: value.row.answer,
domain: value.row.domain,
};
Cell: (row: any) => {
const { original } = row;
const { processing, settings, toggleRewrite } = this.props;
const isEnabledSettings = Boolean(settings && settings.enabled);
return (
<div className="logs__row logs__row--center">
<label className="checkbox">
<input
type="checkbox"
className="checkbox__input"
onChange={() => toggleRewrite(original)}
checked={original.enabled}
disabled={processing || !isEnabledSettings}
/>
<span className="checkbox__label checkbox__label--l" />
</label>
<button
type="button"
className="btn btn-icon btn-outline-primary btn-sm mr-2"
onClick={() => {
this.props.toggleRewritesModal({
type: MODAL_TYPE.EDIT_REWRITE,
currentRewrite,
original,
});
}}
disabled={this.props.processingUpdate}
@ -73,7 +86,7 @@ class Table extends Component<TableProps> {
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => this.props.handleDelete(currentRewrite)}
onClick={() => this.props.handleDelete(original)}
title={this.props.t('delete_table_action')}>
<svg className="icons">
<use xlinkHref="#delete" />

View File

@ -1,5 +1,6 @@
import React, { Component, Fragment } from 'react';
import { Trans, withTranslation } from 'react-i18next';
import cn from 'classnames';
import Table from './Table';
@ -18,12 +19,15 @@ interface RewritesProps {
addRewrite: (...args: unknown[]) => unknown;
deleteRewrite: (...args: unknown[]) => unknown;
updateRewrite: (...args: unknown[]) => unknown;
updateRewriteSettings: (...args: unknown[]) => unknown;
getRewriteSettings: () => (dispatch: any) => void;
rewrites: RewritesData;
}
class Rewrites extends Component<RewritesProps> {
componentDidMount() {
this.props.getRewritesList();
this.props.getRewriteSettings();
}
handleDelete = (values: any) => {
@ -46,6 +50,21 @@ class Rewrites extends Component<RewritesProps> {
}
};
toggleRewrite = (currentRewrite: any) => {
const updatedRewrite = { ...currentRewrite, enabled: !currentRewrite.enabled };
this.props.updateRewrite({
target: currentRewrite,
update: updatedRewrite,
});
};
toggleRewriteSettings = () => {
const { enabled } = this.props.rewrites.settings;
this.props.updateRewriteSettings({ enabled: !enabled });
};
render() {
const {
t,
@ -64,12 +83,19 @@ class Rewrites extends Component<RewritesProps> {
processingUpdate,
modalType,
currentRewrite,
settings
} = rewrites;
const isEnabledSettings = settings.enabled;
return (
<Fragment>
<PageTitle title={t('dns_rewrites')} subtitle={t('rewrite_desc')} />
<div className={cn(isEnabledSettings ? 'text-success' : 'text-warning', 'mb-2')}>
{isEnabledSettings ? this.props.t('rewrites_enabled_table_header') : this.props.t('rewrites_disabled_table_header')}
</div>
<Card id="rewrites" bodyType="card-body box-body--settings">
<Fragment>
<Table
@ -80,15 +106,27 @@ class Rewrites extends Component<RewritesProps> {
processingUpdate={processingUpdate}
handleDelete={this.handleDelete}
toggleRewritesModal={toggleRewritesModal}
toggleRewrite={this.toggleRewrite}
settings={settings}
/>
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleRewritesModal({ type: MODAL_TYPE.ADD_REWRITE })}
disabled={processingAdd}>
<Trans>rewrite_add</Trans>
</button>
<div className="card-actions">
<button
type="button"
className="btn btn-success btn-standard mr-2"
onClick={() => toggleRewritesModal({ type: MODAL_TYPE.ADD_REWRITE })}
disabled={processingAdd}>
<Trans>rewrite_add</Trans>
</button>
<button
type="button"
className="btn btn-primary btn-standard"
onClick={() => this.toggleRewriteSettings()}
disabled={processingUpdate}>
<Trans>{isEnabledSettings ? 'disable_rewrites' : 'enable_rewrites'}</Trans>
</button>
</div>
<Modal
isModalOpen={isModalOpen}

View File

@ -370,6 +370,10 @@
text-overflow: ellipsis;
}
.logs__row--center {
align-items: center;
}
.logs__row--icons {
flex-wrap: wrap;
}

View File

@ -58,6 +58,13 @@
0.3s ease-in-out opacity;
}
.checkbox__label--l {
&:before {
width: 30px;
height: 30px;
}
}
.checkbox__label .checkbox__label-text {
line-height: 1.3;
}

View File

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { getRewritesList, addRewrite, deleteRewrite, updateRewrite, toggleRewritesModal } from '../actions/rewrites';
import { getRewritesList, addRewrite, deleteRewrite, updateRewrite, toggleRewritesModal, updateRewriteSettings, getRewriteSettings } from '../actions/rewrites';
import Rewrites from '../components/Filters/Rewrites';
import { RootState } from '../initialState';
@ -16,6 +16,8 @@ type DispatchProps = {
addRewrite: (...args: unknown[]) => unknown;
deleteRewrite: (...args: unknown[]) => unknown;
updateRewrite: (...args: unknown[]) => unknown;
updateRewriteSettings: (...args: unknown[]) => unknown;
getRewriteSettings: () => (dispatch: any) => void;
}
const mapDispatchToProps: DispatchProps = {
@ -24,6 +26,8 @@ const mapDispatchToProps: DispatchProps = {
deleteRewrite,
updateRewrite,
toggleRewritesModal,
updateRewriteSettings,
getRewriteSettings,
};
export default connect(mapStateToProps, mapDispatchToProps)(Rewrites);

View File

@ -172,11 +172,16 @@ export type RewritesData = {
currentRewrite?: {
answer: string;
domain: string;
enabled: boolean;
};
list: {
answer: string;
domain: string;
enabled: boolean;
}[];
settings: {
enabled: boolean;
};
};
export type NormalizedTopClients = {
@ -577,6 +582,7 @@ export const initialState: RootState = {
isModalOpen: false,
modalType: '',
list: [],
settings: { enabled: false },
},
services: {
processing: true,

View File

@ -66,6 +66,39 @@ const rewrites = handleActions(
};
return newState;
},
[actions.getRewriteSettingsRequest.toString()]: (state: any) => ({
...state,
processing: true,
}),
[actions.getRewriteSettingsFailure.toString()]: (state: any) => ({
...state,
processing: false,
}),
[actions.getRewriteSettingsSuccess.toString()]: (state: any, { payload }: any) => {
const newState = {
...state,
settings: payload,
processing: false,
};
return newState;
},
[actions.updateRewriteSettingsRequest.toString()]: (state: any) => ({
...state,
processingUpdate: true,
}),
[actions.updateRewriteSettingsFailure.toString()]: (state: any) => ({
...state,
processingUpdate: false,
}),
[actions.updateRewriteSettingsSuccess.toString()]: (state: any, { payload }: any) => ({
...state,
settings: {
...state.settings,
...payload,
},
processingUpdate: false,
}),
[actions.toggleRewritesModal.toString()]: (state: any, { payload }: any) => {
if (payload) {
@ -94,6 +127,7 @@ const rewrites = handleActions(
modalType: '',
currentRewrite: {},
list: [],
settings: { enabled: false },
},
);

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

@ -141,6 +141,7 @@ func (m *Migrator) upgradeConfigSchema(
27: m.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,43 @@
package configmigrate
import "context"
// 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(_ context.Context, 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

@ -1294,18 +1294,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

@ -49,7 +49,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

@ -730,9 +730,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)
}
@ -47,9 +61,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, l)
@ -177,8 +197,52 @@ func (d *DNSFilter) handleRewriteUpdate(w http.ResponseWriter, r *http.Request)
return
}
rwDel.Enabled = d.conf.Rewrites[index].Enabled
if updateJSON.Update.Enabled == aghalg.NBNull {
rwAdd.Enabled = rwDel.Enabled
} else {
rwAdd.Enabled = updateJSON.Update.Enabled == aghalg.NBTrue
}
d.conf.Rewrites = slices.Replace(d.conf.Rewrites, index, index+1, rwAdd)
l.DebugContext(ctx, "removed rewrite element", "domain", rwDel.Domain, "answer", rwDel.Answer)
l.DebugContext(ctx, "added rewrite element", "domain", rwAdd.Domain, "answer", rwAdd.Answer)
l.DebugContext(
ctx,
"removed rewrite element",
"domain", rwDel.Domain,
"answer", rwDel.Answer,
"enabled", rwDel.Enabled,
)
l.DebugContext(
ctx,
"added rewrite element",
"domain", rwAdd.Domain,
"answer", rwAdd.Answer,
"enabled", rwAdd.Enabled,
)
}
// 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,11 +4,13 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/testutil"
@ -18,8 +20,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 {
@ -33,16 +45,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)
@ -67,16 +96,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",
@ -91,11 +152,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,
@ -106,19 +170,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",
@ -127,16 +228,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,
@ -146,6 +246,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) {
@ -224,10 +329,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: testLogger,
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

@ -17,9 +17,11 @@ import (
// LegacyRewrite is a single legacy DNS rewrite record.
//
// Instances of *LegacyRewrite must never be nil.
// Instances of *LegacyRewrite must not be nil.
//
// NOTE: Keep fields in sync with [cloneRewrites].
type LegacyRewrite struct {
// Domain is the domain pattern for which this rewrite should work.
// Domain is the pattern to which this rewrite applies.
Domain string `yaml:"domain"`
// Answer is the IP address, canonical name, or one of the special
@ -32,6 +34,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 +167,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 +185,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 +202,7 @@ func findRewrites(
}
}
return rewrites, matched
return rewrites
}
// setRewriteResult sets the Reason or IPList of res if necessary. res must not
@ -221,10 +235,11 @@ func cloneRewrites(entries []*LegacyRewrite) (clone []*LegacyRewrite) {
clone = make([]*LegacyRewrite, len(entries))
for i, rw := range entries {
clone[i] = &LegacyRewrite{
Domain: rw.Domain,
Answer: rw.Answer,
IP: rw.IP,
Type: rw.Type,
Domain: rw.Domain,
Answer: rw.Answer,
IP: rw.IP,
Type: rw.Type,
Enabled: rw.Enabled,
}
}

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':

View File

@ -89,7 +89,7 @@ if [ "$(git diff --cached --name-only -- '*.sh' || :)" != '' ]; then
make VERBOSE="$verbose" sh-lint
fi
if [ "$(git diff --cached --name-only -- '*.md' '*.txt' '*.yaml' '*.yml' || :)" != '' ]; then
if [ "$(git diff --cached --name-only -- '*.md' '*.json' '*.txt' '*.yaml' '*.yml' || :)" != '' ]; then
make VERBOSE="$verbose" txt-lint
fi