Compare commits
32 Commits
v0.108.0-b
...
2926-lla-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de837e4eec | ||
|
|
e528d2f23b | ||
|
|
47c9c946a3 | ||
|
|
11e4f09165 | ||
|
|
c45c02de29 | ||
|
|
4fc045de11 | ||
|
|
cc2388e0c8 | ||
|
|
ab6da05b51 | ||
|
|
8e89cc129c | ||
|
|
9ffe078703 | ||
|
|
27b0251b5b | ||
|
|
ed209daf8a | ||
|
|
95771c7aba | ||
|
|
42bd0615c2 | ||
|
|
3a88ef3be2 | ||
|
|
572fed9f35 | ||
|
|
663f0643f2 | ||
|
|
fc62796e2d | ||
|
|
b9e39c8cca | ||
|
|
fffa656758 | ||
|
|
b74b92fc27 | ||
|
|
bc1503af57 | ||
|
|
b79c08316f | ||
|
|
08799e9d0a | ||
|
|
bedfb47a9f | ||
|
|
53e2c1f7cd | ||
|
|
88812f05f5 | ||
|
|
10a8f79644 | ||
|
|
ccc4f1a2da | ||
|
|
451fd7c445 | ||
|
|
782de99a0a | ||
|
|
d4afd60b08 |
22
.github/stale.yml
vendored
22
.github/stale.yml
vendored
@@ -4,15 +4,17 @@
|
||||
'daysUntilClose': 15
|
||||
# Issues with these labels will never be considered stale.
|
||||
'exemptLabels':
|
||||
- 'bug'
|
||||
- 'documentation'
|
||||
- 'enhancement'
|
||||
- 'feature request'
|
||||
- 'help wanted'
|
||||
- 'localization'
|
||||
- 'needs investigation'
|
||||
- 'recurrent'
|
||||
- 'research'
|
||||
- 'bug'
|
||||
- 'documentation'
|
||||
- 'enhancement'
|
||||
- 'feature request'
|
||||
- 'help wanted'
|
||||
- 'localization'
|
||||
- 'needs investigation'
|
||||
- 'recurrent'
|
||||
- 'research'
|
||||
# Set to true to ignore issues in a milestone.
|
||||
'exemptMilestones': true
|
||||
# Label to use when marking an issue as stale.
|
||||
'staleLabel': 'wontfix'
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable.
|
||||
@@ -22,3 +24,5 @@
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable.
|
||||
'closeComment': false
|
||||
# Limit the number of actions per hour.
|
||||
'limitPerRun': 1
|
||||
|
||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'build'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.18'
|
||||
'GO_VERSION': '1.18.6'
|
||||
'NODE_VERSION': '14'
|
||||
|
||||
'on':
|
||||
@@ -31,7 +31,7 @@
|
||||
'with':
|
||||
'fetch-depth': 0
|
||||
- 'name': 'Set up Go'
|
||||
'uses': 'actions/setup-go@v2'
|
||||
'uses': 'actions/setup-go@v3'
|
||||
'with':
|
||||
'go-version': '${{ env.GO_VERSION }}'
|
||||
- 'name': 'Set up Node'
|
||||
@@ -72,7 +72,7 @@
|
||||
'with':
|
||||
'fetch-depth': 0
|
||||
- 'name': 'Set up Go'
|
||||
'uses': 'actions/setup-go@v2'
|
||||
'uses': 'actions/setup-go@v3'
|
||||
'with':
|
||||
'go-version': '${{ env.GO_VERSION }}'
|
||||
- 'name': 'Set up Node'
|
||||
@@ -112,7 +112,9 @@
|
||||
# Use always() to signal to the runner that this job must run even if the
|
||||
# previous ones failed.
|
||||
'if':
|
||||
${{ always() &&
|
||||
${{
|
||||
always() &&
|
||||
github.repository_owner == 'AdguardTeam' &&
|
||||
(
|
||||
github.event_name == 'push' ||
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'lint'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.18'
|
||||
'GO_VERSION': '1.18.6'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
@@ -17,7 +17,7 @@
|
||||
'steps':
|
||||
- 'uses': 'actions/checkout@v2'
|
||||
- 'name': 'Set up Go'
|
||||
'uses': 'actions/setup-go@v2'
|
||||
'uses': 'actions/setup-go@v3'
|
||||
'with':
|
||||
'go-version': '${{ env.GO_VERSION }}'
|
||||
- 'name': 'run-lint'
|
||||
@@ -43,7 +43,9 @@
|
||||
# Use always() to signal to the runner that this job must run even if the
|
||||
# previous ones failed.
|
||||
'if':
|
||||
${{ always() &&
|
||||
${{
|
||||
always() &&
|
||||
github.repository_owner == 'AdguardTeam' &&
|
||||
(
|
||||
github.event_name == 'push' ||
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -20,18 +20,56 @@ and this project adheres to
|
||||
- Weaker cipher suites that use the CBC (cipher block chaining) mode of
|
||||
operation have been disabled ([#2993]).
|
||||
|
||||
### Added
|
||||
|
||||
- Support for plain (unencrypted) HTTP/2 ([#4930]). This is useful for AdGuard
|
||||
Home installations behind a reverse proxy.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Incorrect path template in DDR responses ([#4927]).
|
||||
|
||||
[#2993]: https://github.com/AdguardTeam/AdGuardHome/issues/2993
|
||||
[#4927]: https://github.com/AdguardTeam/AdGuardHome/issues/4927
|
||||
[#4930]: https://github.com/AdguardTeam/AdGuardHome/issues/4930
|
||||
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
## [v0.107.13] - 2022-10-05 (APPROX.)
|
||||
## [v0.107.14] - 2022-10-05 (APPROX.)
|
||||
|
||||
See also the [v0.107.14 GitHub milestone][ms-v0.107.14].
|
||||
|
||||
[ms-v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/milestone/50?closed=1
|
||||
-->
|
||||
|
||||
|
||||
|
||||
## [v0.107.13] - 2022-09-14
|
||||
|
||||
See also the [v0.107.13 GitHub milestone][ms-v0.107.13].
|
||||
|
||||
### Added
|
||||
|
||||
- The new optional `dns.ipset_file` property, which can be set in the
|
||||
configuration file. It allows loading the `ipset` list from a file, just like
|
||||
`dns.upstream_dns_file` does for upstream servers ([#4686]).
|
||||
|
||||
### Changed
|
||||
|
||||
- The minimum DHCP message size is reassigned back to BOOTP's constraint of 300
|
||||
bytes ([#4904]).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Panic when adding a static lease within the disabled DHCP server ([#4722]).
|
||||
|
||||
[#4686]: https://github.com/AdguardTeam/AdGuardHome/issues/4686
|
||||
[#4722]: https://github.com/AdguardTeam/AdGuardHome/issues/4722
|
||||
[#4904]: https://github.com/AdguardTeam/AdGuardHome/issues/4904
|
||||
|
||||
[ms-v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/milestone/49?closed=1
|
||||
-->
|
||||
|
||||
|
||||
|
||||
@@ -1203,11 +1241,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||
|
||||
|
||||
<!--
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...HEAD
|
||||
[v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.11...v0.107.13
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...HEAD
|
||||
[v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
|
||||
-->
|
||||
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.12...HEAD
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...HEAD
|
||||
[v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.12...v0.107.13
|
||||
[v0.107.12]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.11...v0.107.12
|
||||
[v0.107.11]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.10...v0.107.11
|
||||
[v0.107.10]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.9...v0.107.10
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"dhcp_warning": "Калі вы ўсё адно хочаце ўключыць DHCP-сервер, пераканайцеся, што ў сеціве больш няма актыўных DHCP-сервераў. Інакш гэта можа зламаць доступ у сеціва для падлучаных прылад!",
|
||||
"dhcp_error": "AdGuard Home не можа вызначыць, ці ёсць у сетцы іншы актыўны DHCP-сервер",
|
||||
"dhcp_static_ip_error": "Для таго, каб выкарыстоўваць DHCP-сервер, павінен быць усталяваны статычны IP-адрас. Мы не змаглі вызначыць, ці выкарыстоўвае гэты інтэрфейс сеціва статычны IP-адрас. Калі ласка, усталюйце яго ручна.",
|
||||
"dhcp_dynamic_ip_found": "Ваша сістэма выкарыстоўвае дынамічны IP-адрас для інтэрфейсу <0>{{interfaceName}}</0>. Каб выкарыстоўваць DHCP-сервер трэба ўсталяваць статычны IP-адрас. Ваш бягучы IP-адрас – <0>{{ipAddress}}</0>. Мы аўтаматычна ўсталюем яго як статычны, калі вы націснеце кнопку Ўключыць DHCP.",
|
||||
"dhcp_dynamic_ip_found": "Ваша сістэма выкарыстоўвае дынамічны IP-адрас для інтэрфейсу <0>{{interfaceName}}</0>. Каб выкарыстоўваць DHCP-сервер трэба ўсталяваць статычны IP-адрас. Ваш бягучы IP-адрас – <0>{{ipAddress}}</0>. Мы аўтаматычна ўсталюем яго як статычны, калі вы націснеце кнопку «Ўключыць DHCP».",
|
||||
"dhcp_lease_added": "Статычная арэнда «{{key}}» паспяхова дададзена",
|
||||
"dhcp_lease_deleted": "Статычная арэнда «{{key}}» паспяхова выдалена",
|
||||
"dhcp_new_static_lease": "Новая статычная арэнда",
|
||||
@@ -447,7 +447,7 @@
|
||||
"access_disallowed_title": "Забароненыя кліенты",
|
||||
"access_disallowed_desc": "Спіс CIDR, IP-адрасоў або <a>ClientID</a>. Калі ў гэтым спісе ёсць запісы, AdGuard Home выдаліць запыты ад гэтых кліентаў. Гэта поле ігнаруецца, калі ёсць запісы ў Дазволеныя кліенты.",
|
||||
"access_blocked_title": "Заблакаваныя дамены",
|
||||
"access_blocked_desc": "Не блытайце гэта з фільтрамі. AdGuard Home будзе ігнараваць DNS-запыты з гэтымі даменамі.",
|
||||
"access_blocked_desc": "Не блытаць з фільтрамі. AdGuard Home выдаляе запыты DNS, якія адпавядаюць гэтым даменам, і гэтыя запыты нават не з'яўляюцца ў журнале запытаў. Вы можаце ўказаць дакладныя даменныя імёны, падстаноўныя знакі або правілы фільтрацыі URL-адрасоў, напрыклад, «example.org», «*.example.org» ці «||example.org^» адпаведна.",
|
||||
"access_settings_saved": "Налады доступу паспяхова захаваны",
|
||||
"updates_checked": "Даступная новая версія AdGuard Home",
|
||||
"updates_version_equal": "Версія AdGuard Home актуальная",
|
||||
|
||||
@@ -635,5 +635,6 @@
|
||||
"parental_control": "Parental Control",
|
||||
"safe_browsing": "Safe Browsing",
|
||||
"served_from_cache": "{{value}} <i>(served from cache)</i>",
|
||||
"form_error_password_length": "Password must be at least {{value}} characters long"
|
||||
"form_error_password_length": "Password must be at least {{value}} characters long",
|
||||
"anonymizer_notification": "<0>Note:</0> IP anonymization is enabled. You can disable it in <1>General settings</1>."
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@
|
||||
"access_disallowed_title": "Заборонені клієнти",
|
||||
"access_disallowed_desc": "Перелік CIDR, IP-адрес та <a>ClientIDs</a>. Якщо налаштовано, AdGuard Home буде скасовувати запити від цих клієнтів. Проте якщо налаштовано список Дозволених клієнтів, то це поле проігнорується.",
|
||||
"access_blocked_title": "Заборонені домени",
|
||||
"access_blocked_desc": "Не плутайте з фільтрами. AdGuard Home буде ігнорувати DNS-запити з цими доменами, такі запити навіть не будуть записані до журналу. Ви можете вказати точні доменні імена, замінні знаки та правила фільтрування URL-адрес, наприклад, 'example.org', '*.example.org' або '||example.org^' відповідно.",
|
||||
"access_blocked_desc": "Не плутайте з фільтрами. AdGuard Home буде ігнорувати DNS-запити з цими доменами, такі запити навіть не будуть записані до журналу. Ви можете вказати точні доменні імена, замінні знаки та правила фільтрування URL-адрес, наприклад, «example.org», «*.example.org» або «||example.org^» відповідно.",
|
||||
"access_settings_saved": "Налаштування доступу успішно збережено",
|
||||
"updates_checked": "Доступна нова версія AdGuard Home",
|
||||
"updates_version_equal": "AdGuard Home останньої версії",
|
||||
|
||||
@@ -41,13 +41,13 @@ class Table extends Component {
|
||||
{
|
||||
Header: <Trans>name_table_header</Trans>,
|
||||
accessor: 'name',
|
||||
minWidth: 200,
|
||||
minWidth: 180,
|
||||
Cell: CellWrap,
|
||||
},
|
||||
{
|
||||
Header: <Trans>list_url_table_header</Trans>,
|
||||
accessor: 'url',
|
||||
minWidth: 200,
|
||||
minWidth: 180,
|
||||
Cell: ({ value }) => (
|
||||
<div className="logs__row">
|
||||
{isValidAbsolutePath(value) ? value
|
||||
@@ -73,7 +73,7 @@ class Table extends Component {
|
||||
Header: <Trans>last_time_updated_table_header</Trans>,
|
||||
accessor: 'lastUpdated',
|
||||
className: 'text-center',
|
||||
minWidth: 150,
|
||||
minWidth: 180,
|
||||
Cell: this.getDateCell,
|
||||
},
|
||||
{
|
||||
|
||||
16
client/src/components/Logs/AnonymizerNotification.js
Normal file
16
client/src/components/Logs/AnonymizerNotification.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { HashLink as Link } from 'react-router-hash-link';
|
||||
|
||||
const AnonymizerNotification = () => (
|
||||
<div className="alert alert-primary mt-6">
|
||||
<Trans components={[
|
||||
<strong key="0">text</strong>,
|
||||
<Link to="/settings#logs-config" key="1">link</Link>,
|
||||
]}>
|
||||
anonymizer_notification
|
||||
</Trans>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default AnonymizerNotification;
|
||||
@@ -62,7 +62,7 @@ const ClientCell = ({
|
||||
'white-space--nowrap': isDetailed,
|
||||
});
|
||||
|
||||
const hintClass = classNames('icons mr-4 icon--24 icon--lightgray', {
|
||||
const hintClass = classNames('icons mr-4 icon--24 logs__question icon--lightgray', {
|
||||
'my-3': isDetailed,
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const DomainCell = ({
|
||||
'my-3': isDetailed,
|
||||
});
|
||||
|
||||
const privacyIconClass = classNames('icons mx-2 icon--24 d-none d-sm-block', {
|
||||
const privacyIconClass = classNames('icons mx-2 icon--24 d-none d-sm-block logs__question', {
|
||||
'icon--green': hasTracker,
|
||||
'icon--disabled': !hasTracker,
|
||||
'my-3': isDetailed,
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.grid .key-colon, .grid .title--border {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.grid {
|
||||
grid-template-columns: 35% 55%;
|
||||
@@ -70,10 +76,6 @@
|
||||
grid-column: 2 / span 1;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.grid .key-colon, .grid .title--border {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.grid .key-colon:nth-child(odd)::after {
|
||||
|
||||
@@ -97,7 +97,7 @@ const ResponseCell = ({
|
||||
return (
|
||||
<div className="logs__cell logs__cell--response" role="gridcell">
|
||||
<IconTooltip
|
||||
className={classNames('icons mr-4 icon--24 icon--lightgray', { 'my-3': isDetailed })}
|
||||
className={classNames('icons mr-4 icon--24 icon--lightgray logs__question', { 'my-3': isDetailed })}
|
||||
columnClass='grid grid--limited'
|
||||
tooltipClass='px-5 pb-5 pt-4 mw-75 custom-tooltip__response-details'
|
||||
contentItemClass='text-truncate key-colon o-hidden'
|
||||
|
||||
@@ -485,3 +485,13 @@
|
||||
.bg--green {
|
||||
color: var(--green79);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.logs__question {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.logs__modal {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import InfiniteTable from './InfiniteTable';
|
||||
import './Logs.css';
|
||||
import { BUTTON_PREFIX } from './Cells/helpers';
|
||||
import AnonymizerNotification from './AnonymizerNotification';
|
||||
|
||||
const processContent = (data) => Object.entries(data)
|
||||
.map(([key, value]) => {
|
||||
@@ -73,6 +74,7 @@ const Logs = () => {
|
||||
processingGetConfig,
|
||||
processingAdditionalLogs,
|
||||
processingGetLogs,
|
||||
anonymize_client_ip: anonymizeClientIp,
|
||||
} = useSelector((state) => state.queryLogs, shallowEqual);
|
||||
const filter = useSelector((state) => state.queryLogs.filter, shallowEqual);
|
||||
const logs = useSelector((state) => state.queryLogs.logs, shallowEqual);
|
||||
@@ -104,6 +106,8 @@ const Logs = () => {
|
||||
setIsSmallScreen(e.matches);
|
||||
if (e.matches) {
|
||||
dispatch(toggleDetailedLogs(false));
|
||||
} else {
|
||||
dispatch(toggleDetailedLogs(true));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -180,35 +184,49 @@ const Logs = () => {
|
||||
setButtonType={setButtonType}
|
||||
setModalOpened={setModalOpened}
|
||||
/>
|
||||
<Modal portalClassName='grid' isOpen={isSmallScreen && isModalOpened}
|
||||
onRequestClose={closeModal}
|
||||
style={{
|
||||
content: {
|
||||
width: '100%',
|
||||
height: 'fit-content',
|
||||
left: 0,
|
||||
top: 47,
|
||||
padding: '1rem 1.5rem 1rem',
|
||||
},
|
||||
overlay: {
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
}}
|
||||
<Modal
|
||||
portalClassName='grid'
|
||||
isOpen={isSmallScreen && isModalOpened}
|
||||
onRequestClose={closeModal}
|
||||
style={{
|
||||
content: {
|
||||
width: '100%',
|
||||
height: 'fit-content',
|
||||
left: '50%',
|
||||
top: 47,
|
||||
padding: '1rem 1.5rem 1rem',
|
||||
maxWidth: '720px',
|
||||
transform: 'translateX(-50%)',
|
||||
},
|
||||
overlay: {
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="icon icon--24 icon-cross d-block d-md-none cursor--pointer"
|
||||
onClick={closeModal}>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
{processContent(detailedDataCurrent, buttonType)}
|
||||
<div className="logs__modal-wrap">
|
||||
<svg
|
||||
className="icon icon--24 icon-cross d-block cursor--pointer"
|
||||
onClick={closeModal}
|
||||
>
|
||||
<use xlinkHref="#cross" />
|
||||
</svg>
|
||||
{processContent(detailedDataCurrent, buttonType)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>;
|
||||
|
||||
return <>
|
||||
{enabled && processingGetConfig && <Loading />}
|
||||
{enabled && !processingGetConfig && renderPage()}
|
||||
{!enabled && !processingGetConfig && <Disabled />}
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{enabled && (
|
||||
<>
|
||||
{processingGetConfig && <Loading />}
|
||||
{anonymizeClientIp && <AnonymizerNotification />}
|
||||
{!processingGetConfig && renderPage()}
|
||||
</>
|
||||
)}
|
||||
{!enabled && !processingGetConfig && <Disabled />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Logs;
|
||||
|
||||
@@ -2820,6 +2820,11 @@ fieldset:disabled a.btn {
|
||||
}
|
||||
|
||||
.btn-outline-primary:focus,
|
||||
.btn-outline-primary.focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-outline-primary:focus-visible,
|
||||
.btn-outline-primary.focus {
|
||||
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.5);
|
||||
}
|
||||
@@ -2858,6 +2863,11 @@ fieldset:disabled a.btn {
|
||||
}
|
||||
|
||||
.btn-outline-secondary:focus,
|
||||
.btn-outline-secondary.focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-outline-secondary:focus-visible,
|
||||
.btn-outline-secondary.focus {
|
||||
box-shadow: 0 0 0 2px rgba(134, 142, 150, 0.5);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
height: 24px;
|
||||
margin-bottom: 6px;
|
||||
fill: #4a4a4a;
|
||||
touch-action: initial;
|
||||
}
|
||||
|
||||
.tab__text {
|
||||
|
||||
@@ -526,8 +526,8 @@ export const DEFAULT_DATE_FORMAT_OPTIONS = {
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
hourCycle: 'h23',
|
||||
minute: 'numeric',
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
export const DETAILED_DATE_FORMAT_OPTIONS = {
|
||||
|
||||
@@ -9,6 +9,12 @@ import (
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// HTTP scheme constants.
|
||||
const (
|
||||
SchemeHTTP = "http"
|
||||
SchemeHTTPS = "https"
|
||||
)
|
||||
|
||||
// RegisterFunc is the function that sets the handler to handle the URL for the
|
||||
// method.
|
||||
//
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build darwin || freebsd
|
||||
// +build darwin freebsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build darwin || freebsd
|
||||
// +build darwin freebsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build openbsd
|
||||
// +build openbsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build openbsd
|
||||
// +build openbsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !(windows || linux)
|
||||
// +build !windows,!linux
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd netbsd openbsd solaris
|
||||
//go:build darwin || freebsd || openbsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package aghnet
|
||||
|
||||
@@ -19,27 +18,18 @@ import (
|
||||
|
||||
// How to test on a real Linux machine:
|
||||
//
|
||||
// 1. Run:
|
||||
// 1. Run "sudo ipset create example_set hash:ip family ipv4".
|
||||
//
|
||||
// sudo ipset create example_set hash:ip family ipv4
|
||||
// 2. Run "sudo ipset list example_set". The Members field should be empty.
|
||||
//
|
||||
// 2. Run:
|
||||
// 3. Add the line "example.com/example_set" to your AdGuardHome.yaml.
|
||||
//
|
||||
// sudo ipset list example_set
|
||||
// 4. Start AdGuardHome.
|
||||
//
|
||||
// The Members field should be empty.
|
||||
// 5. Make requests to example.com and its subdomains.
|
||||
//
|
||||
// 3. Add the line "example.com/example_set" to your AdGuardHome.yaml.
|
||||
//
|
||||
// 4. Start AdGuardHome.
|
||||
//
|
||||
// 5. Make requests to example.com and its subdomains.
|
||||
//
|
||||
// 6. Run:
|
||||
//
|
||||
// sudo ipset list example_set
|
||||
//
|
||||
// The Members field should contain the resolved IP addresses.
|
||||
// 6. Run "sudo ipset list example_set". The Members field should contain the
|
||||
// resolved IP addresses.
|
||||
|
||||
// newIpsetMgr returns a new Linux ipset manager.
|
||||
func newIpsetMgr(ipsetConf []string) (set IpsetManager, err error) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
@@ -31,6 +32,12 @@ var (
|
||||
// the IP being static is available.
|
||||
const ErrNoStaticIPInfo errors.Error = "no information about static ip"
|
||||
|
||||
// IPv4Localhost returns 127.0.0.1, which returns true for [netip.Addr.Is4].
|
||||
func IPv4Localhost() (ip netip.Addr) { return netip.AddrFrom4([4]byte{0: 127, 3: 1}) }
|
||||
|
||||
// IPv6Localhost returns ::1, which returns true for [netip.Addr.Is6].
|
||||
func IPv6Localhost() (ip netip.Addr) { return netip.AddrFrom16([16]byte{15: 1}) }
|
||||
|
||||
// IfaceHasStaticIP checks if interface is configured to have static IP address.
|
||||
// If it can't give a definitive answer, it returns false and an error for which
|
||||
// errors.Is(err, ErrNoStaticIPInfo) is true.
|
||||
@@ -47,26 +54,31 @@ func IfaceSetStaticIP(ifaceName string) (err error) {
|
||||
//
|
||||
// TODO(e.burkov): Investigate if the gateway address may be fetched in another
|
||||
// way since not every machine has the software installed.
|
||||
func GatewayIP(ifaceName string) (ip net.IP) {
|
||||
func GatewayIP(ifaceName string) (ip netip.Addr) {
|
||||
code, out, err := aghosRunCommand("ip", "route", "show", "dev", ifaceName)
|
||||
if err != nil {
|
||||
log.Debug("%s", err)
|
||||
|
||||
return nil
|
||||
return ip
|
||||
} else if code != 0 {
|
||||
log.Debug("fetching gateway ip: unexpected exit code: %d", code)
|
||||
|
||||
return nil
|
||||
return ip
|
||||
}
|
||||
|
||||
fields := bytes.Fields(out)
|
||||
// The meaningful "ip route" command output should contain the word
|
||||
// "default" at first field and default gateway IP address at third field.
|
||||
if len(fields) < 3 || string(fields[0]) != "default" {
|
||||
return nil
|
||||
return ip
|
||||
}
|
||||
|
||||
return net.ParseIP(string(fields[2]))
|
||||
ip, err = netip.ParseAddr(string(fields[2]))
|
||||
if err != nil {
|
||||
return netip.Addr{}
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
// CanBindPrivilegedPorts checks if current process can bind to privileged
|
||||
@@ -78,9 +90,9 @@ func CanBindPrivilegedPorts() (can bool, err error) {
|
||||
// NetInterface represents an entry of network interfaces map.
|
||||
type NetInterface struct {
|
||||
// Addresses are the network interface addresses.
|
||||
Addresses []net.IP `json:"ip_addresses,omitempty"`
|
||||
Addresses []netip.Addr `json:"ip_addresses,omitempty"`
|
||||
// Subnets are the IP networks for this network interface.
|
||||
Subnets []*net.IPNet `json:"-"`
|
||||
Subnets []netip.Prefix `json:"-"`
|
||||
Name string `json:"name"`
|
||||
HardwareAddr net.HardwareAddr `json:"hardware_address"`
|
||||
Flags net.Flags `json:"flags"`
|
||||
@@ -101,57 +113,78 @@ func (iface NetInterface) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func NetInterfaceFrom(iface *net.Interface) (niface *NetInterface, err error) {
|
||||
niface = &NetInterface{
|
||||
Name: iface.Name,
|
||||
HardwareAddr: iface.HardwareAddr,
|
||||
Flags: iface.Flags,
|
||||
MTU: iface.MTU,
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err)
|
||||
}
|
||||
|
||||
// Collect network interface addresses.
|
||||
for _, addr := range addrs {
|
||||
n, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
// Should be *net.IPNet, this is weird.
|
||||
return nil, fmt.Errorf("expected %[2]s to be %[1]T, got %[2]T", n, addr)
|
||||
} else if ip4 := n.IP.To4(); ip4 != nil {
|
||||
n.IP = ip4
|
||||
}
|
||||
|
||||
ip, ok := netip.AddrFromSlice(n.IP)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("bad address %s", n.IP)
|
||||
}
|
||||
|
||||
if ip.IsLinkLocalUnicast() {
|
||||
// Ignore link-local IPv4.
|
||||
if ip.Is4() {
|
||||
continue
|
||||
}
|
||||
|
||||
ip = ip.WithZone(iface.Name)
|
||||
}
|
||||
|
||||
ones, _ := n.Mask.Size()
|
||||
p := netip.PrefixFrom(ip, ones)
|
||||
|
||||
niface.Addresses = append(niface.Addresses, ip)
|
||||
niface.Subnets = append(niface.Subnets, p)
|
||||
}
|
||||
|
||||
return niface, nil
|
||||
}
|
||||
|
||||
// GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and
|
||||
// WEB only we do not return link-local addresses here.
|
||||
//
|
||||
// TODO(e.burkov): Can't properly test the function since it's nontrivial to
|
||||
// substitute net.Interface.Addrs and the net.InterfaceAddrs can't be used.
|
||||
func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) {
|
||||
func GetValidNetInterfacesForWeb() (nifaces []*NetInterface, err error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't get interfaces: %w", err)
|
||||
return nil, fmt.Errorf("getting interfaces: %w", err)
|
||||
} else if len(ifaces) == 0 {
|
||||
return nil, errors.Error("couldn't find any legible interface")
|
||||
return nil, errors.Error("no legible interfaces")
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
var addrs []net.Addr
|
||||
addrs, err = iface.Addrs()
|
||||
for i := range ifaces {
|
||||
var niface *NetInterface
|
||||
niface, err = NetInterfaceFrom(&ifaces[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err)
|
||||
}
|
||||
|
||||
netIface := &NetInterface{
|
||||
MTU: iface.MTU,
|
||||
Name: iface.Name,
|
||||
HardwareAddr: iface.HardwareAddr,
|
||||
Flags: iface.Flags,
|
||||
}
|
||||
|
||||
// Collect network interface addresses.
|
||||
for _, addr := range addrs {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
// Should be net.IPNet, this is weird.
|
||||
return nil, fmt.Errorf("got %s that is not net.IPNet, it is %T", addr, addr)
|
||||
}
|
||||
|
||||
// Ignore link-local.
|
||||
if ipNet.IP.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
|
||||
netIface.Addresses = append(netIface.Addresses, ipNet.IP)
|
||||
netIface.Subnets = append(netIface.Subnets, ipNet)
|
||||
}
|
||||
|
||||
// Discard interfaces with no addresses.
|
||||
if len(netIface.Addresses) != 0 {
|
||||
netIfaces = append(netIfaces, netIface)
|
||||
return nil, err
|
||||
} else if len(niface.Addresses) != 0 {
|
||||
// Discard interfaces with no addresses.
|
||||
nifaces = append(nifaces, niface)
|
||||
}
|
||||
}
|
||||
|
||||
return netIfaces, nil
|
||||
return nifaces, nil
|
||||
}
|
||||
|
||||
// InterfaceByIP returns the name of the interface bound to ip.
|
||||
@@ -160,7 +193,7 @@ func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) {
|
||||
// IP address can be shared by multiple interfaces in some configurations.
|
||||
//
|
||||
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
|
||||
func InterfaceByIP(ip net.IP) (ifaceName string) {
|
||||
func InterfaceByIP(ip netip.Addr) (ifaceName string) {
|
||||
ifaces, err := GetValidNetInterfacesForWeb()
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -168,7 +201,7 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
|
||||
|
||||
for _, iface := range ifaces {
|
||||
for _, addr := range iface.Addresses {
|
||||
if ip.Equal(addr) {
|
||||
if ip == addr {
|
||||
return iface.Name
|
||||
}
|
||||
}
|
||||
@@ -177,15 +210,16 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetSubnet returns pointer to net.IPNet for the specified interface or nil if
|
||||
// GetSubnet returns the subnet corresponding to the interface of zero prefix if
|
||||
// the search fails.
|
||||
//
|
||||
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
|
||||
func GetSubnet(ifaceName string) *net.IPNet {
|
||||
func GetSubnet(ifaceName string) (p netip.Prefix) {
|
||||
netIfaces, err := GetValidNetInterfacesForWeb()
|
||||
if err != nil {
|
||||
log.Error("Could not get network interfaces info: %v", err)
|
||||
return nil
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
for _, netIface := range netIfaces {
|
||||
@@ -194,14 +228,14 @@ func GetSubnet(ifaceName string) *net.IPNet {
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return p
|
||||
}
|
||||
|
||||
// CheckPort checks if the port is available for binding. network is expected
|
||||
// to be one of "udp" and "tcp".
|
||||
func CheckPort(network string, ip net.IP, port int) (err error) {
|
||||
func CheckPort(network string, ipp netip.AddrPort) (err error) {
|
||||
var c io.Closer
|
||||
addr := netutil.IPPort{IP: ip, Port: port}.String()
|
||||
addr := ipp.String()
|
||||
switch network {
|
||||
case "tcp":
|
||||
c, err = net.Listen(network, addr)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build darwin || freebsd || openbsd
|
||||
// +build darwin freebsd openbsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package aghnet
|
||||
|
||||
@@ -7,7 +6,7 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -152,7 +151,7 @@ func findIfaceLine(s *bufio.Scanner, name string) (ok bool) {
|
||||
// interface through dhcpcd.conf.
|
||||
func ifaceSetStaticIP(ifaceName string) (err error) {
|
||||
ipNet := GetSubnet(ifaceName)
|
||||
if ipNet.IP == nil {
|
||||
if !ipNet.Addr().IsValid() {
|
||||
return errors.Error("can't get IP address")
|
||||
}
|
||||
|
||||
@@ -175,7 +174,7 @@ func ifaceSetStaticIP(ifaceName string) (err error) {
|
||||
|
||||
// dhcpcdConfIface returns configuration lines for the dhcpdc.conf files that
|
||||
// configure the interface to have a static IP.
|
||||
func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf string) {
|
||||
func dhcpcdConfIface(ifaceName string, subnet netip.Prefix, gateway netip.Addr) (conf string) {
|
||||
b := &strings.Builder{}
|
||||
stringutil.WriteToBuilder(
|
||||
b,
|
||||
@@ -184,15 +183,15 @@ func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf stri
|
||||
" added by AdGuard Home.\ninterface ",
|
||||
ifaceName,
|
||||
"\nstatic ip_address=",
|
||||
ipNet.String(),
|
||||
subnet.String(),
|
||||
"\n",
|
||||
)
|
||||
|
||||
if gwIP != nil {
|
||||
stringutil.WriteToBuilder(b, "static routers=", gwIP.String(), "\n")
|
||||
if gateway.IsValid() {
|
||||
stringutil.WriteToBuilder(b, "static routers=", gateway.String(), "\n")
|
||||
}
|
||||
|
||||
stringutil.WriteToBuilder(b, "static domain_name_servers=", ipNet.IP.String(), "\n\n")
|
||||
stringutil.WriteToBuilder(b, "static domain_name_servers=", subnet.Addr().String(), "\n\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build openbsd
|
||||
// +build openbsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build openbsd
|
||||
// +build openbsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -93,34 +94,34 @@ func TestGatewayIP(t *testing.T) {
|
||||
const cmd = "ip route show dev " + ifaceName
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
shell mapShell
|
||||
want net.IP
|
||||
want netip.Addr
|
||||
name string
|
||||
}{{
|
||||
name: "success_v4",
|
||||
shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil),
|
||||
want: net.IP{1, 2, 3, 4}.To16(),
|
||||
want: netip.AddrFrom4([4]byte{1, 2, 3, 4}),
|
||||
name: "success_v4",
|
||||
}, {
|
||||
name: "success_v6",
|
||||
shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil),
|
||||
want: net.IP{
|
||||
want: netip.AddrFrom16([16]byte{
|
||||
0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0xFF, 0xFF,
|
||||
},
|
||||
}),
|
||||
name: "success_v6",
|
||||
}, {
|
||||
name: "bad_output",
|
||||
shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
|
||||
want: nil,
|
||||
want: netip.Addr{},
|
||||
name: "bad_output",
|
||||
}, {
|
||||
name: "err_runcmd",
|
||||
shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")),
|
||||
want: nil,
|
||||
want: netip.Addr{},
|
||||
name: "err_runcmd",
|
||||
}, {
|
||||
name: "bad_code",
|
||||
shell: theOnlyCmd(cmd, 1, "", nil),
|
||||
want: nil,
|
||||
want: netip.Addr{},
|
||||
name: "bad_code",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -198,17 +199,21 @@ func TestBroadcastFromIPNet(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCheckPort(t *testing.T) {
|
||||
laddr := netip.AddrPortFrom(IPv4Localhost(), 0)
|
||||
|
||||
t.Run("tcp_bound", func(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:")
|
||||
l, err := net.Listen("tcp", laddr.String())
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, l.Close)
|
||||
|
||||
ipp := netutil.IPPortFromAddr(l.Addr())
|
||||
require.NotNil(t, ipp)
|
||||
require.NotNil(t, ipp.IP)
|
||||
require.NotZero(t, ipp.Port)
|
||||
addr := l.Addr()
|
||||
require.IsType(t, new(net.TCPAddr), addr)
|
||||
|
||||
err = CheckPort("tcp", ipp.IP, ipp.Port)
|
||||
ipp := addr.(*net.TCPAddr).AddrPort()
|
||||
require.Equal(t, laddr.Addr(), ipp.Addr())
|
||||
require.NotZero(t, ipp.Port())
|
||||
|
||||
err = CheckPort("tcp", ipp)
|
||||
target := &net.OpError{}
|
||||
require.ErrorAs(t, err, &target)
|
||||
|
||||
@@ -216,16 +221,18 @@ func TestCheckPort(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("udp_bound", func(t *testing.T) {
|
||||
conn, err := net.ListenPacket("udp", "127.0.0.1:")
|
||||
conn, err := net.ListenPacket("udp", laddr.String())
|
||||
require.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, conn.Close)
|
||||
|
||||
ipp := netutil.IPPortFromAddr(conn.LocalAddr())
|
||||
require.NotNil(t, ipp)
|
||||
require.NotNil(t, ipp.IP)
|
||||
require.NotZero(t, ipp.Port)
|
||||
addr := conn.LocalAddr()
|
||||
require.IsType(t, new(net.UDPAddr), addr)
|
||||
|
||||
err = CheckPort("udp", ipp.IP, ipp.Port)
|
||||
ipp := addr.(*net.UDPAddr).AddrPort()
|
||||
require.Equal(t, laddr.Addr(), ipp.Addr())
|
||||
require.NotZero(t, ipp.Port())
|
||||
|
||||
err = CheckPort("udp", ipp)
|
||||
target := &net.OpError{}
|
||||
require.ErrorAs(t, err, &target)
|
||||
|
||||
@@ -233,12 +240,12 @@ func TestCheckPort(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("bad_network", func(t *testing.T) {
|
||||
err := CheckPort("bad_network", nil, 0)
|
||||
err := CheckPort("bad_network", netip.AddrPortFrom(netip.Addr{}, 0))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("can_bind", func(t *testing.T) {
|
||||
err := CheckPort("udp", net.IP{0, 0, 0, 0}, 0)
|
||||
err := CheckPort("udp", netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -322,18 +329,18 @@ func TestNetInterface_MarshalJSON(t *testing.T) {
|
||||
`"mtu":1500` +
|
||||
`}` + "\n"
|
||||
|
||||
ip4, ip6 := net.IP{1, 2, 3, 4}, net.IP{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||
mask4, mask6 := net.CIDRMask(24, netutil.IPv4BitLen), net.CIDRMask(8, netutil.IPv6BitLen)
|
||||
ip4, ok := netip.AddrFromSlice([]byte{1, 2, 3, 4})
|
||||
require.True(t, ok)
|
||||
|
||||
ip6, ok := netip.AddrFromSlice([]byte{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||
require.True(t, ok)
|
||||
|
||||
net4 := netip.PrefixFrom(ip4, 24)
|
||||
net6 := netip.PrefixFrom(ip6, 8)
|
||||
|
||||
iface := &NetInterface{
|
||||
Addresses: []net.IP{ip4, ip6},
|
||||
Subnets: []*net.IPNet{{
|
||||
IP: ip4.Mask(mask4),
|
||||
Mask: mask4,
|
||||
}, {
|
||||
IP: ip6.Mask(mask6),
|
||||
Mask: mask6,
|
||||
}},
|
||||
Addresses: []netip.Addr{ip4, ip6},
|
||||
Subnets: []netip.Prefix{net4, net6},
|
||||
Name: "iface0",
|
||||
HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
|
||||
Flags: net.FlagUp | net.FlagMulticast,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build openbsd || freebsd || linux || darwin
|
||||
// +build openbsd freebsd linux darwin
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package aghnet
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build mips || mips64
|
||||
// +build mips mips64
|
||||
|
||||
// This file is an adapted version of github.com/josharian/native.
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build amd64 || 386 || arm || arm64 || mipsle || mips64le || ppc64le
|
||||
// +build amd64 386 arm arm64 mipsle mips64le ppc64le
|
||||
|
||||
// This file is an adapted version of github.com/josharian/native.
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build darwin || netbsd || openbsd
|
||||
// +build darwin netbsd openbsd
|
||||
//go:build darwin || openbsd
|
||||
|
||||
package aghos
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package aghos
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package aghos
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
// +build darwin freebsd linux openbsd
|
||||
|
||||
package aghos
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package aghos
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build !(windows || plan9)
|
||||
// +build !windows,!plan9
|
||||
//go:build !windows
|
||||
|
||||
package aghos
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows || plan9
|
||||
// +build windows plan9
|
||||
//go:build windows
|
||||
|
||||
package aghos
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build darwin || freebsd || linux || netbsd || openbsd
|
||||
// +build darwin freebsd linux netbsd openbsd
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package aghos
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package aghos
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build freebsd || openbsd
|
||||
// +build freebsd openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
@@ -10,11 +9,10 @@ import (
|
||||
// broadcast sends resp to the broadcast address specific for network interface.
|
||||
func (c *dhcpConn) broadcast(respData []byte, peer *net.UDPAddr) (n int, err error) {
|
||||
// Despite the fact that server4.NewIPv4UDPConn explicitly sets socket
|
||||
// options to allow broadcasting, it also binds the connection to a
|
||||
// specific interface. On FreeBSD and OpenBSD net.UDPConn.WriteTo
|
||||
// causes errors while writing to the addresses that belong to another
|
||||
// interface. So, use the broadcast address specific for the interface
|
||||
// bound.
|
||||
// options to allow broadcasting, it also binds the connection to a specific
|
||||
// interface. On FreeBSD and OpenBSD net.UDPConn.WriteTo causes errors
|
||||
// while writing to the addresses that belong to another interface. So, use
|
||||
// the broadcast address specific for the interface bound.
|
||||
peer.IP = c.bcastIP
|
||||
|
||||
return c.udpConn.WriteTo(respData, peer)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build freebsd || openbsd
|
||||
// +build freebsd openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || linux || netbsd || solaris
|
||||
// +build aix darwin dragonfly linux netbsd solaris
|
||||
//go:build darwin || linux
|
||||
|
||||
package dhcpd
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || linux || netbsd || solaris
|
||||
// +build aix darwin dragonfly linux netbsd solaris
|
||||
//go:build darwin || linux
|
||||
|
||||
package dhcpd
|
||||
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
)
|
||||
|
||||
// ServerConfig is the configuration for the DHCP server. The order of YAML
|
||||
// fields is important, since the YAML configuration file follows it.
|
||||
type ServerConfig struct {
|
||||
// Called when the configuration is changed by HTTP request
|
||||
ConfigModified func() `yaml:"-"`
|
||||
|
||||
// Register an HTTP handler
|
||||
HTTPRegister aghhttp.RegisterFunc `yaml:"-"`
|
||||
|
||||
Enabled bool `yaml:"enabled"`
|
||||
InterfaceName string `yaml:"interface_name"`
|
||||
|
||||
// LocalDomainName is the domain name used for DHCP hosts. For example,
|
||||
// a DHCP client with the hostname "myhost" can be addressed as "myhost.lan"
|
||||
// when LocalDomainName is "lan".
|
||||
LocalDomainName string `yaml:"local_domain_name"`
|
||||
|
||||
Conf4 V4ServerConf `yaml:"dhcpv4"`
|
||||
Conf6 V6ServerConf `yaml:"dhcpv6"`
|
||||
|
||||
WorkDir string `yaml:"-"`
|
||||
DBFilePath string `yaml:"-"`
|
||||
}
|
||||
|
||||
// DHCPServer - DHCP server interface
|
||||
type DHCPServer interface {
|
||||
// ResetLeases resets leases.
|
||||
@@ -80,6 +110,86 @@ type V4ServerConf struct {
|
||||
notify func(uint32)
|
||||
}
|
||||
|
||||
// errNilConfig is an error returned by validation method if the config is nil.
|
||||
const errNilConfig errors.Error = "nil config"
|
||||
|
||||
// ensureV4 returns a 4-byte version of ip. An error is returned if the passed
|
||||
// ip is not an IPv4.
|
||||
func ensureV4(ip net.IP) (ip4 net.IP, err error) {
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("%v is not an IP address", ip)
|
||||
}
|
||||
|
||||
ip4 = ip.To4()
|
||||
if ip4 == nil {
|
||||
return nil, fmt.Errorf("%v is not an IPv4 address", ip)
|
||||
}
|
||||
|
||||
return ip4, nil
|
||||
}
|
||||
|
||||
// Validate returns an error if c is not a valid configuration.
|
||||
//
|
||||
// TODO(e.burkov): Don't set the config fields when the server itself will stop
|
||||
// containing the config.
|
||||
func (c *V4ServerConf) Validate() (err error) {
|
||||
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
|
||||
|
||||
if c == nil {
|
||||
return errNilConfig
|
||||
}
|
||||
|
||||
var gatewayIP net.IP
|
||||
gatewayIP, err = ensureV4(c.GatewayIP)
|
||||
if err != nil {
|
||||
// Don't wrap an errors since it's inforative enough as is and there is
|
||||
// an annotation deferred already.
|
||||
return err
|
||||
}
|
||||
|
||||
if c.SubnetMask == nil {
|
||||
return fmt.Errorf("invalid subnet mask: %v", c.SubnetMask)
|
||||
}
|
||||
|
||||
subnetMask := net.IPMask(netutil.CloneIP(c.SubnetMask.To4()))
|
||||
c.subnet = &net.IPNet{
|
||||
IP: gatewayIP,
|
||||
Mask: subnetMask,
|
||||
}
|
||||
c.broadcastIP = aghnet.BroadcastFromIPNet(c.subnet)
|
||||
|
||||
c.ipRange, err = newIPRange(c.RangeStart, c.RangeEnd)
|
||||
if err != nil {
|
||||
// Don't wrap an errors since it's inforative enough as is and there is
|
||||
// an annotation deferred already.
|
||||
return err
|
||||
}
|
||||
|
||||
if c.ipRange.contains(gatewayIP) {
|
||||
return fmt.Errorf("gateway ip %v in the ip range: %v-%v",
|
||||
gatewayIP,
|
||||
c.RangeStart,
|
||||
c.RangeEnd,
|
||||
)
|
||||
}
|
||||
|
||||
if !c.subnet.Contains(c.RangeStart) {
|
||||
return fmt.Errorf("range start %v is outside network %v",
|
||||
c.RangeStart,
|
||||
c.subnet,
|
||||
)
|
||||
}
|
||||
|
||||
if !c.subnet.Contains(c.RangeEnd) {
|
||||
return fmt.Errorf("range end %v is outside network %v",
|
||||
c.RangeEnd,
|
||||
c.subnet,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// V6ServerConf - server configuration
|
||||
type V6ServerConf struct {
|
||||
Enabled bool `yaml:"-" json:"-"`
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ func normalizeIP(ip net.IP) net.IP {
|
||||
}
|
||||
|
||||
// Load lease table from DB
|
||||
func (s *Server) dbLoad() (err error) {
|
||||
func (s *server) dbLoad() (err error) {
|
||||
dynLeases := []*Lease{}
|
||||
staticLeases := []*Lease{}
|
||||
v6StaticLeases := []*Lease{}
|
||||
@@ -132,7 +132,7 @@ func normalizeLeases(staticLeases, dynLeases []*Lease) []*Lease {
|
||||
}
|
||||
|
||||
// Store lease table in DB
|
||||
func (s *Server) dbStore() (err error) {
|
||||
func (s *server) dbStore() (err error) {
|
||||
// Use an empty slice here as opposed to nil so that it doesn't write
|
||||
// "null" into the database file if leases are empty.
|
||||
leases := []leaseJSON{}
|
||||
|
||||
@@ -6,12 +6,11 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,9 +20,19 @@ const (
|
||||
// TODO(e.burkov): Remove it when static leases determining mechanism
|
||||
// will be improved.
|
||||
leaseExpireStatic = 1
|
||||
|
||||
// DefaultDHCPLeaseTTL is the default time-to-live for leases.
|
||||
DefaultDHCPLeaseTTL = uint32(timeutil.Day / time.Second)
|
||||
|
||||
// DefaultDHCPTimeoutICMP is the default timeout for waiting ICMP responses.
|
||||
DefaultDHCPTimeoutICMP = 1000
|
||||
)
|
||||
|
||||
var webHandlersRegistered = false
|
||||
// Currently used defaults for ifaceDNSAddrs.
|
||||
const (
|
||||
defaultMaxAttempts int = 10
|
||||
defaultBackoff time.Duration = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
// Lease contains the necessary information about a DHCP lease
|
||||
type Lease struct {
|
||||
@@ -119,30 +128,6 @@ func (l *Lease) UnmarshalJSON(data []byte) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerConfig is the configuration for the DHCP server. The order of YAML
|
||||
// fields is important, since the YAML configuration file follows it.
|
||||
type ServerConfig struct {
|
||||
// Called when the configuration is changed by HTTP request
|
||||
ConfigModified func() `yaml:"-"`
|
||||
|
||||
// Register an HTTP handler
|
||||
HTTPRegister aghhttp.RegisterFunc `yaml:"-"`
|
||||
|
||||
Enabled bool `yaml:"enabled"`
|
||||
InterfaceName string `yaml:"interface_name"`
|
||||
|
||||
// LocalDomainName is the domain name used for DHCP hosts. For example,
|
||||
// a DHCP client with the hostname "myhost" can be addressed as "myhost.lan"
|
||||
// when LocalDomainName is "lan".
|
||||
LocalDomainName string `yaml:"local_domain_name"`
|
||||
|
||||
Conf4 V4ServerConf `yaml:"dhcpv4"`
|
||||
Conf6 V6ServerConf `yaml:"dhcpv6"`
|
||||
|
||||
WorkDir string `yaml:"-"`
|
||||
DBFilePath string `yaml:"-"`
|
||||
}
|
||||
|
||||
// OnLeaseChangedT is a callback for lease changes.
|
||||
type OnLeaseChangedT func(flags int)
|
||||
|
||||
@@ -156,8 +141,68 @@ const (
|
||||
LeaseChangedDBStore
|
||||
)
|
||||
|
||||
// Server - the current state of the DHCP server
|
||||
type Server struct {
|
||||
// GetLeasesFlags are the flags for GetLeases.
|
||||
type GetLeasesFlags uint8
|
||||
|
||||
// GetLeasesFlags values
|
||||
const (
|
||||
LeasesDynamic GetLeasesFlags = 0b01
|
||||
LeasesStatic GetLeasesFlags = 0b10
|
||||
|
||||
LeasesAll = LeasesDynamic | LeasesStatic
|
||||
)
|
||||
|
||||
// Interface is the DHCP server that deals with both IP address families.
|
||||
type Interface interface {
|
||||
Start() (err error)
|
||||
Stop() (err error)
|
||||
Enabled() (ok bool)
|
||||
|
||||
Leases(flags GetLeasesFlags) (leases []*Lease)
|
||||
SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT)
|
||||
FindMACbyIP(ip net.IP) (mac net.HardwareAddr)
|
||||
|
||||
WriteDiskConfig(c *ServerConfig)
|
||||
}
|
||||
|
||||
// MockInterface is a mock Interface implementation.
|
||||
//
|
||||
// TODO(e.burkov): Move to aghtest when the API stabilized.
|
||||
type MockInterface struct {
|
||||
OnStart func() (err error)
|
||||
OnStop func() (err error)
|
||||
OnEnabled func() (ok bool)
|
||||
OnLeases func(flags GetLeasesFlags) (leases []*Lease)
|
||||
OnSetOnLeaseChanged func(f OnLeaseChangedT)
|
||||
OnFindMACbyIP func(ip net.IP) (mac net.HardwareAddr)
|
||||
OnWriteDiskConfig func(c *ServerConfig)
|
||||
}
|
||||
|
||||
var _ Interface = (*MockInterface)(nil)
|
||||
|
||||
// Start implements the Interface for *MockInterface.
|
||||
func (s *MockInterface) Start() (err error) { return s.OnStart() }
|
||||
|
||||
// Stop implements the Interface for *MockInterface.
|
||||
func (s *MockInterface) Stop() (err error) { return s.OnStop() }
|
||||
|
||||
// Enabled implements the Interface for *MockInterface.
|
||||
func (s *MockInterface) Enabled() (ok bool) { return s.OnEnabled() }
|
||||
|
||||
// Leases implements the Interface for *MockInterface.
|
||||
func (s *MockInterface) Leases(flags GetLeasesFlags) (ls []*Lease) { return s.OnLeases(flags) }
|
||||
|
||||
// SetOnLeaseChanged implements the Interface for *MockInterface.
|
||||
func (s *MockInterface) SetOnLeaseChanged(f OnLeaseChangedT) { s.OnSetOnLeaseChanged(f) }
|
||||
|
||||
// FindMACbyIP implements the Interface for *MockInterface.
|
||||
func (s *MockInterface) FindMACbyIP(ip net.IP) (mac net.HardwareAddr) { return s.OnFindMACbyIP(ip) }
|
||||
|
||||
// WriteDiskConfig implements the Interface for *MockInterface.
|
||||
func (s *MockInterface) WriteDiskConfig(c *ServerConfig) { s.OnWriteDiskConfig(c) }
|
||||
|
||||
// server is the DHCP service that handles DHCPv4, DHCPv6, and HTTP API.
|
||||
type server struct {
|
||||
srv4 DHCPServer
|
||||
srv6 DHCPServer
|
||||
|
||||
@@ -169,27 +214,15 @@ type Server struct {
|
||||
onLeaseChanged []OnLeaseChangedT
|
||||
}
|
||||
|
||||
// GetLeasesFlags are the flags for GetLeases.
|
||||
type GetLeasesFlags uint8
|
||||
// type check
|
||||
var _ Interface = (*server)(nil)
|
||||
|
||||
// GetLeasesFlags values
|
||||
const (
|
||||
LeasesDynamic GetLeasesFlags = 0b0001
|
||||
LeasesStatic GetLeasesFlags = 0b0010
|
||||
|
||||
LeasesAll = LeasesDynamic | LeasesStatic
|
||||
)
|
||||
|
||||
// ServerInterface is an interface for servers.
|
||||
type ServerInterface interface {
|
||||
Enabled() (ok bool)
|
||||
Leases(flags GetLeasesFlags) (leases []*Lease)
|
||||
SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT)
|
||||
}
|
||||
|
||||
// Create - create object
|
||||
func Create(conf *ServerConfig) (s *Server, err error) {
|
||||
s = &Server{
|
||||
// Create initializes and returns the DHCP server handling both address
|
||||
// families. It also registers the corresponding HTTP API endpoints.
|
||||
//
|
||||
// TODO(e.burkov): Don't register handlers, see TODO on [aghhttp.RegisterFunc].
|
||||
func Create(conf *ServerConfig) (s *server, err error) {
|
||||
s = &server{
|
||||
conf: &ServerConfig{
|
||||
ConfigModified: conf.ConfigModified,
|
||||
|
||||
@@ -204,35 +237,20 @@ func Create(conf *ServerConfig) (s *Server, err error) {
|
||||
},
|
||||
}
|
||||
|
||||
if !webHandlersRegistered && s.conf.HTTPRegister != nil {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Our DHCP server doesn't work on Windows yet, so
|
||||
// signal that to the front with an HTTP 501.
|
||||
//
|
||||
// TODO(a.garipov): This needs refactoring. We
|
||||
// shouldn't even try and initialize a DHCP server on
|
||||
// Windows, but there are currently too many
|
||||
// interconnected parts--such as HTTP handlers and
|
||||
// frontend--to make that work properly.
|
||||
s.registerNotImplementedHandlers()
|
||||
} else {
|
||||
s.registerHandlers()
|
||||
}
|
||||
|
||||
webHandlersRegistered = true
|
||||
}
|
||||
s.registerHandlers()
|
||||
|
||||
v4conf := conf.Conf4
|
||||
v4conf.Enabled = s.conf.Enabled
|
||||
if len(v4conf.RangeStart) == 0 {
|
||||
v4conf.Enabled = false
|
||||
}
|
||||
|
||||
v4conf.InterfaceName = s.conf.InterfaceName
|
||||
v4conf.notify = s.onNotify
|
||||
s.srv4, err = v4Create(v4conf)
|
||||
v4conf.Enabled = s.conf.Enabled && len(v4conf.RangeStart) != 0
|
||||
|
||||
s.srv4, err = v4Create(&v4conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating dhcpv4 srv: %w", err)
|
||||
if v4conf.Enabled {
|
||||
return nil, fmt.Errorf("creating dhcpv4 srv: %w", err)
|
||||
}
|
||||
|
||||
log.Error("creating dhcpv4 srv: %s", err)
|
||||
}
|
||||
|
||||
v6conf := conf.Conf6
|
||||
@@ -265,12 +283,12 @@ func Create(conf *ServerConfig) (s *Server, err error) {
|
||||
}
|
||||
|
||||
// Enabled returns true when the server is enabled.
|
||||
func (s *Server) Enabled() (ok bool) {
|
||||
func (s *server) Enabled() (ok bool) {
|
||||
return s.conf.Enabled
|
||||
}
|
||||
|
||||
// resetLeases resets all leases in the lease database.
|
||||
func (s *Server) resetLeases() (err error) {
|
||||
func (s *server) resetLeases() (err error) {
|
||||
err = s.srv4.ResetLeases(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -287,7 +305,7 @@ func (s *Server) resetLeases() (err error) {
|
||||
}
|
||||
|
||||
// server calls this function after DB is updated
|
||||
func (s *Server) onNotify(flags uint32) {
|
||||
func (s *server) onNotify(flags uint32) {
|
||||
if flags == LeaseChangedDBStore {
|
||||
err := s.dbStore()
|
||||
if err != nil {
|
||||
@@ -301,31 +319,28 @@ func (s *Server) onNotify(flags uint32) {
|
||||
}
|
||||
|
||||
// SetOnLeaseChanged - set callback
|
||||
func (s *Server) SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) {
|
||||
func (s *server) SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) {
|
||||
s.onLeaseChanged = append(s.onLeaseChanged, onLeaseChanged)
|
||||
}
|
||||
|
||||
func (s *Server) notify(flags int) {
|
||||
if len(s.onLeaseChanged) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
func (s *server) notify(flags int) {
|
||||
for _, f := range s.onLeaseChanged {
|
||||
f(flags)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteDiskConfig - write configuration
|
||||
func (s *Server) WriteDiskConfig(c *ServerConfig) {
|
||||
func (s *server) WriteDiskConfig(c *ServerConfig) {
|
||||
c.Enabled = s.conf.Enabled
|
||||
c.InterfaceName = s.conf.InterfaceName
|
||||
c.LocalDomainName = s.conf.LocalDomainName
|
||||
|
||||
s.srv4.WriteDiskConfig4(&c.Conf4)
|
||||
s.srv6.WriteDiskConfig6(&c.Conf6)
|
||||
}
|
||||
|
||||
// Start will listen on port 67 and serve DHCP requests.
|
||||
func (s *Server) Start() (err error) {
|
||||
func (s *server) Start() (err error) {
|
||||
err = s.srv4.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -340,7 +355,7 @@ func (s *Server) Start() (err error) {
|
||||
}
|
||||
|
||||
// Stop closes the listening UDP socket
|
||||
func (s *Server) Stop() (err error) {
|
||||
func (s *server) Stop() (err error) {
|
||||
err = s.srv4.Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -356,12 +371,12 @@ func (s *Server) Stop() (err error) {
|
||||
|
||||
// Leases returns the list of active IPv4 and IPv6 DHCP leases. It's safe for
|
||||
// concurrent use.
|
||||
func (s *Server) Leases(flags GetLeasesFlags) (leases []*Lease) {
|
||||
func (s *server) Leases(flags GetLeasesFlags) (leases []*Lease) {
|
||||
return append(s.srv4.GetLeases(flags), s.srv6.GetLeases(flags)...)
|
||||
}
|
||||
|
||||
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
|
||||
func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr {
|
||||
func (s *server) FindMACbyIP(ip net.IP) net.HardwareAddr {
|
||||
if ip.To4() != nil {
|
||||
return s.srv4.FindMACbyIP(ip)
|
||||
}
|
||||
@@ -369,6 +384,6 @@ func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr {
|
||||
}
|
||||
|
||||
// AddStaticLease - add static v4 lease
|
||||
func (s *Server) AddStaticLease(l *Lease) error {
|
||||
func (s *server) AddStaticLease(l *Lease) error {
|
||||
return s.srv4.AddStaticLease(l)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
@@ -26,13 +25,13 @@ func testNotify(flags uint32) {
|
||||
// Leases database store/load.
|
||||
func TestDB(t *testing.T) {
|
||||
var err error
|
||||
s := Server{
|
||||
s := server{
|
||||
conf: &ServerConfig{
|
||||
DBFilePath: dbFilename,
|
||||
},
|
||||
}
|
||||
|
||||
s.srv4, err = v4Create(V4ServerConf{
|
||||
s.srv4, err = v4Create(&V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.IP{192, 168, 10, 100},
|
||||
RangeEnd: net.IP{192, 168, 10, 200},
|
||||
@@ -88,32 +87,6 @@ func TestDB(t *testing.T) {
|
||||
assert.Equal(t, leases[0].Expiry.Unix(), ll[1].Expiry.Unix())
|
||||
}
|
||||
|
||||
func TestIsValidSubnetMask(t *testing.T) {
|
||||
testCases := []struct {
|
||||
mask net.IP
|
||||
want bool
|
||||
}{{
|
||||
mask: net.IP{255, 255, 255, 0},
|
||||
want: true,
|
||||
}, {
|
||||
mask: net.IP{255, 255, 254, 0},
|
||||
want: true,
|
||||
}, {
|
||||
mask: net.IP{255, 255, 252, 0},
|
||||
want: true,
|
||||
}, {
|
||||
mask: net.IP{255, 255, 253, 0},
|
||||
}, {
|
||||
mask: net.IP{255, 255, 255, 1},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.mask.String(), func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, isValidSubnetMask(tc.mask))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLeases(t *testing.T) {
|
||||
dynLeases := []*Lease{{
|
||||
HWAddr: net.HardwareAddr{1, 2, 3, 4},
|
||||
@@ -174,7 +147,7 @@ func TestV4Server_badRange(t *testing.T) {
|
||||
notify: testNotify,
|
||||
}
|
||||
|
||||
_, err := v4Create(conf)
|
||||
_, err := v4Create(&conf)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
func tryTo4(ip net.IP) (ip4 net.IP, err error) {
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("%v is not an IP address", ip)
|
||||
}
|
||||
|
||||
ip4 = ip.To4()
|
||||
if ip4 == nil {
|
||||
return nil, fmt.Errorf("%v is not an IPv4 address", ip)
|
||||
}
|
||||
|
||||
return ip4, nil
|
||||
}
|
||||
|
||||
// Return TRUE if subnet mask is correct (e.g. 255.255.255.0)
|
||||
func isValidSubnetMask(mask net.IP) bool {
|
||||
var n uint32
|
||||
n = binary.BigEndian.Uint32(mask)
|
||||
for i := 0; i != 32; i++ {
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
if (n & 0x80000000) == 0 {
|
||||
return false
|
||||
}
|
||||
n <<= 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
@@ -8,14 +10,12 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
)
|
||||
|
||||
type v4ServerConfJSON struct {
|
||||
@@ -26,12 +26,12 @@ type v4ServerConfJSON struct {
|
||||
LeaseDuration uint32 `json:"lease_duration"`
|
||||
}
|
||||
|
||||
func v4JSONToServerConf(j *v4ServerConfJSON) V4ServerConf {
|
||||
func (j *v4ServerConfJSON) toServerConf() *V4ServerConf {
|
||||
if j == nil {
|
||||
return V4ServerConf{}
|
||||
return &V4ServerConf{}
|
||||
}
|
||||
|
||||
return V4ServerConf{
|
||||
return &V4ServerConf{
|
||||
GatewayIP: j.GatewayIP,
|
||||
SubnetMask: j.SubnetMask,
|
||||
RangeStart: j.RangeStart,
|
||||
@@ -66,7 +66,7 @@ type dhcpStatusResponse struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status := &dhcpStatusResponse{
|
||||
Enabled: s.conf.Enabled,
|
||||
IfaceName: s.conf.InterfaceName,
|
||||
@@ -81,6 +81,7 @@ func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status.StaticLeases = s.Leases(LeasesStatic)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err := json.NewEncoder(w).Encode(status)
|
||||
if err != nil {
|
||||
aghhttp.Error(
|
||||
@@ -93,28 +94,26 @@ func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) enableDHCP(ifaceName string) (code int, err error) {
|
||||
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
|
||||
var hasStaticIP bool
|
||||
hasStaticIP, err = aghnet.IfaceHasStaticIP(ifaceName)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
// ErrPermission may happen here on Linux systems where
|
||||
// AdGuard Home is installed using Snap. That doesn't
|
||||
// necessarily mean that the machine doesn't have
|
||||
// a static IP, so we can assume that it has and go on.
|
||||
// If the machine doesn't, we'll get an error later.
|
||||
// ErrPermission may happen here on Linux systems where AdGuard Home
|
||||
// is installed using Snap. That doesn't necessarily mean that the
|
||||
// machine doesn't have a static IP, so we can assume that it has
|
||||
// and go on. If the machine doesn't, we'll get an error later.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2667.
|
||||
//
|
||||
// TODO(a.garipov): I was thinking about moving this
|
||||
// into IfaceHasStaticIP, but then we wouldn't be able
|
||||
// to log it. Think about it more.
|
||||
// TODO(a.garipov): I was thinking about moving this into
|
||||
// IfaceHasStaticIP, but then we wouldn't be able to log it. Think
|
||||
// about it more.
|
||||
log.Info("error while checking static ip: %s; "+
|
||||
"assuming machine has static ip and going on", err)
|
||||
hasStaticIP = true
|
||||
} else if errors.Is(err, aghnet.ErrNoStaticIPInfo) {
|
||||
// Couldn't obtain a definitive answer. Assume static
|
||||
// IP an go on.
|
||||
// Couldn't obtain a definitive answer. Assume static IP an go on.
|
||||
log.Info("can't check for static ip; " +
|
||||
"assuming machine has static ip and going on")
|
||||
hasStaticIP = true
|
||||
@@ -149,34 +148,39 @@ type dhcpServerConfigJSON struct {
|
||||
Enabled aghalg.NullBool `json:"enabled"`
|
||||
}
|
||||
|
||||
func (s *Server) handleDHCPSetConfigV4(
|
||||
func (s *server) handleDHCPSetConfigV4(
|
||||
conf *dhcpServerConfigJSON,
|
||||
) (srv4 DHCPServer, enabled bool, err error) {
|
||||
) (srv DHCPServer, enabled bool, err error) {
|
||||
if conf.V4 == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
v4Conf := v4JSONToServerConf(conf.V4)
|
||||
v4Conf := conf.V4.toServerConf()
|
||||
v4Conf.Enabled = conf.Enabled == aghalg.NBTrue
|
||||
if len(v4Conf.RangeStart) == 0 {
|
||||
v4Conf.Enabled = false
|
||||
}
|
||||
|
||||
enabled = v4Conf.Enabled
|
||||
v4Conf.InterfaceName = conf.InterfaceName
|
||||
|
||||
c4 := V4ServerConf{}
|
||||
s.srv4.WriteDiskConfig4(&c4)
|
||||
// Set the default values for the fields not configurable via web API.
|
||||
c4 := &V4ServerConf{
|
||||
notify: s.onNotify,
|
||||
ICMPTimeout: s.conf.Conf4.ICMPTimeout,
|
||||
Options: s.conf.Conf4.Options,
|
||||
}
|
||||
|
||||
s.srv4.WriteDiskConfig4(c4)
|
||||
v4Conf.notify = c4.notify
|
||||
v4Conf.ICMPTimeout = c4.ICMPTimeout
|
||||
v4Conf.Options = c4.Options
|
||||
|
||||
srv4, err = v4Create(v4Conf)
|
||||
srv4, err := v4Create(v4Conf)
|
||||
|
||||
return srv4, enabled, err
|
||||
return srv4, srv4.enabled(), err
|
||||
}
|
||||
|
||||
func (s *Server) handleDHCPSetConfigV6(
|
||||
func (s *server) handleDHCPSetConfigV6(
|
||||
conf *dhcpServerConfigJSON,
|
||||
) (srv6 DHCPServer, enabled bool, err error) {
|
||||
if conf.V6 == nil {
|
||||
@@ -205,7 +209,7 @@ func (s *Server) handleDHCPSetConfigV6(
|
||||
return srv6, enabled, err
|
||||
}
|
||||
|
||||
func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf := &dhcpServerConfigJSON{}
|
||||
conf.Enabled = aghalg.BoolToNullBool(s.conf.Enabled)
|
||||
conf.InterfaceName = s.conf.InterfaceName
|
||||
@@ -287,7 +291,7 @@ type netInterfaceJSON struct {
|
||||
Addrs6 []net.IP `json:"ipv6_addresses"`
|
||||
}
|
||||
|
||||
func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]netInterfaceJSON{}
|
||||
|
||||
ifaces, err := net.Interfaces()
|
||||
@@ -345,6 +349,8 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
// ignore link-local
|
||||
//
|
||||
// TODO(e.burkov): Try to listen DHCP on LLA as well.
|
||||
if ipnet.IP.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
@@ -355,7 +361,7 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 {
|
||||
jsonIface.GatewayIP = aghnet.GatewayIP(iface.Name)
|
||||
jsonIface.GatewayIP = aghnet.GatewayIP(iface.Name).AsSlice()
|
||||
response[iface.Name] = jsonIface
|
||||
}
|
||||
}
|
||||
@@ -410,7 +416,7 @@ type dhcpSearchResult struct {
|
||||
// . Search for another DHCP server running
|
||||
// . Check if a static IP is configured for the network interface
|
||||
// Respond with results
|
||||
func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
|
||||
// This use of ReadAll is safe, because request's body is now limited.
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
@@ -482,7 +488,7 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
|
||||
l := &Lease{}
|
||||
err := json.NewDecoder(r.Body).Decode(l)
|
||||
if err != nil {
|
||||
@@ -497,20 +503,16 @@ func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
ip4 := l.IP.To4()
|
||||
if ip4 == nil {
|
||||
var srv DHCPServer
|
||||
if ip4 := l.IP.To4(); ip4 != nil {
|
||||
l.IP = ip4
|
||||
srv = s.srv4
|
||||
} else {
|
||||
l.IP = l.IP.To16()
|
||||
|
||||
err = s.srv6.AddStaticLease(l)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||
}
|
||||
|
||||
return
|
||||
srv = s.srv6
|
||||
}
|
||||
|
||||
l.IP = ip4
|
||||
err = s.srv4.AddStaticLease(l)
|
||||
err = srv.AddStaticLease(l)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||
|
||||
@@ -518,7 +520,7 @@ func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {
|
||||
l := &Lease{}
|
||||
err := json.NewDecoder(r.Body).Decode(l)
|
||||
if err != nil {
|
||||
@@ -555,14 +557,7 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultDHCPLeaseTTL is the default time-to-live for leases.
|
||||
DefaultDHCPLeaseTTL = uint32(timeutil.Day / time.Second)
|
||||
// DefaultDHCPTimeoutICMP is the default timeout for waiting ICMP responses.
|
||||
DefaultDHCPTimeoutICMP = 1000
|
||||
)
|
||||
|
||||
func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *server) handleReset(w http.ResponseWriter, r *http.Request) {
|
||||
err := s.Stop()
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "stopping dhcp: %s", err)
|
||||
@@ -586,7 +581,7 @@ func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) {
|
||||
DBFilePath: s.conf.DBFilePath,
|
||||
}
|
||||
|
||||
v4conf := V4ServerConf{
|
||||
v4conf := &V4ServerConf{
|
||||
LeaseDuration: DefaultDHCPLeaseTTL,
|
||||
ICMPTimeout: DefaultDHCPTimeoutICMP,
|
||||
notify: s.onNotify,
|
||||
@@ -602,7 +597,7 @@ func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) {
|
||||
s.conf.ConfigModified()
|
||||
}
|
||||
|
||||
func (s *Server) handleResetLeases(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *server) handleResetLeases(w http.ResponseWriter, r *http.Request) {
|
||||
err := s.resetLeases()
|
||||
if err != nil {
|
||||
msg := "resetting leases: %s"
|
||||
@@ -612,7 +607,11 @@ func (s *Server) handleResetLeases(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) registerHandlers() {
|
||||
func (s *server) registerHandlers() {
|
||||
if s.conf.HTTPRegister == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.conf.HTTPRegister(http.MethodGet, "/control/dhcp/status", s.handleDHCPStatus)
|
||||
s.conf.HTTPRegister(http.MethodGet, "/control/dhcp/interfaces", s.handleDHCPInterfaces)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/set_config", s.handleDHCPSetConfig)
|
||||
@@ -622,44 +621,3 @@ func (s *Server) registerHandlers() {
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/reset", s.handleReset)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/reset_leases", s.handleResetLeases)
|
||||
}
|
||||
|
||||
// jsonError is a generic JSON error response.
|
||||
//
|
||||
// TODO(a.garipov): Merge together with the implementations in .../home and
|
||||
// other packages after refactoring the web handler registering.
|
||||
type jsonError struct {
|
||||
// Message is the error message, an opaque string.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// notImplemented returns a handler that replies to any request with an HTTP 501
|
||||
// Not Implemented status and a JSON error with the provided message msg.
|
||||
//
|
||||
// TODO(a.garipov): Either take the logger from the server after we've
|
||||
// refactored logging or make this not a method of *Server.
|
||||
func (s *Server) notImplemented(msg string) (f func(http.ResponseWriter, *http.Request)) {
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
|
||||
err := json.NewEncoder(w).Encode(&jsonError{
|
||||
Message: msg,
|
||||
})
|
||||
if err != nil {
|
||||
log.Debug("writing 501 json response: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) registerNotImplementedHandlers() {
|
||||
h := s.notImplemented("dhcp is not supported on windows")
|
||||
|
||||
s.conf.HTTPRegister(http.MethodGet, "/control/dhcp/status", h)
|
||||
s.conf.HTTPRegister(http.MethodGet, "/control/dhcp/interfaces", h)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/set_config", h)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/find_active_dhcp", h)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/add_static_lease", h)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/remove_static_lease", h)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/reset", h)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/reset_leases", h)
|
||||
}
|
||||
55
internal/dhcpd/http_windows.go
Normal file
55
internal/dhcpd/http_windows.go
Normal file
@@ -0,0 +1,55 @@
|
||||
//go:build windows
|
||||
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// jsonError is a generic JSON error response.
|
||||
//
|
||||
// TODO(a.garipov): Merge together with the implementations in .../home and
|
||||
// other packages after refactoring the web handler registering.
|
||||
type jsonError struct {
|
||||
// Message is the error message, an opaque string.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// notImplemented is a handler that replies to any request with an HTTP 501 Not
|
||||
// Implemented status and a JSON error with the provided message msg.
|
||||
//
|
||||
// TODO(a.garipov): Either take the logger from the server after we've
|
||||
// refactored logging or make this not a method of *Server.
|
||||
func (s *server) notImplemented(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
|
||||
err := json.NewEncoder(w).Encode(&jsonError{
|
||||
Message: aghos.Unsupported("dhcp").Error(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Debug("writing 501 json response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// registerHandlers sets the handlers for DHCP HTTP API that always respond with
|
||||
// an HTTP 501, since DHCP server doesn't work on Windows yet.
|
||||
//
|
||||
// TODO(a.garipov): This needs refactoring. We shouldn't even try and
|
||||
// initialize a DHCP server on Windows, but there are currently too many
|
||||
// interconnected parts--such as HTTP handlers and frontend--to make that work
|
||||
// properly.
|
||||
func (s *server) registerHandlers() {
|
||||
s.conf.HTTPRegister(http.MethodGet, "/control/dhcp/status", s.notImplemented)
|
||||
s.conf.HTTPRegister(http.MethodGet, "/control/dhcp/interfaces", s.notImplemented)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/set_config", s.notImplemented)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/find_active_dhcp", s.notImplemented)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/add_static_lease", s.notImplemented)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/remove_static_lease", s.notImplemented)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/reset", s.notImplemented)
|
||||
s.conf.HTTPRegister(http.MethodPost, "/control/dhcp/reset_leases", s.notImplemented)
|
||||
}
|
||||
@@ -1,23 +1,28 @@
|
||||
//go:build windows
|
||||
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServer_notImplemented(t *testing.T) {
|
||||
s := &Server{}
|
||||
h := s.notImplemented("never!")
|
||||
s := &server{}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r, err := http.NewRequest(http.MethodGet, "/unsupported", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
h(w, r)
|
||||
s.notImplemented(w, r)
|
||||
assert.Equal(t, http.StatusNotImplemented, w.Code)
|
||||
assert.Equal(t, `{"message":"never!"}`+"\n", w.Body.String())
|
||||
|
||||
wantStr := fmt.Sprintf("{%q:%q}", "message", aghos.Unsupported("dhcp"))
|
||||
assert.JSONEq(t, wantStr, w.Body.String())
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
@@ -197,10 +196,10 @@ func parseDHCPOption(s string) (code dhcpv4.OptionCode, val dhcpv4.OptionValue,
|
||||
|
||||
// prepareOptions builds the set of DHCP options according to host requirements
|
||||
// document and values from conf.
|
||||
func prepareOptions(conf V4ServerConf) (implicit, explicit dhcpv4.Options) {
|
||||
func (s *v4Server) prepareOptions() {
|
||||
// Set default values of host configuration parameters listed in Appendix A
|
||||
// of RFC-2131.
|
||||
implicit = dhcpv4.OptionsFromList(
|
||||
s.implicitOpts = dhcpv4.OptionsFromList(
|
||||
// IP-Layer Per Host
|
||||
|
||||
// An Internet host that includes embedded gateway code MUST have a
|
||||
@@ -376,14 +375,14 @@ func prepareOptions(conf V4ServerConf) (implicit, explicit dhcpv4.Options) {
|
||||
|
||||
// Set the Router Option to working subnet's IP since it's initialized
|
||||
// with the address of the gateway.
|
||||
dhcpv4.OptRouter(conf.subnet.IP),
|
||||
dhcpv4.OptRouter(s.conf.subnet.IP),
|
||||
|
||||
dhcpv4.OptSubnetMask(conf.subnet.Mask),
|
||||
dhcpv4.OptSubnetMask(s.conf.subnet.Mask),
|
||||
)
|
||||
|
||||
// Set values for explicitly configured options.
|
||||
explicit = dhcpv4.Options{}
|
||||
for i, o := range conf.Options {
|
||||
s.explicitOpts = dhcpv4.Options{}
|
||||
for i, o := range s.conf.Options {
|
||||
code, val, err := parseDHCPOption(o)
|
||||
if err != nil {
|
||||
log.Error("dhcpv4: bad option string at index %d: %s", i, err)
|
||||
@@ -391,17 +390,15 @@ func prepareOptions(conf V4ServerConf) (implicit, explicit dhcpv4.Options) {
|
||||
continue
|
||||
}
|
||||
|
||||
explicit.Update(dhcpv4.Option{Code: code, Value: val})
|
||||
s.explicitOpts.Update(dhcpv4.Option{Code: code, Value: val})
|
||||
// Remove those from the implicit options.
|
||||
delete(implicit, code.Code())
|
||||
delete(s.implicitOpts, code.Code())
|
||||
}
|
||||
|
||||
log.Debug("dhcpv4: implicit options:\n%s", implicit.Summary(nil))
|
||||
log.Debug("dhcpv4: explicit options:\n%s", explicit.Summary(nil))
|
||||
log.Debug("dhcpv4: implicit options:\n%s", s.implicitOpts.Summary(nil))
|
||||
log.Debug("dhcpv4: explicit options:\n%s", s.explicitOpts.Summary(nil))
|
||||
|
||||
if len(explicit) == 0 {
|
||||
explicit = nil
|
||||
if len(s.explicitOpts) == 0 {
|
||||
s.explicitOpts = nil
|
||||
}
|
||||
|
||||
return implicit, explicit
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
@@ -250,17 +249,21 @@ func TestPrepareOptions(t *testing.T) {
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
implicit, explicit := prepareOptions(V4ServerConf{
|
||||
s := &v4Server{
|
||||
conf: &V4ServerConf{
|
||||
// Just to avoid nil pointer dereference.
|
||||
subnet: &net.IPNet{},
|
||||
Options: tc.opts,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.wantExplicit, explicit)
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s.prepareOptions()
|
||||
|
||||
for c := range explicit {
|
||||
assert.NotContains(t, implicit, c)
|
||||
assert.Equal(t, tc.wantExplicit, s.explicitOpts)
|
||||
|
||||
for c := range s.explicitOpts {
|
||||
assert.NotContains(t, s.implicitOpts, c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package dhcpd
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Currently used defaults for ifaceDNSAddrs.
|
||||
const (
|
||||
defaultMaxAttempts int = 10
|
||||
|
||||
defaultBackoff time.Duration = 500 * time.Millisecond
|
||||
)
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package dhcpd
|
||||
|
||||
@@ -9,15 +8,19 @@ import "net"
|
||||
|
||||
type winServer struct{}
|
||||
|
||||
func (s *winServer) ResetLeases(_ []*Lease) (err error) { return nil }
|
||||
func (s *winServer) GetLeases(_ GetLeasesFlags) (leases []*Lease) { return nil }
|
||||
func (s *winServer) getLeasesRef() []*Lease { return nil }
|
||||
func (s *winServer) AddStaticLease(_ *Lease) (err error) { return nil }
|
||||
func (s *winServer) RemoveStaticLease(_ *Lease) (err error) { return nil }
|
||||
func (s *winServer) FindMACbyIP(ip net.IP) (mac net.HardwareAddr) { return nil }
|
||||
func (s *winServer) WriteDiskConfig4(c *V4ServerConf) {}
|
||||
func (s *winServer) WriteDiskConfig6(c *V6ServerConf) {}
|
||||
func (s *winServer) Start() (err error) { return nil }
|
||||
func (s *winServer) Stop() (err error) { return nil }
|
||||
func v4Create(conf V4ServerConf) (DHCPServer, error) { return &winServer{}, nil }
|
||||
func v6Create(conf V6ServerConf) (DHCPServer, error) { return &winServer{}, nil }
|
||||
// type check
|
||||
var _ DHCPServer = winServer{}
|
||||
|
||||
func (winServer) ResetLeases(_ []*Lease) (err error) { return nil }
|
||||
func (winServer) GetLeases(_ GetLeasesFlags) (leases []*Lease) { return nil }
|
||||
func (winServer) getLeasesRef() []*Lease { return nil }
|
||||
func (winServer) AddStaticLease(_ *Lease) (err error) { return nil }
|
||||
func (winServer) RemoveStaticLease(_ *Lease) (err error) { return nil }
|
||||
func (winServer) FindMACbyIP(_ net.IP) (mac net.HardwareAddr) { return nil }
|
||||
func (winServer) WriteDiskConfig4(_ *V4ServerConf) {}
|
||||
func (winServer) WriteDiskConfig6(_ *V6ServerConf) {}
|
||||
func (winServer) Start() (err error) { return nil }
|
||||
func (winServer) Stop() (err error) { return nil }
|
||||
|
||||
func v4Create(_ *V4ServerConf) (s DHCPServer, err error) { return winServer{}, nil }
|
||||
func v6Create(_ V6ServerConf) (s DHCPServer, err error) { return winServer{}, nil }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
@@ -30,8 +29,9 @@ import (
|
||||
//
|
||||
// TODO(a.garipov): Think about unifying this and v6Server.
|
||||
type v4Server struct {
|
||||
conf V4ServerConf
|
||||
srv *server4.Server
|
||||
conf *V4ServerConf
|
||||
|
||||
srv *server4.Server
|
||||
|
||||
// implicitOpts are the options listed in Appendix A of RFC 2131 initialized
|
||||
// with default values. It must not have intersections with [explicitOpts].
|
||||
@@ -55,9 +55,15 @@ type v4Server struct {
|
||||
leases []*Lease
|
||||
}
|
||||
|
||||
func (s *v4Server) enabled() (ok bool) {
|
||||
return s.conf != nil && s.conf.Enabled
|
||||
}
|
||||
|
||||
// WriteDiskConfig4 - write configuration
|
||||
func (s *v4Server) WriteDiskConfig4(c *V4ServerConf) {
|
||||
*c = s.conf
|
||||
if s.conf != nil {
|
||||
*c = *s.conf
|
||||
}
|
||||
}
|
||||
|
||||
// WriteDiskConfig6 - write configuration
|
||||
@@ -114,8 +120,8 @@ func (s *v4Server) validHostnameForClient(cliHostname string, ip net.IP) (hostna
|
||||
func (s *v4Server) ResetLeases(leases []*Lease) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
|
||||
|
||||
if !s.conf.Enabled {
|
||||
return
|
||||
if s.conf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.leasedOffsets = newBitSet()
|
||||
@@ -129,12 +135,7 @@ func (s *v4Server) ResetLeases(leases []*Lease) (err error) {
|
||||
err = s.addLease(l)
|
||||
if err != nil {
|
||||
// TODO(a.garipov): Wrap and bubble up the error.
|
||||
log.Error(
|
||||
"dhcpv4: reset: re-adding a lease for %s (%s): %s",
|
||||
l.IP,
|
||||
l.HWAddr,
|
||||
err,
|
||||
)
|
||||
log.Error("dhcpv4: reset: re-adding a lease for %s (%s): %s", l.IP, l.HWAddr, err)
|
||||
|
||||
continue
|
||||
}
|
||||
@@ -336,11 +337,19 @@ func (s *v4Server) rmLease(lease *Lease) (err error) {
|
||||
return errors.Error("lease not found")
|
||||
}
|
||||
|
||||
// ErrUnconfigured is returned from the server's method when it requires the
|
||||
// server to be configured and it's not.
|
||||
const ErrUnconfigured errors.Error = "server is unconfigured"
|
||||
|
||||
// AddStaticLease implements the DHCPServer interface for *v4Server. It is safe
|
||||
// for concurrent use.
|
||||
func (s *v4Server) AddStaticLease(l *Lease) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "dhcpv4: adding static lease: %w") }()
|
||||
|
||||
if s.conf == nil {
|
||||
return ErrUnconfigured
|
||||
}
|
||||
|
||||
ip := l.IP.To4()
|
||||
if ip == nil {
|
||||
return fmt.Errorf("invalid ip %q, only ipv4 is supported", l.IP)
|
||||
@@ -414,6 +423,10 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
|
||||
func (s *v4Server) RemoveStaticLease(l *Lease) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
|
||||
|
||||
if s.conf == nil {
|
||||
return ErrUnconfigured
|
||||
}
|
||||
|
||||
if len(l.IP) != 4 {
|
||||
return fmt.Errorf("invalid IP")
|
||||
}
|
||||
@@ -1086,12 +1099,6 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4
|
||||
s.send(peer, conn, req, resp)
|
||||
}
|
||||
|
||||
// minDHCPMsgSize is the minimum length of the encoded DHCP message in bytes
|
||||
// according to RFC-2131.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc2131#section-2.
|
||||
const minDHCPMsgSize = 576
|
||||
|
||||
// send writes resp for peer to conn considering the req's parameters according
|
||||
// to RFC-2131.
|
||||
//
|
||||
@@ -1133,16 +1140,6 @@ func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DH
|
||||
}
|
||||
|
||||
pktData := resp.ToBytes()
|
||||
pktLen := len(pktData)
|
||||
if pktLen < minDHCPMsgSize {
|
||||
// Expand the packet to match the minimum DHCP message length. Although
|
||||
// the dhpcv4 package deals with the BOOTP's lower packet length
|
||||
// constraint, it seems some clients expecting the length being at least
|
||||
// 576 bytes as per RFC 2131 (and an obsolete RFC 1533).
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/4337.
|
||||
pktData = append(pktData, make([]byte, minDHCPMsgSize-pktLen)...)
|
||||
}
|
||||
|
||||
log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary())
|
||||
|
||||
@@ -1156,7 +1153,7 @@ func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DH
|
||||
func (s *v4Server) Start() (err error) {
|
||||
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
|
||||
|
||||
if !s.conf.Enabled {
|
||||
if !s.enabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1248,62 +1245,20 @@ func (s *v4Server) Stop() (err error) {
|
||||
}
|
||||
|
||||
// Create DHCPv4 server
|
||||
func v4Create(conf V4ServerConf) (srv DHCPServer, err error) {
|
||||
s := &v4Server{}
|
||||
s.conf = conf
|
||||
s.leaseHosts = stringutil.NewSet()
|
||||
|
||||
// TODO(a.garipov): Don't use a disabled server in other places or just
|
||||
// use an interface.
|
||||
if !conf.Enabled {
|
||||
return s, nil
|
||||
func v4Create(conf *V4ServerConf) (srv *v4Server, err error) {
|
||||
s := &v4Server{
|
||||
leaseHosts: stringutil.NewSet(),
|
||||
}
|
||||
|
||||
var routerIP net.IP
|
||||
routerIP, err = tryTo4(s.conf.GatewayIP)
|
||||
err = conf.Validate()
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("dhcpv4: %w", err)
|
||||
// TODO(a.garipov): Don't use a disabled server in other places or just
|
||||
// use an interface.
|
||||
return s, err
|
||||
}
|
||||
|
||||
if s.conf.SubnetMask == nil {
|
||||
return s, fmt.Errorf("dhcpv4: invalid subnet mask: %v", s.conf.SubnetMask)
|
||||
}
|
||||
|
||||
subnetMask := make([]byte, 4)
|
||||
copy(subnetMask, s.conf.SubnetMask.To4())
|
||||
|
||||
s.conf.subnet = &net.IPNet{
|
||||
IP: routerIP,
|
||||
Mask: subnetMask,
|
||||
}
|
||||
s.conf.broadcastIP = aghnet.BroadcastFromIPNet(s.conf.subnet)
|
||||
|
||||
s.conf.ipRange, err = newIPRange(conf.RangeStart, conf.RangeEnd)
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("dhcpv4: %w", err)
|
||||
}
|
||||
|
||||
if s.conf.ipRange.contains(routerIP) {
|
||||
return s, fmt.Errorf("dhcpv4: gateway ip %v in the ip range: %v-%v",
|
||||
routerIP,
|
||||
conf.RangeStart,
|
||||
conf.RangeEnd,
|
||||
)
|
||||
}
|
||||
|
||||
if !s.conf.subnet.Contains(conf.RangeStart) {
|
||||
return s, fmt.Errorf("dhcpv4: range start %v is outside network %v",
|
||||
conf.RangeStart,
|
||||
s.conf.subnet,
|
||||
)
|
||||
}
|
||||
|
||||
if !s.conf.subnet.Contains(conf.RangeEnd) {
|
||||
return s, fmt.Errorf("dhcpv4: range end %v is outside network %v",
|
||||
conf.RangeEnd,
|
||||
s.conf.subnet,
|
||||
)
|
||||
}
|
||||
s.conf = &V4ServerConf{}
|
||||
*s.conf = *conf
|
||||
|
||||
// TODO(a.garipov, d.seregin): Check that every lease is inside the IPRange.
|
||||
s.leasedOffsets = newBitSet()
|
||||
@@ -1315,7 +1270,7 @@ func v4Create(conf V4ServerConf) (srv DHCPServer, err error) {
|
||||
s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)
|
||||
}
|
||||
|
||||
s.implicitOpts, s.explicitOpts = prepareOptions(s.conf)
|
||||
s.prepareOptions()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
@@ -30,19 +29,16 @@ var (
|
||||
DefaultSubnetMask = net.IP{255, 255, 255, 0}
|
||||
)
|
||||
|
||||
func notify4(flags uint32) {
|
||||
}
|
||||
|
||||
// defaultV4ServerConf returns the default configuration for *v4Server to use in
|
||||
// tests.
|
||||
func defaultV4ServerConf() (conf V4ServerConf) {
|
||||
return V4ServerConf{
|
||||
func defaultV4ServerConf() (conf *V4ServerConf) {
|
||||
return &V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: DefaultRangeStart,
|
||||
RangeEnd: DefaultRangeEnd,
|
||||
GatewayIP: DefaultGatewayIP,
|
||||
SubnetMask: DefaultSubnetMask,
|
||||
notify: notify4,
|
||||
notify: testNotify,
|
||||
dnsIPAddrs: []net.IP{DefaultSelfIP},
|
||||
}
|
||||
}
|
||||
@@ -350,13 +346,10 @@ func TestV4Server_handle_optionsPriority(t *testing.T) {
|
||||
defer func() { s.implicitOpts.Update(dhcpv4.OptDNS(defaultIP)) }()
|
||||
}
|
||||
|
||||
ss, err := v4Create(conf)
|
||||
var err error
|
||||
s, err = v4Create(conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
var ok bool
|
||||
s, ok = ss.(*v4Server)
|
||||
require.True(t, ok)
|
||||
|
||||
s.conf.dnsIPAddrs = []net.IP{defaultIP}
|
||||
|
||||
return s
|
||||
@@ -490,10 +483,9 @@ func TestV4Server_updateOptions(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.IsType(t, (*v4Server)(nil), s)
|
||||
s4, _ := s.(*v4Server)
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s4.updateOptions(req, resp)
|
||||
s.updateOptions(req, resp)
|
||||
|
||||
for c, v := range tc.wantOpts {
|
||||
if v == nil {
|
||||
@@ -596,13 +588,9 @@ func TestV4DynamicLease_Get(t *testing.T) {
|
||||
"82 ip 1.2.3.4",
|
||||
}
|
||||
|
||||
var err error
|
||||
sIface, err := v4Create(conf)
|
||||
s, err := v4Create(conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
s, ok := sIface.(*v4Server)
|
||||
require.True(t, ok)
|
||||
|
||||
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
|
||||
s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs...))
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
@@ -173,7 +172,7 @@ func (s *v6Server) rmDynamicLease(lease *Lease) (err error) {
|
||||
func (s *v6Server) AddStaticLease(l *Lease) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
|
||||
|
||||
if len(l.IP) != 16 {
|
||||
if len(l.IP) != net.IPv6len {
|
||||
return fmt.Errorf("invalid IP")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
//go:build darwin || freebsd || linux || openbsd
|
||||
|
||||
package dhcpd
|
||||
|
||||
@@ -128,9 +128,14 @@ type FilteringConfig struct {
|
||||
// IpsetList is the ipset configuration that allows AdGuard Home to add
|
||||
// IP addresses of the specified domain names to an ipset list. Syntax:
|
||||
//
|
||||
// DOMAIN[,DOMAIN].../IPSET_NAME
|
||||
// DOMAIN[,DOMAIN].../IPSET_NAME
|
||||
//
|
||||
// This field is ignored if [IpsetListFileName] is set.
|
||||
IpsetList []string `yaml:"ipset"`
|
||||
|
||||
// IpsetListFileName, if set, points to the file with ipset configuration.
|
||||
// The format is the same as in [IpsetList].
|
||||
IpsetListFileName string `yaml:"ipset_file"`
|
||||
}
|
||||
|
||||
// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
|
||||
@@ -400,6 +405,26 @@ func setProxyUpstreamMode(
|
||||
}
|
||||
}
|
||||
|
||||
// prepareIpsetListSettings reads and prepares the ipset configuration either
|
||||
// from a file or from the data in the configuration file.
|
||||
func (s *Server) prepareIpsetListSettings() (err error) {
|
||||
fn := s.conf.IpsetListFileName
|
||||
if fn == "" {
|
||||
return s.ipset.init(s.conf.IpsetList)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ipsets := stringutil.SplitTrimmed(string(data), "\n")
|
||||
|
||||
log.Debug("dns: using %d ipset rules from file %q", len(ipsets), fn)
|
||||
|
||||
return s.ipset.init(ipsets)
|
||||
}
|
||||
|
||||
// prepareTLS - prepares TLS configuration for the DNS proxy
|
||||
func (s *Server) prepareTLS(proxyConfig *proxy.Config) error {
|
||||
if len(s.conf.CertificateChainData) == 0 || len(s.conf.PrivateKeyData) == 0 {
|
||||
|
||||
@@ -296,7 +296,7 @@ func (s *Server) makeDDRResponse(req *dns.Msg) (resp *dns.Msg) {
|
||||
values := []dns.SVCBKeyValue{
|
||||
&dns.SVCBAlpn{Alpn: []string{"h2"}},
|
||||
&dns.SVCBPort{Port: uint16(addr.Port)},
|
||||
&dns.SVCBDoHPath{Template: "/dns-query?dns"},
|
||||
&dns.SVCBDoHPath{Template: "/dns-query{?dns}"},
|
||||
}
|
||||
|
||||
ans := &dns.SVCB{
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestServer_ProcessDDRQuery(t *testing.T) {
|
||||
Value: []dns.SVCBKeyValue{
|
||||
&dns.SVCBAlpn{Alpn: []string{"h2"}},
|
||||
&dns.SVCBPort{Port: 8044},
|
||||
&dns.SVCBDoHPath{Template: "/dns-query?dns"},
|
||||
&dns.SVCBDoHPath{Template: "/dns-query{?dns}"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ func TestServer_ProcessDHCPHosts_localRestriction(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := &Server{
|
||||
dhcpServer: &testDHCP{},
|
||||
dhcpServer: testDHCP,
|
||||
localDomainSuffix: defaultLocalDomainSuffix,
|
||||
tableHostToIP: hostToIPTable{
|
||||
"example." + defaultLocalDomainSuffix: knownIP,
|
||||
@@ -378,7 +378,7 @@ func TestServer_ProcessDHCPHosts(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
s := &Server{
|
||||
dhcpServer: &testDHCP{},
|
||||
dhcpServer: testDHCP,
|
||||
localDomainSuffix: tc.suffix,
|
||||
tableHostToIP: hostToIPTable{
|
||||
"example." + tc.suffix: knownIP,
|
||||
|
||||
@@ -58,10 +58,10 @@ type hostToIPTable = map[string]net.IP
|
||||
//
|
||||
// The zero Server is empty and ready for use.
|
||||
type Server struct {
|
||||
dnsProxy *proxy.Proxy // DNS proxy instance
|
||||
dnsFilter *filtering.DNSFilter // DNS filter instance
|
||||
dhcpServer dhcpd.ServerInterface // DHCP server instance (optional)
|
||||
queryLog querylog.QueryLog // Query log instance
|
||||
dnsProxy *proxy.Proxy // DNS proxy instance
|
||||
dnsFilter *filtering.DNSFilter // DNS filter instance
|
||||
dhcpServer dhcpd.Interface // DHCP server instance (optional)
|
||||
queryLog querylog.QueryLog // Query log instance
|
||||
stats stats.Interface
|
||||
access *accessCtx
|
||||
|
||||
@@ -110,7 +110,7 @@ type DNSCreateParams struct {
|
||||
DNSFilter *filtering.DNSFilter
|
||||
Stats stats.Interface
|
||||
QueryLog querylog.QueryLog
|
||||
DHCPServer dhcpd.ServerInterface
|
||||
DHCPServer dhcpd.Interface
|
||||
PrivateNets netutil.SubnetSet
|
||||
Anonymizer *aghnet.IPMut
|
||||
LocalDomain string
|
||||
@@ -446,10 +446,10 @@ func (s *Server) Prepare(conf *ServerConfig) (err error) {
|
||||
|
||||
s.initDefaultSettings()
|
||||
|
||||
err = s.ipset.init(s.conf.IpsetList)
|
||||
err = s.prepareIpsetListSettings()
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return err
|
||||
return fmt.Errorf("preparing ipset settings: %w", err)
|
||||
}
|
||||
|
||||
err = s.prepareUpstreamSettings()
|
||||
|
||||
@@ -67,12 +67,13 @@ func createTestServer(
|
||||
ID: 0, Data: []byte(rules),
|
||||
}}
|
||||
|
||||
f := filtering.New(filterConf, filters)
|
||||
f, err := filtering.New(filterConf, filters)
|
||||
require.NoError(t, err)
|
||||
|
||||
f.SetEnabled(true)
|
||||
|
||||
var err error
|
||||
s, err = NewServer(DNSCreateParams{
|
||||
DHCPServer: &testDHCP{},
|
||||
DHCPServer: testDHCP,
|
||||
DNSFilter: f,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
})
|
||||
@@ -774,9 +775,11 @@ func TestBlockedCustomIP(t *testing.T) {
|
||||
Data: []byte(rules),
|
||||
}}
|
||||
|
||||
f := filtering.New(&filtering.Config{}, filters)
|
||||
f, err := filtering.New(&filtering.Config{}, filters)
|
||||
require.NoError(t, err)
|
||||
|
||||
s, err := NewServer(DNSCreateParams{
|
||||
DHCPServer: &testDHCP{},
|
||||
DHCPServer: testDHCP,
|
||||
DNSFilter: f,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
})
|
||||
@@ -906,11 +909,13 @@ func TestRewrite(t *testing.T) {
|
||||
Type: dns.TypeCNAME,
|
||||
}},
|
||||
}
|
||||
f := filtering.New(c, nil)
|
||||
f, err := filtering.New(c, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
f.SetEnabled(true)
|
||||
|
||||
s, err := NewServer(DNSCreateParams{
|
||||
DHCPServer: &testDHCP{},
|
||||
DHCPServer: testDHCP,
|
||||
DNSFilter: f,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
})
|
||||
@@ -1005,26 +1010,31 @@ func publicKey(priv any) any {
|
||||
}
|
||||
}
|
||||
|
||||
type testDHCP struct{}
|
||||
|
||||
func (d *testDHCP) Enabled() (ok bool) { return true }
|
||||
|
||||
func (d *testDHCP) Leases(flags dhcpd.GetLeasesFlags) (leases []*dhcpd.Lease) {
|
||||
return []*dhcpd.Lease{{
|
||||
IP: net.IP{192, 168, 12, 34},
|
||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
Hostname: "myhost",
|
||||
}}
|
||||
var testDHCP = &dhcpd.MockInterface{
|
||||
OnStart: func() (err error) { panic("not implemented") },
|
||||
OnStop: func() (err error) { panic("not implemented") },
|
||||
OnEnabled: func() (ok bool) { return true },
|
||||
OnLeases: func(flags dhcpd.GetLeasesFlags) (leases []*dhcpd.Lease) {
|
||||
return []*dhcpd.Lease{{
|
||||
IP: net.IP{192, 168, 12, 34},
|
||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
Hostname: "myhost",
|
||||
}}
|
||||
},
|
||||
OnSetOnLeaseChanged: func(olct dhcpd.OnLeaseChangedT) {},
|
||||
OnFindMACbyIP: func(ip net.IP) (mac net.HardwareAddr) { panic("not implemented") },
|
||||
OnWriteDiskConfig: func(c *dhcpd.ServerConfig) { panic("not implemented") },
|
||||
}
|
||||
|
||||
func (d *testDHCP) SetOnLeaseChanged(onLeaseChanged dhcpd.OnLeaseChangedT) {}
|
||||
|
||||
func TestPTRResponseFromDHCPLeases(t *testing.T) {
|
||||
const localDomain = "lan"
|
||||
|
||||
flt, err := filtering.New(&filtering.Config{}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
s, err := NewServer(DNSCreateParams{
|
||||
DNSFilter: filtering.New(&filtering.Config{}, nil),
|
||||
DHCPServer: &testDHCP{},
|
||||
DNSFilter: flt,
|
||||
DHCPServer: testDHCP,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
LocalDomain: localDomain,
|
||||
})
|
||||
@@ -1090,14 +1100,16 @@ func TestPTRResponseFromHosts(t *testing.T) {
|
||||
assert.Equal(t, uint32(1), atomic.LoadUint32(&eventsCalledCounter))
|
||||
})
|
||||
|
||||
flt := filtering.New(&filtering.Config{
|
||||
flt, err := filtering.New(&filtering.Config{
|
||||
EtcHosts: hc,
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
flt.SetEnabled(true)
|
||||
|
||||
var s *Server
|
||||
s, err = NewServer(DNSCreateParams{
|
||||
DHCPServer: &testDHCP{},
|
||||
DHCPServer: testDHCP,
|
||||
DNSFilter: flt,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
})
|
||||
|
||||
@@ -35,11 +35,12 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
|
||||
ID: 0, Data: []byte(rules),
|
||||
}}
|
||||
|
||||
f := filtering.New(&filtering.Config{}, filters)
|
||||
f, err := filtering.New(&filtering.Config{}, filters)
|
||||
require.NoError(t, err)
|
||||
f.SetEnabled(true)
|
||||
|
||||
s, err := NewServer(DNSCreateParams{
|
||||
DHCPServer: &testDHCP{},
|
||||
DHCPServer: testDHCP,
|
||||
DNSFilter: f,
|
||||
PrivateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),
|
||||
})
|
||||
|
||||
@@ -421,31 +421,34 @@ func initBlockedServices() {
|
||||
}
|
||||
|
||||
// BlockedSvcKnown - return TRUE if a blocked service name is known
|
||||
func BlockedSvcKnown(s string) bool {
|
||||
_, ok := serviceRules[s]
|
||||
func BlockedSvcKnown(s string) (ok bool) {
|
||||
_, ok = serviceRules[s]
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// ApplyBlockedServices - set blocked services settings for this DNS request
|
||||
func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string, global bool) {
|
||||
func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string) {
|
||||
setts.ServicesRules = []ServiceEntry{}
|
||||
if global {
|
||||
if list == nil {
|
||||
d.confLock.RLock()
|
||||
defer d.confLock.RUnlock()
|
||||
|
||||
list = d.Config.BlockedServices
|
||||
}
|
||||
|
||||
for _, name := range list {
|
||||
rules, ok := serviceRules[name]
|
||||
|
||||
if !ok {
|
||||
log.Error("unknown service name: %s", name)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
s := ServiceEntry{}
|
||||
s.Name = name
|
||||
s.Rules = rules
|
||||
setts.ServicesRules = append(setts.ServicesRules, s)
|
||||
setts.ServicesRules = append(setts.ServicesRules, ServiceEntry{
|
||||
Name: name,
|
||||
Rules: rules,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,10 +493,3 @@ func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
d.ConfigModified()
|
||||
}
|
||||
|
||||
// registerBlockedServicesHandlers - register HTTP handlers
|
||||
func (d *DNSFilter) registerBlockedServicesHandlers() {
|
||||
d.Config.HTTPRegister(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesAvailableServices)
|
||||
d.Config.HTTPRegister(http.MethodGet, "/control/blocked_services/list", d.handleBlockedServicesList)
|
||||
d.Config.HTTPRegister(http.MethodPost, "/control/blocked_services/set", d.handleBlockedServicesSet)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package home
|
||||
package filtering
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -34,7 +34,7 @@ func validateFilterURL(urlStr string) (err error) {
|
||||
return fmt.Errorf("checking filter url: %w", err)
|
||||
}
|
||||
|
||||
if s := url.Scheme; s != schemeHTTP && s != schemeHTTPS {
|
||||
if s := url.Scheme; s != aghhttp.SchemeHTTP && s != aghhttp.SchemeHTTPS {
|
||||
return fmt.Errorf("checking filter url: invalid scheme %q", s)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ type filterAddJSON struct {
|
||||
Whitelist bool `json:"whitelist"`
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
|
||||
func (d *DNSFilter) handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
|
||||
fj := filterAddJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&fj)
|
||||
if err != nil {
|
||||
@@ -65,14 +65,14 @@ func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
if filterExists(fj.URL) {
|
||||
if d.filterExists(fj.URL) {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "Filter URL already added -- %s", fj.URL)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Set necessary properties
|
||||
filt := filter{
|
||||
filt := FilterYAML{
|
||||
Enabled: true,
|
||||
URL: fj.URL,
|
||||
Name: fj.Name,
|
||||
@@ -81,7 +81,7 @@ func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
|
||||
filt.ID = assignUniqueFilterID()
|
||||
|
||||
// Download the filter contents
|
||||
ok, err := f.update(&filt)
|
||||
ok, err := d.update(&filt)
|
||||
if err != nil {
|
||||
aghhttp.Error(
|
||||
r,
|
||||
@@ -109,14 +109,14 @@ func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// URL is assumed valid so append it to filters, update config, write new
|
||||
// file and reload it to engines.
|
||||
if !filterAdd(filt) {
|
||||
if !d.filterAdd(filt) {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "Filter URL already added -- %s", filt.URL)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
onConfigModified()
|
||||
enableFilters(true)
|
||||
d.ConfigModified()
|
||||
d.EnableFilters(true)
|
||||
|
||||
_, err = fmt.Fprintf(w, "OK %d rules\n", filt.RulesCount)
|
||||
if err != nil {
|
||||
@@ -124,7 +124,7 @@ func (f *Filtering) handleFilteringAddURL(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
|
||||
func (d *DNSFilter) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
|
||||
type request struct {
|
||||
URL string `json:"url"`
|
||||
Whitelist bool `json:"whitelist"`
|
||||
@@ -138,23 +138,23 @@ func (f *Filtering) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
config.Lock()
|
||||
filters := &config.Filters
|
||||
d.filtersMu.Lock()
|
||||
filters := &d.Filters
|
||||
if req.Whitelist {
|
||||
filters = &config.WhitelistFilters
|
||||
filters = &d.WhitelistFilters
|
||||
}
|
||||
|
||||
var deleted filter
|
||||
var newFilters []filter
|
||||
for _, f := range *filters {
|
||||
if f.URL != req.URL {
|
||||
newFilters = append(newFilters, f)
|
||||
var deleted FilterYAML
|
||||
var newFilters []FilterYAML
|
||||
for _, flt := range *filters {
|
||||
if flt.URL != req.URL {
|
||||
newFilters = append(newFilters, flt)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
deleted = f
|
||||
path := f.Path()
|
||||
deleted = flt
|
||||
path := flt.Path(d.DataDir)
|
||||
err = os.Rename(path, path+".old")
|
||||
if err != nil {
|
||||
log.Error("deleting filter %q: %s", path, err)
|
||||
@@ -162,10 +162,10 @@ func (f *Filtering) handleFilteringRemoveURL(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
*filters = newFilters
|
||||
config.Unlock()
|
||||
d.filtersMu.Unlock()
|
||||
|
||||
onConfigModified()
|
||||
enableFilters(true)
|
||||
d.ConfigModified()
|
||||
d.EnableFilters(true)
|
||||
|
||||
// NOTE: The old files "filter.txt.old" aren't deleted. It's not really
|
||||
// necessary, but will require the additional complicated code to run
|
||||
@@ -191,55 +191,51 @@ type filterURLReq struct {
|
||||
Whitelist bool `json:"whitelist"`
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {
|
||||
func (d *DNSFilter) handleFilteringSetURL(w http.ResponseWriter, r *http.Request) {
|
||||
fj := filterURLReq{}
|
||||
err := json.NewDecoder(r.Body).Decode(&fj)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "decoding request: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if fj.Data == nil {
|
||||
err = errors.Error("data cannot be null")
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", errors.Error("data is absent"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = validateFilterURL(fj.Data.URL)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid url: %s", err)
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "invalid url: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
filt := filter{
|
||||
filt := FilterYAML{
|
||||
Enabled: fj.Data.Enabled,
|
||||
Name: fj.Data.Name,
|
||||
URL: fj.Data.URL,
|
||||
}
|
||||
status := f.filterSetProperties(fj.URL, filt, fj.Whitelist)
|
||||
status := d.filterSetProperties(fj.URL, filt, fj.Whitelist)
|
||||
if (status & statusFound) == 0 {
|
||||
http.Error(w, "URL doesn't exist", http.StatusBadRequest)
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "URL doesn't exist")
|
||||
|
||||
return
|
||||
}
|
||||
if (status & statusURLExists) != 0 {
|
||||
http.Error(w, "URL already exists", http.StatusBadRequest)
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "URL already exists")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
onConfigModified()
|
||||
d.ConfigModified()
|
||||
|
||||
restart := (status & statusEnabledChanged) != 0
|
||||
if (status&statusUpdateRequired) != 0 && fj.Data.Enabled {
|
||||
// download new filter and apply its rules
|
||||
flags := filterRefreshBlocklists
|
||||
if fj.Whitelist {
|
||||
flags = filterRefreshAllowlists
|
||||
}
|
||||
nUpdated, _ := f.refreshFilters(flags, true)
|
||||
// download new filter and apply its rules.
|
||||
nUpdated := d.refreshFilters(!fj.Whitelist, fj.Whitelist, false)
|
||||
// if at least 1 filter has been updated, refreshFilters() restarts the filtering automatically
|
||||
// if not - we restart the filtering ourselves
|
||||
restart = false
|
||||
@@ -249,11 +245,11 @@ func (f *Filtering) handleFilteringSetURL(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
if restart {
|
||||
enableFilters(true)
|
||||
d.EnableFilters(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
|
||||
func (d *DNSFilter) handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
|
||||
// This use of ReadAll is safe, because request's body is now limited.
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
@@ -262,12 +258,12 @@ func (f *Filtering) handleFilteringSetRules(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
config.UserRules = strings.Split(string(body), "\n")
|
||||
onConfigModified()
|
||||
enableFilters(true)
|
||||
d.UserRules = strings.Split(string(body), "\n")
|
||||
d.ConfigModified()
|
||||
d.EnableFilters(true)
|
||||
}
|
||||
|
||||
func (f *Filtering) handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
|
||||
func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
|
||||
type Req struct {
|
||||
White bool `json:"whitelist"`
|
||||
}
|
||||
@@ -285,35 +281,27 @@ func (f *Filtering) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
flags := filterRefreshBlocklists
|
||||
if req.White {
|
||||
flags = filterRefreshAllowlists
|
||||
}
|
||||
func() {
|
||||
// Temporarily unlock the Context.controlLock because the
|
||||
// f.refreshFilters waits for it to be unlocked but it's
|
||||
// actually locked in ensure wrapper.
|
||||
//
|
||||
// TODO(e.burkov): Reconsider this messy syncing process.
|
||||
Context.controlLock.Unlock()
|
||||
defer Context.controlLock.Lock()
|
||||
|
||||
resp.Updated, err = f.refreshFilters(flags|filterRefreshForce, false)
|
||||
}()
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
|
||||
var ok bool
|
||||
resp.Updated, _, ok = d.tryRefreshFilters(!req.White, req.White, true)
|
||||
if !ok {
|
||||
aghhttp.Error(
|
||||
r,
|
||||
w,
|
||||
http.StatusInternalServerError,
|
||||
"filters update procedure is already running",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
js, err := json.Marshal(resp)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(js)
|
||||
}
|
||||
|
||||
type filterJSON struct {
|
||||
@@ -333,7 +321,7 @@ type filteringConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func filterToJSON(f filter) filterJSON {
|
||||
func filterToJSON(f FilterYAML) filterJSON {
|
||||
fj := filterJSON{
|
||||
ID: f.ID,
|
||||
Enabled: f.Enabled,
|
||||
@@ -350,21 +338,21 @@ func filterToJSON(f filter) filterJSON {
|
||||
}
|
||||
|
||||
// Get filtering configuration
|
||||
func (f *Filtering) handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
|
||||
func (d *DNSFilter) handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
|
||||
resp := filteringConfig{}
|
||||
config.RLock()
|
||||
resp.Enabled = config.DNS.FilteringEnabled
|
||||
resp.Interval = config.DNS.FiltersUpdateIntervalHours
|
||||
for _, f := range config.Filters {
|
||||
d.filtersMu.RLock()
|
||||
resp.Enabled = d.FilteringEnabled
|
||||
resp.Interval = d.FiltersUpdateIntervalHours
|
||||
for _, f := range d.Filters {
|
||||
fj := filterToJSON(f)
|
||||
resp.Filters = append(resp.Filters, fj)
|
||||
}
|
||||
for _, f := range config.WhitelistFilters {
|
||||
for _, f := range d.WhitelistFilters {
|
||||
fj := filterToJSON(f)
|
||||
resp.WhitelistFilters = append(resp.WhitelistFilters, fj)
|
||||
}
|
||||
resp.UserRules = config.UserRules
|
||||
config.RUnlock()
|
||||
resp.UserRules = d.UserRules
|
||||
d.filtersMu.RUnlock()
|
||||
|
||||
jsonVal, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
@@ -380,7 +368,7 @@ func (f *Filtering) handleFilteringStatus(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
// Set filtering configuration
|
||||
func (f *Filtering) handleFilteringConfig(w http.ResponseWriter, r *http.Request) {
|
||||
func (d *DNSFilter) handleFilteringConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req := filteringConfig{}
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
@@ -389,22 +377,22 @@ func (f *Filtering) handleFilteringConfig(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
if !checkFiltersUpdateIntervalHours(req.Interval) {
|
||||
if !ValidateUpdateIvl(req.Interval) {
|
||||
aghhttp.Error(r, w, http.StatusBadRequest, "Unsupported interval")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func() {
|
||||
config.Lock()
|
||||
defer config.Unlock()
|
||||
d.filtersMu.Lock()
|
||||
defer d.filtersMu.Unlock()
|
||||
|
||||
config.DNS.FilteringEnabled = req.Enabled
|
||||
config.DNS.FiltersUpdateIntervalHours = req.Interval
|
||||
d.FilteringEnabled = req.Enabled
|
||||
d.FiltersUpdateIntervalHours = req.Interval
|
||||
}()
|
||||
|
||||
onConfigModified()
|
||||
enableFilters(true)
|
||||
d.ConfigModified()
|
||||
d.EnableFilters(true)
|
||||
}
|
||||
|
||||
type checkHostRespRule struct {
|
||||
@@ -435,15 +423,15 @@ type checkHostResp struct {
|
||||
FilterID int64 `json:"filter_id"`
|
||||
}
|
||||
|
||||
func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
host := q.Get("name")
|
||||
func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.URL.Query().Get("name")
|
||||
|
||||
setts := Context.dnsFilter.GetConfig()
|
||||
setts := d.GetConfig()
|
||||
setts.FilteringEnabled = true
|
||||
setts.ProtectionEnabled = true
|
||||
Context.dnsFilter.ApplyBlockedServices(&setts, nil, true)
|
||||
result, err := Context.dnsFilter.CheckHost(host, dns.TypeA, &setts)
|
||||
|
||||
d.ApplyBlockedServices(&setts, nil)
|
||||
result, err := d.CheckHost(host, dns.TypeA, &setts)
|
||||
if err != nil {
|
||||
aghhttp.Error(
|
||||
r,
|
||||
@@ -457,18 +445,20 @@ func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := checkHostResp{}
|
||||
resp.Reason = result.Reason.String()
|
||||
resp.SvcName = result.ServiceName
|
||||
resp.CanonName = result.CanonName
|
||||
resp.IPList = result.IPList
|
||||
rulesLen := len(result.Rules)
|
||||
resp := checkHostResp{
|
||||
Reason: result.Reason.String(),
|
||||
SvcName: result.ServiceName,
|
||||
CanonName: result.CanonName,
|
||||
IPList: result.IPList,
|
||||
Rules: make([]*checkHostRespRule, len(result.Rules)),
|
||||
}
|
||||
|
||||
if len(result.Rules) > 0 {
|
||||
if rulesLen > 0 {
|
||||
resp.FilterID = result.Rules[0].FilterListID
|
||||
resp.Rule = result.Rules[0].Text
|
||||
}
|
||||
|
||||
resp.Rules = make([]*checkHostRespRule, len(result.Rules))
|
||||
for i, r := range result.Rules {
|
||||
resp.Rules[i] = &checkHostRespRule{
|
||||
FilterListID: r.FilterListID,
|
||||
@@ -476,28 +466,51 @@ func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
js, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(js)
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterFilteringHandlers - register handlers
|
||||
func (f *Filtering) RegisterFilteringHandlers() {
|
||||
httpRegister(http.MethodGet, "/control/filtering/status", f.handleFilteringStatus)
|
||||
httpRegister(http.MethodPost, "/control/filtering/config", f.handleFilteringConfig)
|
||||
httpRegister(http.MethodPost, "/control/filtering/add_url", f.handleFilteringAddURL)
|
||||
httpRegister(http.MethodPost, "/control/filtering/remove_url", f.handleFilteringRemoveURL)
|
||||
httpRegister(http.MethodPost, "/control/filtering/set_url", f.handleFilteringSetURL)
|
||||
httpRegister(http.MethodPost, "/control/filtering/refresh", f.handleFilteringRefresh)
|
||||
httpRegister(http.MethodPost, "/control/filtering/set_rules", f.handleFilteringSetRules)
|
||||
httpRegister(http.MethodGet, "/control/filtering/check_host", f.handleCheckHost)
|
||||
func (d *DNSFilter) RegisterFilteringHandlers() {
|
||||
registerHTTP := d.HTTPRegister
|
||||
if registerHTTP == nil {
|
||||
return
|
||||
}
|
||||
|
||||
registerHTTP(http.MethodPost, "/control/safebrowsing/enable", d.handleSafeBrowsingEnable)
|
||||
registerHTTP(http.MethodPost, "/control/safebrowsing/disable", d.handleSafeBrowsingDisable)
|
||||
registerHTTP(http.MethodGet, "/control/safebrowsing/status", d.handleSafeBrowsingStatus)
|
||||
|
||||
registerHTTP(http.MethodPost, "/control/parental/enable", d.handleParentalEnable)
|
||||
registerHTTP(http.MethodPost, "/control/parental/disable", d.handleParentalDisable)
|
||||
registerHTTP(http.MethodGet, "/control/parental/status", d.handleParentalStatus)
|
||||
|
||||
registerHTTP(http.MethodPost, "/control/safesearch/enable", d.handleSafeSearchEnable)
|
||||
registerHTTP(http.MethodPost, "/control/safesearch/disable", d.handleSafeSearchDisable)
|
||||
registerHTTP(http.MethodGet, "/control/safesearch/status", d.handleSafeSearchStatus)
|
||||
|
||||
registerHTTP(http.MethodGet, "/control/rewrite/list", d.handleRewriteList)
|
||||
registerHTTP(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd)
|
||||
registerHTTP(http.MethodPost, "/control/rewrite/delete", d.handleRewriteDelete)
|
||||
|
||||
registerHTTP(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesAvailableServices)
|
||||
registerHTTP(http.MethodGet, "/control/blocked_services/list", d.handleBlockedServicesList)
|
||||
registerHTTP(http.MethodPost, "/control/blocked_services/set", d.handleBlockedServicesSet)
|
||||
|
||||
registerHTTP(http.MethodGet, "/control/filtering/status", d.handleFilteringStatus)
|
||||
registerHTTP(http.MethodPost, "/control/filtering/config", d.handleFilteringConfig)
|
||||
registerHTTP(http.MethodPost, "/control/filtering/add_url", d.handleFilteringAddURL)
|
||||
registerHTTP(http.MethodPost, "/control/filtering/remove_url", d.handleFilteringRemoveURL)
|
||||
registerHTTP(http.MethodPost, "/control/filtering/set_url", d.handleFilteringSetURL)
|
||||
registerHTTP(http.MethodPost, "/control/filtering/refresh", d.handleFilteringRefresh)
|
||||
registerHTTP(http.MethodPost, "/control/filtering/set_rules", d.handleFilteringSetRules)
|
||||
registerHTTP(http.MethodGet, "/control/filtering/check_host", d.handleCheckHost)
|
||||
}
|
||||
|
||||
func checkFiltersUpdateIntervalHours(i uint32) bool {
|
||||
// ValidateUpdateIvl returns false if i is not a valid filters update interval.
|
||||
func ValidateUpdateIvl(i uint32) bool {
|
||||
return i == 0 || i == 1 || i == 12 || i == 1*24 || i == 3*24 || i == 7*24
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) {
|
||||
|1.2.3.5.in-addr.arpa^$dnsrewrite=NOERROR;PTR;new-ptr-with-dot.
|
||||
`
|
||||
|
||||
f := newForTest(t, nil, []Filter{{ID: 0, Data: []byte(text)}})
|
||||
f, _ := newForTest(t, nil, []Filter{{ID: 0, Data: []byte(text)}})
|
||||
setts := &Settings{
|
||||
FilteringEnabled: true,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package home
|
||||
package filtering
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -8,63 +8,29 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var nextFilterID = time.Now().Unix() // semi-stable way to generate an unique ID
|
||||
// filterDir is the subdirectory of a data directory to store downloaded
|
||||
// filters.
|
||||
const filterDir = "filters"
|
||||
|
||||
// Filtering - module object
|
||||
type Filtering struct {
|
||||
// conf FilteringConf
|
||||
refreshStatus uint32 // 0:none; 1:in progress
|
||||
refreshLock sync.Mutex
|
||||
filterTitleRegexp *regexp.Regexp
|
||||
}
|
||||
// nextFilterID is a way to seed a unique ID generation.
|
||||
//
|
||||
// TODO(e.burkov): Use more deterministic approach.
|
||||
var nextFilterID = time.Now().Unix()
|
||||
|
||||
// Init - initialize the module
|
||||
func (f *Filtering) Init() {
|
||||
f.filterTitleRegexp = regexp.MustCompile(`^! Title: +(.*)$`)
|
||||
_ = os.MkdirAll(filepath.Join(Context.getDataDir(), filterDir), 0o755)
|
||||
f.loadFilters(config.Filters)
|
||||
f.loadFilters(config.WhitelistFilters)
|
||||
deduplicateFilters()
|
||||
updateUniqueFilterID(config.Filters)
|
||||
updateUniqueFilterID(config.WhitelistFilters)
|
||||
}
|
||||
|
||||
// Start - start the module
|
||||
func (f *Filtering) Start() {
|
||||
f.RegisterFilteringHandlers()
|
||||
|
||||
// Here we should start updating filters,
|
||||
// but currently we can't wake up the periodic task to do so.
|
||||
// So for now we just start this periodic task from here.
|
||||
go f.periodicallyRefreshFilters()
|
||||
}
|
||||
|
||||
// Close - close the module
|
||||
func (f *Filtering) Close() {
|
||||
}
|
||||
|
||||
func defaultFilters() []filter {
|
||||
return []filter{
|
||||
{Filter: filtering.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard DNS filter"},
|
||||
{Filter: filtering.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway Default Blocklist"},
|
||||
}
|
||||
}
|
||||
|
||||
// field ordering is important -- yaml fields will mirror ordering from here
|
||||
type filter struct {
|
||||
// FilterYAML respresents a filter list in the configuration file.
|
||||
//
|
||||
// TODO(e.burkov): Investigate if the field oredering is important.
|
||||
type FilterYAML struct {
|
||||
Enabled bool
|
||||
URL string // URL or a file path
|
||||
Name string `yaml:"name"`
|
||||
@@ -73,91 +39,108 @@ type filter struct {
|
||||
checksum uint32 // checksum of the file data
|
||||
white bool
|
||||
|
||||
filtering.Filter `yaml:",inline"`
|
||||
Filter `yaml:",inline"`
|
||||
}
|
||||
|
||||
// Clear filter rules
|
||||
func (filter *FilterYAML) unload() {
|
||||
filter.RulesCount = 0
|
||||
filter.checksum = 0
|
||||
}
|
||||
|
||||
// Path to the filter contents
|
||||
func (filter *FilterYAML) Path(dataDir string) string {
|
||||
return filepath.Join(dataDir, filterDir, strconv.FormatInt(filter.ID, 10)+".txt")
|
||||
}
|
||||
|
||||
const (
|
||||
statusFound = 1
|
||||
statusEnabledChanged = 2
|
||||
statusURLChanged = 4
|
||||
statusURLExists = 8
|
||||
statusUpdateRequired = 0x10
|
||||
statusFound = 1 << iota
|
||||
statusEnabledChanged
|
||||
statusURLChanged
|
||||
statusURLExists
|
||||
statusUpdateRequired
|
||||
)
|
||||
|
||||
// Update properties for a filter specified by its URL
|
||||
// Return status* flags.
|
||||
func (f *Filtering) filterSetProperties(url string, newf filter, whitelist bool) int {
|
||||
func (d *DNSFilter) filterSetProperties(url string, newf FilterYAML, whitelist bool) int {
|
||||
r := 0
|
||||
config.Lock()
|
||||
defer config.Unlock()
|
||||
d.filtersMu.Lock()
|
||||
defer d.filtersMu.Unlock()
|
||||
|
||||
filters := &config.Filters
|
||||
filters := d.Filters
|
||||
if whitelist {
|
||||
filters = &config.WhitelistFilters
|
||||
filters = d.WhitelistFilters
|
||||
}
|
||||
|
||||
for i := range *filters {
|
||||
filt := &(*filters)[i]
|
||||
if filt.URL != url {
|
||||
continue
|
||||
i := slices.IndexFunc(filters, func(filt FilterYAML) bool {
|
||||
return filt.URL == url
|
||||
})
|
||||
if i == -1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
filt := &filters[i]
|
||||
|
||||
log.Debug("filter: set properties: %s: {%s %s %v}", filt.URL, newf.Name, newf.URL, newf.Enabled)
|
||||
filt.Name = newf.Name
|
||||
|
||||
if filt.URL != newf.URL {
|
||||
r |= statusURLChanged | statusUpdateRequired
|
||||
if d.filterExistsNoLock(newf.URL) {
|
||||
return statusURLExists
|
||||
}
|
||||
|
||||
log.Debug("filter: set properties: %s: {%s %s %v}",
|
||||
filt.URL, newf.Name, newf.URL, newf.Enabled)
|
||||
filt.Name = newf.Name
|
||||
filt.URL = newf.URL
|
||||
filt.unload()
|
||||
filt.LastUpdated = time.Time{}
|
||||
filt.checksum = 0
|
||||
filt.RulesCount = 0
|
||||
}
|
||||
|
||||
if filt.URL != newf.URL {
|
||||
r |= statusURLChanged | statusUpdateRequired
|
||||
if filterExistsNoLock(newf.URL) {
|
||||
return statusURLExists
|
||||
}
|
||||
filt.URL = newf.URL
|
||||
filt.unload()
|
||||
filt.LastUpdated = time.Time{}
|
||||
filt.checksum = 0
|
||||
filt.RulesCount = 0
|
||||
}
|
||||
if filt.Enabled != newf.Enabled {
|
||||
r |= statusEnabledChanged
|
||||
filt.Enabled = newf.Enabled
|
||||
if filt.Enabled {
|
||||
if (r & statusURLChanged) == 0 {
|
||||
err := d.load(filt)
|
||||
if err != nil {
|
||||
// TODO(e.burkov): It seems the error is only returned when
|
||||
// the file exists and couldn't be open. Investigate and
|
||||
// improve.
|
||||
log.Error("loading filter %d: %s", filt.ID, err)
|
||||
|
||||
if filt.Enabled != newf.Enabled {
|
||||
r |= statusEnabledChanged
|
||||
filt.Enabled = newf.Enabled
|
||||
if filt.Enabled {
|
||||
if (r & statusURLChanged) == 0 {
|
||||
e := f.load(filt)
|
||||
if e != nil {
|
||||
// This isn't a fatal error,
|
||||
// because it may occur when someone removes the file from disk.
|
||||
filt.LastUpdated = time.Time{}
|
||||
filt.checksum = 0
|
||||
filt.RulesCount = 0
|
||||
r |= statusUpdateRequired
|
||||
}
|
||||
filt.LastUpdated = time.Time{}
|
||||
filt.checksum = 0
|
||||
filt.RulesCount = 0
|
||||
r |= statusUpdateRequired
|
||||
}
|
||||
} else {
|
||||
filt.unload()
|
||||
}
|
||||
} else {
|
||||
filt.unload()
|
||||
}
|
||||
|
||||
return r | statusFound
|
||||
}
|
||||
return 0
|
||||
|
||||
return r | statusFound
|
||||
}
|
||||
|
||||
// Return TRUE if a filter with this URL exists
|
||||
func filterExists(url string) bool {
|
||||
config.RLock()
|
||||
r := filterExistsNoLock(url)
|
||||
config.RUnlock()
|
||||
func (d *DNSFilter) filterExists(url string) bool {
|
||||
d.filtersMu.RLock()
|
||||
defer d.filtersMu.RUnlock()
|
||||
|
||||
r := d.filterExistsNoLock(url)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func filterExistsNoLock(url string) bool {
|
||||
for _, f := range config.Filters {
|
||||
func (d *DNSFilter) filterExistsNoLock(url string) bool {
|
||||
for _, f := range d.Filters {
|
||||
if f.URL == url {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, f := range config.WhitelistFilters {
|
||||
for _, f := range d.WhitelistFilters {
|
||||
if f.URL == url {
|
||||
return true
|
||||
}
|
||||
@@ -167,26 +150,26 @@ func filterExistsNoLock(url string) bool {
|
||||
|
||||
// Add a filter
|
||||
// Return FALSE if a filter with this URL exists
|
||||
func filterAdd(f filter) bool {
|
||||
config.Lock()
|
||||
defer config.Unlock()
|
||||
func (d *DNSFilter) filterAdd(flt FilterYAML) bool {
|
||||
d.filtersMu.Lock()
|
||||
defer d.filtersMu.Unlock()
|
||||
|
||||
// Check for duplicates
|
||||
if filterExistsNoLock(f.URL) {
|
||||
if d.filterExistsNoLock(flt.URL) {
|
||||
return false
|
||||
}
|
||||
|
||||
if f.white {
|
||||
config.WhitelistFilters = append(config.WhitelistFilters, f)
|
||||
if flt.white {
|
||||
d.WhitelistFilters = append(d.WhitelistFilters, flt)
|
||||
} else {
|
||||
config.Filters = append(config.Filters, f)
|
||||
d.Filters = append(d.Filters, flt)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Load filters from the disk
|
||||
// And if any filter has zero ID, assign a new one
|
||||
func (f *Filtering) loadFilters(array []filter) {
|
||||
func (d *DNSFilter) loadFilters(array []FilterYAML) {
|
||||
for i := range array {
|
||||
filter := &array[i] // otherwise we're operating on a copy
|
||||
if filter.ID == 0 {
|
||||
@@ -198,32 +181,30 @@ func (f *Filtering) loadFilters(array []filter) {
|
||||
continue
|
||||
}
|
||||
|
||||
err := f.load(filter)
|
||||
err := d.load(filter)
|
||||
if err != nil {
|
||||
log.Error("Couldn't load filter %d contents due to %s", filter.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deduplicateFilters() {
|
||||
// Deduplicate filters
|
||||
i := 0 // output index, used for deletion later
|
||||
urls := map[string]bool{}
|
||||
for _, filter := range config.Filters {
|
||||
if _, ok := urls[filter.URL]; !ok {
|
||||
// we didn't see it before, keep it
|
||||
urls[filter.URL] = true // remember the URL
|
||||
config.Filters[i] = filter
|
||||
i++
|
||||
func deduplicateFilters(filters []FilterYAML) (deduplicated []FilterYAML) {
|
||||
urls := stringutil.NewSet()
|
||||
lastIdx := 0
|
||||
|
||||
for _, filter := range filters {
|
||||
if !urls.Has(filter.URL) {
|
||||
urls.Add(filter.URL)
|
||||
filters[lastIdx] = filter
|
||||
lastIdx++
|
||||
}
|
||||
}
|
||||
|
||||
// all entries we want to keep are at front, delete the rest
|
||||
config.Filters = config.Filters[:i]
|
||||
return filters[:lastIdx]
|
||||
}
|
||||
|
||||
// Set the next filter ID to max(filter.ID) + 1
|
||||
func updateUniqueFilterID(filters []filter) {
|
||||
func updateUniqueFilterID(filters []FilterYAML) {
|
||||
for _, filter := range filters {
|
||||
if nextFilterID < filter.ID {
|
||||
nextFilterID = filter.ID + 1
|
||||
@@ -238,22 +219,19 @@ func assignUniqueFilterID() int64 {
|
||||
}
|
||||
|
||||
// Sets up a timer that will be checking for filters updates periodically
|
||||
func (f *Filtering) periodicallyRefreshFilters() {
|
||||
func (d *DNSFilter) periodicallyRefreshFilters() {
|
||||
const maxInterval = 1 * 60 * 60
|
||||
intval := 5 // use a dynamically increasing time interval
|
||||
for {
|
||||
isNetworkErr := false
|
||||
if config.DNS.FiltersUpdateIntervalHours != 0 && atomic.CompareAndSwapUint32(&f.refreshStatus, 0, 1) {
|
||||
f.refreshLock.Lock()
|
||||
_, isNetworkErr = f.refreshFiltersIfNecessary(filterRefreshBlocklists | filterRefreshAllowlists)
|
||||
f.refreshLock.Unlock()
|
||||
f.refreshStatus = 0
|
||||
if !isNetworkErr {
|
||||
isNetErr, ok := false, false
|
||||
if d.FiltersUpdateIntervalHours != 0 {
|
||||
_, isNetErr, ok = d.tryRefreshFilters(true, true, false)
|
||||
if ok && !isNetErr {
|
||||
intval = maxInterval
|
||||
}
|
||||
}
|
||||
|
||||
if isNetworkErr {
|
||||
if isNetErr {
|
||||
intval *= 2
|
||||
if intval > maxInterval {
|
||||
intval = maxInterval
|
||||
@@ -264,51 +242,73 @@ func (f *Filtering) periodicallyRefreshFilters() {
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh filters
|
||||
// flags: filterRefresh*
|
||||
// important:
|
||||
// tryRefreshFilters is like [refreshFilters], but backs down if the update is
|
||||
// already going on.
|
||||
//
|
||||
// TRUE: ignore the fact that we're currently updating the filters
|
||||
func (f *Filtering) refreshFilters(flags int, important bool) (int, error) {
|
||||
set := atomic.CompareAndSwapUint32(&f.refreshStatus, 0, 1)
|
||||
if !important && !set {
|
||||
return 0, fmt.Errorf("filters update procedure is already running")
|
||||
// TODO(e.burkov): Get rid of the concurrency pattern which requires the
|
||||
// sync.Mutex.TryLock.
|
||||
func (d *DNSFilter) tryRefreshFilters(block, allow, force bool) (updated int, isNetworkErr, ok bool) {
|
||||
if ok = d.refreshLock.TryLock(); !ok {
|
||||
return 0, false, ok
|
||||
}
|
||||
defer d.refreshLock.Unlock()
|
||||
|
||||
f.refreshLock.Lock()
|
||||
nUpdated, _ := f.refreshFiltersIfNecessary(flags)
|
||||
f.refreshLock.Unlock()
|
||||
f.refreshStatus = 0
|
||||
return nUpdated, nil
|
||||
updated, isNetworkErr = d.refreshFiltersIntl(block, allow, force)
|
||||
|
||||
return updated, isNetworkErr, ok
|
||||
}
|
||||
|
||||
func (f *Filtering) refreshFiltersArray(filters *[]filter, force bool) (int, []filter, []bool, bool) {
|
||||
var updateFilters []filter
|
||||
// refreshFilters updates the lists and returns the number of updated ones.
|
||||
// It's safe for concurrent use, but blocks at least until the previous
|
||||
// refreshing is finished.
|
||||
func (d *DNSFilter) refreshFilters(block, allow, force bool) (updated int) {
|
||||
d.refreshLock.Lock()
|
||||
defer d.refreshLock.Unlock()
|
||||
|
||||
updated, _ = d.refreshFiltersIntl(block, allow, force)
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
// listsToUpdate returns the slice of filter lists that could be updated.
|
||||
func (d *DNSFilter) listsToUpdate(filters *[]FilterYAML, force bool) (toUpd []FilterYAML) {
|
||||
now := time.Now()
|
||||
|
||||
d.filtersMu.RLock()
|
||||
defer d.filtersMu.RUnlock()
|
||||
|
||||
for i := range *filters {
|
||||
flt := &(*filters)[i] // otherwise we will be operating on a copy
|
||||
log.Debug("checking list at index %d: %v", i, flt)
|
||||
|
||||
if !flt.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if !force {
|
||||
exp := flt.LastUpdated.Add(time.Duration(d.FiltersUpdateIntervalHours) * time.Hour)
|
||||
if now.Before(exp) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
toUpd = append(toUpd, FilterYAML{
|
||||
Filter: Filter{
|
||||
ID: flt.ID,
|
||||
},
|
||||
URL: flt.URL,
|
||||
Name: flt.Name,
|
||||
checksum: flt.checksum,
|
||||
})
|
||||
}
|
||||
|
||||
return toUpd
|
||||
}
|
||||
|
||||
func (d *DNSFilter) refreshFiltersArray(filters *[]FilterYAML, force bool) (int, []FilterYAML, []bool, bool) {
|
||||
var updateFlags []bool // 'true' if filter data has changed
|
||||
|
||||
now := time.Now()
|
||||
config.RLock()
|
||||
for i := range *filters {
|
||||
f := &(*filters)[i] // otherwise we will be operating on a copy
|
||||
|
||||
if !f.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
expireTime := f.LastUpdated.Unix() + int64(config.DNS.FiltersUpdateIntervalHours)*60*60
|
||||
if !force && expireTime > now.Unix() {
|
||||
continue
|
||||
}
|
||||
|
||||
var uf filter
|
||||
uf.ID = f.ID
|
||||
uf.URL = f.URL
|
||||
uf.Name = f.Name
|
||||
uf.checksum = f.checksum
|
||||
updateFilters = append(updateFilters, uf)
|
||||
}
|
||||
config.RUnlock()
|
||||
|
||||
updateFilters := d.listsToUpdate(filters, force)
|
||||
if len(updateFilters) == 0 {
|
||||
return 0, nil, nil, false
|
||||
}
|
||||
@@ -316,7 +316,7 @@ func (f *Filtering) refreshFiltersArray(filters *[]filter, force bool) (int, []f
|
||||
nfail := 0
|
||||
for i := range updateFilters {
|
||||
uf := &updateFilters[i]
|
||||
updated, err := f.update(uf)
|
||||
updated, err := d.update(uf)
|
||||
updateFlags = append(updateFlags, updated)
|
||||
if err != nil {
|
||||
nfail++
|
||||
@@ -334,7 +334,7 @@ func (f *Filtering) refreshFiltersArray(filters *[]filter, force bool) (int, []f
|
||||
uf := &updateFilters[i]
|
||||
updated := updateFlags[i]
|
||||
|
||||
config.Lock()
|
||||
d.filtersMu.Lock()
|
||||
for k := range *filters {
|
||||
f := &(*filters)[k]
|
||||
if f.ID != uf.ID || f.URL != uf.URL {
|
||||
@@ -352,20 +352,14 @@ func (f *Filtering) refreshFiltersArray(filters *[]filter, force bool) (int, []f
|
||||
f.checksum = uf.checksum
|
||||
updateCount++
|
||||
}
|
||||
config.Unlock()
|
||||
d.filtersMu.Unlock()
|
||||
}
|
||||
|
||||
return updateCount, updateFilters, updateFlags, false
|
||||
}
|
||||
|
||||
const (
|
||||
filterRefreshForce = 1 // ignore last file modification date
|
||||
filterRefreshAllowlists = 2 // update allow-lists
|
||||
filterRefreshBlocklists = 4 // update block-lists
|
||||
)
|
||||
|
||||
// refreshFiltersIfNecessary checks filters and updates them if necessary. If
|
||||
// force is true, it ignores the filter.LastUpdated field value.
|
||||
// refreshFiltersIntl checks filters and updates them if necessary. If force is
|
||||
// true, it ignores the filter.LastUpdated field value.
|
||||
//
|
||||
// Algorithm:
|
||||
//
|
||||
@@ -378,53 +372,49 @@ const (
|
||||
// that this method works only on Unix systems. On Windows, don't pass
|
||||
// files to filtering, pass the whole data.
|
||||
//
|
||||
// refreshFiltersIfNecessary returns the number of updated filters. It also
|
||||
// returns true if there was a network error and nothing could be updated.
|
||||
// refreshFiltersIntl returns the number of updated filters. It also returns
|
||||
// true if there was a network error and nothing could be updated.
|
||||
//
|
||||
// TODO(a.garipov, e.burkov): What the hell?
|
||||
func (f *Filtering) refreshFiltersIfNecessary(flags int) (int, bool) {
|
||||
log.Debug("Filters: updating...")
|
||||
func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
|
||||
log.Debug("filtering: updating...")
|
||||
|
||||
updateCount := 0
|
||||
var updateFilters []filter
|
||||
var updateFlags []bool
|
||||
netError := false
|
||||
netErrorW := false
|
||||
force := false
|
||||
if (flags & filterRefreshForce) != 0 {
|
||||
force = true
|
||||
updNum := 0
|
||||
var lists []FilterYAML
|
||||
var toUpd []bool
|
||||
isNetErr := false
|
||||
|
||||
if block {
|
||||
updNum, lists, toUpd, isNetErr = d.refreshFiltersArray(&d.Filters, force)
|
||||
}
|
||||
if (flags & filterRefreshBlocklists) != 0 {
|
||||
updateCount, updateFilters, updateFlags, netError = f.refreshFiltersArray(&config.Filters, force)
|
||||
if allow {
|
||||
updNumAl, listsAl, toUpdAl, isNetErrAl := d.refreshFiltersArray(&d.WhitelistFilters, force)
|
||||
|
||||
updNum += updNumAl
|
||||
lists = append(lists, listsAl...)
|
||||
toUpd = append(toUpd, toUpdAl...)
|
||||
isNetErr = isNetErr || isNetErrAl
|
||||
}
|
||||
if (flags & filterRefreshAllowlists) != 0 {
|
||||
updateCountW := 0
|
||||
var updateFiltersW []filter
|
||||
var updateFlagsW []bool
|
||||
updateCountW, updateFiltersW, updateFlagsW, netErrorW = f.refreshFiltersArray(&config.WhitelistFilters, force)
|
||||
updateCount += updateCountW
|
||||
updateFilters = append(updateFilters, updateFiltersW...)
|
||||
updateFlags = append(updateFlags, updateFlagsW...)
|
||||
}
|
||||
if netError && netErrorW {
|
||||
if isNetErr {
|
||||
return 0, true
|
||||
}
|
||||
|
||||
if updateCount != 0 {
|
||||
enableFilters(false)
|
||||
if updNum != 0 {
|
||||
d.EnableFilters(false)
|
||||
|
||||
for i := range updateFilters {
|
||||
uf := &updateFilters[i]
|
||||
updated := updateFlags[i]
|
||||
for i := range lists {
|
||||
uf := &lists[i]
|
||||
updated := toUpd[i]
|
||||
if !updated {
|
||||
continue
|
||||
}
|
||||
_ = os.Remove(uf.Path() + ".old")
|
||||
_ = os.Remove(uf.Path(d.DataDir) + ".old")
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("Filters: update finished")
|
||||
return updateCount, false
|
||||
log.Debug("filtering: update finished")
|
||||
|
||||
return updNum, false
|
||||
}
|
||||
|
||||
// Allows printable UTF-8 text with CR, LF, TAB characters
|
||||
@@ -440,7 +430,7 @@ func isPrintableText(data []byte, len int) bool {
|
||||
}
|
||||
|
||||
// A helper function that parses filter contents and returns a number of rules and a filter name (if there's any)
|
||||
func (f *Filtering) parseFilterContents(file io.Reader) (int, uint32, string) {
|
||||
func (d *DNSFilter) parseFilterContents(file io.Reader) (int, uint32, string) {
|
||||
rulesCount := 0
|
||||
name := ""
|
||||
seenTitle := false
|
||||
@@ -455,7 +445,7 @@ func (f *Filtering) parseFilterContents(file io.Reader) (int, uint32, string) {
|
||||
if len(line) == 0 {
|
||||
//
|
||||
} else if line[0] == '!' {
|
||||
m := f.filterTitleRegexp.FindAllStringSubmatch(line, -1)
|
||||
m := d.filterTitleRegexp.FindAllStringSubmatch(line, -1)
|
||||
if len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
|
||||
name = m[0][1]
|
||||
seenTitle = true
|
||||
@@ -476,11 +466,11 @@ func (f *Filtering) parseFilterContents(file io.Reader) (int, uint32, string) {
|
||||
}
|
||||
|
||||
// Perform upgrade on a filter and update LastUpdated value
|
||||
func (f *Filtering) update(filter *filter) (bool, error) {
|
||||
b, err := f.updateIntl(filter)
|
||||
func (d *DNSFilter) update(filter *FilterYAML) (bool, error) {
|
||||
b, err := d.updateIntl(filter)
|
||||
filter.LastUpdated = time.Now()
|
||||
if !b {
|
||||
e := os.Chtimes(filter.Path(), filter.LastUpdated, filter.LastUpdated)
|
||||
e := os.Chtimes(filter.Path(d.DataDir), filter.LastUpdated, filter.LastUpdated)
|
||||
if e != nil {
|
||||
log.Error("os.Chtimes(): %v", e)
|
||||
}
|
||||
@@ -488,7 +478,7 @@ func (f *Filtering) update(filter *filter) (bool, error) {
|
||||
return b, err
|
||||
}
|
||||
|
||||
func (f *Filtering) read(reader io.Reader, tmpFile *os.File, filter *filter) (int, error) {
|
||||
func (d *DNSFilter) read(reader io.Reader, tmpFile *os.File, filter *FilterYAML) (int, error) {
|
||||
htmlTest := true
|
||||
firstChunk := make([]byte, 4*1024)
|
||||
firstChunkLen := 0
|
||||
@@ -539,20 +529,20 @@ func (f *Filtering) read(reader io.Reader, tmpFile *os.File, filter *filter) (in
|
||||
// finalizeUpdate closes and gets rid of temporary file f with filter's content
|
||||
// according to updated. It also saves new values of flt's name, rules number
|
||||
// and checksum if sucсeeded.
|
||||
func finalizeUpdate(
|
||||
f *os.File,
|
||||
flt *filter,
|
||||
func (d *DNSFilter) finalizeUpdate(
|
||||
file *os.File,
|
||||
flt *FilterYAML,
|
||||
updated bool,
|
||||
name string,
|
||||
rnum int,
|
||||
cs uint32,
|
||||
) (err error) {
|
||||
tmpFileName := f.Name()
|
||||
tmpFileName := file.Name()
|
||||
|
||||
// Close the file before renaming it because it's required on Windows.
|
||||
//
|
||||
// See https://github.com/adguardTeam/adGuardHome/issues/1553.
|
||||
if err = f.Close(); err != nil {
|
||||
if err = file.Close(); err != nil {
|
||||
return fmt.Errorf("closing temporary file: %w", err)
|
||||
}
|
||||
|
||||
@@ -562,9 +552,9 @@ func finalizeUpdate(
|
||||
return os.Remove(tmpFileName)
|
||||
}
|
||||
|
||||
log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path())
|
||||
log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path(d.DataDir))
|
||||
|
||||
if err = os.Rename(tmpFileName, flt.Path()); err != nil {
|
||||
if err = os.Rename(tmpFileName, flt.Path(d.DataDir)); err != nil {
|
||||
return errors.WithDeferred(err, os.Remove(tmpFileName))
|
||||
}
|
||||
|
||||
@@ -578,12 +568,12 @@ func finalizeUpdate(
|
||||
// processUpdate copies filter's content from src to dst and returns the name,
|
||||
// rules number, and checksum for it. It also returns the number of bytes read
|
||||
// from src.
|
||||
func (f *Filtering) processUpdate(
|
||||
func (d *DNSFilter) processUpdate(
|
||||
src io.Reader,
|
||||
dst *os.File,
|
||||
flt *filter,
|
||||
flt *FilterYAML,
|
||||
) (name string, rnum int, cs uint32, n int, err error) {
|
||||
if n, err = f.read(src, dst, flt); err != nil {
|
||||
if n, err = d.read(src, dst, flt); err != nil {
|
||||
return "", 0, 0, 0, err
|
||||
}
|
||||
|
||||
@@ -591,14 +581,14 @@ func (f *Filtering) processUpdate(
|
||||
return "", 0, 0, 0, err
|
||||
}
|
||||
|
||||
rnum, cs, name = f.parseFilterContents(dst)
|
||||
rnum, cs, name = d.parseFilterContents(dst)
|
||||
|
||||
return name, rnum, cs, n, nil
|
||||
}
|
||||
|
||||
// updateIntl updates the flt rewriting it's actual file. It returns true if
|
||||
// the actual update has been performed.
|
||||
func (f *Filtering) updateIntl(flt *filter) (ok bool, err error) {
|
||||
func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||
log.Tracef("downloading update for filter %d from %s", flt.ID, flt.URL)
|
||||
|
||||
var name string
|
||||
@@ -606,12 +596,12 @@ func (f *Filtering) updateIntl(flt *filter) (ok bool, err error) {
|
||||
var cs uint32
|
||||
|
||||
var tmpFile *os.File
|
||||
tmpFile, err = os.CreateTemp(filepath.Join(Context.getDataDir(), filterDir), "")
|
||||
tmpFile, err = os.CreateTemp(filepath.Join(d.DataDir, filterDir), "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() {
|
||||
err = errors.WithDeferred(err, finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
|
||||
err = errors.WithDeferred(err, d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
|
||||
ok = ok && err == nil
|
||||
if ok {
|
||||
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
|
||||
@@ -638,7 +628,7 @@ func (f *Filtering) updateIntl(flt *filter) (ok bool, err error) {
|
||||
r = file
|
||||
} else {
|
||||
var resp *http.Response
|
||||
resp, err = Context.client.Get(flt.URL)
|
||||
resp, err = d.HTTPClient.Get(flt.URL)
|
||||
if err != nil {
|
||||
log.Printf("requesting filter from %s, skip: %s", flt.URL, err)
|
||||
|
||||
@@ -655,16 +645,16 @@ func (f *Filtering) updateIntl(flt *filter) (ok bool, err error) {
|
||||
r = resp.Body
|
||||
}
|
||||
|
||||
name, rnum, cs, n, err = f.processUpdate(r, tmpFile, flt)
|
||||
name, rnum, cs, n, err = d.processUpdate(r, tmpFile, flt)
|
||||
|
||||
return cs != flt.checksum, err
|
||||
}
|
||||
|
||||
// loads filter contents from the file in dataDir
|
||||
func (f *Filtering) load(filter *filter) (err error) {
|
||||
filterFilePath := filter.Path()
|
||||
func (d *DNSFilter) load(filter *FilterYAML) (err error) {
|
||||
filterFilePath := filter.Path(d.DataDir)
|
||||
|
||||
log.Tracef("filtering: loading filter %d contents to: %s", filter.ID, filterFilePath)
|
||||
log.Tracef("filtering: loading filter %d from %s", filter.ID, filterFilePath)
|
||||
|
||||
file, err := os.Open(filterFilePath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
@@ -682,7 +672,7 @@ func (f *Filtering) load(filter *filter) (err error) {
|
||||
|
||||
log.Tracef("filtering: File %s, id %d, length %d", filterFilePath, filter.ID, st.Size())
|
||||
|
||||
rulesCount, checksum, _ := f.parseFilterContents(file)
|
||||
rulesCount, checksum, _ := d.parseFilterContents(file)
|
||||
|
||||
filter.RulesCount = rulesCount
|
||||
filter.checksum = checksum
|
||||
@@ -691,56 +681,45 @@ func (f *Filtering) load(filter *filter) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear filter rules
|
||||
func (filter *filter) unload() {
|
||||
filter.RulesCount = 0
|
||||
filter.checksum = 0
|
||||
func (d *DNSFilter) EnableFilters(async bool) {
|
||||
d.filtersMu.RLock()
|
||||
defer d.filtersMu.RUnlock()
|
||||
|
||||
d.enableFiltersLocked(async)
|
||||
}
|
||||
|
||||
// Path to the filter contents
|
||||
func (filter *filter) Path() string {
|
||||
return filepath.Join(Context.getDataDir(), filterDir, strconv.FormatInt(filter.ID, 10)+".txt")
|
||||
}
|
||||
|
||||
func enableFilters(async bool) {
|
||||
config.RLock()
|
||||
defer config.RUnlock()
|
||||
|
||||
enableFiltersLocked(async)
|
||||
}
|
||||
|
||||
func enableFiltersLocked(async bool) {
|
||||
filters := []filtering.Filter{{
|
||||
ID: filtering.CustomListID,
|
||||
Data: []byte(strings.Join(config.UserRules, "\n")),
|
||||
func (d *DNSFilter) enableFiltersLocked(async bool) {
|
||||
filters := []Filter{{
|
||||
ID: CustomListID,
|
||||
Data: []byte(strings.Join(d.UserRules, "\n")),
|
||||
}}
|
||||
|
||||
for _, filter := range config.Filters {
|
||||
for _, filter := range d.Filters {
|
||||
if !filter.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
filters = append(filters, filtering.Filter{
|
||||
filters = append(filters, Filter{
|
||||
ID: filter.ID,
|
||||
FilePath: filter.Path(),
|
||||
FilePath: filter.Path(d.DataDir),
|
||||
})
|
||||
}
|
||||
|
||||
var allowFilters []filtering.Filter
|
||||
for _, filter := range config.WhitelistFilters {
|
||||
var allowFilters []Filter
|
||||
for _, filter := range d.WhitelistFilters {
|
||||
if !filter.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
allowFilters = append(allowFilters, filtering.Filter{
|
||||
allowFilters = append(allowFilters, Filter{
|
||||
ID: filter.ID,
|
||||
FilePath: filter.Path(),
|
||||
FilePath: filter.Path(d.DataDir),
|
||||
})
|
||||
}
|
||||
|
||||
if err := Context.dnsFilter.SetFilters(filters, allowFilters, async); err != nil {
|
||||
if err := d.SetFilters(filters, allowFilters, async); err != nil {
|
||||
log.Debug("enabling filters: %s", err)
|
||||
}
|
||||
|
||||
Context.dnsFilter.SetEnabled(config.DNS.FilteringEnabled)
|
||||
d.SetEnabled(d.FilteringEnabled)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package home
|
||||
package filtering
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
@@ -51,15 +51,17 @@ func TestFilters(t *testing.T) {
|
||||
|
||||
l := testStartFilterListener(t, &fltContent)
|
||||
|
||||
Context = homeContext{
|
||||
workDir: t.TempDir(),
|
||||
client: &http.Client{
|
||||
tempDir := t.TempDir()
|
||||
|
||||
filters, err := New(&Config{
|
||||
DataDir: tempDir,
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
}
|
||||
Context.filters.Init()
|
||||
}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
f := &filter{
|
||||
f := &FilterYAML{
|
||||
URL: (&url.URL{
|
||||
Scheme: "http",
|
||||
Host: (&netutil.IPPort{
|
||||
@@ -71,21 +73,22 @@ func TestFilters(t *testing.T) {
|
||||
}
|
||||
|
||||
updateAndAssert := func(t *testing.T, want require.BoolAssertionFunc, wantRulesCount int) {
|
||||
ok, err := Context.filters.update(f)
|
||||
var ok bool
|
||||
ok, err = filters.update(f)
|
||||
require.NoError(t, err)
|
||||
want(t, ok)
|
||||
|
||||
assert.Equal(t, wantRulesCount, f.RulesCount)
|
||||
|
||||
var dir []fs.DirEntry
|
||||
dir, err = os.ReadDir(filepath.Join(Context.getDataDir(), filterDir))
|
||||
dir, err = os.ReadDir(filepath.Join(tempDir, filterDir))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, dir, 1)
|
||||
|
||||
require.FileExists(t, f.Path())
|
||||
require.FileExists(t, f.Path(tempDir))
|
||||
|
||||
err = Context.filters.load(f)
|
||||
err = filters.load(f)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -105,11 +108,9 @@ func TestFilters(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("load_unload", func(t *testing.T) {
|
||||
err := Context.filters.load(f)
|
||||
err = filters.load(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
f.unload()
|
||||
})
|
||||
|
||||
require.NoError(t, os.Remove(f.Path()))
|
||||
}
|
||||
@@ -6,7 +6,10 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
@@ -24,6 +27,7 @@ import (
|
||||
"github.com/AdguardTeam/urlfilter/filterlist"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// The IDs of built-in filter lists.
|
||||
@@ -69,8 +73,13 @@ type Config struct {
|
||||
// enabled is used to be returned within Settings.
|
||||
//
|
||||
// It is of type uint32 to be accessed by atomic.
|
||||
//
|
||||
// TODO(e.burkov): Use atomic.Bool in Go 1.19.
|
||||
enabled uint32
|
||||
|
||||
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
|
||||
FiltersUpdateIntervalHours uint32 `yaml:"filters_update_interval"` // time period to update filters (in hours)
|
||||
|
||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
|
||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||
@@ -98,6 +107,24 @@ type Config struct {
|
||||
|
||||
// CustomResolver is the resolver used by DNSFilter.
|
||||
CustomResolver Resolver `yaml:"-"`
|
||||
|
||||
// HTTPClient is the client to use for updating the remote filters.
|
||||
HTTPClient *http.Client `yaml:"-"`
|
||||
|
||||
// DataDir is used to store filters' contents.
|
||||
DataDir string `yaml:"-"`
|
||||
|
||||
// filtersMu protects filter lists.
|
||||
filtersMu *sync.RWMutex
|
||||
|
||||
// Filters are the blocking filter lists.
|
||||
Filters []FilterYAML `yaml:"-"`
|
||||
|
||||
// WhitelistFilters are the allowing filter lists.
|
||||
WhitelistFilters []FilterYAML `yaml:"-"`
|
||||
|
||||
// UserRules is the global list of custom rules.
|
||||
UserRules []string `yaml:"-"`
|
||||
}
|
||||
|
||||
// LookupStats store stats collected during safebrowsing or parental checks
|
||||
@@ -128,11 +155,13 @@ type hostChecker struct {
|
||||
|
||||
// DNSFilter matches hostnames and DNS requests against filtering rules.
|
||||
type DNSFilter struct {
|
||||
rulesStorage *filterlist.RuleStorage
|
||||
filteringEngine *urlfilter.DNSEngine
|
||||
rulesStorage *filterlist.RuleStorage
|
||||
filteringEngine *urlfilter.DNSEngine
|
||||
|
||||
rulesStorageAllow *filterlist.RuleStorage
|
||||
filteringEngineAllow *urlfilter.DNSEngine
|
||||
engineLock sync.RWMutex
|
||||
|
||||
engineLock sync.RWMutex
|
||||
|
||||
parentalServer string // access via methods
|
||||
safeBrowsingServer string // access via methods
|
||||
@@ -156,6 +185,12 @@ type DNSFilter struct {
|
||||
// TODO(e.burkov): Use upstream that configured in dnsforward instead.
|
||||
resolver Resolver
|
||||
|
||||
refreshLock *sync.Mutex
|
||||
|
||||
// filterTitleRegexp is the regular expression to retrieve a name of a
|
||||
// filter list.
|
||||
filterTitleRegexp *regexp.Regexp
|
||||
|
||||
hostCheckers []hostChecker
|
||||
}
|
||||
|
||||
@@ -168,7 +203,7 @@ type Filter struct {
|
||||
Data []byte `yaml:"-"`
|
||||
|
||||
// ID is automatically assigned when filter is added using nextFilterID.
|
||||
ID int64
|
||||
ID int64 `yaml:"id"`
|
||||
}
|
||||
|
||||
// Reason holds an enum detailing why it was filtered or not filtered
|
||||
@@ -245,15 +280,7 @@ func (r Reason) String() string {
|
||||
}
|
||||
|
||||
// In returns true if reasons include r.
|
||||
func (r Reason) In(reasons ...Reason) (ok bool) {
|
||||
for _, reason := range reasons {
|
||||
if r == reason {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
func (r Reason) In(reasons ...Reason) (ok bool) { return slices.Contains(reasons, r) }
|
||||
|
||||
// SetEnabled sets the status of the *DNSFilter.
|
||||
func (d *DNSFilter) SetEnabled(enabled bool) {
|
||||
@@ -261,6 +288,7 @@ func (d *DNSFilter) SetEnabled(enabled bool) {
|
||||
if enabled {
|
||||
i = 1
|
||||
}
|
||||
|
||||
atomic.StoreUint32(&d.enabled, uint32(i))
|
||||
}
|
||||
|
||||
@@ -279,11 +307,20 @@ func (d *DNSFilter) GetConfig() (s Settings) {
|
||||
|
||||
// WriteDiskConfig - write configuration
|
||||
func (d *DNSFilter) WriteDiskConfig(c *Config) {
|
||||
d.confLock.Lock()
|
||||
defer d.confLock.Unlock()
|
||||
func() {
|
||||
d.confLock.Lock()
|
||||
defer d.confLock.Unlock()
|
||||
|
||||
*c = d.Config
|
||||
c.Rewrites = cloneRewrites(c.Rewrites)
|
||||
*c = d.Config
|
||||
c.Rewrites = cloneRewrites(c.Rewrites)
|
||||
}()
|
||||
|
||||
d.filtersMu.RLock()
|
||||
defer d.filtersMu.RUnlock()
|
||||
|
||||
c.Filters = slices.Clone(d.Filters)
|
||||
c.WhitelistFilters = slices.Clone(d.WhitelistFilters)
|
||||
c.UserRules = slices.Clone(d.UserRules)
|
||||
}
|
||||
|
||||
// cloneRewrites returns a deep copy of entries.
|
||||
@@ -309,6 +346,8 @@ func (d *DNSFilter) SetFilters(blockFilters, allowFilters []Filter, async bool)
|
||||
}
|
||||
|
||||
d.filtersInitializerLock.Lock() // prevent multiple writers from adding more than 1 task
|
||||
defer d.filtersInitializerLock.Unlock()
|
||||
|
||||
// remove all pending tasks
|
||||
stop := false
|
||||
for !stop {
|
||||
@@ -321,7 +360,6 @@ func (d *DNSFilter) SetFilters(blockFilters, allowFilters []Filter, async bool)
|
||||
}
|
||||
|
||||
d.filtersInitializerChan <- params
|
||||
d.filtersInitializerLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -350,22 +388,19 @@ func (d *DNSFilter) filtersInitializer() {
|
||||
func (d *DNSFilter) Close() {
|
||||
d.engineLock.Lock()
|
||||
defer d.engineLock.Unlock()
|
||||
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *DNSFilter) reset() {
|
||||
var err error
|
||||
|
||||
if d.rulesStorage != nil {
|
||||
err = d.rulesStorage.Close()
|
||||
if err != nil {
|
||||
if err := d.rulesStorage.Close(); err != nil {
|
||||
log.Error("filtering: rulesStorage.Close: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if d.rulesStorageAllow != nil {
|
||||
err = d.rulesStorageAllow.Close()
|
||||
if err != nil {
|
||||
if err := d.rulesStorageAllow.Close(); err != nil {
|
||||
log.Error("filtering: rulesStorageAllow.Close: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -885,29 +920,30 @@ func InitModule() {
|
||||
initBlockedServices()
|
||||
}
|
||||
|
||||
// New creates properly initialized DNS Filter that is ready to be used.
|
||||
func New(c *Config, blockFilters []Filter) (d *DNSFilter) {
|
||||
// New creates properly initialized DNS Filter that is ready to be used. c must
|
||||
// be non-nil.
|
||||
func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
|
||||
d = &DNSFilter{
|
||||
resolver: net.DefaultResolver,
|
||||
resolver: net.DefaultResolver,
|
||||
refreshLock: &sync.Mutex{},
|
||||
filterTitleRegexp: regexp.MustCompile(`^! Title: +(.*)$`),
|
||||
}
|
||||
if c != nil {
|
||||
|
||||
d.safebrowsingCache = cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxSize: c.SafeBrowsingCacheSize,
|
||||
})
|
||||
d.safeSearchCache = cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxSize: c.SafeSearchCacheSize,
|
||||
})
|
||||
d.parentalCache = cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxSize: c.ParentalCacheSize,
|
||||
})
|
||||
d.safebrowsingCache = cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxSize: c.SafeBrowsingCacheSize,
|
||||
})
|
||||
d.safeSearchCache = cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxSize: c.SafeSearchCacheSize,
|
||||
})
|
||||
d.parentalCache = cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxSize: c.ParentalCacheSize,
|
||||
})
|
||||
|
||||
if c.CustomResolver != nil {
|
||||
d.resolver = c.CustomResolver
|
||||
}
|
||||
if r := c.CustomResolver; r != nil {
|
||||
d.resolver = r
|
||||
}
|
||||
|
||||
d.hostCheckers = []hostChecker{{
|
||||
@@ -930,27 +966,26 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter) {
|
||||
name: "safe search",
|
||||
}}
|
||||
|
||||
err := d.initSecurityServices()
|
||||
if err != nil {
|
||||
log.Error("filtering: initialize services: %s", err)
|
||||
defer func() { err = errors.Annotate(err, "filtering: %w") }()
|
||||
|
||||
return nil
|
||||
err = d.initSecurityServices()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initializing services: %s", err)
|
||||
}
|
||||
|
||||
if c != nil {
|
||||
d.Config = *c
|
||||
err = d.prepareRewrites()
|
||||
if err != nil {
|
||||
log.Error("rewrites: preparing: %s", err)
|
||||
d.Config = *c
|
||||
d.filtersMu = &sync.RWMutex{}
|
||||
|
||||
return nil
|
||||
}
|
||||
err = d.prepareRewrites()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rewrites: preparing: %s", err)
|
||||
}
|
||||
|
||||
bsvcs := []string{}
|
||||
for _, s := range d.BlockedServices {
|
||||
if !BlockedSvcKnown(s) {
|
||||
log.Debug("skipping unknown blocked-service %q", s)
|
||||
|
||||
continue
|
||||
}
|
||||
bsvcs = append(bsvcs, s)
|
||||
@@ -960,13 +995,24 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter) {
|
||||
if blockFilters != nil {
|
||||
err = d.initFiltering(nil, blockFilters)
|
||||
if err != nil {
|
||||
log.Error("Can't initialize filtering subsystem: %s", err)
|
||||
d.Close()
|
||||
return nil
|
||||
|
||||
return nil, fmt.Errorf("initializing filtering subsystem: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return d
|
||||
_ = os.MkdirAll(filepath.Join(d.DataDir, filterDir), 0o755)
|
||||
|
||||
d.loadFilters(d.Filters)
|
||||
d.loadFilters(d.WhitelistFilters)
|
||||
|
||||
d.Filters = deduplicateFilters(d.Filters)
|
||||
d.WhitelistFilters = deduplicateFilters(d.WhitelistFilters)
|
||||
|
||||
updateUniqueFilterID(d.Filters)
|
||||
updateUniqueFilterID(d.WhitelistFilters)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Start - start the module:
|
||||
@@ -976,9 +1022,10 @@ func (d *DNSFilter) Start() {
|
||||
d.filtersInitializerChan = make(chan filtersInitializerParams, 1)
|
||||
go d.filtersInitializer()
|
||||
|
||||
if d.Config.HTTPRegister != nil { // for tests
|
||||
d.registerSecurityHandlers()
|
||||
d.registerRewritesHandlers()
|
||||
d.registerBlockedServicesHandlers()
|
||||
}
|
||||
d.RegisterFilteringHandlers()
|
||||
|
||||
// Here we should start updating filters,
|
||||
// but currently we can't wake up the periodic task to do so.
|
||||
// So for now we just start this periodic task from here.
|
||||
go d.periodicallyRefreshFilters()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user