Compare commits
14 Commits
v0.108.0-b
...
AG-21485
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
521aedc5bc | ||
|
|
1842f7d888 | ||
|
|
40ff26ea21 | ||
|
|
4afd39b22f | ||
|
|
e43ba17884 | ||
|
|
6d402dc86c | ||
|
|
18acdf9b09 | ||
|
|
5da7751463 | ||
|
|
7631ca4ab3 | ||
|
|
c6d4f2317e | ||
|
|
0ea224a9e4 | ||
|
|
d78a3edb22 | ||
|
|
bbbdea2635 | ||
|
|
6c8d89a4da |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -23,6 +23,26 @@ See also the [v0.107.29 GitHub milestone][ms-v0.107.29].
|
||||
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.
|
||||
-->
|
||||
@@ -149,12 +169,12 @@ In this release, the schema version has changed from 17 to 20.
|
||||
|
||||
[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163
|
||||
[#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333
|
||||
[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1717
|
||||
[#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472
|
||||
[#1717]: https://github.com/AdguardTeam/AdGuardHome/issues/1717
|
||||
[#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290
|
||||
[#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459
|
||||
[#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
|
||||
[#5701]: https://github.com/AdguardTeam/AdGuardHome/issues/5701
|
||||
|
||||
|
||||
@@ -12,11 +12,40 @@
|
||||
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279">
|
||||
<link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48">
|
||||
<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>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</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>
|
||||
</html>
|
||||
|
||||
@@ -17,5 +17,12 @@
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<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>
|
||||
</html>
|
||||
|
||||
@@ -257,12 +257,12 @@
|
||||
"query_log_cleared": "The query log has been successfully cleared",
|
||||
"query_log_updated": "The query log has been successfully updated",
|
||||
"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_configuration": "Logs configuration",
|
||||
"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_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_desc": "Don't save the client's full IP address to logs or statistics",
|
||||
"dns_config": "DNS server configuration",
|
||||
@@ -668,5 +668,11 @@
|
||||
"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}}"
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -2,21 +2,6 @@ import { createAction } from 'redux-actions';
|
||||
import apiClient from '../api/Api';
|
||||
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 getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE');
|
||||
export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS');
|
||||
|
||||
@@ -479,19 +479,12 @@ class Api {
|
||||
}
|
||||
|
||||
// Blocked services
|
||||
BLOCKED_SERVICES_SERVICES = { path: 'blocked_services/services', method: 'GET' };
|
||||
|
||||
BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' };
|
||||
|
||||
BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
|
||||
|
||||
BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };
|
||||
|
||||
getBlockedServicesAvailableServices() {
|
||||
const { path, method } = this.BLOCKED_SERVICES_SERVICES;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
|
||||
getAllBlockedServices() {
|
||||
const { path, method } = this.BLOCKED_SERVICES_ALL;
|
||||
return this.makeRequest(path, method);
|
||||
|
||||
@@ -41,6 +41,17 @@ const settingsCheckboxes = [
|
||||
placeholder: 'use_adguard_parental',
|
||||
},
|
||||
];
|
||||
|
||||
const logAndStatsCheckboxes = [
|
||||
{
|
||||
name: 'ignore_querylog',
|
||||
placeholder: 'ignore_query_log',
|
||||
},
|
||||
{
|
||||
name: 'ignore_statistics',
|
||||
placeholder: 'ignore_statistics',
|
||||
},
|
||||
];
|
||||
const validate = (values) => {
|
||||
const errors = {};
|
||||
const { name, ids } = values;
|
||||
@@ -148,6 +159,9 @@ let Form = (props) => {
|
||||
settings: {
|
||||
title: '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) => (
|
||||
<div className="form__group" key={setting.name}>
|
||||
<Field
|
||||
@@ -185,6 +199,19 @@ let Form = (props) => {
|
||||
</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>,
|
||||
},
|
||||
block_services: {
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
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 flow from 'lodash/flow';
|
||||
|
||||
import {
|
||||
CheckboxField,
|
||||
renderRadioField,
|
||||
toFloatNumber,
|
||||
renderTextareaField,
|
||||
renderTextareaField, renderInputField, renderRadioField,
|
||||
} from '../../../helpers/form';
|
||||
import {
|
||||
FORM_NAME,
|
||||
QUERY_LOG_INTERVALS_DAYS,
|
||||
HOUR,
|
||||
DAY,
|
||||
RETENTION_CUSTOM,
|
||||
RETENTION_CUSTOM_INPUT,
|
||||
RETENTION_RANGE,
|
||||
CUSTOM_INTERVAL,
|
||||
} from '../../../helpers/constants';
|
||||
import '../FormButton.css';
|
||||
|
||||
|
||||
const getIntervalTitle = (interval, t) => {
|
||||
switch (interval) {
|
||||
case RETENTION_CUSTOM:
|
||||
return t('settings_custom');
|
||||
case 6 * HOUR:
|
||||
return t('interval_6_hour');
|
||||
case DAY:
|
||||
@@ -42,11 +54,26 @@ const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS.
|
||||
/>
|
||||
));
|
||||
|
||||
const Form = (props) => {
|
||||
let Form = (props) => {
|
||||
const {
|
||||
handleSubmit, submitting, invalid, processing, processingClear, handleClear, t,
|
||||
handleSubmit,
|
||||
submitting,
|
||||
invalid,
|
||||
processing,
|
||||
processingClear,
|
||||
handleClear,
|
||||
t,
|
||||
interval,
|
||||
customInterval,
|
||||
dispatch,
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (QUERY_LOG_INTERVALS_DAYS.includes(interval)) {
|
||||
dispatch(change(FORM_NAME.LOG_CONFIG, CUSTOM_INTERVAL, null));
|
||||
}
|
||||
}, [interval]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form__group form__group--settings">
|
||||
@@ -73,6 +100,37 @@ const Form = (props) => {
|
||||
</label>
|
||||
<div className="form__group form__group--settings">
|
||||
<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)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,7 +154,12 @@ const Form = (props) => {
|
||||
<button
|
||||
type="submit"
|
||||
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>
|
||||
</button>
|
||||
@@ -121,8 +184,22 @@ Form.propTypes = {
|
||||
processing: PropTypes.bool.isRequired,
|
||||
processingClear: PropTypes.bool.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([
|
||||
withTranslation(),
|
||||
reduxForm({ form: FORM_NAME.LOG_CONFIG }),
|
||||
|
||||
@@ -4,15 +4,22 @@ import { withTranslation } from 'react-i18next';
|
||||
|
||||
import Card from '../../ui/Card';
|
||||
import Form from './Form';
|
||||
import { HOUR } from '../../../helpers/constants';
|
||||
|
||||
class LogsConfig extends Component {
|
||||
handleFormSubmit = (values) => {
|
||||
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
|
||||
if (window.confirm(t('query_log_retention_confirm'))) {
|
||||
this.props.setLogsConfig(data);
|
||||
@@ -32,7 +39,14 @@ class LogsConfig extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
t, enabled, interval, processing, processingClear, anonymize_client_ip, ignored,
|
||||
t,
|
||||
enabled,
|
||||
interval,
|
||||
processing,
|
||||
processingClear,
|
||||
anonymize_client_ip,
|
||||
ignored,
|
||||
customInterval,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -46,6 +60,7 @@ class LogsConfig extends Component {
|
||||
initialValues={{
|
||||
enabled,
|
||||
interval,
|
||||
customInterval,
|
||||
anonymize_client_ip,
|
||||
ignored: ignored.join('\n'),
|
||||
}}
|
||||
@@ -62,6 +77,7 @@ class LogsConfig extends Component {
|
||||
|
||||
LogsConfig.propTypes = {
|
||||
interval: PropTypes.number.isRequired,
|
||||
customInterval: PropTypes.number,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
anonymize_client_ip: PropTypes.bool.isRequired,
|
||||
processing: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form__group--input {
|
||||
max-width: 300px;
|
||||
margin: 0 1.5rem 10px;
|
||||
}
|
||||
|
||||
.form__group--checkbox {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
@@ -100,6 +105,14 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form__label--bot {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form__label--top {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.form__status {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
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 flow from 'lodash/flow';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import {
|
||||
renderRadioField,
|
||||
toNumber,
|
||||
CheckboxField,
|
||||
renderTextareaField,
|
||||
toFloatNumber,
|
||||
renderInputField,
|
||||
} from '../../../helpers/form';
|
||||
import {
|
||||
FORM_NAME,
|
||||
STATS_INTERVALS_DAYS,
|
||||
DAY,
|
||||
RETENTION_CUSTOM,
|
||||
RETENTION_CUSTOM_INPUT,
|
||||
CUSTOM_INTERVAL,
|
||||
RETENTION_RANGE,
|
||||
} from '../../../helpers/constants';
|
||||
import '../FormButton.css';
|
||||
|
||||
const getIntervalTitle = (intervalMs, t) => {
|
||||
switch (intervalMs / DAY) {
|
||||
case 1:
|
||||
switch (intervalMs) {
|
||||
case RETENTION_CUSTOM:
|
||||
return t('settings_custom');
|
||||
case DAY:
|
||||
return t('interval_24_hour');
|
||||
default:
|
||||
return t('interval_days', { count: intervalMs / DAY });
|
||||
}
|
||||
};
|
||||
|
||||
const Form = (props) => {
|
||||
let Form = (props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
processing,
|
||||
@@ -35,8 +47,17 @@ const Form = (props) => {
|
||||
handleReset,
|
||||
processingReset,
|
||||
t,
|
||||
interval,
|
||||
customInterval,
|
||||
dispatch,
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (STATS_INTERVALS_DAYS.includes(interval)) {
|
||||
dispatch(change(FORM_NAME.STATS_CONFIG, CUSTOM_INTERVAL, null));
|
||||
}
|
||||
}, [interval]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form__group form__group--settings">
|
||||
@@ -56,6 +77,37 @@ const Form = (props) => {
|
||||
</div>
|
||||
<div className="form__group form__group--settings mt-2">
|
||||
<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) => (
|
||||
<Field
|
||||
key={interval}
|
||||
@@ -90,7 +142,12 @@ const Form = (props) => {
|
||||
<button
|
||||
type="submit"
|
||||
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>
|
||||
</button>
|
||||
@@ -116,8 +173,22 @@ Form.propTypes = {
|
||||
processing: PropTypes.bool.isRequired,
|
||||
processingReset: PropTypes.bool.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([
|
||||
withTranslation(),
|
||||
reduxForm({ form: FORM_NAME.STATS_CONFIG }),
|
||||
|
||||
@@ -4,13 +4,18 @@ import { withTranslation } from 'react-i18next';
|
||||
|
||||
import Card from '../../ui/Card';
|
||||
import Form from './Form';
|
||||
import { HOUR } from '../../../helpers/constants';
|
||||
|
||||
class StatsConfig extends Component {
|
||||
handleFormSubmit = ({ enabled, interval, ignored }) => {
|
||||
handleFormSubmit = ({
|
||||
enabled, interval, ignored, customInterval,
|
||||
}) => {
|
||||
const { t, interval: prevInterval } = this.props;
|
||||
const newInterval = customInterval ? customInterval * HOUR : interval;
|
||||
|
||||
const config = {
|
||||
enabled,
|
||||
interval,
|
||||
interval: newInterval,
|
||||
ignored: ignored ? ignored.split('\n') : [],
|
||||
};
|
||||
|
||||
@@ -33,7 +38,13 @@ class StatsConfig extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
t, interval, processing, processingReset, ignored, enabled,
|
||||
t,
|
||||
interval,
|
||||
customInterval,
|
||||
processing,
|
||||
processingReset,
|
||||
ignored,
|
||||
enabled,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -46,6 +57,7 @@ class StatsConfig extends Component {
|
||||
<Form
|
||||
initialValues={{
|
||||
interval,
|
||||
customInterval,
|
||||
enabled,
|
||||
ignored: ignored.join('\n'),
|
||||
}}
|
||||
@@ -62,6 +74,7 @@ class StatsConfig extends Component {
|
||||
|
||||
StatsConfig.propTypes = {
|
||||
interval: PropTypes.number.isRequired,
|
||||
customInterval: PropTypes.number,
|
||||
ignored: PropTypes.array.isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
processing: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -124,6 +124,7 @@ class Settings extends Component {
|
||||
enabled={queryLogs.enabled}
|
||||
ignored={queryLogs.ignored}
|
||||
interval={queryLogs.interval}
|
||||
customInterval={queryLogs.customInterval}
|
||||
anonymize_client_ip={queryLogs.anonymize_client_ip}
|
||||
processing={queryLogs.processingSetConfig}
|
||||
processingClear={queryLogs.processingClear}
|
||||
@@ -134,6 +135,7 @@ class Settings extends Component {
|
||||
<div className="col-md-12">
|
||||
<StatsConfig
|
||||
interval={stats.interval}
|
||||
customInterval={stats.customInterval}
|
||||
ignored={stats.ignored}
|
||||
enabled={stats.enabled}
|
||||
processing={stats.processingSetConfig}
|
||||
@@ -166,6 +168,7 @@ Settings.propTypes = {
|
||||
stats: PropTypes.shape({
|
||||
processingGetConfig: PropTypes.bool,
|
||||
interval: PropTypes.number,
|
||||
customInterval: PropTypes.number,
|
||||
enabled: PropTypes.bool,
|
||||
ignored: PropTypes.array,
|
||||
processingSetConfig: PropTypes.bool,
|
||||
@@ -174,6 +177,7 @@ Settings.propTypes = {
|
||||
queryLogs: PropTypes.shape({
|
||||
enabled: PropTypes.bool,
|
||||
interval: PropTypes.number,
|
||||
customInterval: PropTypes.number,
|
||||
anonymize_client_ip: PropTypes.bool,
|
||||
processingSetConfig: PropTypes.bool,
|
||||
processingClear: PropTypes.bool,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
@@ -42,12 +42,6 @@ const Footer = () => {
|
||||
const isLoggedIn = profileName !== '';
|
||||
const [currentThemeLocal, setCurrentThemeLocal] = useState(THEMES.auto);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
setUITheme(currentThemeLocal);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getYear = () => {
|
||||
const today = new Date();
|
||||
return today.getFullYear();
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
background-color: var(--rt-nodata-bgcolor);
|
||||
}
|
||||
|
||||
.overlay--visible {
|
||||
|
||||
@@ -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 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];
|
||||
|
||||
// Note that translation strings contain these modes (blocking_mode_CONSTANT)
|
||||
@@ -462,6 +468,11 @@ export const UINT32_RANGE = {
|
||||
MAX: 4294967295,
|
||||
};
|
||||
|
||||
export const RETENTION_RANGE = {
|
||||
MIN: 1,
|
||||
MAX: 365 * 24,
|
||||
};
|
||||
|
||||
export const DHCP_VALUES_PLACEHOLDERS = {
|
||||
ipv4: {
|
||||
subnet_mask: '255.255.255.0',
|
||||
@@ -537,3 +548,5 @@ export const DISABLE_PROTECTION_TIMINGS = {
|
||||
HOUR: 60 * 60 * 1000,
|
||||
TOMORROW: 24 * 60 * 60 * 1000,
|
||||
};
|
||||
|
||||
export const LOCAL_STORAGE_THEME_KEY = 'account_theme';
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
STANDARD_WEB_PORT,
|
||||
SPECIAL_FILTER_ID,
|
||||
THEMES,
|
||||
LOCAL_STORAGE_THEME_KEY,
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
@@ -679,19 +680,60 @@ export const setHtmlLangAttr = (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.
|
||||
*
|
||||
* @param theme
|
||||
*/
|
||||
export const setUITheme = (theme) => {
|
||||
let currentTheme = theme;
|
||||
let currentTheme = theme || getTheme();
|
||||
|
||||
if (currentTheme === THEMES.auto) {
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
currentTheme = prefersDark ? THEMES.dark : THEMES.light;
|
||||
}
|
||||
|
||||
setTheme(currentTheme);
|
||||
document.body.dataset.theme = currentTheme;
|
||||
};
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ const dashboard = handleActions(
|
||||
autoClients: [],
|
||||
supportedTags: [],
|
||||
name: '',
|
||||
theme: 'auto',
|
||||
theme: undefined,
|
||||
checkUpdateFlag: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
|
||||
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(
|
||||
{
|
||||
@@ -59,6 +61,9 @@ const queryLogs = handleActions(
|
||||
[actions.getLogsConfigSuccess]: (state, { payload }) => ({
|
||||
...state,
|
||||
...payload,
|
||||
customInterval: !QUERY_LOG_INTERVALS_DAYS.includes(payload.interval)
|
||||
? payload.interval / HOUR
|
||||
: null,
|
||||
processingGetConfig: false,
|
||||
}),
|
||||
|
||||
@@ -95,6 +100,7 @@ const queryLogs = handleActions(
|
||||
anonymize_client_ip: false,
|
||||
isDetailed: true,
|
||||
isEntireLog: false,
|
||||
customInterval: null,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
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';
|
||||
|
||||
@@ -27,6 +27,9 @@ const stats = handleActions(
|
||||
[actions.getStatsConfigSuccess]: (state, { payload }) => ({
|
||||
...state,
|
||||
...payload,
|
||||
customInterval: !STATS_INTERVALS_DAYS.includes(payload.interval)
|
||||
? payload.interval / HOUR
|
||||
: null,
|
||||
processingGetConfig: false,
|
||||
}),
|
||||
|
||||
@@ -93,6 +96,7 @@ const stats = handleActions(
|
||||
processingStats: true,
|
||||
processingReset: false,
|
||||
interval: DAY,
|
||||
customInterval: null,
|
||||
...defaultStats,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,19 +4,27 @@
|
||||
|
||||
/^[[:space:]]+- .+/ {
|
||||
if (FNR - prev_line == 1) {
|
||||
addrs[addrsnum++] = $2
|
||||
addrs[$2] = true
|
||||
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 }
|
||||
|
||||
END {
|
||||
for (i in addrs) {
|
||||
if (match(addrs[i], ":")) {
|
||||
print "[" addrs[i] "]:" port
|
||||
for (addr in addrs) {
|
||||
if (match(addr, ":")) {
|
||||
print "[" addr "]:" port
|
||||
} else {
|
||||
print addrs[i] ":" port
|
||||
print addr ":" port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
go.mod
6
go.mod
@@ -7,7 +7,7 @@ require (
|
||||
github.com/AdguardTeam/golibs v0.13.2
|
||||
github.com/AdguardTeam/urlfilter v0.16.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/dimfeld/httptreemux/v5 v5.5.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/netlink v1.7.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/quic-go/quic-go v0.33.0
|
||||
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/golang/mock v1.6.0 // 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/onsi/ginkgo/v2 v2.9.2 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -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/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
|
||||
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.6/go.mod h1:qPWhwz6FdSmuK7W4sMyvogrez4MWdtzosdqlr0Rg3ow=
|
||||
github.com/ameshkov/dnscrypt/v2 v2.2.7 h1:aEitLIR8HcxVodZ79mgRcCiC0A0I5kZPBuWGFwwulAw=
|
||||
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/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
|
||||
|
||||
293
internal/dhcpd/conn_darwin.go
Normal file
293
internal/dhcpd/conn_darwin.go
Normal 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)
|
||||
}
|
||||
}
|
||||
219
internal/dhcpd/conn_darwin_internal_test.go
Normal file
219
internal/dhcpd/conn_darwin_internal_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
//go:build freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
@@ -238,3 +239,53 @@ func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []b
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
//go:build freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
@@ -110,3 +110,108 @@ func TestBuildEtherPkt(t *testing.T) {
|
||||
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())
|
||||
})
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/go-ping/ping"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
||||
"github.com/mdlayher/packet"
|
||||
"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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (s *v4Server) Start() (err error) {
|
||||
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/mdlayher/packet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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)
|
||||
}
|
||||
|
||||
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) {
|
||||
const (
|
||||
staticName = "static-client"
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
)
|
||||
@@ -44,6 +45,9 @@ type clientJSON struct {
|
||||
SafeSearchEnabled bool `json:"safesearch_enabled"`
|
||||
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
|
||||
UseGlobalSettings bool `json:"use_global_settings"`
|
||||
|
||||
IgnoreQueryLog aghalg.NullBool `json:"ignore_querylog"`
|
||||
IgnoreStatistics aghalg.NullBool `json:"ignore_statistics"`
|
||||
}
|
||||
|
||||
type runtimeClientJSON struct {
|
||||
@@ -90,7 +94,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// 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
|
||||
if cj.SafeSearchConf != nil {
|
||||
safeSearchConf = *cj.SafeSearchConf
|
||||
@@ -129,6 +133,18 @@ func (clients *clientsContainer) jsonToClient(cj clientJSON) (c *Client, err err
|
||||
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 {
|
||||
err = c.setSafeSearch(
|
||||
safeSearchConf,
|
||||
@@ -165,6 +181,9 @@ func clientToJSON(c *Client) (cj *clientJSON) {
|
||||
BlockedServices: c.BlockedServices,
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
c, err := clients.jsonToClient(cj)
|
||||
c, err := clients.jsonToClient(cj, nil)
|
||||
if err != nil {
|
||||
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.
|
||||
//
|
||||
// TODO(s.chzhen): Accept updated parameters instead of whole structure.
|
||||
func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
dj := updateJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&dj)
|
||||
@@ -247,7 +268,21 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
|
||||
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 {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||
|
||||
|
||||
@@ -296,12 +296,26 @@ var config = &configuration{
|
||||
MaxGoroutines: 300,
|
||||
},
|
||||
DnsfilterConf: &filtering.Config{
|
||||
SafeBrowsingCacheSize: 1 * 1024 * 1024,
|
||||
SafeSearchCacheSize: 1 * 1024 * 1024,
|
||||
ParentalCacheSize: 1 * 1024 * 1024,
|
||||
CacheTime: 30,
|
||||
FilteringEnabled: true,
|
||||
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},
|
||||
UsePrivateRDNS: true,
|
||||
|
||||
@@ -249,13 +249,15 @@ var cmdLineOpts = []cmdLineOpt{{
|
||||
updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil },
|
||||
effect: func(_ options, _ string) (f effect, err error) {
|
||||
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
|
||||
},
|
||||
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",
|
||||
shortName: "",
|
||||
}, {
|
||||
|
||||
@@ -58,11 +58,11 @@ func (e *logEntry) addResponse(resp *dns.Msg, isOrig bool) {
|
||||
|
||||
var err error
|
||||
if isOrig {
|
||||
e.Answer, err = resp.Pack()
|
||||
err = errors.Annotate(err, "packing answer: %w")
|
||||
} else {
|
||||
e.OrigAnswer, err = resp.Pack()
|
||||
err = errors.Annotate(err, "packing orig answer: %w")
|
||||
} else {
|
||||
e.Answer, err = resp.Pack()
|
||||
err = errors.Annotate(err, "packing answer: %w")
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("querylog: %s", err)
|
||||
|
||||
@@ -288,6 +288,10 @@ func (l *queryLog) readNextEntry(
|
||||
// Go on and try to match anyway.
|
||||
}
|
||||
|
||||
if e.client != nil && e.client.IgnoreQueryLog {
|
||||
return nil, ts, nil
|
||||
}
|
||||
|
||||
ts = e.Time.UnixNano()
|
||||
if !params.match(e) {
|
||||
return nil, ts, nil
|
||||
|
||||
@@ -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] }),
|
||||
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 }),
|
||||
TopClients: topsCollector(units, maxClients, nil, func(u *unitDB) (pairs []countPair) { return u.Clients }),
|
||||
TopClients: topsCollector(units, maxClients, nil, topClientPairs(s)),
|
||||
}
|
||||
|
||||
// Total counters:
|
||||
@@ -460,3 +460,17 @@ func (s *StatsCtx) getData(limit uint32) (StatsResp, bool) {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,18 @@
|
||||
|
||||
## 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
|
||||
|
||||
### The new optional fields `"edns_cs_use_custom"` and `"edns_cs_custom_ip"` in `DNSConfig`
|
||||
|
||||
@@ -2494,6 +2494,26 @@
|
||||
'items':
|
||||
'type': 'string'
|
||||
'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':
|
||||
'type': 'object'
|
||||
'description': 'Auto-Client information'
|
||||
@@ -2547,6 +2567,8 @@
|
||||
'whois_info': {}
|
||||
'disallowed': false
|
||||
'disallowed_rule': ''
|
||||
'ignore_querylog': false
|
||||
'ignore_statistics': false
|
||||
- '1.2.3.4':
|
||||
'name': 'Client 1-2-3-4'
|
||||
'ids': ['1.2.3.4']
|
||||
@@ -2562,6 +2584,8 @@
|
||||
'whois_info': {}
|
||||
'disallowed': false
|
||||
'disallowed_rule': ''
|
||||
'ignore_querylog': false
|
||||
'ignore_statistics': false
|
||||
'AccessListResponse':
|
||||
'$ref': '#/components/schemas/AccessList'
|
||||
'AccessSetRequest':
|
||||
@@ -2643,7 +2667,10 @@
|
||||
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
|
||||
the allowed list.
|
||||
|
||||
'ignore_querylog':
|
||||
'type': 'boolean'
|
||||
'ignore_statistics':
|
||||
'type': 'boolean'
|
||||
'WhoisInfo':
|
||||
'type': 'object'
|
||||
'additionalProperties':
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# 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.
|
||||
#
|
||||
# AdGuard-Project-Version: 2
|
||||
# AdGuard-Project-Version: 3
|
||||
|
||||
verbose="${VERBOSE:-0}"
|
||||
readonly verbose
|
||||
@@ -31,12 +31,11 @@ set -f -u
|
||||
|
||||
# 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.
|
||||
#
|
||||
# TODO(a.garipov): Add to the standard skeleton project.
|
||||
trailing_newlines() {
|
||||
nl="$( printf "\n" )"
|
||||
readonly nl
|
||||
|
||||
# NOTE: Adjust for your project.
|
||||
git ls-files\
|
||||
':!*.png'\
|
||||
':!*.tar.gz'\
|
||||
|
||||
Reference in New Issue
Block a user