Pull request: 1333-protection-pause vol.1

Merge in DNS/adguard-home from 1333-protection-pause-1 to master

Squashed commit of the following:

commit 5ff98385bc5ff66e214d80782eb4dc41e344aa38
Merge: 97f94a54 0bc3ef89
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Fri Mar 24 19:08:21 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1

commit 97f94a5498ac221f88f2f7dfef4b255f4945329e
Author: Arseny Lisin <a.lisin@adguard.com>
Date:   Fri Mar 24 13:03:20 2023 +0200

    Fix protection timer bugs

commit 1cc61af1996bd803f3fa638cb9e2388470072bf0
Merge: 5a144ea3 235ce458
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 23 22:27:47 2023 +0700

    Merge remote-tracking branch 'origin/1333-protection-pause-1' into 1333-protection-pause-1

commit 5a144ea3a48c3d0d5e57dd14232ab7a8e77a8c1e
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 23 22:25:08 2023 +0700

    dnsforward: imp code

commit 235ce458a62b3152f36e32580ed0226a56580ec6
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Mar 23 17:35:06 2023 +0300

    dnsforward: imp locks

commit 0ea3a0a176b810a2b3f0b307aa406fe1670c9219
Merge: 52f66810 df61741f
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 23 19:30:41 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1

    # Conflicts:
    #	CHANGELOG.md
    #	openapi/CHANGELOG.md

commit 52f668109673286a50909c042e6352cd803e8eed
Merge: 9a7eb7b3 306c1983
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Mar 23 11:23:50 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1

    # Conflicts:
    #	CHANGELOG.md
    #	internal/dnsforward/http.go

commit 9a7eb7b3ab2b5f6ad321aa3245d33839c3aa6fbd
Author: Arseny Lisin <a.lisin@adguard.com>
Date:   Wed Mar 22 06:56:55 2023 +0200

    Review fix

commit 5612d51252ba91842bd6811baec1c91136bb3bf2
Merge: c0a918a5 c3edab43
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Mar 21 22:00:39 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1

    # Conflicts:
    #	client/src/__locales/en.json

commit c0a918a518ad9b37041aed159d215516258bc987
Author: Arseny Lisin <a.lisin@adguard.com>
Date:   Tue Mar 21 12:13:18 2023 +0200

    Review fix

commit 34faa61cc1e6210a612e7a2f4895a1504df37680
Author: Arseny Lisin <a.lisin@adguard.com>
Date:   Tue Mar 21 10:43:37 2023 +0200

    Fix props to new api

commit 158e582373863495f0e0ca177d7b365cc66ad671
Author: Arseny Lisin <a.lisin@adguard.com>
Date:   Mon Mar 20 18:44:34 2023 +0200

    Review fix

commit 9e8b8c3778b8e1dfad0d39e44f70886dfd3aeb9a
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 22:31:28 2023 +0700

    all: docs

commit 761a203f53b535ca235cfe62f289bd0e02b90be2
Merge: d0b07231 48431f8b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 20 22:26:13 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1

commit d0b07231b6f29b534930f1fcfc82b4934c295ff8
Merge: ea448760 a2053526
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Mar 13 13:00:52 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1

    # Conflicts:
    #	CHANGELOG.md
    #	client/src/components/App/index.css
    #	internal/dnsforward/config.go

commit ea4487608a9c81d25f155ff63fee7c9dcf21f448
Merge: dfd0f33f a556ce8f
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Feb 21 11:54:27 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1

    # Conflicts:
    #	CHANGELOG.md

commit dfd0f33fb474d497cbc9237ee466276728eea397
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Tue Feb 21 11:51:40 2023 +0700

    all: docs

commit d36df96fba8c6d923faef85c198b6bd0743b7ee8
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Feb 20 12:41:49 2023 +0700

    all: safesearch

commit 60f2ceec563221337f34bb60baa96aa2b2429c40
Merge: 7c514427 6f6ced33
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Mon Feb 20 12:30:42 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1

    # Conflicts:
    #	CHANGELOG.md

commit 7c514427e77c5b09d8e148c78220a71046e68cd1
Merge: 0fa4ff99 4d295a38
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu Feb 16 11:55:21 2023 +0700

    Merge remote-tracking branch 'origin/master' into 1333-protection-pause-1

    # Conflicts:
    #	CHANGELOG.md

... and 26 more commits
This commit is contained in:
Dimitry Kolyshev 2023-03-24 15:11:47 +03:00
parent 0bc3ef89ea
commit 9c48e96939
23 changed files with 528 additions and 46 deletions

View file

@ -399,6 +399,7 @@ Response:
"protection_enabled":true,
"running":true,
"dhcp_available":true,
"protection_disabled_duration":0
"version":"undefined"
}

View file

@ -25,6 +25,11 @@ NOTE: Add new changes BELOW THIS COMMENT.
### Added
- The new HTTP API `POST /control/protection`, that updates protection state
and adds an optional pause duration ([#1333]). The format of request body
is described in `openapi/openapi.yaml`. The duration of this pause could
also be set with the config field `protection_disabled_until` in `dns`
section of the YAML configuration file.
- Two new HTTP APIs, `PUT /control/stats/config/update` and `GET
control/stats/config`, which can be used to set and receive the query log
configuration. See openapi/openapi.yaml for the full description.
@ -122,6 +127,7 @@ In this release, the schema version has changed from 17 to 20.
([#5584]).
[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163
[#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333
[#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472
[#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567
[#5584]: https://github.com/AdguardTeam/AdGuardHome/issues/5584

View file

@ -650,5 +650,20 @@
"confirm_dns_cache_clear": "Are you sure you want to clear DNS cache?",
"cache_cleared": "DNS cache successfully cleared",
"clear_cache": "Clear cache",
"make_static": "Make static"
"make_static": "Make static",
"disable_for_seconds": "For {{count}} second",
"disable_for_seconds_plural": "For {{count}} seconds",
"disable_for_minutes": "For {{count}} minute",
"disable_for_minutes_plural": "For {{count}} minutes",
"disable_for_hours": "For {{count}} hour",
"disable_for_hours_plural": "For {{count}} hours",
"disable_until_tomorrow": "Until tomorrow",
"disable_notify_for_seconds": "Disable protection for {{count}} second",
"disable_notify_for_seconds_plural": "Disable protection for {{count}} seconds",
"disable_notify_for_minutes": "Disable protection for {{count}} minute",
"disable_notify_for_minutes_plural": "Disable protection for {{count}} minutes",
"disable_notify_for_hours": "Disable protection for {{count}} hour",
"disable_notify_for_hours_plural": "Disable protection for {{count}} hours",
"disable_notify_until_tomorrow": "Disable protection until tomorrow",
"enable_protection_timer": "Protection will be enabled in {{time}}"
}

View file

@ -6,7 +6,14 @@ import endsWith from 'lodash/endsWith';
import escapeRegExp from 'lodash/escapeRegExp';
import React from 'react';
import { compose } from 'redux';
import { splitByNewLine, sortClients, filterOutComments } from '../helpers/helpers';
import {
splitByNewLine,
sortClients,
filterOutComments,
msToSeconds,
msToMinutes,
msToHours,
} from '../helpers/helpers';
import {
BLOCK_ACTIONS,
CHECK_TIMEOUT,
@ -14,6 +21,7 @@ import {
SETTINGS_NAMES,
FORM_NAME,
MANUAL_UPDATE_LINK,
DISABLE_PROTECTION_TIMINGS,
} from '../helpers/constants';
import { areEqualVersions } from '../helpers/version';
import { getTlsStatus } from './encryption';
@ -108,19 +116,54 @@ export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST')
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS');
export const toggleProtection = (status) => async (dispatch) => {
const getDisabledMessage = (time) => {
switch (time) {
case DISABLE_PROTECTION_TIMINGS.HALF_MINUTE:
return i18next.t(
'disable_notify_for_seconds',
{ count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) },
);
case DISABLE_PROTECTION_TIMINGS.MINUTE:
return i18next.t(
'disable_notify_for_minutes',
{ count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) },
);
case DISABLE_PROTECTION_TIMINGS.TEN_MINUTES:
return i18next.t(
'disable_notify_for_minutes',
{ count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) },
);
case DISABLE_PROTECTION_TIMINGS.HOUR:
return i18next.t(
'disable_notify_for_hours',
{ count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) },
);
case DISABLE_PROTECTION_TIMINGS.TOMORROW:
return i18next.t('disable_notify_until_tomorrow');
default:
return 'disabled_protection';
}
};
export const toggleProtection = (status, time = null) => async (dispatch) => {
dispatch(toggleProtectionRequest());
try {
const successMessage = status ? 'disabled_protection' : 'enabled_protection';
await apiClient.setDnsConfig({ protection_enabled: !status });
const successMessage = status ? getDisabledMessage(time) : 'enabled_protection';
await apiClient.setProtection({ enabled: !status, duration: time });
dispatch(addSuccessToast(successMessage));
dispatch(toggleProtectionSuccess());
dispatch(toggleProtectionSuccess({ disabledDuration: time }));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(toggleProtectionFailure());
}
};
export const setDisableDurationTime = createAction('SET_DISABLED_DURATION_TIME');
export const setProtectionTimerTime = (updatedTime) => async (dispatch) => {
dispatch(setDisableDurationTime({ timeToEnableProtection: updatedTime }));
};
export const getVersionRequest = createAction('GET_VERSION_REQUEST');
export const getVersionFailure = createAction('GET_VERSION_FAILURE');
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
@ -273,6 +316,9 @@ export const getDnsStatus = () => async (dispatch) => {
const handleRequestSuccess = (response) => {
const dnsStatus = response.data;
if (dnsStatus.protection_disabled_duration === 0) {
dnsStatus.protection_disabled_duration = null;
}
const { running } = dnsStatus;
const runningStatus = dnsStatus && running;
if (runningStatus === true) {

View file

@ -627,6 +627,15 @@ class Api {
return this.makeRequest(path, method, config);
}
SET_PROTECTION = { path: 'protection', method: 'POST' };
setProtection(data) {
const { enabled, duration } = data;
const { path, method } = this.SET_PROTECTION;
return this.makeRequest(path, method, { data: { enabled, duration } });
}
// Cache
CLEAR_CACHE = { path: 'cache_clear', method: 'POST' };

View file

@ -43,6 +43,7 @@ import DnsRewrites from '../../containers/DnsRewrites';
import CustomRules from '../../containers/CustomRules';
import Services from '../Filters/Services';
import Logs from '../Logs';
import ProtectionTimer from '../ProtectionTimer';
const ROUTES = [
{
@ -191,6 +192,7 @@ const App = () => {
{!processingEncryption && <EncryptionTopline />}
<LoadingBar className="loading-bar" updateTime={1000} />
<Header />
<ProtectionTimer />
<div className="container container--wrap pb-5">
{processing && <Loading />}
{!isCoreRunning && <div className="row row-cards">

View file

@ -1,3 +1,9 @@
.dashboard-protection-button.btn-gray {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right-color: #a4a4a4;
}
.stats__table .popover__body {
left: -10px;
min-width: 270px;
@ -34,20 +40,11 @@
align-items: center;
}
.dashboard-title__button {
margin: 0 0.5rem;
}
@media (max-width: 767.98px) {
.page-title--dashboard {
flex-direction: column;
align-items: flex-start;
}
.dashboard-title__button {
margin: 0.5rem 0;
display: block;
}
}
.counters__row {

View file

@ -9,18 +9,20 @@ import Counters from './Counters';
import Clients from './Clients';
import QueriedDomains from './QueriedDomains';
import BlockedDomains from './BlockedDomains';
import { SETTINGS_URLS } from '../../helpers/constants';
import { DISABLE_PROTECTION_TIMINGS, ONE_SECOND_IN_MS, SETTINGS_URLS } from '../../helpers/constants';
import { msToSeconds, msToMinutes, msToHours } from '../../helpers/helpers';
import PageTitle from '../ui/PageTitle';
import Loading from '../ui/Loading';
import './Dashboard.css';
import Dropdown from '../ui/Dropdown';
const Dashboard = ({
getAccessList,
getStats,
getStatsConfig,
dashboard,
dashboard: { protectionEnabled, processingProtection },
dashboard: { protectionEnabled, processingProtection, protectionDisabledDuration },
toggleProtection,
stats,
access,
@ -36,7 +38,6 @@ const Dashboard = ({
useEffect(() => {
getAllStats();
}, []);
const getSubtitle = () => {
if (stats.interval === 0) {
return t('stats_disabled_short');
@ -47,9 +48,7 @@ const Dashboard = ({
: t('for_last_days', { count: stats.interval });
};
const buttonText = protectionEnabled ? 'disable_protection' : 'enable_protection';
const buttonClass = classNames('btn btn-sm dashboard-title__button', {
const buttonClass = classNames('btn btn-sm dashboard-protection-button', {
'btn-gray': protectionEnabled,
'btn-success': !protectionEnabled,
});
@ -71,16 +70,87 @@ const Dashboard = ({
const subtitle = getSubtitle();
const DISABLE_PROTECTION_ITEMS = [
{
text: t('disable_for_seconds', { count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) }),
disableTime: DISABLE_PROTECTION_TIMINGS.HALF_MINUTE,
},
{
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) }),
disableTime: DISABLE_PROTECTION_TIMINGS.MINUTE,
},
{
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) }),
disableTime: DISABLE_PROTECTION_TIMINGS.TEN_MINUTES,
},
{
text: t('disable_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }),
disableTime: DISABLE_PROTECTION_TIMINGS.HOUR,
},
{
text: t('disable_until_tomorrow'),
disableTime: DISABLE_PROTECTION_TIMINGS.TOMORROW,
},
];
const getDisableProtectionItems = () => (
Object.values(DISABLE_PROTECTION_ITEMS)
.map((item, index) => (
<div
key={`disable_timings_${index}`}
className="dropdown-item"
onClick={() => {
toggleProtection(protectionEnabled, item.disableTime - ONE_SECOND_IN_MS);
}}
>
{item.text}
</div>
))
);
const getRemaningTimeText = (milliseconds) => {
if (!milliseconds) {
return '';
}
const date = new Date(milliseconds);
const hh = date.getUTCHours();
const mm = `0${date.getUTCMinutes()}`.slice(-2);
const ss = `0${date.getUTCSeconds()}`.slice(-2);
const formattedHH = `0${hh}`.slice(-2);
return hh ? `${formattedHH}:${mm}:${ss}` : `${mm}:${ss}`;
};
const getProtectionBtnText = (status) => (status ? t('disable_protection') : t('enable_protection'));
return <>
<PageTitle title={t('dashboard')} containerClass="page-title--dashboard">
<button
type="button"
className={buttonClass}
onClick={() => toggleProtection(protectionEnabled)}
disabled={processingProtection}
>
<Trans>{buttonText}</Trans>
</button>
<div className="page-title__protection">
<button
type="button"
className={buttonClass}
onClick={() => {
toggleProtection(protectionEnabled);
}}
disabled={processingProtection}
>
{protectionDisabledDuration
? `${t('enable_protection_timer')} ${getRemaningTimeText(protectionDisabledDuration)}`
: getProtectionBtnText(protectionEnabled)
}
</button>
{protectionEnabled && <Dropdown
label=""
baseClassName="dropdown-protection"
icon="arrow-down"
controlClassName="dropdown-protection__toggle"
menuClassName="dropdown-menu dropdown-menu-arrow dropdown-menu--protection"
>
{getDisableProtectionItems()}
</Dropdown>}
</div>
<button
type="button"
className="btn btn-outline-primary btn-sm"

View file

@ -0,0 +1,52 @@
import { useEffect } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { ONE_SECOND_IN_MS } from '../../helpers/constants';
import { setProtectionTimerTime, toggleProtectionSuccess } from '../../actions';
let interval = null;
const ProtectionTimer = ({
protectionDisabledDuration,
toggleProtectionSuccess,
setProtectionTimerTime,
}) => {
useEffect(() => {
if (protectionDisabledDuration !== null && protectionDisabledDuration < ONE_SECOND_IN_MS) {
toggleProtectionSuccess({ disabledDuration: null });
}
if (protectionDisabledDuration) {
interval = setInterval(() => {
setProtectionTimerTime(protectionDisabledDuration - ONE_SECOND_IN_MS);
}, ONE_SECOND_IN_MS);
}
return () => {
clearInterval(interval);
};
}, [protectionDisabledDuration]);
return null;
};
ProtectionTimer.propTypes = {
setProtectionTimerTime: PropTypes.func.isRequired,
};
const mapStateToProps = (state) => {
const { dashboard } = state;
const { protectionEnabled, protectionDisabledDuration } = dashboard;
return { protectionEnabled, protectionDisabledDuration };
};
const mapDispatchToProps = {
toggleProtectionSuccess,
setProtectionTimerTime,
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ProtectionTimer);

View file

@ -1,3 +1,7 @@
.dropdown-item {
cursor: pointer;
}
.dropdown-item.active,
.dropdown-item:active {
background-color: var(--btn-success-bgcolor);
@ -6,3 +10,55 @@
.dropdown-menu {
cursor: default;
}
.dropdown-menu.dropdown-menu--protection {
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
}
.dropdown-menu.dropdown-menu-arrow.dropdown-menu--protection::before,
.dropdown-menu.dropdown-menu-arrow.dropdown-menu--protection::after {
left: 50%;
transform: translateX(-50%);
}
.dropdown-protection {
align-self: stretch;
width: 26px;
display: flex;
position: relative;
border: 1px solid #868e96;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
border-left: none;
cursor: pointer;
}
.dropdown-protection__toggle {
width: 100%;
display: block;
position: relative;
background-color: #868e96;
transition: background-color 0.15s ease-in-out;
}
.dropdown-protection__toggle:hover {
background-color: #727b84;
}
.dropdown-protection__toggle .nav-icon {
width: 100%;
height: 100%;
display: block;
position: absolute;
top: 0;
left: 0;
color: var(--white);
transition: 0.15s ease-in-out transform;
transform-origin: center;
}
.dropdown-protection.show .nav-icon {
transform: rotate(180deg);
}

View file

@ -181,6 +181,12 @@ const Icons = () => (
</svg>
</symbol>
<symbol id="arrow-down" viewBox="0 0 24 24" fill="currentColor">
<svg xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" d="M6.2 8.2a.64.64 0 0 1 .94 0L12 13.32l4.86-5.1a.64.64 0 0 1 .94 0c.27.27.27.71 0 .98l-5.33 5.6a.64.64 0 0 1-.94 0L6.2 9.2a.72.72 0 0 1 0-.98Z" clipRule="evenodd"/>
</svg>
</symbol>
<symbol id="arrow-right" viewBox="0 0 24 24" stroke="currentColor"
strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5">
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">

View file

@ -10216,6 +10216,18 @@ body.fixed-header .page {
line-height: 2.5rem;
}
.page-title__protection {
display: flex;
align-items: stretch;
margin: 0 0.5rem;
}
@media (max-width: 767.98px) {
.page-title__protection {
margin: 0.5rem 0;
}
}
.page-title-icon {
color: #9aa0ac;
font-size: 1.25rem;

View file

@ -498,6 +498,8 @@ export const TOAST_TYPES = {
};
export const SUCCESS_TOAST_TIMEOUT = 5000;
export const ONE_SECOND_IN_MS = 1000;
export const FAILURE_TOAST_TIMEOUT = 30000;
export const TOAST_TIMEOUTS = {
@ -526,3 +528,12 @@ export const MOBILE_CONFIG_LINKS = {
DOT: 'apple/dot.mobileconfig',
DOH: 'apple/doh.mobileconfig',
};
// Timings for disable protection in milliseconds
export const DISABLE_PROTECTION_TIMINGS = {
HALF_MINUTE: 30 * 1000,
MINUTE: 60 * 1000,
TEN_MINUTES: 10 * 60 * 1000,
HOUR: 60 * 60 * 1000,
TOMORROW: 24 * 60 * 60 * 1000,
};

View file

@ -388,6 +388,12 @@ export const toggleAllServices = (services, change, isSelected) => {
services.forEach((service) => change(`blocked_services.${service.id}`, isSelected));
};
export const msToSeconds = (milliseconds) => Math.floor(milliseconds / 1000);
export const msToMinutes = (milliseconds) => Math.floor(milliseconds / 1000 / 60);
export const msToHours = (milliseconds) => Math.floor(milliseconds / 1000 / 60 / 60);
export const secondsToMilliseconds = (seconds) => {
if (seconds) {
return seconds * 1000;

View file

@ -25,6 +25,7 @@ const dashboard = handleActions(
dns_port: dnsPort,
dns_addresses: dnsAddresses,
protection_enabled: protectionEnabled,
protection_disabled_duration: protectionDisabledDuration,
http_port: httpPort,
language,
} = payload;
@ -36,9 +37,11 @@ const dashboard = handleActions(
dnsPort,
dnsAddresses,
protectionEnabled,
protectionDisabledDuration,
language,
httpPort,
};
return newState;
},
@ -103,15 +106,22 @@ const dashboard = handleActions(
...state,
processingProtection: false,
}),
[actions.toggleProtectionSuccess]: (state) => {
[actions.toggleProtectionSuccess]: (state, { payload }) => {
const newState = {
...state,
protectionEnabled: !state.protectionEnabled,
processingProtection: false,
protectionDisabledDuration: payload.disabledDuration,
};
return newState;
},
[actions.setDisableDurationTime]: (state, { payload }) => ({
...state,
protectionDisabledDuration: payload.timeToEnableProtection,
}),
[actions.getClientsRequest]: (state) => ({
...state,
processingClients: true,
@ -156,6 +166,8 @@ const dashboard = handleActions(
processingUpdate: false,
processingProfile: true,
protectionEnabled: false,
protectionDisabledDuration: null,
protectionCountdownActive: false,
processingProtection: false,
httpPort: STANDARD_WEB_PORT,
dnsPort: STANDARD_DNS_PORT,

View file

@ -81,6 +81,10 @@ type FilteringConfig struct {
// 0, then default value is used (3600).
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"`
// ProtectionDisabledUntil is the timestamp until when the protection is
// disabled.
ProtectionDisabledUntil *time.Time `yaml:"protection_disabled_until"`
// ParentalBlockHost is the IP (or domain name) which is used to respond to
// DNS requests blocked by parental control.
ParentalBlockHost string `yaml:"parental_block_host"`
@ -635,3 +639,33 @@ func (s *Server) onGetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, er
}
return &s.conf.cert, nil
}
// UpdatedProtectionStatus updates protection state, if the protection was
// disabled temporarily. Returns the updated state of protection.
func (s *Server) UpdatedProtectionStatus() (enabled bool) {
changed := false
defer func() {
if changed {
log.Info("dns: protection is restarted after pause")
s.conf.ConfigModified()
}
}()
s.serverLock.Lock()
defer s.serverLock.Unlock()
disabledUntil := s.conf.ProtectionDisabledUntil
if disabledUntil == nil {
return s.conf.ProtectionEnabled
}
if time.Now().Before(*disabledUntil) {
return false
}
s.conf.ProtectionEnabled = true
s.conf.ProtectionDisabledUntil = nil
changed = true
return true
}

View file

@ -191,7 +191,7 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
dctx.clientID = string(s.clientIDCache.Get(key[:]))
// Get the client-specific filtering settings.
dctx.protectionEnabled = s.conf.ProtectionEnabled
dctx.protectionEnabled = s.UpdatedProtectionStatus()
dctx.setts = s.getClientRequestFilteringSettings(dctx)
return resultCodeSuccess

View file

@ -88,6 +88,9 @@ type jsonDNSConfig struct {
// BlockingIPv6 is custom IPv6 address for blocked AAAA requests.
BlockingIPv6 net.IP `json:"blocking_ipv6"`
// DisabledUntil is a timestamp until when the protection is disabled.
DisabledUntil *time.Time `json:"protection_disabled_until"`
// EDNSCSCustomIP is custom IP for EDNS Client Subnet.
EDNSCSCustomIP netip.Addr `json:"edns_cs_custom_ip"`
@ -98,13 +101,14 @@ type jsonDNSConfig struct {
}
func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
protectionEnabled := s.UpdatedProtectionStatus()
s.serverLock.RLock()
defer s.serverLock.RUnlock()
upstreams := stringutil.CloneSliceOrEmpty(s.conf.UpstreamDNS)
upstreamFile := s.conf.UpstreamDNSFileName
bootstraps := stringutil.CloneSliceOrEmpty(s.conf.BootstrapDNS)
protectionEnabled := s.conf.ProtectionEnabled
blockingMode := s.conf.BlockingMode
blockingIPv4 := s.conf.BlockingIPv4
blockingIPv6 := s.conf.BlockingIPv6
@ -123,6 +127,13 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
resolveClients := s.conf.ResolveClients
usePrivateRDNS := s.conf.UsePrivateRDNS
localPTRUpstreams := stringutil.CloneSliceOrEmpty(s.conf.LocalPTRResolvers)
var disabledUntil *time.Time
if s.conf.ProtectionDisabledUntil != nil {
t := *s.conf.ProtectionDisabledUntil
disabledUntil = &t
}
var upstreamMode string
if s.conf.FastestAddr {
upstreamMode = "fastest_addr"
@ -158,6 +169,7 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
UsePrivateRDNS: &usePrivateRDNS,
LocalPTRUpstreams: &localPTRUpstreams,
DefaultLocalPTRUpstreams: defLocalPTRUps,
DisabledUntil: disabledUntil,
}
}
@ -741,6 +753,52 @@ func (s *Server) handleCacheClear(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, "OK")
}
// protectionJSON is an object for /control/protection endpoint.
type protectionJSON struct {
Enabled bool `json:"enabled"`
Duration uint `json:"duration"`
}
// handleSetProtection is a handler for the POST /control/protection HTTP API.
func (s *Server) handleSetProtection(w http.ResponseWriter, r *http.Request) {
protectionReq := &protectionJSON{}
err := json.NewDecoder(r.Body).Decode(protectionReq)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err)
return
}
var disabledUntil *time.Time
if protectionReq.Duration > 0 {
if protectionReq.Enabled {
aghhttp.Error(
r,
w,
http.StatusBadRequest,
"Setting a duration is only allowed with protection disabling",
)
return
}
calcTime := time.Now().Add(time.Duration(protectionReq.Duration) * time.Millisecond)
disabledUntil = &calcTime
}
func() {
s.serverLock.Lock()
defer s.serverLock.Unlock()
s.conf.ProtectionEnabled = protectionReq.Enabled
s.conf.ProtectionDisabledUntil = disabledUntil
}()
s.conf.ConfigModified()
aghhttp.OK(w)
}
// handleDoH is the DNS-over-HTTPs handler.
//
// Control flow:
@ -775,6 +833,7 @@ func (s *Server) registerHandlers() {
s.conf.HTTPRegister(http.MethodGet, "/control/dns_info", s.handleGetConfig)
s.conf.HTTPRegister(http.MethodPost, "/control/dns_config", s.handleSetConfig)
s.conf.HTTPRegister(http.MethodPost, "/control/test_upstream_dns", s.handleTestUpstreamDNS)
s.conf.HTTPRegister(http.MethodPost, "/control/protection", s.handleSetProtection)
s.conf.HTTPRegister(http.MethodGet, "/control/access/list", s.handleAccessList)
s.conf.HTTPRegister(http.MethodPost, "/control/access/set", s.handleAccessSet)

View file

@ -12,6 +12,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -43,6 +44,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -74,6 +76,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",

View file

@ -19,6 +19,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -54,6 +55,7 @@
"9.9.9.10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -90,6 +92,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "refused",
"blocking_ipv4": "",
@ -126,6 +129,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -162,6 +166,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 6,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -198,6 +203,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -236,6 +242,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -274,6 +281,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -310,6 +318,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -346,6 +355,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -382,6 +392,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -418,6 +429,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -456,6 +468,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -494,6 +507,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -531,6 +545,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -567,6 +582,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -605,6 +621,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -646,6 +663,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
@ -682,6 +700,7 @@
"2620:fe::fe:10"
],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",

View file

@ -103,6 +103,8 @@ type statusResponse struct {
DNSPort int `json:"dns_port"`
HTTPPort int `json:"http_port"`
IsProtectionEnabled bool `json:"protection_enabled"`
// ProtectionDisabledDuration is a pause duration in milliseconds.
ProtectionDisabledDuration int64 `json:"protection_disabled_duration"`
// TODO(e.burkov): Inspect if front-end doesn't requires this field as
// openapi.yaml declares.
IsDHCPAvailable bool `json:"dhcp_available"`
@ -119,28 +121,36 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
return
}
isProtectionEnabled := false
var c *dnsforward.FilteringConfig
if Context.dnsServer != nil {
c = &dnsforward.FilteringConfig{}
Context.dnsServer.WriteDiskConfig(c)
isProtectionEnabled = Context.dnsServer.UpdatedProtectionStatus()
}
var resp statusResponse
func() {
config.RLock()
defer config.RUnlock()
var pauseDuration int64
if until := config.DNS.ProtectionDisabledUntil; until != nil {
pauseDuration = time.Until(*until).Milliseconds()
}
resp = statusResponse{
Version: version.Version(),
DNSAddrs: dnsAddrs,
DNSPort: config.DNS.Port,
HTTPPort: config.BindPort,
Language: config.Language,
IsRunning: isRunning(),
Version: version.Version(),
DNSAddrs: dnsAddrs,
DNSPort: config.DNS.Port,
HTTPPort: config.BindPort,
Language: config.Language,
IsRunning: isRunning(),
ProtectionDisabledDuration: pauseDuration,
IsProtectionEnabled: isProtectionEnabled,
}
}()
var c *dnsforward.FilteringConfig
if Context.dnsServer != nil {
c = &dnsforward.FilteringConfig{}
Context.dnsServer.WriteDiskConfig(c)
resp.IsProtectionEnabled = c.ProtectionEnabled
}
// IsDHCPAvailable field is now false by default for Windows.
if runtime.GOOS != "windows" {
resp.IsDHCPAvailable = Context.dhcpServer != nil

View file

@ -83,7 +83,29 @@ accept and return a JSON object with the following format:
## v0.107.27: API changes
### New `"protection_disabled_until"` field in `GET /control/dns_info` response
* The new field `"protection_disabled_until"` in `GET /control/dns_info` is the
timestamp until when the protection is disabled.
### New `"protection_disabled_duration"` field in `GET /control/status` response
* The new field `"protection_disabled_duration"` is the duration of protection
pause in milliseconds.
### `POST /control/protection`
* The new `POST /control/protection` HTTP API allows to pause protection for
specified duration in milliseconds.
This API accepts a JSON object with the following format:
```json
{
"enabled": false,
"duration": 10000
}
```
### Deprecated HTTP APIs

View file

@ -94,6 +94,20 @@
'responses':
'200':
'description': 'OK'
'/protection':
'post':
'tags':
- 'global'
'operationId': 'setProtection'
'summary': 'Set protection state and duration'
'requestBody':
'content':
'application/json':
'schema':
'$ref': '#/components/schemas/SetProtectionRequest'
'responses':
'200':
'description': 'OK'
'/cache_clear':
'post':
'tags':
@ -1306,6 +1320,7 @@
- 'dns_port'
- 'http_port'
- 'protection_enabled'
- 'protection_disabled_until'
- 'running'
- 'version'
- 'language'
@ -1329,6 +1344,9 @@
'maximum': 65535
'protection_enabled':
'type': 'boolean'
'protection_disabled_duration':
'type': 'integer'
'format': 'int64'
'dhcp_available':
'type': 'boolean'
'running':
@ -1381,6 +1399,10 @@
'type': 'string'
'blocking_ipv6':
'type': 'string'
'protection_disabled_until':
'type': 'string'
'description': 'Protection is pause until this time. Nullable.'
'example': '2018-11-26T00:02:41+03:00'
'edns_cs_enabled':
'type': 'boolean'
'edns_cs_use_custom':
@ -2384,6 +2406,18 @@
'type': 'integer'
'format': 'uint16'
'example': 80
'SetProtectionRequest':
'type': 'object'
'description': 'Protection state configuration'
'properties':
'enabled':
'type': 'boolean'
'duration':
'type': 'integer'
'format': 'uint64'
'description': 'Duration of a pause, in milliseconds. Enabled should be false.'
'required':
- 'enabled'
'ProfileInfo':
'type': 'object'
'description': 'Information about the current user'