Compare commits

...

14 Commits

Author SHA1 Message Date
Artem Krisanov
521aedc5bc Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home into AG-21485 2023-04-18 14:27:28 +03:00
Ainar Garipov
1842f7d888 Pull request 1831: home: imp depr option doc
Merge in DNS/adguard-home from imp-option-doc to master

Squashed commit of the following:

commit 267410fcc2c9e757c7d8fb7d9059a709932dda9d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Apr 17 14:30:12 2023 +0300

    home: imp depr option doc
2023-04-18 14:18:28 +03:00
Artem Krisanov
40ff26ea21 Login theme bugfix. 2023-04-18 14:11:28 +03:00
Artem Krisanov
4afd39b22f AG-21136 - Added local storage theme key.
Updates#5444

Squashed commit of the following:

commit 7b0b108f41ebb5e98861cdd20029c12d3a3fc5f4
Merge: 38df28db0 e43ba1788
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Mon Apr 17 15:58:15 2023 +0300

    Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home into 5444-white-screen

commit 38df28db0739e47d3fb605f648fa493b58709d77
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Fri Apr 14 17:54:00 2023 +0300

    Deleted useless tag.

commit 78ef9d911ccf74b69a9ae5626ea8f31cb9338ae0
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Fri Apr 14 17:53:17 2023 +0300

    Set initial body data-theme.

commit f470b3aa79500edd0726b7ed37e6e5940b6ce3ff
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Thu Apr 13 16:42:25 2023 +0300

    Revert login changes.

commit 7c4734ed02a670a59d0b9ff04e06bc1d396223a8
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Thu Apr 13 15:51:24 2023 +0300

    Added setting theme into html.Changed overlay background color to variable.

commit a3743be0e69489489755db8ff55541b9a6281300
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Wed Apr 12 17:58:47 2023 +0300

    Added local storage theme key.
2023-04-17 16:07:20 +03:00
Artem Krisanov
e43ba17884 AG-21212 - Custom logs and stats retention
Updates#3404

Squashed commit of the following:

commit b68a1d08b0676ebb7abbb13c9274c8d509cd6eed
Merge: 81265147 6d402dc8
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Mon Apr 17 15:48:33 2023 +0300

    Merge master

commit 81265147b5613be11a6621a416f9588c0e1c0ef5
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Thu Apr 13 10:54:39 2023 +0300

    Changed query log 'retention' --> 'rotation'.

commit 02c5dc0b54bca9ec293ee8629d769489bc5dc533
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Wed Apr 12 13:22:22 2023 +0300

    Custom inputs for query log and stats configs.

commit 21dbfbd8aac868baeea0f8b25d14786aecf09a0d
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Tue Apr 11 18:12:40 2023 +0300

    Temporary changes.
2023-04-17 15:57:57 +03:00
Eugene Burkov
6d402dc86c Pull request 1830: 5712-rollback-dhcp
Merge in DNS/adguard-home from 5712-rollback-dhcp to master

Updates #5712.

Squashed commit of the following:

commit 3d53a6385ad08dfad0b7ac28bb057cf25608554d
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Apr 14 16:30:18 2023 +0300

    dhcpd: imp import

commit 86bd55b0225b5d9067bd0bf9e6def1e52dd27124
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Apr 14 16:26:41 2023 +0300

    all: return todo

commit 629c548989a464a9cf461fffc0815b99a00c4851
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Apr 14 16:24:10 2023 +0300

    all: log changes

commit e4c369e55cbcc7c73d73d8df333996862e1e146a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Apr 14 16:03:03 2023 +0300

    dhcpd: revert raw for darwin
2023-04-14 16:58:07 +03:00
Stanislav Chzhen
18acdf9b09 Pull request 1809: 4299-querylog-stats-clients-api
Merge in DNS/adguard-home from 4299-querylog-stats-clients-api to master

Squashed commit of the following:

commit 066100a7869d7572c4ae65b3c7b1487ac50baf15
Merge: 95bc00c0 5da77514
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 14 13:57:30 2023 +0300

    Merge branch 'master' into 4299-querylog-stats-clients-api

commit 95bc00c0b3d05b262ee0b90be9757e61cac0778c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 13 11:48:39 2023 +0300

    all: fix typo

commit 4b868da48f0c976d204346e40ba948803be6397f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 13 11:42:52 2023 +0300

    all: fix text label

commit 7a3ba5c7f688bd53cf761b5e8e614fbe251bd006
Merge: 315256e3 6c8d89a4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 13 11:34:59 2023 +0300

    Merge branch 'master' into 4299-querylog-stats-clients-api

commit 315256e3f3861b5116962f7c47384b7c72e41813
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 11 19:07:18 2023 +0300

    all: ignore search, unit

commit 28c6ffec9558e7c38d7bd12055eabddb8f5675c2
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Tue Apr 11 15:08:35 2023 +0300

    Added 'Protection' and 'Query Log and statistics' sections to client settings. Added checkboxes to ignore client in (query log/statistics)

commit 2657bd2b820d8b2b3d71d23e4545c867b9ae6cdf
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 10 17:28:59 2023 +0300

    all: add todo

commit e151fcbc0c36d8e6a5c091fbf374bf0e35804699
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 10 15:15:46 2023 +0300

    openapi: imp docs

commit 31875cbbd1bd09a73baa3636d0cc242b5ac35059
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 10 13:02:31 2023 +0300

    all: add querylog stats client ignore api
2023-04-14 15:25:04 +03:00
Ainar Garipov
5da7751463 Pull request 1829: 5725-querylog-orig-ans
Closes #5725.

Squashed commit of the following:

commit a9e5fc47fc0a752f427e006ab1c59e260239ee5a
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 13 20:25:12 2023 +0300

    querylog: fix orig ans assignment
2023-04-13 20:51:57 +03:00
Ainar Garipov
7631ca4ab3 Pull request 1828: AG-21377-default-safe-search
Merge in DNS/adguard-home from AG-21377-default-safe-search to master

Squashed commit of the following:

commit 35c66b97c787d02fe6f2ffb6902dcd9b6f9b9569
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 13 20:05:13 2023 +0300

    home: fix default safe search svcs
2023-04-13 20:17:59 +03:00
Ainar Garipov
c6d4f2317e Pull request 1827: upd-deps
Merge in DNS/adguard-home from upd-deps to master

Squashed commit of the following:

commit 4892dc4ed6df76d8733e6799744b095c5db1db6c
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 13 18:22:10 2023 +0300

    all: upd dnscrypt, skel
2023-04-13 19:13:11 +03:00
Eugene Burkov
0ea224a9e4 Pull request 1826: 5714 fix-docker-health
Merge in DNS/adguard-home from 5714-fix-docker-health to master

Updates #5714.

Squashed commit of the following:

commit 61251bffd7a21f1ceb867cc89de0a171645ca4c2
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Apr 13 16:45:41 2023 +0300

    docker: use localhost for unspecified
2023-04-13 17:40:45 +03:00
Ainar Garipov
d78a3edb22 Pull request 1825: 5721-dnscrypt-panic
Updates #5721.

Squashed commit of the following:

commit edf7801e2028aa31d59440158d3fcf2ef95d7013
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Apr 13 15:44:18 2023 +0300

    all: fix dnscrypt panic
2023-04-13 15:50:01 +03:00
Stanislav Chzhen
bbbdea2635 Pull request 1824: fix-chlog
Merge in DNS/adguard-home from fix-chlog to master

Squashed commit of the following:

commit 98e8f5e1436f6c2049f002a4c2211dd2a2e9920c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 13 12:39:11 2023 +0300

    all: fix chlog more

commit deef876541877bc7773e596b39f40c3341ae903a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 13 11:57:10 2023 +0300

    all: fix chlog
2023-04-13 13:07:50 +03:00
Artem Krisanov
6c8d89a4da AG-20835 - Deleted unused methods and variable.
Squashed commit of the following:

commit 3d633703fc60e42d26ccf3b5697370c49ffa1a82
Merge: 09119e2e f082312e
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Thu Apr 13 10:58:31 2023 +0300

    Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home into AG-20835

commit 09119e2ec6f3116986d3ddeb48ee2eb18c1cd9d7
Merge: 085bbb5c 67d8b7df
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Wed Apr 12 14:55:22 2023 +0300

    Merge branch 'master' of ssh://bit.adguard.com:7999/dns/adguard-home into AG-20835

commit 085bbb5cabb14d5388e45ab2022840d44ae42874
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Wed Apr 12 14:12:28 2023 +0300

    Deleted unused methods and variable.
2023-04-13 11:07:13 +03:00
38 changed files with 1194 additions and 246 deletions

View File

@@ -23,6 +23,26 @@ See also the [v0.107.29 GitHub milestone][ms-v0.107.29].
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Added
- The ability to exclude client activity from the query log or statistics by
editing client's settings on the Clients settings page in the UI ([#1717],
[#4299]).
### Fixed
- The `github.com/mdlayher/raw` dependency has been temporarily returned to
support raw connections on Darwin ([#5712]).
- Incorrect recording of blocked results as “Blocked by CNAME or IP” in the
query log ([#5725]).
- All Safe Search services being unchecked by default.
- Panic when a DNSCrypt stamp is invalid ([#5721]).
[#1717]: https://github.com/AdguardTeam/AdGuardHome/issues/1717
[#4299]: https://github.com/AdguardTeam/AdGuardHome/issues/4299
[#5721]: https://github.com/AdguardTeam/AdGuardHome/issues/5721
[#5725]: https://github.com/AdguardTeam/AdGuardHome/issues/5725
<!-- <!--
NOTE: Add new changes ABOVE THIS COMMENT. NOTE: Add new changes ABOVE THIS COMMENT.
--> -->
@@ -149,12 +169,12 @@ In this release, the schema version has changed from 17 to 20.
[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163 [#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163
[#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333 [#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333
[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1717
[#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472 [#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472
[#1717]: https://github.com/AdguardTeam/AdGuardHome/issues/1717
[#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290 [#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290
[#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459 [#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459
[#4262]: https://github.com/AdguardTeam/AdGuardHome/issues/4262 [#4262]: https://github.com/AdguardTeam/AdGuardHome/issues/4262
[#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/4299 [#4299]: https://github.com/AdguardTeam/AdGuardHome/issues/4299
[#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567 [#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567
[#5701]: https://github.com/AdguardTeam/AdGuardHome/issues/5701 [#5701]: https://github.com/AdguardTeam/AdGuardHome/issues/5701

View File

@@ -12,11 +12,40 @@
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279"> <link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279">
<link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48"> <link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48">
<title>AdGuard Home</title> <title>AdGuard Home</title>
<style>
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
[data-theme="DARK"] .wrapper {
background-color: #f5f7fb;
}
</style>
</head> </head>
<body> <body>
<noscript> <noscript>
You need to enable JavaScript to run this app. You need to enable JavaScript to run this app.
</noscript> </noscript>
<div id="root"></div> <div id="root">
<div class="wrapper"></div>
</div>
<script>
(function() {
var LOCAL_STORAGE_THEME_KEY = 'account_theme';
var theme = 'light';
try {
theme = window.localStorage.getItem(LOCAL_STORAGE_THEME_KEY);
} catch(e) {
console.error(e);
}
document.body.dataset.theme = theme;
})();
</script>
</body> </body>
</html> </html>

View File

@@ -17,5 +17,12 @@
You need to enable JavaScript to run this app. You need to enable JavaScript to run this app.
</noscript> </noscript>
<div id="root"></div> <div id="root"></div>
<script>
(function() {
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var currentTheme = prefersDark ? 'dark' : 'light';
document.body.dataset.theme = currentTheme;
})();
</script>
</body> </body>
</html> </html>

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "The query log has been successfully cleared", "query_log_cleared": "The query log has been successfully cleared",
"query_log_updated": "The query log has been successfully updated", "query_log_updated": "The query log has been successfully updated",
"query_log_clear": "Clear query logs", "query_log_clear": "Clear query logs",
"query_log_retention": "Query logs retention", "query_log_retention": "Query logs rotation",
"query_log_enable": "Enable log", "query_log_enable": "Enable log",
"query_log_configuration": "Logs configuration", "query_log_configuration": "Logs configuration",
"query_log_disabled": "The query log is disabled and can be configured in the <0>settings</0>", "query_log_disabled": "The query log is disabled and can be configured in the <0>settings</0>",
"query_log_strict_search": "Use double quotes for strict search", "query_log_strict_search": "Use double quotes for strict search",
"query_log_retention_confirm": "Are you sure you want to change query log retention? If you decrease the interval value, some data will be lost", "query_log_retention_confirm": "Are you sure you want to change query log rotation? If you decrease the interval value, some data will be lost",
"anonymize_client_ip": "Anonymize client IP", "anonymize_client_ip": "Anonymize client IP",
"anonymize_client_ip_desc": "Don't save the client's full IP address to logs or statistics", "anonymize_client_ip_desc": "Don't save the client's full IP address to logs or statistics",
"dns_config": "DNS server configuration", "dns_config": "DNS server configuration",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Disable protection for {{count}} hour", "disable_notify_for_hours": "Disable protection for {{count}} hour",
"disable_notify_for_hours_plural": "Disable protection for {{count}} hours", "disable_notify_for_hours_plural": "Disable protection for {{count}} hours",
"disable_notify_until_tomorrow": "Disable protection until tomorrow", "disable_notify_until_tomorrow": "Disable protection until tomorrow",
"enable_protection_timer": "Protection will be enabled in {{time}}" "enable_protection_timer": "Protection will be enabled in {{time}}",
"custom_retention_input": "Enter retention in hours",
"custom_rotation_input": "Enter rotation in hours",
"protection_section_label": "Protection",
"log_and_stats_section_label": "Query log and statistics",
"ignore_query_log": "Ignore this client in query log",
"ignore_statistics": "Ignore this client in statistics"
} }

View File

@@ -2,21 +2,6 @@ import { createAction } from 'redux-actions';
import apiClient from '../api/Api'; import apiClient from '../api/Api';
import { addErrorToast, addSuccessToast } from './toasts'; import { addErrorToast, addSuccessToast } from './toasts';
export const getBlockedServicesAvailableServicesRequest = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_REQUEST');
export const getBlockedServicesAvailableServicesFailure = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_FAILURE');
export const getBlockedServicesAvailableServicesSuccess = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_SUCCESS');
export const getBlockedServicesAvailableServices = () => async (dispatch) => {
dispatch(getBlockedServicesAvailableServicesRequest());
try {
const data = await apiClient.getBlockedServicesAvailableServices();
dispatch(getBlockedServicesAvailableServicesSuccess(data));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getBlockedServicesAvailableServicesFailure());
}
};
export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST'); export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST');
export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE'); export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE');
export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS'); export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS');

View File

@@ -479,19 +479,12 @@ class Api {
} }
// Blocked services // Blocked services
BLOCKED_SERVICES_SERVICES = { path: 'blocked_services/services', method: 'GET' };
BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' }; BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' };
BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' }; BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' }; BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };
getBlockedServicesAvailableServices() {
const { path, method } = this.BLOCKED_SERVICES_SERVICES;
return this.makeRequest(path, method);
}
getAllBlockedServices() { getAllBlockedServices() {
const { path, method } = this.BLOCKED_SERVICES_ALL; const { path, method } = this.BLOCKED_SERVICES_ALL;
return this.makeRequest(path, method); return this.makeRequest(path, method);

View File

@@ -41,6 +41,17 @@ const settingsCheckboxes = [
placeholder: 'use_adguard_parental', placeholder: 'use_adguard_parental',
}, },
]; ];
const logAndStatsCheckboxes = [
{
name: 'ignore_querylog',
placeholder: 'ignore_query_log',
},
{
name: 'ignore_statistics',
placeholder: 'ignore_statistics',
},
];
const validate = (values) => { const validate = (values) => {
const errors = {}; const errors = {};
const { name, ids } = values; const { name, ids } = values;
@@ -148,6 +159,9 @@ let Form = (props) => {
settings: { settings: {
title: 'settings', title: 'settings',
component: <div label="settings" title={props.t('main_settings')}> component: <div label="settings" title={props.t('main_settings')}>
<div className="form__label--bot form__label--bold">
{t('protection_section_label')}
</div>
{settingsCheckboxes.map((setting) => ( {settingsCheckboxes.map((setting) => (
<div className="form__group" key={setting.name}> <div className="form__group" key={setting.name}>
<Field <Field
@@ -185,6 +199,19 @@ let Form = (props) => {
</div> </div>
))} ))}
</div> </div>
<div className="form__label--bold form__label--top form__label--bot">
{t('log_and_stats_section_label')}
</div>
{logAndStatsCheckboxes.map((setting) => (
<div className="form__group" key={setting.name}>
<Field
name={setting.name}
type="checkbox"
component={CheckboxField}
placeholder={t(setting.placeholder)}
/>
</div>
))}
</div>, </div>,
}, },
block_services: { block_services: {

View File

@@ -1,25 +1,37 @@
import React from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form'; import {
change,
Field,
formValueSelector,
reduxForm,
} from 'redux-form';
import { connect } from 'react-redux';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import { import {
CheckboxField, CheckboxField,
renderRadioField,
toFloatNumber, toFloatNumber,
renderTextareaField, renderTextareaField, renderInputField, renderRadioField,
} from '../../../helpers/form'; } from '../../../helpers/form';
import { import {
FORM_NAME, FORM_NAME,
QUERY_LOG_INTERVALS_DAYS, QUERY_LOG_INTERVALS_DAYS,
HOUR, HOUR,
DAY, DAY,
RETENTION_CUSTOM,
RETENTION_CUSTOM_INPUT,
RETENTION_RANGE,
CUSTOM_INTERVAL,
} from '../../../helpers/constants'; } from '../../../helpers/constants';
import '../FormButton.css'; import '../FormButton.css';
const getIntervalTitle = (interval, t) => { const getIntervalTitle = (interval, t) => {
switch (interval) { switch (interval) {
case RETENTION_CUSTOM:
return t('settings_custom');
case 6 * HOUR: case 6 * HOUR:
return t('interval_6_hour'); return t('interval_6_hour');
case DAY: case DAY:
@@ -42,11 +54,26 @@ const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS.
/> />
)); ));
const Form = (props) => { let Form = (props) => {
const { const {
handleSubmit, submitting, invalid, processing, processingClear, handleClear, t, handleSubmit,
submitting,
invalid,
processing,
processingClear,
handleClear,
t,
interval,
customInterval,
dispatch,
} = props; } = props;
useEffect(() => {
if (QUERY_LOG_INTERVALS_DAYS.includes(interval)) {
dispatch(change(FORM_NAME.LOG_CONFIG, CUSTOM_INTERVAL, null));
}
}, [interval]);
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
@@ -73,6 +100,37 @@ const Form = (props) => {
</label> </label>
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
<div className="custom-controls-stacked"> <div className="custom-controls-stacked">
<Field
key={RETENTION_CUSTOM}
name="interval"
type="radio"
component={renderRadioField}
value={QUERY_LOG_INTERVALS_DAYS.includes(interval)
? RETENTION_CUSTOM
: interval
}
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
normalize={toFloatNumber}
disabled={processing}
/>
{!QUERY_LOG_INTERVALS_DAYS.includes(interval) && (
<div className="form__group--input">
<div className="form__desc form__desc--top">
{t('custom_rotation_input')}
</div>
<Field
key={RETENTION_CUSTOM_INPUT}
name={CUSTOM_INTERVAL}
type="number"
className="form-control"
component={renderInputField}
disabled={processing}
normalize={toFloatNumber}
min={RETENTION_RANGE.MIN}
max={RETENTION_RANGE.MAX}
/>
</div>
)}
{getIntervalFields(processing, t, toFloatNumber)} {getIntervalFields(processing, t, toFloatNumber)}
</div> </div>
</div> </div>
@@ -96,7 +154,12 @@ const Form = (props) => {
<button <button
type="submit" type="submit"
className="btn btn-success btn-standard btn-large" className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processing} disabled={
submitting
|| invalid
|| processing
|| (!QUERY_LOG_INTERVALS_DAYS.includes(interval) && !customInterval)
}
> >
<Trans>save_btn</Trans> <Trans>save_btn</Trans>
</button> </button>
@@ -121,8 +184,22 @@ Form.propTypes = {
processing: PropTypes.bool.isRequired, processing: PropTypes.bool.isRequired,
processingClear: PropTypes.bool.isRequired, processingClear: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
interval: PropTypes.number,
customInterval: PropTypes.number,
dispatch: PropTypes.func.isRequired,
}; };
const selector = formValueSelector(FORM_NAME.LOG_CONFIG);
Form = connect((state) => {
const interval = selector(state, 'interval');
const customInterval = selector(state, CUSTOM_INTERVAL);
return {
interval,
customInterval,
};
})(Form);
export default flow([ export default flow([
withTranslation(), withTranslation(),
reduxForm({ form: FORM_NAME.LOG_CONFIG }), reduxForm({ form: FORM_NAME.LOG_CONFIG }),

View File

@@ -4,15 +4,22 @@ import { withTranslation } from 'react-i18next';
import Card from '../../ui/Card'; import Card from '../../ui/Card';
import Form from './Form'; import Form from './Form';
import { HOUR } from '../../../helpers/constants';
class LogsConfig extends Component { class LogsConfig extends Component {
handleFormSubmit = (values) => { handleFormSubmit = (values) => {
const { t, interval: prevInterval } = this.props; const { t, interval: prevInterval } = this.props;
const { interval } = values; const { interval, customInterval, ...rest } = values;
const data = { ...values, ignored: values.ignored ? values.ignored.split('\n') : [] }; const newInterval = customInterval ? customInterval * HOUR : interval;
if (interval !== prevInterval) { const data = {
...rest,
ignored: values.ignored ? values.ignored.split('\n') : [],
interval: newInterval,
};
if (newInterval < prevInterval) {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (window.confirm(t('query_log_retention_confirm'))) { if (window.confirm(t('query_log_retention_confirm'))) {
this.props.setLogsConfig(data); this.props.setLogsConfig(data);
@@ -32,7 +39,14 @@ class LogsConfig extends Component {
render() { render() {
const { const {
t, enabled, interval, processing, processingClear, anonymize_client_ip, ignored, t,
enabled,
interval,
processing,
processingClear,
anonymize_client_ip,
ignored,
customInterval,
} = this.props; } = this.props;
return ( return (
@@ -46,6 +60,7 @@ class LogsConfig extends Component {
initialValues={{ initialValues={{
enabled, enabled,
interval, interval,
customInterval,
anonymize_client_ip, anonymize_client_ip,
ignored: ignored.join('\n'), ignored: ignored.join('\n'),
}} }}
@@ -62,6 +77,7 @@ class LogsConfig extends Component {
LogsConfig.propTypes = { LogsConfig.propTypes = {
interval: PropTypes.number.isRequired, interval: PropTypes.number.isRequired,
customInterval: PropTypes.number,
enabled: PropTypes.bool.isRequired, enabled: PropTypes.bool.isRequired,
anonymize_client_ip: PropTypes.bool.isRequired, anonymize_client_ip: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired, processing: PropTypes.bool.isRequired,

View File

@@ -18,6 +18,11 @@
font-size: 14px; font-size: 14px;
} }
.form__group--input {
max-width: 300px;
margin: 0 1.5rem 10px;
}
.form__group--checkbox { .form__group--checkbox {
margin-bottom: 25px; margin-bottom: 25px;
} }
@@ -100,6 +105,14 @@
margin-bottom: 0; margin-bottom: 0;
} }
.form__label--bot {
margin-bottom: 10px;
}
.form__label--top {
margin-top: 10px;
}
.form__status { .form__status {
margin-top: 10px; margin-top: 10px;
font-size: 14px; font-size: 14px;

View File

@@ -1,32 +1,44 @@
import React from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form'; import {
change, Field, formValueSelector, reduxForm,
} from 'redux-form';
import { Trans, withTranslation } from 'react-i18next'; import { Trans, withTranslation } from 'react-i18next';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import { connect } from 'react-redux';
import { import {
renderRadioField, renderRadioField,
toNumber, toNumber,
CheckboxField, CheckboxField,
renderTextareaField, renderTextareaField,
toFloatNumber,
renderInputField,
} from '../../../helpers/form'; } from '../../../helpers/form';
import { import {
FORM_NAME, FORM_NAME,
STATS_INTERVALS_DAYS, STATS_INTERVALS_DAYS,
DAY, DAY,
RETENTION_CUSTOM,
RETENTION_CUSTOM_INPUT,
CUSTOM_INTERVAL,
RETENTION_RANGE,
} from '../../../helpers/constants'; } from '../../../helpers/constants';
import '../FormButton.css'; import '../FormButton.css';
const getIntervalTitle = (intervalMs, t) => { const getIntervalTitle = (intervalMs, t) => {
switch (intervalMs / DAY) { switch (intervalMs) {
case 1: case RETENTION_CUSTOM:
return t('settings_custom');
case DAY:
return t('interval_24_hour'); return t('interval_24_hour');
default: default:
return t('interval_days', { count: intervalMs / DAY }); return t('interval_days', { count: intervalMs / DAY });
} }
}; };
const Form = (props) => { let Form = (props) => {
const { const {
handleSubmit, handleSubmit,
processing, processing,
@@ -35,8 +47,17 @@ const Form = (props) => {
handleReset, handleReset,
processingReset, processingReset,
t, t,
interval,
customInterval,
dispatch,
} = props; } = props;
useEffect(() => {
if (STATS_INTERVALS_DAYS.includes(interval)) {
dispatch(change(FORM_NAME.STATS_CONFIG, CUSTOM_INTERVAL, null));
}
}, [interval]);
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="form__group form__group--settings"> <div className="form__group form__group--settings">
@@ -56,6 +77,37 @@ const Form = (props) => {
</div> </div>
<div className="form__group form__group--settings mt-2"> <div className="form__group form__group--settings mt-2">
<div className="custom-controls-stacked"> <div className="custom-controls-stacked">
<Field
key={RETENTION_CUSTOM}
name="interval"
type="radio"
component={renderRadioField}
value={STATS_INTERVALS_DAYS.includes(interval)
? RETENTION_CUSTOM
: interval
}
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
normalize={toFloatNumber}
disabled={processing}
/>
{!STATS_INTERVALS_DAYS.includes(interval) && (
<div className="form__group--input">
<div className="form__desc form__desc--top">
{t('custom_retention_input')}
</div>
<Field
key={RETENTION_CUSTOM_INPUT}
name={CUSTOM_INTERVAL}
type="number"
className="form-control"
component={renderInputField}
disabled={processing}
normalize={toFloatNumber}
min={RETENTION_RANGE.MIN}
max={RETENTION_RANGE.MAX}
/>
</div>
)}
{STATS_INTERVALS_DAYS.map((interval) => ( {STATS_INTERVALS_DAYS.map((interval) => (
<Field <Field
key={interval} key={interval}
@@ -90,7 +142,12 @@ const Form = (props) => {
<button <button
type="submit" type="submit"
className="btn btn-success btn-standard btn-large" className="btn btn-success btn-standard btn-large"
disabled={submitting || invalid || processing} disabled={
submitting
|| invalid
|| processing
|| (!STATS_INTERVALS_DAYS.includes(interval) && !customInterval)
}
> >
<Trans>save_btn</Trans> <Trans>save_btn</Trans>
</button> </button>
@@ -116,8 +173,22 @@ Form.propTypes = {
processing: PropTypes.bool.isRequired, processing: PropTypes.bool.isRequired,
processingReset: PropTypes.bool.isRequired, processingReset: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
interval: PropTypes.number,
customInterval: PropTypes.number,
dispatch: PropTypes.func.isRequired,
}; };
const selector = formValueSelector(FORM_NAME.STATS_CONFIG);
Form = connect((state) => {
const interval = selector(state, 'interval');
const customInterval = selector(state, CUSTOM_INTERVAL);
return {
interval,
customInterval,
};
})(Form);
export default flow([ export default flow([
withTranslation(), withTranslation(),
reduxForm({ form: FORM_NAME.STATS_CONFIG }), reduxForm({ form: FORM_NAME.STATS_CONFIG }),

View File

@@ -4,13 +4,18 @@ import { withTranslation } from 'react-i18next';
import Card from '../../ui/Card'; import Card from '../../ui/Card';
import Form from './Form'; import Form from './Form';
import { HOUR } from '../../../helpers/constants';
class StatsConfig extends Component { class StatsConfig extends Component {
handleFormSubmit = ({ enabled, interval, ignored }) => { handleFormSubmit = ({
enabled, interval, ignored, customInterval,
}) => {
const { t, interval: prevInterval } = this.props; const { t, interval: prevInterval } = this.props;
const newInterval = customInterval ? customInterval * HOUR : interval;
const config = { const config = {
enabled, enabled,
interval, interval: newInterval,
ignored: ignored ? ignored.split('\n') : [], ignored: ignored ? ignored.split('\n') : [],
}; };
@@ -33,7 +38,13 @@ class StatsConfig extends Component {
render() { render() {
const { const {
t, interval, processing, processingReset, ignored, enabled, t,
interval,
customInterval,
processing,
processingReset,
ignored,
enabled,
} = this.props; } = this.props;
return ( return (
@@ -46,6 +57,7 @@ class StatsConfig extends Component {
<Form <Form
initialValues={{ initialValues={{
interval, interval,
customInterval,
enabled, enabled,
ignored: ignored.join('\n'), ignored: ignored.join('\n'),
}} }}
@@ -62,6 +74,7 @@ class StatsConfig extends Component {
StatsConfig.propTypes = { StatsConfig.propTypes = {
interval: PropTypes.number.isRequired, interval: PropTypes.number.isRequired,
customInterval: PropTypes.number,
ignored: PropTypes.array.isRequired, ignored: PropTypes.array.isRequired,
enabled: PropTypes.bool.isRequired, enabled: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired, processing: PropTypes.bool.isRequired,

View File

@@ -124,6 +124,7 @@ class Settings extends Component {
enabled={queryLogs.enabled} enabled={queryLogs.enabled}
ignored={queryLogs.ignored} ignored={queryLogs.ignored}
interval={queryLogs.interval} interval={queryLogs.interval}
customInterval={queryLogs.customInterval}
anonymize_client_ip={queryLogs.anonymize_client_ip} anonymize_client_ip={queryLogs.anonymize_client_ip}
processing={queryLogs.processingSetConfig} processing={queryLogs.processingSetConfig}
processingClear={queryLogs.processingClear} processingClear={queryLogs.processingClear}
@@ -134,6 +135,7 @@ class Settings extends Component {
<div className="col-md-12"> <div className="col-md-12">
<StatsConfig <StatsConfig
interval={stats.interval} interval={stats.interval}
customInterval={stats.customInterval}
ignored={stats.ignored} ignored={stats.ignored}
enabled={stats.enabled} enabled={stats.enabled}
processing={stats.processingSetConfig} processing={stats.processingSetConfig}
@@ -166,6 +168,7 @@ Settings.propTypes = {
stats: PropTypes.shape({ stats: PropTypes.shape({
processingGetConfig: PropTypes.bool, processingGetConfig: PropTypes.bool,
interval: PropTypes.number, interval: PropTypes.number,
customInterval: PropTypes.number,
enabled: PropTypes.bool, enabled: PropTypes.bool,
ignored: PropTypes.array, ignored: PropTypes.array,
processingSetConfig: PropTypes.bool, processingSetConfig: PropTypes.bool,
@@ -174,6 +177,7 @@ Settings.propTypes = {
queryLogs: PropTypes.shape({ queryLogs: PropTypes.shape({
enabled: PropTypes.bool, enabled: PropTypes.bool,
interval: PropTypes.number, interval: PropTypes.number,
customInterval: PropTypes.number,
anonymize_client_ip: PropTypes.bool, anonymize_client_ip: PropTypes.bool,
processingSetConfig: PropTypes.bool, processingSetConfig: PropTypes.bool,
processingClear: PropTypes.bool, processingClear: PropTypes.bool,

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import cn from 'classnames'; import cn from 'classnames';
@@ -42,12 +42,6 @@ const Footer = () => {
const isLoggedIn = profileName !== ''; const isLoggedIn = profileName !== '';
const [currentThemeLocal, setCurrentThemeLocal] = useState(THEMES.auto); const [currentThemeLocal, setCurrentThemeLocal] = useState(THEMES.auto);
useEffect(() => {
if (!isLoggedIn) {
setUITheme(currentThemeLocal);
}
}, []);
const getYear = () => { const getYear = () => {
const today = new Date(); const today = new Date();
return today.getFullYear(); return today.getFullYear();

View File

@@ -13,7 +13,7 @@
font-size: 28px; font-size: 28px;
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
background-color: rgba(255, 255, 255, 0.8); background-color: var(--rt-nodata-bgcolor);
} }
.overlay--visible { .overlay--visible {

View File

@@ -220,6 +220,12 @@ export const STATS_INTERVALS_DAYS = [DAY, DAY * 7, DAY * 30, DAY * 90];
export const QUERY_LOG_INTERVALS_DAYS = [HOUR * 6, DAY, DAY * 7, DAY * 30, DAY * 90]; export const QUERY_LOG_INTERVALS_DAYS = [HOUR * 6, DAY, DAY * 7, DAY * 30, DAY * 90];
export const RETENTION_CUSTOM = 1;
export const RETENTION_CUSTOM_INPUT = 'custom_retention_input';
export const CUSTOM_INTERVAL = 'customInterval';
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168]; export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];
// Note that translation strings contain these modes (blocking_mode_CONSTANT) // Note that translation strings contain these modes (blocking_mode_CONSTANT)
@@ -462,6 +468,11 @@ export const UINT32_RANGE = {
MAX: 4294967295, MAX: 4294967295,
}; };
export const RETENTION_RANGE = {
MIN: 1,
MAX: 365 * 24,
};
export const DHCP_VALUES_PLACEHOLDERS = { export const DHCP_VALUES_PLACEHOLDERS = {
ipv4: { ipv4: {
subnet_mask: '255.255.255.0', subnet_mask: '255.255.255.0',
@@ -537,3 +548,5 @@ export const DISABLE_PROTECTION_TIMINGS = {
HOUR: 60 * 60 * 1000, HOUR: 60 * 60 * 1000,
TOMORROW: 24 * 60 * 60 * 1000, TOMORROW: 24 * 60 * 60 * 1000,
}; };
export const LOCAL_STORAGE_THEME_KEY = 'account_theme';

View File

@@ -26,6 +26,7 @@ import {
STANDARD_WEB_PORT, STANDARD_WEB_PORT,
SPECIAL_FILTER_ID, SPECIAL_FILTER_ID,
THEMES, THEMES,
LOCAL_STORAGE_THEME_KEY,
} from './constants'; } from './constants';
/** /**
@@ -679,19 +680,60 @@ export const setHtmlLangAttr = (language) => {
window.document.documentElement.lang = language; window.document.documentElement.lang = language;
}; };
/**
* Set local storage field
*
* @param {string} key
* @param {string} value
*/
export const setStorageItem = (key, value) => {
if (window.localStorage) {
window.localStorage.setItem(key, value);
}
};
/**
* Get local storage field
*
* @param {string} key
*/
export const getStorageItem = (key) => (window.localStorage
? window.localStorage.getItem(key)
: null);
/**
* Set local storage theme field
*
* @param {string} theme
*/
export const setTheme = (theme) => {
setStorageItem(LOCAL_STORAGE_THEME_KEY, theme);
};
/**
* Get local storage theme field
*
* @returns {string}
*/
export const getTheme = () => getStorageItem(LOCAL_STORAGE_THEME_KEY) || THEMES.light;
/** /**
* Sets UI theme. * Sets UI theme.
* *
* @param theme * @param theme
*/ */
export const setUITheme = (theme) => { export const setUITheme = (theme) => {
let currentTheme = theme; let currentTheme = theme || getTheme();
if (currentTheme === THEMES.auto) { if (currentTheme === THEMES.auto) {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
currentTheme = prefersDark ? THEMES.dark : THEMES.light; currentTheme = prefersDark ? THEMES.dark : THEMES.light;
} }
setTheme(currentTheme);
document.body.dataset.theme = currentTheme; document.body.dataset.theme = currentTheme;
}; };

View File

@@ -177,7 +177,7 @@ const dashboard = handleActions(
autoClients: [], autoClients: [],
supportedTags: [], supportedTags: [],
name: '', name: '',
theme: 'auto', theme: undefined,
checkUpdateFlag: false, checkUpdateFlag: false,
}, },
); );

View File

@@ -1,7 +1,9 @@
import { handleActions } from 'redux-actions'; import { handleActions } from 'redux-actions';
import * as actions from '../actions/queryLogs'; import * as actions from '../actions/queryLogs';
import { DEFAULT_LOGS_FILTER, DAY } from '../helpers/constants'; import {
DEFAULT_LOGS_FILTER, DAY, QUERY_LOG_INTERVALS_DAYS, HOUR,
} from '../helpers/constants';
const queryLogs = handleActions( const queryLogs = handleActions(
{ {
@@ -59,6 +61,9 @@ const queryLogs = handleActions(
[actions.getLogsConfigSuccess]: (state, { payload }) => ({ [actions.getLogsConfigSuccess]: (state, { payload }) => ({
...state, ...state,
...payload, ...payload,
customInterval: !QUERY_LOG_INTERVALS_DAYS.includes(payload.interval)
? payload.interval / HOUR
: null,
processingGetConfig: false, processingGetConfig: false,
}), }),
@@ -95,6 +100,7 @@ const queryLogs = handleActions(
anonymize_client_ip: false, anonymize_client_ip: false,
isDetailed: true, isDetailed: true,
isEntireLog: false, isEntireLog: false,
customInterval: null,
}, },
); );

View File

@@ -1,6 +1,6 @@
import { handleActions } from 'redux-actions'; import { handleActions } from 'redux-actions';
import { normalizeTopClients } from '../helpers/helpers'; import { normalizeTopClients } from '../helpers/helpers';
import { DAY } from '../helpers/constants'; import { DAY, HOUR, STATS_INTERVALS_DAYS } from '../helpers/constants';
import * as actions from '../actions/stats'; import * as actions from '../actions/stats';
@@ -27,6 +27,9 @@ const stats = handleActions(
[actions.getStatsConfigSuccess]: (state, { payload }) => ({ [actions.getStatsConfigSuccess]: (state, { payload }) => ({
...state, ...state,
...payload, ...payload,
customInterval: !STATS_INTERVALS_DAYS.includes(payload.interval)
? payload.interval / HOUR
: null,
processingGetConfig: false, processingGetConfig: false,
}), }),
@@ -93,6 +96,7 @@ const stats = handleActions(
processingStats: true, processingStats: true,
processingReset: false, processingReset: false,
interval: DAY, interval: DAY,
customInterval: null,
...defaultStats, ...defaultStats,
}, },
); );

View File

@@ -4,19 +4,27 @@
/^[[:space:]]+- .+/ { /^[[:space:]]+- .+/ {
if (FNR - prev_line == 1) { if (FNR - prev_line == 1) {
addrs[addrsnum++] = $2 addrs[$2] = true
prev_line = FNR prev_line = FNR
if ($2 == "0.0.0.0" || $2 == "::") {
delete addrs
addrs["localhost"] = true
# Drop all the other addresses.
prev_line = -1
}
} }
} }
/^[[:space:]]+port:/ { if (is_dns) port = $2 } /^[[:space:]]+port:/ { if (is_dns) port = $2 }
END { END {
for (i in addrs) { for (addr in addrs) {
if (match(addrs[i], ":")) { if (match(addr, ":")) {
print "[" addrs[i] "]:" port print "[" addr "]:" port
} else { } else {
print addrs[i] ":" port print addr ":" port
} }
} }
} }

6
go.mod
View File

@@ -7,7 +7,7 @@ require (
github.com/AdguardTeam/golibs v0.13.2 github.com/AdguardTeam/golibs v0.13.2
github.com/AdguardTeam/urlfilter v0.16.1 github.com/AdguardTeam/urlfilter v0.16.1
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.2.6 github.com/ameshkov/dnscrypt/v2 v2.2.7
github.com/digineo/go-ipset/v2 v2.2.1 github.com/digineo/go-ipset/v2 v2.2.1
github.com/dimfeld/httptreemux/v5 v5.5.0 github.com/dimfeld/httptreemux/v5 v5.5.0
github.com/fsnotify/fsnotify v1.6.0 github.com/fsnotify/fsnotify v1.6.0
@@ -22,6 +22,9 @@ require (
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
github.com/mdlayher/netlink v1.7.1 github.com/mdlayher/netlink v1.7.1
github.com/mdlayher/packet v1.1.1 github.com/mdlayher/packet v1.1.1
// TODO(a.garipov): This package is deprecated; find a new one or use our
// own code for that. Perhaps, use gopacket.
github.com/mdlayher/raw v0.1.0
github.com/miekg/dns v1.1.53 github.com/miekg/dns v1.1.53
github.com/quic-go/quic-go v0.33.0 github.com/quic-go/quic-go v0.33.0
github.com/stretchr/testify v1.8.2 github.com/stretchr/testify v1.8.2
@@ -46,7 +49,6 @@ require (
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/mock v1.6.0 // indirect github.com/golang/mock v1.6.0 // indirect
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 // indirect github.com/google/pprof v0.0.0-20230406165453-00490a63f317 // indirect
github.com/mdlayher/raw v0.1.0 // indirect
github.com/mdlayher/socket v0.4.0 // indirect github.com/mdlayher/socket v0.4.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.2 // indirect github.com/onsi/ginkgo/v2 v2.9.2 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect

4
go.sum
View File

@@ -15,8 +15,8 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
github.com/ameshkov/dnscrypt/v2 v2.2.6 h1:rE7AFbPWebq7me7RVS66Cipd1m7ef1yf2+C8QzjQXXE= github.com/ameshkov/dnscrypt/v2 v2.2.7 h1:aEitLIR8HcxVodZ79mgRcCiC0A0I5kZPBuWGFwwulAw=
github.com/ameshkov/dnscrypt/v2 v2.2.6/go.mod h1:qPWhwz6FdSmuK7W4sMyvogrez4MWdtzosdqlr0Rg3ow= github.com/ameshkov/dnscrypt/v2 v2.2.7/go.mod h1:qPWhwz6FdSmuK7W4sMyvogrez4MWdtzosdqlr0Rg3ow=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM= github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=

View File

@@ -0,0 +1,293 @@
//go:build darwin
package dhcpd
import (
"fmt"
"net"
"os"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/mdlayher/ethernet"
//lint:ignore SA1019 See the TODO in go.mod.
"github.com/mdlayher/raw"
)
// dhcpUnicastAddr is the combination of MAC and IP addresses for responding to
// the unconfigured host.
type dhcpUnicastAddr struct {
// raw.Addr is embedded here to make *dhcpUcastAddr a net.Addr without
// actually implementing all methods. It also contains the client's
// hardware address.
raw.Addr
// yiaddr is an IP address just allocated by server for the host.
yiaddr net.IP
}
// dhcpConn is the net.PacketConn capable of handling both net.UDPAddr and
// net.HardwareAddr.
type dhcpConn struct {
// udpConn is the connection for UDP addresses.
udpConn net.PacketConn
// bcastIP is the broadcast address specific for the configured
// interface's subnet.
bcastIP net.IP
// rawConn is the connection for MAC addresses.
rawConn net.PacketConn
// srcMAC is the hardware address of the configured network interface.
srcMAC net.HardwareAddr
// srcIP is the IP address of the configured network interface.
srcIP net.IP
}
// newDHCPConn creates the special connection for DHCP server.
func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err error) {
var ucast net.PacketConn
if ucast, err = raw.ListenPacket(iface, uint16(ethernet.EtherTypeIPv4), nil); err != nil {
return nil, fmt.Errorf("creating raw udp connection: %w", err)
}
// Create the UDP connection.
var bcast net.PacketConn
bcast, err = server4.NewIPv4UDPConn(iface.Name, &net.UDPAddr{
// TODO(e.burkov): Listening on zeroes makes the server handle
// requests from all the interfaces. Inspect the ways to
// specify the interface-specific listening addresses.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.
IP: net.IP{0, 0, 0, 0},
Port: dhcpv4.ServerPort,
})
if err != nil {
return nil, fmt.Errorf("creating ipv4 udp connection: %w", err)
}
return &dhcpConn{
udpConn: bcast,
bcastIP: s.conf.broadcastIP.AsSlice(),
rawConn: ucast,
srcMAC: iface.HardwareAddr,
srcIP: s.conf.dnsIPAddrs[0].AsSlice(),
}, nil
}
// wrapErrs is a helper to wrap the errors from two independent underlying
// connections.
func (*dhcpConn) wrapErrs(action string, udpConnErr, rawConnErr error) (err error) {
switch {
case udpConnErr != nil && rawConnErr != nil:
return errors.List(fmt.Sprintf("%s both connections", action), udpConnErr, rawConnErr)
case udpConnErr != nil:
return fmt.Errorf("%s udp connection: %w", action, udpConnErr)
case rawConnErr != nil:
return fmt.Errorf("%s raw connection: %w", action, rawConnErr)
default:
return nil
}
}
// WriteTo implements net.PacketConn for *dhcpConn. It selects the underlying
// connection to write to based on the type of addr.
func (c *dhcpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
switch addr := addr.(type) {
case *dhcpUnicastAddr:
// Unicast the message to the client's MAC address. Use the raw
// connection.
//
// Note: unicasting is performed on the only network interface
// that is configured. For now it may be not what users expect
// so additionally broadcast the message via UDP connection.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.
var rerr error
n, rerr = c.unicast(p, addr)
_, uerr := c.broadcast(p, &net.UDPAddr{
IP: netutil.IPv4bcast(),
Port: dhcpv4.ClientPort,
})
return n, c.wrapErrs("writing to", uerr, rerr)
case *net.UDPAddr:
if addr.IP.Equal(net.IPv4bcast) {
// Broadcast the message for the client which supports
// it. Use the UDP connection.
return c.broadcast(p, addr)
}
// Unicast the message to the client's IP address. Use the UDP
// connection.
return c.udpConn.WriteTo(p, addr)
default:
return 0, fmt.Errorf("addr has an unexpected type %T", addr)
}
}
// ReadFrom implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
return c.udpConn.ReadFrom(p)
}
// unicast wraps respData with required frames and writes it to the peer.
func (c *dhcpConn) unicast(respData []byte, peer *dhcpUnicastAddr) (n int, err error) {
var data []byte
data, err = c.buildEtherPkt(respData, peer)
if err != nil {
return 0, err
}
return c.rawConn.WriteTo(data, &peer.Addr)
}
// Close implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) Close() (err error) {
rerr := c.rawConn.Close()
if errors.Is(rerr, os.ErrClosed) {
// Ignore the error since the actual file is closed already.
rerr = nil
}
return c.wrapErrs("closing", c.udpConn.Close(), rerr)
}
// LocalAddr implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) LocalAddr() (a net.Addr) {
return c.udpConn.LocalAddr()
}
// SetDeadline implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) SetDeadline(t time.Time) (err error) {
return c.wrapErrs("setting deadline on", c.udpConn.SetDeadline(t), c.rawConn.SetDeadline(t))
}
// SetReadDeadline implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) SetReadDeadline(t time.Time) error {
return c.wrapErrs(
"setting reading deadline on",
c.udpConn.SetReadDeadline(t),
c.rawConn.SetReadDeadline(t),
)
}
// SetWriteDeadline implements net.PacketConn for *dhcpConn.
func (c *dhcpConn) SetWriteDeadline(t time.Time) error {
return c.wrapErrs(
"setting writing deadline on",
c.udpConn.SetWriteDeadline(t),
c.rawConn.SetWriteDeadline(t),
)
}
// ipv4DefaultTTL is the default Time to Live value in seconds as recommended by
// RFC-1700.
//
// See https://datatracker.ietf.org/doc/html/rfc1700.
const ipv4DefaultTTL = 64
// buildEtherPkt wraps the payload with IPv4, UDP and Ethernet frames.
// Validation of the payload is a caller's responsibility.
func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []byte, err error) {
udpLayer := &layers.UDP{
SrcPort: dhcpv4.ServerPort,
DstPort: dhcpv4.ClientPort,
}
ipv4Layer := &layers.IPv4{
Version: uint8(layers.IPProtocolIPv4),
Flags: layers.IPv4DontFragment,
TTL: ipv4DefaultTTL,
Protocol: layers.IPProtocolUDP,
SrcIP: c.srcIP,
DstIP: peer.yiaddr,
}
// Ignore the error since it's only returned for invalid network layer's
// type.
_ = udpLayer.SetNetworkLayerForChecksum(ipv4Layer)
ethLayer := &layers.Ethernet{
SrcMAC: c.srcMAC,
DstMAC: peer.HardwareAddr,
EthernetType: layers.EthernetTypeIPv4,
}
buf := gopacket.NewSerializeBuffer()
setts := gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}
err = gopacket.SerializeLayers(
buf,
setts,
ethLayer,
ipv4Layer,
udpLayer,
gopacket.Payload(payload),
)
if err != nil {
return nil, fmt.Errorf("serializing layers: %w", err)
}
return buf.Bytes(), nil
}
// send writes resp for peer to conn considering the req's parameters according
// to RFC-2131.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
case giaddr != nil && !giaddr.IsUnspecified():
// Send any return messages to the server port on the BOOTP
// relay agent whose address appears in giaddr.
peer = &net.UDPAddr{
IP: giaddr,
Port: dhcpv4.ServerPort,
}
if mtype == dhcpv4.MessageTypeNak {
// Set the broadcast bit in the DHCPNAK, so that the relay agent
// broadcasts it to the client, because the client may not have
// a correct network address or subnet mask, and the client may not
// be answering ARP requests.
resp.SetBroadcast()
}
case mtype == dhcpv4.MessageTypeNak:
// Broadcast any DHCPNAK messages to 0xffffffff.
case ciaddr != nil && !ciaddr.IsUnspecified():
// Unicast DHCPOFFER and DHCPACK messages to the address in
// ciaddr.
peer = &net.UDPAddr{
IP: ciaddr,
Port: dhcpv4.ClientPort,
}
case !req.IsBroadcast() && req.ClientHWAddr != nil:
// Unicast DHCPOFFER and DHCPACK messages to the client's
// hardware address and yiaddr.
peer = &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: req.ClientHWAddr},
yiaddr: resp.YourIPAddr,
}
default:
// Go on since peer is already set to broadcast.
}
pktData := resp.ToBytes()
log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary())
_, err := conn.WriteTo(pktData, peer)
if err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
}
}

View File

@@ -0,0 +1,219 @@
//go:build darwin
package dhcpd
import (
"net"
"testing"
"github.com/AdguardTeam/golibs/testutil"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
//lint:ignore SA1019 See the TODO in go.mod.
"github.com/mdlayher/raw"
)
func TestDHCPConn_WriteTo_common(t *testing.T) {
respData := (&dhcpv4.DHCPv4{}).ToBytes()
udpAddr := &net.UDPAddr{
IP: net.IP{1, 2, 3, 4},
Port: dhcpv4.ClientPort,
}
t.Run("unicast_ip", func(t *testing.T) {
writeTo := func(_ []byte, addr net.Addr) (_ int, _ error) {
assert.Equal(t, udpAddr, addr)
return 0, nil
}
conn := &dhcpConn{udpConn: &fakePacketConn{writeTo: writeTo}}
_, err := conn.WriteTo(respData, udpAddr)
assert.NoError(t, err)
})
t.Run("unexpected_addr_type", func(t *testing.T) {
type unexpectedAddrType struct {
net.Addr
}
conn := &dhcpConn{}
n, err := conn.WriteTo(nil, &unexpectedAddrType{})
require.Error(t, err)
testutil.AssertErrorMsg(t, "addr has an unexpected type *dhcpd.unexpectedAddrType", err)
assert.Zero(t, n)
})
}
func TestBuildEtherPkt(t *testing.T) {
conn := &dhcpConn{
srcMAC: net.HardwareAddr{1, 2, 3, 4, 5, 6},
srcIP: net.IP{1, 2, 3, 4},
}
peer := &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}},
yiaddr: net.IP{4, 3, 2, 1},
}
payload := (&dhcpv4.DHCPv4{}).ToBytes()
t.Run("success", func(t *testing.T) {
pkt, err := conn.buildEtherPkt(payload, peer)
require.NoError(t, err)
assert.NotEmpty(t, pkt)
actualPkt := gopacket.NewPacket(pkt, layers.LayerTypeEthernet, gopacket.DecodeOptions{
NoCopy: true,
})
require.NotNil(t, actualPkt)
wantTypes := []gopacket.LayerType{
layers.LayerTypeEthernet,
layers.LayerTypeIPv4,
layers.LayerTypeUDP,
layers.LayerTypeDHCPv4,
}
actualLayers := actualPkt.Layers()
require.Len(t, actualLayers, len(wantTypes))
for i, wantType := range wantTypes {
layer := actualLayers[i]
require.NotNil(t, layer)
assert.Equal(t, wantType, layer.LayerType())
}
})
t.Run("bad_payload", func(t *testing.T) {
// Create an invalid DHCP packet.
invalidPayload := []byte{1, 2, 3, 4}
pkt, err := conn.buildEtherPkt(invalidPayload, peer)
require.NoError(t, err)
assert.NotEmpty(t, pkt)
})
t.Run("serializing_error", func(t *testing.T) {
// Create a peer with invalid MAC.
badPeer := &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}},
yiaddr: net.IP{4, 3, 2, 1},
}
pkt, err := conn.buildEtherPkt(payload, badPeer)
require.Error(t, err)
assert.Empty(t, pkt)
})
}
func TestV4Server_Send(t *testing.T) {
s := &v4Server{}
var (
defaultIP = net.IP{99, 99, 99, 99}
knownIP = net.IP{4, 2, 4, 2}
knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1}
)
defaultPeer := &net.UDPAddr{
IP: defaultIP,
// Use neither client nor server port to check it actually
// changed.
Port: dhcpv4.ClientPort + dhcpv4.ServerPort,
}
defaultResp := &dhcpv4.DHCPv4{}
testCases := []struct {
want net.Addr
req *dhcpv4.DHCPv4
resp *dhcpv4.DHCPv4
name string
}{{
name: "giaddr",
req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},
resp: defaultResp,
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ServerPort,
},
}, {
name: "nak",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
},
want: defaultPeer,
}, {
name: "ciaddr",
req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP},
resp: &dhcpv4.DHCPv4{},
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ClientPort,
},
}, {
name: "chaddr",
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
want: &dhcpUnicastAddr{
Addr: raw.Addr{HardwareAddr: knownMAC},
yiaddr: knownIP,
},
}, {
name: "who_are_you",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{},
want: defaultPeer,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) {
assert.Equal(t, tc.want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
})
}
t.Run("giaddr_nak", func(t *testing.T) {
req := &dhcpv4.DHCPv4{
GatewayIPAddr: knownIP,
}
// Ensure the request is for unicast.
req.SetUnicast()
resp := &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
}
want := &net.UDPAddr{
IP: req.GatewayIPAddr,
Port: dhcpv4.ServerPort,
}
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (n int, err error) {
assert.Equal(t, want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, req, resp)
assert.True(t, resp.IsBroadcast())
})
}

View File

@@ -1,4 +1,4 @@
//go:build darwin || freebsd || linux || openbsd //go:build freebsd || linux || openbsd
package dhcpd package dhcpd
@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/google/gopacket" "github.com/google/gopacket"
"github.com/google/gopacket/layers" "github.com/google/gopacket/layers"
@@ -238,3 +239,53 @@ func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []b
return buf.Bytes(), nil return buf.Bytes(), nil
} }
// send writes resp for peer to conn considering the req's parameters according
// to RFC-2131.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
case giaddr != nil && !giaddr.IsUnspecified():
// Send any return messages to the server port on the BOOTP
// relay agent whose address appears in giaddr.
peer = &net.UDPAddr{
IP: giaddr,
Port: dhcpv4.ServerPort,
}
if mtype == dhcpv4.MessageTypeNak {
// Set the broadcast bit in the DHCPNAK, so that the relay agent
// broadcasts it to the client, because the client may not have
// a correct network address or subnet mask, and the client may not
// be answering ARP requests.
resp.SetBroadcast()
}
case mtype == dhcpv4.MessageTypeNak:
// Broadcast any DHCPNAK messages to 0xffffffff.
case ciaddr != nil && !ciaddr.IsUnspecified():
// Unicast DHCPOFFER and DHCPACK messages to the address in
// ciaddr.
peer = &net.UDPAddr{
IP: ciaddr,
Port: dhcpv4.ClientPort,
}
case !req.IsBroadcast() && req.ClientHWAddr != nil:
// Unicast DHCPOFFER and DHCPACK messages to the client's
// hardware address and yiaddr.
peer = &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: req.ClientHWAddr},
yiaddr: resp.YourIPAddr,
}
default:
// Go on since peer is already set to broadcast.
}
pktData := resp.ToBytes()
log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary())
_, err := conn.WriteTo(pktData, peer)
if err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
}
}

View File

@@ -1,4 +1,4 @@
//go:build darwin || freebsd || linux || openbsd //go:build freebsd || linux || openbsd
package dhcpd package dhcpd
@@ -110,3 +110,108 @@ func TestBuildEtherPkt(t *testing.T) {
assert.Empty(t, pkt) assert.Empty(t, pkt)
}) })
} }
func TestV4Server_Send(t *testing.T) {
s := &v4Server{}
var (
defaultIP = net.IP{99, 99, 99, 99}
knownIP = net.IP{4, 2, 4, 2}
knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1}
)
defaultPeer := &net.UDPAddr{
IP: defaultIP,
// Use neither client nor server port to check it actually
// changed.
Port: dhcpv4.ClientPort + dhcpv4.ServerPort,
}
defaultResp := &dhcpv4.DHCPv4{}
testCases := []struct {
want net.Addr
req *dhcpv4.DHCPv4
resp *dhcpv4.DHCPv4
name string
}{{
name: "giaddr",
req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},
resp: defaultResp,
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ServerPort,
},
}, {
name: "nak",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
},
want: defaultPeer,
}, {
name: "ciaddr",
req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP},
resp: &dhcpv4.DHCPv4{},
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ClientPort,
},
}, {
name: "chaddr",
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
want: &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: knownMAC},
yiaddr: knownIP,
},
}, {
name: "who_are_you",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{},
want: defaultPeer,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) {
assert.Equal(t, tc.want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
})
}
t.Run("giaddr_nak", func(t *testing.T) {
req := &dhcpv4.DHCPv4{
GatewayIPAddr: knownIP,
}
// Ensure the request is for unicast.
req.SetUnicast()
resp := &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
}
want := &net.UDPAddr{
IP: req.GatewayIPAddr,
Port: dhcpv4.ServerPort,
}
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (n int, err error) {
assert.Equal(t, want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, req, resp)
assert.True(t, resp.IsBroadcast())
})
}

View File

@@ -20,7 +20,6 @@ import (
"github.com/go-ping/ping" "github.com/go-ping/ping"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4" "github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/mdlayher/packet"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@@ -1132,56 +1131,6 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4
s.send(peer, conn, req, resp) s.send(peer, conn, req, resp)
} }
// send writes resp for peer to conn considering the req's parameters according
// to RFC-2131.
//
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
case giaddr != nil && !giaddr.IsUnspecified():
// Send any return messages to the server port on the BOOTP
// relay agent whose address appears in giaddr.
peer = &net.UDPAddr{
IP: giaddr,
Port: dhcpv4.ServerPort,
}
if mtype == dhcpv4.MessageTypeNak {
// Set the broadcast bit in the DHCPNAK, so that the relay agent
// broadcasts it to the client, because the client may not have
// a correct network address or subnet mask, and the client may not
// be answering ARP requests.
resp.SetBroadcast()
}
case mtype == dhcpv4.MessageTypeNak:
// Broadcast any DHCPNAK messages to 0xffffffff.
case ciaddr != nil && !ciaddr.IsUnspecified():
// Unicast DHCPOFFER and DHCPACK messages to the address in
// ciaddr.
peer = &net.UDPAddr{
IP: ciaddr,
Port: dhcpv4.ClientPort,
}
case !req.IsBroadcast() && req.ClientHWAddr != nil:
// Unicast DHCPOFFER and DHCPACK messages to the client's
// hardware address and yiaddr.
peer = &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: req.ClientHWAddr},
yiaddr: resp.YourIPAddr,
}
default:
// Go on since peer is already set to broadcast.
}
pktData := resp.ToBytes()
log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary())
_, err := conn.WriteTo(pktData, peer)
if err != nil {
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
}
}
// Start starts the IPv4 DHCP server. // Start starts the IPv4 DHCP server.
func (s *v4Server) Start() (err error) { func (s *v4Server) Start() (err error) {
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }() defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()

View File

@@ -15,7 +15,6 @@ import (
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/mdlayher/packet"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -771,111 +770,6 @@ func (fc *fakePacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return fc.writeTo(p, addr) return fc.writeTo(p, addr)
} }
func TestV4Server_Send(t *testing.T) {
s := &v4Server{}
var (
defaultIP = net.IP{99, 99, 99, 99}
knownIP = net.IP{4, 2, 4, 2}
knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1}
)
defaultPeer := &net.UDPAddr{
IP: defaultIP,
// Use neither client nor server port to check it actually
// changed.
Port: dhcpv4.ClientPort + dhcpv4.ServerPort,
}
defaultResp := &dhcpv4.DHCPv4{}
testCases := []struct {
want net.Addr
req *dhcpv4.DHCPv4
resp *dhcpv4.DHCPv4
name string
}{{
name: "giaddr",
req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},
resp: defaultResp,
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ServerPort,
},
}, {
name: "nak",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
},
want: defaultPeer,
}, {
name: "ciaddr",
req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP},
resp: &dhcpv4.DHCPv4{},
want: &net.UDPAddr{
IP: knownIP,
Port: dhcpv4.ClientPort,
},
}, {
name: "chaddr",
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
want: &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: knownMAC},
yiaddr: knownIP,
},
}, {
name: "who_are_you",
req: &dhcpv4.DHCPv4{},
resp: &dhcpv4.DHCPv4{},
want: defaultPeer,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) {
assert.Equal(t, tc.want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
})
}
t.Run("giaddr_nak", func(t *testing.T) {
req := &dhcpv4.DHCPv4{
GatewayIPAddr: knownIP,
}
// Ensure the request is for unicast.
req.SetUnicast()
resp := &dhcpv4.DHCPv4{
Options: dhcpv4.OptionsFromList(
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
),
}
want := &net.UDPAddr{
IP: req.GatewayIPAddr,
Port: dhcpv4.ServerPort,
}
conn := &fakePacketConn{
writeTo: func(_ []byte, addr net.Addr) (n int, err error) {
assert.Equal(t, want, addr)
return 0, nil
},
}
s.send(cloneUDPAddr(defaultPeer), conn, req, resp)
assert.True(t, resp.IsBroadcast())
})
}
func TestV4Server_FindMACbyIP(t *testing.T) { func TestV4Server_FindMACbyIP(t *testing.T) {
const ( const (
staticName = "static-client" staticName = "static-client"

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"net/netip" "net/netip"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
) )
@@ -44,6 +45,9 @@ type clientJSON struct {
SafeSearchEnabled bool `json:"safesearch_enabled"` SafeSearchEnabled bool `json:"safesearch_enabled"`
UseGlobalBlockedServices bool `json:"use_global_blocked_services"` UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
UseGlobalSettings bool `json:"use_global_settings"` UseGlobalSettings bool `json:"use_global_settings"`
IgnoreQueryLog aghalg.NullBool `json:"ignore_querylog"`
IgnoreStatistics aghalg.NullBool `json:"ignore_statistics"`
} }
type runtimeClientJSON struct { type runtimeClientJSON struct {
@@ -90,7 +94,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
} }
// jsonToClient converts JSON object to Client object. // jsonToClient converts JSON object to Client object.
func (clients *clientsContainer) jsonToClient(cj clientJSON) (c *Client, err error) { func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *Client, err error) {
var safeSearchConf filtering.SafeSearchConfig var safeSearchConf filtering.SafeSearchConfig
if cj.SafeSearchConf != nil { if cj.SafeSearchConf != nil {
safeSearchConf = *cj.SafeSearchConf safeSearchConf = *cj.SafeSearchConf
@@ -129,6 +133,18 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON) (c *Client, err err
UseOwnBlockedServices: !cj.UseGlobalBlockedServices, UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
} }
if cj.IgnoreQueryLog != aghalg.NBNull {
c.IgnoreQueryLog = cj.IgnoreQueryLog == aghalg.NBTrue
} else if prev != nil {
c.IgnoreQueryLog = prev.IgnoreQueryLog
}
if cj.IgnoreStatistics != aghalg.NBNull {
c.IgnoreStatistics = cj.IgnoreStatistics == aghalg.NBTrue
} else if prev != nil {
c.IgnoreStatistics = prev.IgnoreStatistics
}
if safeSearchConf.Enabled { if safeSearchConf.Enabled {
err = c.setSafeSearch( err = c.setSafeSearch(
safeSearchConf, safeSearchConf,
@@ -165,6 +181,9 @@ func clientToJSON(c *Client) (cj *clientJSON) {
BlockedServices: c.BlockedServices, BlockedServices: c.BlockedServices,
Upstreams: c.Upstreams, Upstreams: c.Upstreams,
IgnoreQueryLog: aghalg.BoolToNullBool(c.IgnoreQueryLog),
IgnoreStatistics: aghalg.BoolToNullBool(c.IgnoreStatistics),
} }
} }
@@ -178,7 +197,7 @@ func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.
return return
} }
c, err := clients.jsonToClient(cj) c, err := clients.jsonToClient(cj, nil)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -232,6 +251,8 @@ type updateJSON struct {
} }
// handleUpdateClient is the handler for POST /control/clients/update HTTP API. // handleUpdateClient is the handler for POST /control/clients/update HTTP API.
//
// TODO(s.chzhen): Accept updated parameters instead of whole structure.
func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) { func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) {
dj := updateJSON{} dj := updateJSON{}
err := json.NewDecoder(r.Body).Decode(&dj) err := json.NewDecoder(r.Body).Decode(&dj)
@@ -247,7 +268,21 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
return return
} }
c, err := clients.jsonToClient(dj.Data) var prev *Client
var ok bool
func() {
clients.lock.Lock()
defer clients.lock.Unlock()
prev, ok = clients.list[dj.Name]
}()
if !ok {
aghhttp.Error(r, w, http.StatusBadRequest, "client not found")
}
c, err := clients.jsonToClient(dj.Data, prev)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err) aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)

View File

@@ -296,12 +296,26 @@ var config = &configuration{
MaxGoroutines: 300, MaxGoroutines: 300,
}, },
DnsfilterConf: &filtering.Config{ DnsfilterConf: &filtering.Config{
SafeBrowsingCacheSize: 1 * 1024 * 1024,
SafeSearchCacheSize: 1 * 1024 * 1024,
ParentalCacheSize: 1 * 1024 * 1024,
CacheTime: 30,
FilteringEnabled: true, FilteringEnabled: true,
FiltersUpdateIntervalHours: 24, FiltersUpdateIntervalHours: 24,
ParentalEnabled: false,
SafeBrowsingEnabled: false,
SafeBrowsingCacheSize: 1 * 1024 * 1024,
SafeSearchCacheSize: 1 * 1024 * 1024,
ParentalCacheSize: 1 * 1024 * 1024,
CacheTime: 30,
SafeSearchConf: filtering.SafeSearchConfig{
Enabled: false,
Bing: true,
DuckDuckGo: true,
Google: true,
Pixabay: true,
Yandex: true,
YouTube: true,
},
}, },
UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout}, UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
UsePrivateRDNS: true, UsePrivateRDNS: true,

View File

@@ -249,13 +249,15 @@ var cmdLineOpts = []cmdLineOpt{{
updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil }, updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil },
effect: func(_ options, _ string) (f effect, err error) { effect: func(_ options, _ string) (f effect, err error) {
log.Info( log.Info(
"warning: --no-etc-hosts flag is deprecated and will be removed in the future versions", "warning: --no-etc-hosts flag is deprecated " +
"and will be removed in the future versions; " +
"set clients.runtime_sources.hosts in the configuration file to false instead",
) )
return nil, nil return nil, nil
}, },
serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts }, serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts },
description: "Deprecated. Do not use the OS-provided hosts.", description: "Deprecated: use clients.runtime_sources.hosts instead. Do not use the OS-provided hosts.",
longName: "no-etc-hosts", longName: "no-etc-hosts",
shortName: "", shortName: "",
}, { }, {

View File

@@ -58,11 +58,11 @@ func (e *logEntry) addResponse(resp *dns.Msg, isOrig bool) {
var err error var err error
if isOrig { if isOrig {
e.Answer, err = resp.Pack()
err = errors.Annotate(err, "packing answer: %w")
} else {
e.OrigAnswer, err = resp.Pack() e.OrigAnswer, err = resp.Pack()
err = errors.Annotate(err, "packing orig answer: %w") err = errors.Annotate(err, "packing orig answer: %w")
} else {
e.Answer, err = resp.Pack()
err = errors.Annotate(err, "packing answer: %w")
} }
if err != nil { if err != nil {
log.Error("querylog: %s", err) log.Error("querylog: %s", err)

View File

@@ -288,6 +288,10 @@ func (l *queryLog) readNextEntry(
// Go on and try to match anyway. // Go on and try to match anyway.
} }
if e.client != nil && e.client.IgnoreQueryLog {
return nil, ts, nil
}
ts = e.Time.UnixNano() ts = e.Time.UnixNano()
if !params.match(e) { if !params.match(e) {
return nil, ts, nil return nil, ts, nil

View File

@@ -423,7 +423,7 @@ func (s *StatsCtx) getData(limit uint32) (StatsResp, bool) {
ReplacedParental: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RParental] }), ReplacedParental: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RParental] }),
TopQueried: topsCollector(units, maxDomains, s.ignored, func(u *unitDB) (pairs []countPair) { return u.Domains }), TopQueried: topsCollector(units, maxDomains, s.ignored, func(u *unitDB) (pairs []countPair) { return u.Domains }),
TopBlocked: topsCollector(units, maxDomains, s.ignored, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }), TopBlocked: topsCollector(units, maxDomains, s.ignored, func(u *unitDB) (pairs []countPair) { return u.BlockedDomains }),
TopClients: topsCollector(units, maxClients, nil, func(u *unitDB) (pairs []countPair) { return u.Clients }), TopClients: topsCollector(units, maxClients, nil, topClientPairs(s)),
} }
// Total counters: // Total counters:
@@ -460,3 +460,17 @@ func (s *StatsCtx) getData(limit uint32) (StatsResp, bool) {
return data, true return data, true
} }
func topClientPairs(s *StatsCtx) (pg pairsGetter) {
return func(u *unitDB) (clients []countPair) {
for _, c := range u.Clients {
if c.Name != "" && !s.shouldCountClient([]string{c.Name}) {
continue
}
clients = append(clients, c)
}
return clients
}
}

View File

@@ -4,6 +4,18 @@
## v0.108.0: API changes ## v0.108.0: API changes
## v0.107.29: API changes
### `GET /control/clients` And `GET /control/clients/find`
* The new optional fields `"ignore_querylog"` and `"ignore_statistics"` are set
if AdGuard Home excludes client activity from query log or statistics.
### `POST /control/clients/add` And `POST /control/clients/update`
* The new optional fields `"ignore_querylog"` and `"ignore_statistics"` make
AdGuard Home exclude client activity from query log or statistics. If not
set AdGuard Home will use default value (false). It can be changed in the
future versions.
## v0.107.27: API changes ## v0.107.27: API changes
### The new optional fields `"edns_cs_use_custom"` and `"edns_cs_custom_ip"` in `DNSConfig` ### The new optional fields `"edns_cs_use_custom"` and `"edns_cs_custom_ip"` in `DNSConfig`

View File

@@ -2494,6 +2494,26 @@
'items': 'items':
'type': 'string' 'type': 'string'
'type': 'array' 'type': 'array'
'ignore_querylog':
'description': |
NOTE: If `ignore_querylog` is not set in HTTP API `GET /clients/add`
request then default value (false) will be used.
If `ignore_querylog` is not set in HTTP API `GET /clients/update`
request then the existing value will not be changed.
This behaviour can be changed in the future versions.
'type': 'boolean'
'ignore_statistics':
'description': |
NOTE: If `ignore_statistics` is not set in HTTP API `GET
/clients/add` request then default value (false) will be used.
If `ignore_statistics` is not set in HTTP API `GET /clients/update`
request then the existing value will not be changed.
This behaviour can be changed in the future versions.
'type': 'boolean'
'ClientAuto': 'ClientAuto':
'type': 'object' 'type': 'object'
'description': 'Auto-Client information' 'description': 'Auto-Client information'
@@ -2547,6 +2567,8 @@
'whois_info': {} 'whois_info': {}
'disallowed': false 'disallowed': false
'disallowed_rule': '' 'disallowed_rule': ''
'ignore_querylog': false
'ignore_statistics': false
- '1.2.3.4': - '1.2.3.4':
'name': 'Client 1-2-3-4' 'name': 'Client 1-2-3-4'
'ids': ['1.2.3.4'] 'ids': ['1.2.3.4']
@@ -2562,6 +2584,8 @@
'whois_info': {} 'whois_info': {}
'disallowed': false 'disallowed': false
'disallowed_rule': '' 'disallowed_rule': ''
'ignore_querylog': false
'ignore_statistics': false
'AccessListResponse': 'AccessListResponse':
'$ref': '#/components/schemas/AccessList' '$ref': '#/components/schemas/AccessList'
'AccessSetRequest': 'AccessSetRequest':
@@ -2643,7 +2667,10 @@
set to true, and this string is empty, then the client IP is set to true, and this string is empty, then the client IP is
disallowed by the "allowed IP list", that is it is not included in disallowed by the "allowed IP list", that is it is not included in
the allowed list. the allowed list.
'ignore_querylog':
'type': 'boolean'
'ignore_statistics':
'type': 'boolean'
'WhoisInfo': 'WhoisInfo':
'type': 'object' 'type': 'object'
'additionalProperties': 'additionalProperties':

View File

@@ -3,7 +3,7 @@
# This comment is used to simplify checking local copies of the script. Bump # This comment is used to simplify checking local copies of the script. Bump
# this number every time a remarkable change is made to this script. # this number every time a remarkable change is made to this script.
# #
# AdGuard-Project-Version: 2 # AdGuard-Project-Version: 3
verbose="${VERBOSE:-0}" verbose="${VERBOSE:-0}"
readonly verbose readonly verbose
@@ -31,12 +31,11 @@ set -f -u
# trailing_newlines is a simple check that makes sure that all plain-text files # trailing_newlines is a simple check that makes sure that all plain-text files
# have a trailing newlines to make sure that all tools work correctly with them. # have a trailing newlines to make sure that all tools work correctly with them.
#
# TODO(a.garipov): Add to the standard skeleton project.
trailing_newlines() { trailing_newlines() {
nl="$( printf "\n" )" nl="$( printf "\n" )"
readonly nl readonly nl
# NOTE: Adjust for your project.
git ls-files\ git ls-files\
':!*.png'\ ':!*.png'\
':!*.tar.gz'\ ':!*.tar.gz'\