Compare commits

...

14 Commits

Author SHA1 Message Date
Stanislav Chzhen
b34a8c169c filtering: imp tests 2023-05-15 10:33:52 +03:00
Stanislav Chzhen
1f2ba07eae filtering: wildcard interference 2023-05-12 12:25:33 +03:00
Dimitry Kolyshev
c77b2a0ce5 Pull request: home: imp code
Merge in DNS/adguard-home from home-imp-code to master

Squashed commit of the following:

commit 459297e189c55393bf0340dd51ec9608d3475e55
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 10 11:42:34 2023 +0300

    home: imp code

commit ab38e1e80fed7b24fe57d4afdc57b70608f65d73
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 10 11:01:23 2023 +0300

    all: lint script

commit 7df68b128bf32172ef2e3bf7116f4f72a97baa2b
Merge: bcb482714 db52f7a3a
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 10 10:59:40 2023 +0300

    Merge remote-tracking branch 'origin/master' into home-imp-code

commit bcb482714780da882e69c261be08511ea4f36f3b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu May 4 13:48:27 2023 +0300

    all: lint script

commit 1c017f27715202ec1f40881f069a96f11f9822e8
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu May 4 13:45:25 2023 +0300

    all: lint script

commit ee3d427a7d6ee7e377e67c5eb99eebc7fb1e6acc
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu May 4 13:44:53 2023 +0300

    home: imp code

commit bc50430469123415216e60e178bd8e30fc229300
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu May 4 13:12:10 2023 +0300

    home: imp code

commit fc07e416aeab2612e68cf0e3f933aaed95931115
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu May 4 11:42:32 2023 +0300

    aghos: service precheck

commit a68480fd9c4cd6f3c89210bee6917c53074f7a82
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Thu May 4 11:07:05 2023 +0300

    home: imp code

commit 61b743a340ac1564c48212452c7a9acd1808d352
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 3 17:17:21 2023 +0300

    all: lint script

commit c6fe620510c4af5b65456e90cb3424831334e004
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 3 17:16:37 2023 +0300

    home: imp code

commit 4b2fb47ea9c932054ccc72b1fd1d11793c93e39c
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 3 16:55:44 2023 +0300

    home: imp code

commit 63df3e2ab58482920a074cfd5f4188e49a0f8448
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 3 16:25:38 2023 +0300

    home: imp code

commit c7f1502f976482c2891e0c64426218b549585e83
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 3 15:54:30 2023 +0300

    home: imp code

commit c64cdaf1c82495bb70d9cdcaf7be9eeee9a7c773
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 3 14:35:04 2023 +0300

    home: imp code

commit a50436e040b3a064ef51d5f936b879fe8de72d41
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 3 14:24:02 2023 +0300

    home: imp code

commit 2b66464f472df732ea27cbbe5ac5c673a13bc14b
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 3 14:11:53 2023 +0300

    home: imp code

commit 713ce2963c210887faa0a06e41e01e4ebbf96894
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed May 3 14:10:54 2023 +0300

    home: imp code
2023-05-10 16:30:03 +03:00
Stanislav Chzhen
db52f7a3ac Pull request 1841: AG-21462-safebrowsing-parental-http-tests
Merge in DNS/adguard-home from AG-21462-safebrowsing-parental-http-tests to master

Squashed commit of the following:

commit 22a83ebad08a27939a443530137a7c195f512ee4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed May 3 17:39:46 2023 +0300

    filtering: fix test

commit c3ca8b4987245cdd552f6f09759804e716bcae80
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed May 3 16:43:35 2023 +0300

    filtering: imp tests even more

commit 7643bfae350373b5b6dfb61b64e57da66c6ab952
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed May 3 16:17:42 2023 +0300

    filtering: imp tests more

commit 399c05ee4d479a727b61378b7a07158a568d0181
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed May 3 14:45:41 2023 +0300

    filtering: imp tests

commit f361df39e784ec9c5191666736a6c64b332928e8
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue May 2 12:49:26 2023 +0300

    filtering: add tests
2023-05-03 19:52:06 +03:00
Stanislav Chzhen
381f2f651d Pull request 1837: AG-21462-imp-safebrowsing-parental
Merge in DNS/adguard-home from AG-21462-imp-safebrowsing-parental to master

Squashed commit of the following:

commit 85016d4f1105e21a407efade0bd45b8362808061
Merge: 0e61edade 620b51e3e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 27 16:36:30 2023 +0300

    Merge branch 'master' into AG-21462-imp-safebrowsing-parental

commit 0e61edadeff34f6305e941c1db94575c82f238d9
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 27 14:51:37 2023 +0300

    filtering: imp tests

commit 994255514cc0f67dfe33d5a0892432e8924d1e36
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 27 11:13:19 2023 +0300

    filtering: fix typo

commit 96d1069573171538333330d6af94ef0f4208a9c4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 27 11:00:18 2023 +0300

    filtering: imp code more

commit c2a5620b04c4a529eea69983f1520cd2bc82ea9b
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 26 19:13:26 2023 +0300

    all: add todo

commit e5dcc2e9701f8bccfde6ef8c01a4a2e7eb31599e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 26 14:36:08 2023 +0300

    all: imp code more

commit b6e734ccbeda82669023f6578481260b7c1f7161
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 25 15:01:56 2023 +0300

    filtering: imp code

commit 530648dadf836c1a4bd9917e0d3b47256fa8ff52
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 24 20:06:36 2023 +0300

    all: imp code

commit 49fa6e587052a40bb431fea457701ee860493527
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 24 14:57:19 2023 +0300

    all: rm safe browsing ctx

commit bbcb66cb03e18fa875e3c33cf16295892739e507
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 21 17:54:18 2023 +0300

    filtering: add cache item

commit cb7c9fffe8c4ff5e7a21ca912c223c799f61385f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 20 18:43:02 2023 +0300

    filtering: fix hashes

commit 153fec46270212af03f3631bfb42c5d680c4e142
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 20 16:15:15 2023 +0300

    filtering: add test cases

commit 09372f92bbb1fc082f1b1283594ee589100209c5
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Apr 20 15:38:05 2023 +0300

    filtering: imp code

commit 466bc26d524ea6d1c3efb33692a7785d39e491ca
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 19 18:38:40 2023 +0300

    filtering: add tests

commit 24365ecf8c60512fdac65833ee603c80864ae018
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 19 11:38:57 2023 +0300

    filtering: add hashprefix
2023-04-27 16:39:35 +03:00
Eugene Burkov
620b51e3ea Pull request 1840: 5752-unspec-ipv6
Merge in DNS/adguard-home from 5752-unspec-ipv6 to master

Closes #5752.

Squashed commit of the following:

commit 654b808d17c6d2374b6be919515113b361fc5ff7
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Apr 21 18:11:34 2023 +0300

    home: imp docs

commit 28b4c36df790f1eaa05b11a1f0a7b986894d37dc
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Apr 21 16:50:16 2023 +0300

    all: fix empty bind host
2023-04-21 18:57:53 +03:00
Ainar Garipov
757ddb06f8 Pull request 1839: 5716-write-json
Updates #5716.

Squashed commit of the following:

commit 8cf7c4f404fffb646c9df8643924eb8dc1d8f49d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Apr 19 18:09:35 2023 +0300

    aghhttp: write json properly
2023-04-19 18:15:17 +03:00
Ainar Garipov
d6043e2352 Pull request 1838: 5716-content-type
Updates #5716.

Squashed commit of the following:

commit 584e6771c82b92857e3c13232e942cad5c183682
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Apr 19 14:54:43 2023 +0300

    all: fix content types
2023-04-19 14:58:56 +03:00
Eugene Burkov
aeec9a86e2 Pull request 1836: 5714-handle-zeroes-health
Merge in DNS/adguard-home from 5714-handle-zeroes-health to master

Updates #5714.

Squashed commit of the following:

commit 24faab01faf723e313050294b3a35e249c3cd3e3
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Apr 19 13:10:24 2023 +0300

    docker: add curly brackets

commit 67365d02856200685551a79aa23cf59df4a3484b
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Apr 18 20:16:12 2023 +0300

    docker: imp zeroes check
2023-04-19 13:48:59 +03:00
Eugene Burkov
584182e264 Pull request 1835: bootstrap-plain
Merge in DNS/adguard-home from bootstrap-plain to master

Updates AdguardTeam/dnsproxy#324.

Squashed commit of the following:

commit bd5d569dc26154985857977e81650eb0a51559a5
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Apr 18 17:41:53 2023 +0300

    querylog: rm proxyutil dep

commit 9db4053555e06eba264f7d3e6c75c747f8d73b56
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Apr 18 17:34:29 2023 +0300

    all: upd proxy
2023-04-18 17:52:22 +03:00
Ainar Garipov
96cd512d32 Pull request 1834: upd-chlog
Merge in DNS/adguard-home from upd-chlog to master

Squashed commit of the following:

commit 0afc6fe1d2500a54ae9cea50249f6c4904f418db
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Apr 18 17:00:10 2023 +0300

    all: upd chlog
2023-04-18 17:03:27 +03:00
Ainar Garipov
11898a3f73 Pull request 1833: upd-all
Merge in DNS/adguard-home from upd-all to master

Squashed commit of the following:

commit e5acaddd5cec1a53f2aa1f48d16af53052341d31
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Apr 18 15:43:36 2023 +0300

    all: upd i18n, svcs, tools
2023-04-18 15:49:03 +03:00
Stanislav Chzhen
a91a257b15 Pull request 1817: AG-20352-imp-leases-db
Merge in DNS/adguard-home from AG-20352-imp-leases-db to master

Squashed commit of the following:

commit 2235fb4671bb3f80c933847362cd35b5704dd18d
Merge: 0c4d76d4f 76a74b271
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 18 15:09:34 2023 +0300

    Merge branch 'master' into AG-20352-imp-leases-db

commit 0c4d76d4f6222ae06c568864d366df866dc55a54
Merge: e586b82c7 4afd39b22
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 18 11:07:27 2023 +0300

    Merge branch 'master' into AG-20352-imp-leases-db

commit e586b82c700c4d432e34f36400519eb08b2653ad
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Apr 18 11:06:40 2023 +0300

    dhcpd: imp docs

commit 411d4e6f6e36051bf6a66c709380ed268c161c41
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 17 16:56:56 2023 +0300

    dhcpd: imp code

commit e457dc2c385ab62b36df7f96c949e6b90ed2034a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Apr 17 14:29:29 2023 +0300

    all: imp code more

commit c2df20d0125d368d0155af0808af979921763e1f
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Apr 14 15:07:53 2023 +0300

    all: imp code

commit a4e9ffb9ae769c828c22d62ddf231f7bcfea14db
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 12 19:19:35 2023 +0300

    dhcpd: fix test more

commit 138d89414f1a89558b23962acb7174dce28346d9
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 12 18:08:29 2023 +0300

    dhcpd: fix test

commit e07e7a23e7c913951c8ecb38c12a3345ebe473be
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 12 17:22:27 2023 +0300

    all: upd chlog

commit 1b6a76e79cf4beed9ca980766ce97930b375bfde
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Apr 12 13:24:11 2023 +0300

    all: migrate leases db
2023-04-18 15:12:11 +03:00
Artem Krisanov
76a74b271b AG-21485 - Login theme bugfix.
Squashed commit of the following:

commit 521aedc5bc
Merge: 40ff26ea2 1842f7d88
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Tue Apr 18 14:27:28 2023 +0300

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

commit 40ff26ea21
Author: Artem Krisanov <a.krisanov@adguard.com>
Date:   Tue Apr 18 14:11:28 2023 +0300

    Login theme bugfix.
2023-04-18 14:31:20 +03:00
81 changed files with 2187 additions and 1277 deletions

1
.gitignore vendored
View File

@@ -21,7 +21,6 @@
/snapcraft_login /snapcraft_login
AdGuardHome* AdGuardHome*
coverage.txt coverage.txt
leases.db
node_modules/ node_modules/
!/build/gitkeep !/build/gitkeep

View File

@@ -14,20 +14,48 @@ and this project adheres to
<!-- <!--
## [v0.108.0] - TBA ## [v0.108.0] - TBA
## [v0.107.29] - 2023-04-26 (APPROX.) ## [v0.107.30] - 2023-04-26 (APPROX.)
See also the [v0.107.29 GitHub milestone][ms-v0.107.29]. See also the [v0.107.30 GitHub milestone][ms-v0.107.30].
[ms-v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/milestone/65?closed=1 [ms-v0.107.30]: https://github.com/AdguardTeam/AdGuardHome/milestone/66?closed=1
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Fixed
- Unquoted IPv6 bind hosts with trailing colons erroneously considered
unspecified addresses are now properly validated ([#5752]).
**NOTE:** the Docker healthcheck script now also doesn't interpret the `""`
value as unspecified address.
- Incorrect `Content-Type` header value in `POST /control/version.json` and `GET
/control/dhcp/interfaces` HTTP APIs ([#5716]).
- Provided bootstrap servers are now used to resolve the hostnames of plain
UDP/TCP upstream servers.
[#5716]: https://github.com/AdguardTeam/AdGuardHome/issues/5716
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
## [v0.107.29] - 2023-04-18
See also the [v0.107.29 GitHub milestone][ms-v0.107.29].
### Added ### Added
- The ability to exclude client activity from the query log or statistics by - The ability to exclude client activity from the query log or statistics by
editing client's settings on the Clients settings page in the UI ([#1717], editing client's settings on the respective page in the UI ([#1717], [#4299]).
[#4299]).
### Changed
- Stored DHCP leases moved from `leases.db` to `data/leases.json`. The file
format has also been optimized.
### Fixed ### Fixed
@@ -38,14 +66,12 @@ NOTE: Add new changes BELOW THIS COMMENT.
- All Safe Search services being unchecked by default. - All Safe Search services being unchecked by default.
- Panic when a DNSCrypt stamp is invalid ([#5721]). - Panic when a DNSCrypt stamp is invalid ([#5721]).
[#1717]: https://github.com/AdguardTeam/AdGuardHome/issues/1717 [#5712]: https://github.com/AdguardTeam/AdGuardHome/issues/5712
[#4299]: https://github.com/AdguardTeam/AdGuardHome/issues/4299
[#5721]: https://github.com/AdguardTeam/AdGuardHome/issues/5721 [#5721]: https://github.com/AdguardTeam/AdGuardHome/issues/5721
[#5725]: https://github.com/AdguardTeam/AdGuardHome/issues/5725 [#5725]: https://github.com/AdguardTeam/AdGuardHome/issues/5725
[#5752]: https://github.com/AdguardTeam/AdGuardHome/issues/5752
<!-- [ms-v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/milestone/65?closed=1
NOTE: Add new changes ABOVE THIS COMMENT.
-->
@@ -170,11 +196,9 @@ In this release, the schema version has changed from 17 to 20.
[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163 [#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163
[#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333 [#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333
[#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472 [#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472
[#1717]: https://github.com/AdguardTeam/AdGuardHome/issues/1717
[#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290 [#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290
[#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459 [#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459
[#4262]: https://github.com/AdguardTeam/AdGuardHome/issues/4262 [#4262]: https://github.com/AdguardTeam/AdGuardHome/issues/4262
[#4299]: https://github.com/AdguardTeam/AdGuardHome/issues/4299
[#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567 [#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567
[#5701]: https://github.com/AdguardTeam/AdGuardHome/issues/5701 [#5701]: https://github.com/AdguardTeam/AdGuardHome/issues/5701
@@ -1940,11 +1964,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
<!-- <!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.29...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.30...HEAD
[v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...v0.107.29 [v0.107.30]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.29...v0.107.30
--> -->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.29...HEAD
[v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...v0.107.29
[v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.27...v0.107.28 [v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.27...v0.107.28
[v0.107.27]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.26...v0.107.27 [v0.107.27]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.26...v0.107.27
[v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.25...v0.107.26 [v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.25...v0.107.26

View File

@@ -19,7 +19,9 @@
<div id="root"></div> <div id="root"></div>
<script> <script>
(function() { (function() {
document.body.dataset.theme = 'auto'; var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var currentTheme = prefersDark ? 'dark' : 'light';
document.body.dataset.theme = currentTheme;
})(); })();
</script> </script>
</body> </body>

View File

@@ -635,5 +635,6 @@
"parental_control": "الرقابة الابويه", "parental_control": "الرقابة الابويه",
"safe_browsing": "تصفح آمن", "safe_browsing": "تصفح آمن",
"served_from_cache": "{{value}} <i>(يتم تقديمه من ذاكرة التخزين المؤقت)</i>", "served_from_cache": "{{value}} <i>(يتم تقديمه من ذاكرة التخزين المؤقت)</i>",
"form_error_password_length": "يجب أن تتكون كلمة المرور من {{value}} من الأحرف على الأقل" "form_error_password_length": "يجب أن تتكون كلمة المرور من {{value}} من الأحرف على الأقل",
"protection_section_label": "الحماية"
} }

View File

@@ -642,5 +642,6 @@
"anonymizer_notification": "<0>Заўвага:</0> Ананімізацыя IP уключана. Вы можаце адключыць яе ў <1>Агульных наладах</1>.", "anonymizer_notification": "<0>Заўвага:</0> Ананімізацыя IP уключана. Вы можаце адключыць яе ў <1>Агульных наладах</1>.",
"confirm_dns_cache_clear": "Вы ўпэўнены, што хочаце ачысціць кэш DNS?", "confirm_dns_cache_clear": "Вы ўпэўнены, што хочаце ачысціць кэш DNS?",
"cache_cleared": "Кэш DNS паспяхова ачышчаны", "cache_cleared": "Кэш DNS паспяхова ачышчаны",
"clear_cache": "Ачысціць кэш" "clear_cache": "Ачысціць кэш",
"protection_section_label": "Ахова"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Protokol dotazů byl úspěšně vymazán", "query_log_cleared": "Protokol dotazů byl úspěšně vymazán",
"query_log_updated": "Protokol dotazů byl úspěšně aktualizován", "query_log_updated": "Protokol dotazů byl úspěšně aktualizován",
"query_log_clear": "Vymazat protokoly dotazů", "query_log_clear": "Vymazat protokoly dotazů",
"query_log_retention": "Uchování protokolů dotazů", "query_log_retention": "Rotace protokolů dotazů",
"query_log_enable": "Povolit protokol", "query_log_enable": "Povolit protokol",
"query_log_configuration": "Konfigurace protokolů", "query_log_configuration": "Konfigurace protokolů",
"query_log_disabled": "Protokol dotazu je zakázán a lze jej nakonfigurovat v <0>nastavení</0>", "query_log_disabled": "Protokol dotazu je zakázán a lze jej nakonfigurovat v <0>nastavení</0>",
"query_log_strict_search": "Pro striktní vyhledávání použijte dvojité uvozovky", "query_log_strict_search": "Pro striktní vyhledávání použijte dvojité uvozovky",
"query_log_retention_confirm": "Opravdu chcete změnit uchovávání protokolu dotazů? Pokud snížíte hodnotu intervalu, některá data budou ztracena", "query_log_retention_confirm": "Opravdu chcete změnit rotaci protokolu dotazů? Pokud snížíte hodnotu intervalu, některá data budou ztracena",
"anonymize_client_ip": "Anonymizovat IP klienta", "anonymize_client_ip": "Anonymizovat IP klienta",
"anonymize_client_ip_desc": "Neukládat úplnou IP adresu klienta do protokolů a statistik", "anonymize_client_ip_desc": "Neukládat úplnou IP adresu klienta do protokolů a statistik",
"dns_config": "Konfigurace DNS serveru", "dns_config": "Konfigurace DNS serveru",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Vypnout ochranu na {{count}} hod.", "disable_notify_for_hours": "Vypnout ochranu na {{count}} hod.",
"disable_notify_for_hours_plural": "Vypnout ochranu na {{count}} hod.", "disable_notify_for_hours_plural": "Vypnout ochranu na {{count}} hod.",
"disable_notify_until_tomorrow": "Vypnout ochranu do zítřka", "disable_notify_until_tomorrow": "Vypnout ochranu do zítřka",
"enable_protection_timer": "Ochrana bude zapnuta za {{time}}" "enable_protection_timer": "Ochrana bude zapnuta za {{time}}",
"custom_retention_input": "Zadejte retenci v hodinách",
"custom_rotation_input": "Zadejte rotaci v hodinách",
"protection_section_label": "Ochrana",
"log_and_stats_section_label": "Protokol dotazů a statistiky",
"ignore_query_log": "Ignorovat tohoto klienta v protokolu dotazů",
"ignore_statistics": "Ignorovat tohoto klienta ve statistikách"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Forespørgselsloggen er blevet ryddet", "query_log_cleared": "Forespørgselsloggen er blevet ryddet",
"query_log_updated": "Forespørgselsloggen er blevet opdateret", "query_log_updated": "Forespørgselsloggen er blevet opdateret",
"query_log_clear": "Ryd forespørgselslogfiler", "query_log_clear": "Ryd forespørgselslogfiler",
"query_log_retention": "Opbevar forespørgselslogger i", "query_log_retention": "Rotation af forespørgselslog",
"query_log_enable": "Aktivér log", "query_log_enable": "Aktivér log",
"query_log_configuration": "Opsætning af logger", "query_log_configuration": "Opsætning af logger",
"query_log_disabled": "Forespørgselsloggen er deaktiveret og kan opsættes i <0>indstillingerne</0>", "query_log_disabled": "Forespørgselsloggen er deaktiveret og kan opsættes i <0>indstillingerne</0>",
"query_log_strict_search": "Brug dobbelt anførselstegn til stringent søgning", "query_log_strict_search": "Brug dobbelt anførselstegn til stringent søgning",
"query_log_retention_confirm": "Sikker på, at du vil ændre forespørgselsloggens opbevaringperiode? Mindskes intervalværdien, mistes data", "query_log_retention_confirm": "Sikker på, at forespørgselsloggens rotationstid skal ændres? Mindskes intervalværdien, mistes nogle data",
"anonymize_client_ip": "Anonymisér klient-IP", "anonymize_client_ip": "Anonymisér klient-IP",
"anonymize_client_ip_desc": "Gem ikke fuld klient IP-adresse i logfiler eller statistikker", "anonymize_client_ip_desc": "Gem ikke fuld klient IP-adresse i logfiler eller statistikker",
"dns_config": "DNS-serveropsætning", "dns_config": "DNS-serveropsætning",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Deaktivere beskyttelse i {{count}} time", "disable_notify_for_hours": "Deaktivere beskyttelse i {{count}} time",
"disable_notify_for_hours_plural": "Deaktivere beskyttelse i {{count}} timer", "disable_notify_for_hours_plural": "Deaktivere beskyttelse i {{count}} timer",
"disable_notify_until_tomorrow": "Deaktiver beskyttelse indtil i morgen", "disable_notify_until_tomorrow": "Deaktiver beskyttelse indtil i morgen",
"enable_protection_timer": "Beskyttelse deaktiveres om {{time}}" "enable_protection_timer": "Beskyttelse deaktiveres om {{time}}",
"custom_retention_input": "Angiv opbevaringstid i timer",
"custom_rotation_input": "Angiv rotationstid i timer",
"protection_section_label": "Beskyttelse",
"log_and_stats_section_label": "Forespørgselslog og statistik",
"ignore_query_log": "Ignorér denne klient i forespørgselslog",
"ignore_statistics": "Ignorér denne klient i statistik"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Das Abfrageprotokoll wurde erfolgreich gelöscht", "query_log_cleared": "Das Abfrageprotokoll wurde erfolgreich gelöscht",
"query_log_updated": "Das Abfrageprotokoll wurde erfolgreich aktualisiert", "query_log_updated": "Das Abfrageprotokoll wurde erfolgreich aktualisiert",
"query_log_clear": "Abfrageprotokolle leeren", "query_log_clear": "Abfrageprotokolle leeren",
"query_log_retention": "Abfrageprotokolle aufbewahren", "query_log_retention": "Rotation der Abfrageprotokolle",
"query_log_enable": "Protokoll aktivieren", "query_log_enable": "Protokoll aktivieren",
"query_log_configuration": "Konfiguration der Protokolle", "query_log_configuration": "Konfiguration der Protokolle",
"query_log_disabled": "Das Abfrageprotokoll ist deaktiviert und kann in den <0>Einstellungen</0> konfiguriert werden.", "query_log_disabled": "Das Abfrageprotokoll ist deaktiviert und kann in den <0>Einstellungen</0> konfiguriert werden.",
"query_log_strict_search": "Doppelte Anführungszeichen für die strikte Suche verwenden", "query_log_strict_search": "Doppelte Anführungszeichen für die strikte Suche verwenden",
"query_log_retention_confirm": "Möchten Sie die Aufbewahrung des Abfrageprotokolls wirklich ändern? Wenn Sie den Zeitabstand verringern, gehen einige Daten verloren.", "query_log_retention_confirm": "Möchten Sie die Abfrageprotokollrotation wirklich ändern? Wenn Sie den Intervallwert verringern, gehen einige Daten verloren",
"anonymize_client_ip": "Client-IP anonymisieren", "anonymize_client_ip": "Client-IP anonymisieren",
"anonymize_client_ip_desc": "Vollständige IP-Adresse des Clients nicht in Protokollen und Statistiken speichern", "anonymize_client_ip_desc": "Vollständige IP-Adresse des Clients nicht in Protokollen und Statistiken speichern",
"dns_config": "DNS-Serverkonfiguration", "dns_config": "DNS-Serverkonfiguration",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Schutz für {{count}} Stunde deaktivieren", "disable_notify_for_hours": "Schutz für {{count}} Stunde deaktivieren",
"disable_notify_for_hours_plural": "Schutz für {{count}} Stunden deaktivieren", "disable_notify_for_hours_plural": "Schutz für {{count}} Stunden deaktivieren",
"disable_notify_until_tomorrow": "Schutz bis morgen deaktivieren", "disable_notify_until_tomorrow": "Schutz bis morgen deaktivieren",
"enable_protection_timer": "Der Schutz wird in {{time}} wieder aktiviert" "enable_protection_timer": "Der Schutz wird in {{time}} wieder aktiviert",
"custom_retention_input": "Rückhaltezeit in Stunden eingeben",
"custom_rotation_input": "Rotation in Stunden eingeben",
"protection_section_label": "Schutz",
"log_and_stats_section_label": "Abfrageprotokoll und Statistik",
"ignore_query_log": "Diesen Client im Abfrageprotokoll ignorieren",
"ignore_statistics": "Diesen Client in der Statistik ignorieren"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "El registro de consultas se ha borrado correctamente", "query_log_cleared": "El registro de consultas se ha borrado correctamente",
"query_log_updated": "El registro de consultas se ha actualizado correctamente", "query_log_updated": "El registro de consultas se ha actualizado correctamente",
"query_log_clear": "Borrar registros de consultas", "query_log_clear": "Borrar registros de consultas",
"query_log_retention": "Retención de registros de consultas", "query_log_retention": "Rotanción de registros de consultas",
"query_log_enable": "Habilitar registro", "query_log_enable": "Habilitar registro",
"query_log_configuration": "Configuración de registros", "query_log_configuration": "Configuración de registros",
"query_log_disabled": "El registro de consultas está deshabilitado y se puede configurar en la <0>configuración</0>", "query_log_disabled": "El registro de consultas está deshabilitado y se puede configurar en la <0>configuración</0>",
"query_log_strict_search": "Usar comillas dobles para una búsqueda estricta", "query_log_strict_search": "Usar comillas dobles para una búsqueda estricta",
"query_log_retention_confirm": "¿Estás seguro de que deseas cambiar la retención del registro de consultas? Si disminuye el valor del intervalo, se perderán algunos datos", "query_log_retention_confirm": "¿Está seguro de que deseas cambiar la rotación del registro de consultas? Si reduces el valor del intervalo, se perderán algunos datos",
"anonymize_client_ip": "Anonimizar IP del cliente", "anonymize_client_ip": "Anonimizar IP del cliente",
"anonymize_client_ip_desc": "No guarda la dirección IP completa del cliente en registros o estadísticas", "anonymize_client_ip_desc": "No guarda la dirección IP completa del cliente en registros o estadísticas",
"dns_config": "Configuración del servidor DNS", "dns_config": "Configuración del servidor DNS",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Desactivar la protección por {{count}} hora", "disable_notify_for_hours": "Desactivar la protección por {{count}} hora",
"disable_notify_for_hours_plural": "Desactivar la protección por {{count}} horas", "disable_notify_for_hours_plural": "Desactivar la protección por {{count}} horas",
"disable_notify_until_tomorrow": "Desactivar la protección hasta mañana", "disable_notify_until_tomorrow": "Desactivar la protección hasta mañana",
"enable_protection_timer": "La protección se activará en {{time}}" "enable_protection_timer": "La protección se activará en {{time}}",
"custom_retention_input": "Ingresa la retención en horas",
"custom_rotation_input": "Ingresa la rotación en horas",
"protection_section_label": "Protección",
"log_and_stats_section_label": "Registro de consultas y estadísticas",
"ignore_query_log": "Ignorar este cliente en el registro de consultas",
"ignore_statistics": "Ignorar este cliente en las estadísticas"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Pyyntöhistorian tyhjennys onnistui", "query_log_cleared": "Pyyntöhistorian tyhjennys onnistui",
"query_log_updated": "Pyyntöhistorian päivitys onnistui", "query_log_updated": "Pyyntöhistorian päivitys onnistui",
"query_log_clear": "Tyhjennä pyyntöhistoria", "query_log_clear": "Tyhjennä pyyntöhistoria",
"query_log_retention": "Pyyntöhistorian säilytys", "query_log_retention": "Kyselylokien kierto",
"query_log_enable": "Käytä historiaa", "query_log_enable": "Käytä historiaa",
"query_log_configuration": "Historian määritys", "query_log_configuration": "Historian määritys",
"query_log_disabled": "Pyyntöhistoria ei ole käytössä. Voit ottaa sen käyttöön <0>asetuksissa</0>", "query_log_disabled": "Pyyntöhistoria ei ole käytössä. Voit ottaa sen käyttöön <0>asetuksissa</0>",
"query_log_strict_search": "Käytä tarkalle haulle lainausmerkkejä", "query_log_strict_search": "Käytä tarkalle haulle lainausmerkkejä",
"query_log_retention_confirm": "Haluatko varmasti muuttaa pyyntöhistoriasi säilytysaikaa? Jos lyhennät aikaa, joitakin tietoja menetetään", "query_log_retention_confirm": "Haluatko varmasti muuttaa kyselylokin kiertoa? Jos pienennät intervalliarvoa, osa tiedoista menetetään",
"anonymize_client_ip": "Piilota päätelaitteen IP-osoite", "anonymize_client_ip": "Piilota päätelaitteen IP-osoite",
"anonymize_client_ip_desc": "Älä tallenna päätelaitteen täydellistä IP-osoitetta historiaan ja tilastoihin.", "anonymize_client_ip_desc": "Älä tallenna päätelaitteen täydellistä IP-osoitetta historiaan ja tilastoihin.",
"dns_config": "DNS-palvelimen määritys", "dns_config": "DNS-palvelimen määritys",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Poista suojaus käytöstä {{count}} tunniksi", "disable_notify_for_hours": "Poista suojaus käytöstä {{count}} tunniksi",
"disable_notify_for_hours_plural": "Poista suojaus käytöstä {{count}} tunniksi", "disable_notify_for_hours_plural": "Poista suojaus käytöstä {{count}} tunniksi",
"disable_notify_until_tomorrow": "Poista suojaus käytöstä huomiseen asti", "disable_notify_until_tomorrow": "Poista suojaus käytöstä huomiseen asti",
"enable_protection_timer": "Suojaus otetaan käyttöön {{time}} kuluttua" "enable_protection_timer": "Suojaus otetaan käyttöön {{time}} kuluttua",
"custom_retention_input": "Syötä säilytysaika tunteina",
"custom_rotation_input": "Syötä uudistusaika tunteina",
"protection_section_label": "Suojaus",
"log_and_stats_section_label": "Kyselyhistoria ja tilastot",
"ignore_query_log": "Älä huomioi tätä päätettä kyselyhistoriassa",
"ignore_statistics": "Älä huomioi tätä päätettä tilastoissa"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Le journal des requêtes a été effacé", "query_log_cleared": "Le journal des requêtes a été effacé",
"query_log_updated": "Le journal des requêtes a été mis à jour", "query_log_updated": "Le journal des requêtes a été mis à jour",
"query_log_clear": "Effacer journal des requêtes", "query_log_clear": "Effacer journal des requêtes",
"query_log_retention": "Rétention du journal des requêtes", "query_log_retention": "Rotation des journaux de requêtes",
"query_log_enable": "Activer le journal", "query_log_enable": "Activer le journal",
"query_log_configuration": "Configuration du journal", "query_log_configuration": "Configuration du journal",
"query_log_disabled": "Le journal des requêtes est désactivé et peut être configuré dans les <0>paramètres</0>", "query_log_disabled": "Le journal des requêtes est désactivé et peut être configuré dans les <0>paramètres</0>",
"query_log_strict_search": "Utilisez les doubles guillemets pour une recherche stricte", "query_log_strict_search": "Utilisez les doubles guillemets pour une recherche stricte",
"query_log_retention_confirm": "Êtes-vous sûr de vouloir modifier la rétention des journaux de requêtes ? Si vous diminuez la valeur de l'intervalle, certaines données seront perdues", "query_log_retention_confirm": "Êtes-vous sûr de souhaiter modifier la rotation des journaux de requêtes ? Si vous diminuez la valeur de l'intervalle, certaines données seront perdues",
"anonymize_client_ip": "Anonymiser lIP du client", "anonymize_client_ip": "Anonymiser lIP du client",
"anonymize_client_ip_desc": "Ne pas enregistrer ladresse IP complète du client dans les journaux et statistiques", "anonymize_client_ip_desc": "Ne pas enregistrer ladresse IP complète du client dans les journaux et statistiques",
"dns_config": "Configuration du serveur DNS", "dns_config": "Configuration du serveur DNS",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Désactiver la protection pendant {{count}} heure", "disable_notify_for_hours": "Désactiver la protection pendant {{count}} heure",
"disable_notify_for_hours_plural": "Désactiver la protection pendant {{count}} heures", "disable_notify_for_hours_plural": "Désactiver la protection pendant {{count}} heures",
"disable_notify_until_tomorrow": "Désactiver la protection jusqu'à demain", "disable_notify_until_tomorrow": "Désactiver la protection jusqu'à demain",
"enable_protection_timer": "La protection sera activée dans {{time}}" "enable_protection_timer": "La protection sera activée dans {{time}}",
"custom_retention_input": "Saisir la rétention en heures",
"custom_rotation_input": "Saisir la rotation en heures",
"protection_section_label": "Protection",
"log_and_stats_section_label": "Journal des requêtes et statistiques",
"ignore_query_log": "Ignorer ce client dans le journal des requêtes",
"ignore_statistics": "Ignorer ce client dans les statistiques"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Zapisnik upita je uspješno uklonjen", "query_log_cleared": "Zapisnik upita je uspješno uklonjen",
"query_log_updated": "Zapisnik upita je uspješno ažuriran", "query_log_updated": "Zapisnik upita je uspješno ažuriran",
"query_log_clear": "Očisti zapisnik upita", "query_log_clear": "Očisti zapisnik upita",
"query_log_retention": "Spremanje zapisnika upita", "query_log_retention": "Rotacija dnevnika upita",
"query_log_enable": "Omogući zapise", "query_log_enable": "Omogući zapise",
"query_log_configuration": "Postavke zapisa", "query_log_configuration": "Postavke zapisa",
"query_log_disabled": "Zapisnik upita je onemogućen i može se postaviti u <0>postavkama</0>", "query_log_disabled": "Zapisnik upita je onemogućen i može se postaviti u <0>postavkama</0>",
"query_log_strict_search": "Koristite dvostruke navodnike za strogo pretraživanje", "query_log_strict_search": "Koristite dvostruke navodnike za strogo pretraživanje",
"query_log_retention_confirm": "Jeste li sigurni da želite promijeniti zadržavanje zapisnika upita? Ako smanjite vrijednost intervala, neki će podaci biti izgubljeni", "query_log_retention_confirm": "Jeste li sigurni da želite promijeniti rotaciju dnevnika upita? Ako smanjite vrijednost intervala, neki će se podaci izgubiti",
"anonymize_client_ip": "Anonimiraj IP klijenta", "anonymize_client_ip": "Anonimiraj IP klijenta",
"anonymize_client_ip_desc": "Ne spremajte cijelu IP adresu klijenta u zapisnike i statistike", "anonymize_client_ip_desc": "Ne spremajte cijelu IP adresu klijenta u zapisnike i statistike",
"dns_config": "DNS postavke poslužitelja", "dns_config": "DNS postavke poslužitelja",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Isključi zaštitu na {{count}} sati", "disable_notify_for_hours": "Isključi zaštitu na {{count}} sati",
"disable_notify_for_hours_plural": "Isključi zaštitu na {{count}} sati", "disable_notify_for_hours_plural": "Isključi zaštitu na {{count}} sati",
"disable_notify_until_tomorrow": "Isključi zaštitu do sutra", "disable_notify_until_tomorrow": "Isključi zaštitu do sutra",
"enable_protection_timer": "Zaštita će biti omogućena u {{time}}" "enable_protection_timer": "Zaštita će biti omogućena u {{time}}",
"custom_retention_input": "Unesite zadržavanje u satima",
"custom_rotation_input": "Unesite rotaciju u satima",
"protection_section_label": "Zaštita",
"log_and_stats_section_label": "Zapisnik upita i statistika",
"ignore_query_log": "Zanemari ovog klijenta u zapisniku upita",
"ignore_statistics": "Ignorirajte ovog klijenta u statistici"
} }

View File

@@ -642,5 +642,6 @@
"anonymizer_notification": "<0>Megjegyzés:</0> Az IP anonimizálás engedélyezve van. Az <1>Általános beállításoknál letilthatja</1> .", "anonymizer_notification": "<0>Megjegyzés:</0> Az IP anonimizálás engedélyezve van. Az <1>Általános beállításoknál letilthatja</1> .",
"confirm_dns_cache_clear": "Biztos benne, hogy törölni szeretné a DNS-gyorsítótárat?", "confirm_dns_cache_clear": "Biztos benne, hogy törölni szeretné a DNS-gyorsítótárat?",
"cache_cleared": "A DNS gyorsítótár sikeresen törlődött", "cache_cleared": "A DNS gyorsítótár sikeresen törlődött",
"clear_cache": "Gyorsítótár törlése" "clear_cache": "Gyorsítótár törlése",
"protection_section_label": "Védelem"
} }

View File

@@ -641,5 +641,6 @@
"anonymizer_notification": "<0>Catatan:</0> Anonimisasi IP diaktifkan. Anda dapat menonaktifkannya di <1>Pengaturan umum</1> .", "anonymizer_notification": "<0>Catatan:</0> Anonimisasi IP diaktifkan. Anda dapat menonaktifkannya di <1>Pengaturan umum</1> .",
"confirm_dns_cache_clear": "Apakah Anda yakin ingin menghapus cache DNS?", "confirm_dns_cache_clear": "Apakah Anda yakin ingin menghapus cache DNS?",
"cache_cleared": "Cache DNS berhasil dibersihkan", "cache_cleared": "Cache DNS berhasil dibersihkan",
"clear_cache": "Hapus cache" "clear_cache": "Hapus cache",
"protection_section_label": "Perlindungan"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Il registro richieste è stato correttamente cancellato", "query_log_cleared": "Il registro richieste è stato correttamente cancellato",
"query_log_updated": "Il registro richieste è stato correttamente aggiornato", "query_log_updated": "Il registro richieste è stato correttamente aggiornato",
"query_log_clear": "Cancella registri richieste", "query_log_clear": "Cancella registri richieste",
"query_log_retention": "Conservazione dei registri richieste", "query_log_retention": "Rotazione dei registri richieste",
"query_log_enable": "Attiva registro", "query_log_enable": "Attiva registro",
"query_log_configuration": "Configurazione registri", "query_log_configuration": "Configurazione registri",
"query_log_disabled": "Il registro richieste è stato disattivato e può essere configurata dalle <0>impostazioni</0>", "query_log_disabled": "Il registro richieste è stato disattivato e può essere configurata dalle <0>impostazioni</0>",
"query_log_strict_search": "Utilizzare le doppie virgolette per una ricerca precisa", "query_log_strict_search": "Utilizzare le doppie virgolette per una ricerca precisa",
"query_log_retention_confirm": "Sei sicuro di voler modificare il registro delle richieste? Se il valore di intervallo dovesse diminuire, alcuni dati andranno persi", "query_log_retention_confirm": "Sei sicuro di voler modificare il registro delle richieste? Se si riduce il valore dell'intervallo, alcuni dati andranno persi",
"anonymize_client_ip": "Anonimizza client IP", "anonymize_client_ip": "Anonimizza client IP",
"anonymize_client_ip_desc": "Non salvare l'indirizzo IP completo del client nel registro o nelle statistiche", "anonymize_client_ip_desc": "Non salvare l'indirizzo IP completo del client nel registro o nelle statistiche",
"dns_config": "Configurazione server DNS", "dns_config": "Configurazione server DNS",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Disattiva la protezione per {{count}} ora", "disable_notify_for_hours": "Disattiva la protezione per {{count}} ora",
"disable_notify_for_hours_plural": "Disattiva la protezione per {{count}} ore", "disable_notify_for_hours_plural": "Disattiva la protezione per {{count}} ore",
"disable_notify_until_tomorrow": "Disattiva la protezione fino a domani", "disable_notify_until_tomorrow": "Disattiva la protezione fino a domani",
"enable_protection_timer": "La protezione verrà attivata in {{time}}" "enable_protection_timer": "La protezione verrà attivata in {{time}}",
"custom_retention_input": "Inserisci la conservazione in ore",
"custom_rotation_input": "Inserisci la rotazione in ore",
"protection_section_label": "Protezione",
"log_and_stats_section_label": "Registro richieste e statistiche",
"ignore_query_log": "Ignora questo client nel registro delle richieste",
"ignore_statistics": "Ignora questo cliente nelle statistiche"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "クエリ・ログの消去に成功しました", "query_log_cleared": "クエリ・ログの消去に成功しました",
"query_log_updated": "クエリ・ログの更新が成功しました", "query_log_updated": "クエリ・ログの更新が成功しました",
"query_log_clear": "クエリ・ログを消去する", "query_log_clear": "クエリ・ログを消去する",
"query_log_retention": "クエリ・ログの保持", "query_log_retention": "クエリ・ログのローテーション",
"query_log_enable": "ログを有効にする", "query_log_enable": "ログを有効にする",
"query_log_configuration": "ログ設定", "query_log_configuration": "ログ設定",
"query_log_disabled": "クエリ・ログは無効になっており、<0>設定</0>で構成できます", "query_log_disabled": "クエリ・ログは無効になっており、<0>設定</0>で構成できます",
"query_log_strict_search": "完全一致検索には二重引用符を使用します", "query_log_strict_search": "完全一致検索には二重引用符を使用します",
"query_log_retention_confirm": "クエリ・ログの保持を変更してもよろしいですか? 期間を短くすると、一部のデータが失われます", "query_log_retention_confirm": "クエリ・ログのローテーションを変更してもよろしいですか? 間隔の値を減らすと、一部のデータが失われます",
"anonymize_client_ip": "クライアントIPを匿名化する", "anonymize_client_ip": "クライアントIPを匿名化する",
"anonymize_client_ip_desc": "ログと統計にクライアントのフルIPアドレスを保存しないようにします。", "anonymize_client_ip_desc": "ログと統計にクライアントのフルIPアドレスを保存しないようにします。",
"dns_config": "DNSサーバ設定", "dns_config": "DNSサーバ設定",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "保護を {{count}} 時間無効にする", "disable_notify_for_hours": "保護を {{count}} 時間無効にする",
"disable_notify_for_hours_plural": "保護を {{count}} 時間無効にする", "disable_notify_for_hours_plural": "保護を {{count}} 時間無効にする",
"disable_notify_until_tomorrow": "明日まで保護を無効にする", "disable_notify_until_tomorrow": "明日まで保護を無効にする",
"enable_protection_timer": "保護は後 {{time}} で有効になります" "enable_protection_timer": "保護は後 {{time}} で有効になります",
"custom_retention_input": "保持期間を入力してください(時間単位)",
"custom_rotation_input": "ローテーションを入力してください(時間単位)",
"protection_section_label": "AdGuardによる保護",
"log_and_stats_section_label": "クエリ・ログと統計情報",
"ignore_query_log": "クエリ・ログでこのクライアントを無視する",
"ignore_statistics": "統計でこのクライアントを無視する"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "쿼리 로그를 성공적으로 초기화했습니다", "query_log_cleared": "쿼리 로그를 성공적으로 초기화했습니다",
"query_log_updated": "질의 로그가 성공적으로 업데이트되었습니다", "query_log_updated": "질의 로그가 성공적으로 업데이트되었습니다",
"query_log_clear": "쿼리 로그 비우기", "query_log_clear": "쿼리 로그 비우기",
"query_log_retention": "쿼리 로그 저장 기간", "query_log_retention": "쿼리 로그 로테이션",
"query_log_enable": "로그 활성화", "query_log_enable": "로그 활성화",
"query_log_configuration": "로그 구성", "query_log_configuration": "로그 구성",
"query_log_disabled": "쿼리 로그가 비활성화되어 있으며 <0>설정</0>에서 설정할 수 있습니다", "query_log_disabled": "쿼리 로그가 비활성화되어 있으며 <0>설정</0>에서 설정할 수 있습니다",
"query_log_strict_search": "검색을 제한하려면 쌍따옴표를 사용해주세요", "query_log_strict_search": "검색을 제한하려면 쌍따옴표를 사용해주세요",
"query_log_retention_confirm": "정말로 쿼리 로그 저장 기간을 변경하시겠습니까? 저장 주기를 낮출 경우, 일부 데이터가 손실됩니다", "query_log_retention_confirm": "쿼리 로그 로테이션을 변경하시겠습니까? 간격 값을 줄이면 일부 데이터가 손실됩니다.",
"anonymize_client_ip": "클라이언트 IP 익명화", "anonymize_client_ip": "클라이언트 IP 익명화",
"anonymize_client_ip_desc": "클라이언트의 전체 IP 주소를 로그와 통계에 저장하저장하지 마세요", "anonymize_client_ip_desc": "클라이언트의 전체 IP 주소를 로그와 통계에 저장하저장하지 마세요",
"dns_config": "DNS 서버 설정", "dns_config": "DNS 서버 설정",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "{{count}}시간 동안 보호 기능 비활성화", "disable_notify_for_hours": "{{count}}시간 동안 보호 기능 비활성화",
"disable_notify_for_hours_plural": "{{count}}시간 동안 보호 기능 비활성화", "disable_notify_for_hours_plural": "{{count}}시간 동안 보호 기능 비활성화",
"disable_notify_until_tomorrow": "내일까지 보호 기능 비활성화", "disable_notify_until_tomorrow": "내일까지 보호 기능 비활성화",
"enable_protection_timer": "{{time}}에 보호 기능이 활성화됩니다." "enable_protection_timer": "{{time}}에 보호 기능이 활성화됩니다.",
"custom_retention_input": "시간 단위로 보존 기간 입력",
"custom_rotation_input": "시간 단위로 로테이션 입력",
"protection_section_label": "보호",
"log_and_stats_section_label": "쿼리 로그 및 통계",
"ignore_query_log": "쿼리 로그에서 이 클라이언트 무시",
"ignore_statistics": "통계에서 이 클라이언트 무시"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Het query logboek is succesvol geleegd", "query_log_cleared": "Het query logboek is succesvol geleegd",
"query_log_updated": "Het query logboek is succesvol bijgewerkt", "query_log_updated": "Het query logboek is succesvol bijgewerkt",
"query_log_clear": "Leeg query logs", "query_log_clear": "Leeg query logs",
"query_log_retention": "Query logs bewaartermijn", "query_log_retention": "Query logs rotatie",
"query_log_enable": "Log bestanden inschakelen", "query_log_enable": "Log bestanden inschakelen",
"query_log_configuration": "Logbestanden instellingen", "query_log_configuration": "Logbestanden instellingen",
"query_log_disabled": "Het query logboek is uitgeschakeld en kan worden geconfigureerd in de <0>instellingen</0>", "query_log_disabled": "Het query logboek is uitgeschakeld en kan worden geconfigureerd in de <0>instellingen</0>",
"query_log_strict_search": "Gebruik dubbele aanhalingstekens voor strikt zoeken", "query_log_strict_search": "Gebruik dubbele aanhalingstekens voor strikt zoeken",
"query_log_retention_confirm": "Weet u zeker dat u de bewaartermijn van het query logboek wilt wijzigen? Als u de intervalwaarde verlaagt, gaan sommige gegevens verloren", "query_log_retention_confirm": "Weet u zeker dat u de rotatie van het querylogboek wilt wijzigen? Als u de intervalwaarde verlaagt, gaan sommige gegevens verloren",
"anonymize_client_ip": "Cliënt IP anonimiseren", "anonymize_client_ip": "Cliënt IP anonimiseren",
"anonymize_client_ip_desc": "Het volledige IP-adres van de cliënt niet opnemen in logboeken en statistiekbestanden", "anonymize_client_ip_desc": "Het volledige IP-adres van de cliënt niet opnemen in logboeken en statistiekbestanden",
"dns_config": "DNS-server configuratie", "dns_config": "DNS-server configuratie",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Beveiliging uitschakelen voor {{count}} uur", "disable_notify_for_hours": "Beveiliging uitschakelen voor {{count}} uur",
"disable_notify_for_hours_plural": "Beveiliging uitschakelen voor {{count}} uren", "disable_notify_for_hours_plural": "Beveiliging uitschakelen voor {{count}} uren",
"disable_notify_until_tomorrow": "Beveiliging uitschakelen tot morgen", "disable_notify_until_tomorrow": "Beveiliging uitschakelen tot morgen",
"enable_protection_timer": "Bescherming wordt ingeschakeld over {{time}}" "enable_protection_timer": "Bescherming wordt ingeschakeld over {{time}}",
"custom_retention_input": "Voer retentie in uren in",
"custom_rotation_input": "Voer rotatie in uren in",
"protection_section_label": "Bescherming",
"log_and_stats_section_label": "Aanvragenlogboek en statistieken",
"ignore_query_log": "Deze client negeren in het aanvragenlogboek",
"ignore_statistics": "Deze client negeren in de statistieken"
} }

View File

@@ -614,5 +614,6 @@
"use_saved_key": "Bruk den tidligere lagrede nøkkelen", "use_saved_key": "Bruk den tidligere lagrede nøkkelen",
"parental_control": "Foreldrekontroll", "parental_control": "Foreldrekontroll",
"safe_browsing": "Sikker surfing", "safe_browsing": "Sikker surfing",
"served_from_cache": "{{value}} <i>(formidlet fra mellomlageret)</i>" "served_from_cache": "{{value}} <i>(formidlet fra mellomlageret)</i>",
"protection_section_label": "Beskyttelse"
} }

View File

@@ -167,6 +167,7 @@
"enabled_parental_toast": "Włączona Kontrola Rodzicielska", "enabled_parental_toast": "Włączona Kontrola Rodzicielska",
"disabled_safe_search_toast": "Wyłączone bezpieczne wyszukiwanie", "disabled_safe_search_toast": "Wyłączone bezpieczne wyszukiwanie",
"enabled_save_search_toast": "Włączone bezpieczne wyszukiwanie", "enabled_save_search_toast": "Włączone bezpieczne wyszukiwanie",
"updated_save_search_toast": "Zaktualizowano ustawienia bezpiecznego wyszukiwania",
"enabled_table_header": "Włączone", "enabled_table_header": "Włączone",
"name_table_header": "Nazwa", "name_table_header": "Nazwa",
"list_url_table_header": "Adres URL listy", "list_url_table_header": "Adres URL listy",
@@ -256,12 +257,12 @@
"query_log_cleared": "Dziennik zapytań został pomyślnie wyczyszczony", "query_log_cleared": "Dziennik zapytań został pomyślnie wyczyszczony",
"query_log_updated": "Dziennik zapytań został zaktualizowany", "query_log_updated": "Dziennik zapytań został zaktualizowany",
"query_log_clear": "Wyczyść dzienniki zapytań", "query_log_clear": "Wyczyść dzienniki zapytań",
"query_log_retention": "Przechowywanie dzienników zapytań", "query_log_retention": "Rotacja dzienników zapytań",
"query_log_enable": "Włącz dziennik", "query_log_enable": "Włącz dziennik",
"query_log_configuration": "Konfiguracja dzienników", "query_log_configuration": "Konfiguracja dzienników",
"query_log_disabled": "Dziennik zapytań jest wyłączony i można go skonfigurować w <0>ustawieniach</0>", "query_log_disabled": "Dziennik zapytań jest wyłączony i można go skonfigurować w <0>ustawieniach</0>",
"query_log_strict_search": "Używaj podwójnych cudzysłowów do ścisłego wyszukiwania", "query_log_strict_search": "Używaj podwójnych cudzysłowów do ścisłego wyszukiwania",
"query_log_retention_confirm": "Czy na pewno chcesz zmienić sposób przechowywania dziennika zapytań? Jeśli zmniejszysz wartość interwału, niektóre dane zostaną utracone", "query_log_retention_confirm": "Czy na pewno chcesz zmienić rotację dziennika zapytań? Jeśli zmniejszysz wartość interwału, niektóre dane zostaną utracone",
"anonymize_client_ip": "Anonimizuj adres IP klienta", "anonymize_client_ip": "Anonimizuj adres IP klienta",
"anonymize_client_ip_desc": "Nie zapisuj pełnego adresu IP w dziennikach i statystykach", "anonymize_client_ip_desc": "Nie zapisuj pełnego adresu IP w dziennikach i statystykach",
"dns_config": "Konfiguracja serwera DNS", "dns_config": "Konfiguracja serwera DNS",
@@ -290,6 +291,8 @@
"rate_limit": "Limit ilościowy", "rate_limit": "Limit ilościowy",
"edns_enable": "Włącz podsieć klienta EDNS", "edns_enable": "Włącz podsieć klienta EDNS",
"edns_cs_desc": "Dodaj opcję podsieci klienta EDNS (ECS) do żądań nadrzędnych i rejestruj wartości wysyłane przez klientów w dzienniku zapytań.", "edns_cs_desc": "Dodaj opcję podsieci klienta EDNS (ECS) do żądań nadrzędnych i rejestruj wartości wysyłane przez klientów w dzienniku zapytań.",
"edns_use_custom_ip": "Użyj niestandardowego adresu IP dla EDNS",
"edns_use_custom_ip_desc": "Zezwól na użycie niestandardowego adresu IP dla EDNS",
"rate_limit_desc": "Liczba żądań na sekundę dozwolona na klienta. Ustawienie wartości 0 oznacza brak ograniczeń.", "rate_limit_desc": "Liczba żądań na sekundę dozwolona na klienta. Ustawienie wartości 0 oznacza brak ograniczeń.",
"blocking_ipv4_desc": "Adres IP, który ma zostać zwrócony w przypadku zablokowanego żądania A", "blocking_ipv4_desc": "Adres IP, który ma zostać zwrócony w przypadku zablokowanego żądania A",
"blocking_ipv6_desc": "Adres IP, który ma zostać zwrócony w przypadku zablokowanego żądania AAAA", "blocking_ipv6_desc": "Adres IP, który ma zostać zwrócony w przypadku zablokowanego żądania AAAA",
@@ -523,6 +526,10 @@
"statistics_retention_confirm": "Czy chcesz zmienić sposób przechowania statystyk? Jeżeli obniżysz wartość interwału, niektóre dane będą utracone", "statistics_retention_confirm": "Czy chcesz zmienić sposób przechowania statystyk? Jeżeli obniżysz wartość interwału, niektóre dane będą utracone",
"statistics_cleared": "Statystyki zostały pomyślnie wyczyszczone", "statistics_cleared": "Statystyki zostały pomyślnie wyczyszczone",
"statistics_enable": "Włącz statystyki", "statistics_enable": "Włącz statystyki",
"ignore_domains": "Ignorowane domeny (każda w nowym wierszu)",
"ignore_domains_title": "Ignorowane domeny",
"ignore_domains_desc_stats": "Zapytania dla tych domen nie są zapisywane do statystyk",
"ignore_domains_desc_query": "Zapytania dla tych domen nie są zapisywane do dziennika",
"interval_hours": "{{count}} godzina", "interval_hours": "{{count}} godzina",
"interval_hours_plural": "{{count}} godziny", "interval_hours_plural": "{{count}} godziny",
"filters_configuration": "Konfiguracja filtrów", "filters_configuration": "Konfiguracja filtrów",
@@ -642,5 +649,30 @@
"anonymizer_notification": "<0>Uwaga:</0> Anonimizacja IP jest włączona. Możesz ją wyłączyć w <1>Ustawieniach ogólnych</1>.", "anonymizer_notification": "<0>Uwaga:</0> Anonimizacja IP jest włączona. Możesz ją wyłączyć w <1>Ustawieniach ogólnych</1>.",
"confirm_dns_cache_clear": "Czy na pewno chcesz wyczyścić pamięć podręczną DNS?", "confirm_dns_cache_clear": "Czy na pewno chcesz wyczyścić pamięć podręczną DNS?",
"cache_cleared": "Pamięć podręczna DNS została pomyślnie wyczyszczona", "cache_cleared": "Pamięć podręczna DNS została pomyślnie wyczyszczona",
"clear_cache": "Wyczyść pamięć podręczną" "clear_cache": "Wyczyść pamięć podręczną",
"make_static": "Ustaw adres statyczny",
"theme_auto_desc": "Automatycznie (na podstawie schematu kolorów Twojego urządzenia)",
"theme_dark_desc": "Ciemny motyw",
"theme_light_desc": "Jasny motyw",
"disable_for_seconds": "Na {{count}} sekundę",
"disable_for_seconds_plural": "Na {{count}} sekund",
"disable_for_minutes": "Na {{count}} minutę",
"disable_for_minutes_plural": "Na {{count}} minut",
"disable_for_hours": "Na {{count}} godzinę",
"disable_for_hours_plural": "Na {{count}} godziny",
"disable_until_tomorrow": "Do jutra",
"disable_notify_for_seconds": "Wyłącz ochronę na {{count}} sekundę",
"disable_notify_for_seconds_plural": "Wyłącz ochronę na {{count}} sekund",
"disable_notify_for_minutes": "Wyłącz ochronę na {{count}} minutę",
"disable_notify_for_minutes_plural": "Wyłącz ochronę na {{count}} minut",
"disable_notify_for_hours": "Wyłącz ochronę na {{count}} godzinę",
"disable_notify_for_hours_plural": "Wyłącz ochronę na {{count}} godziny",
"disable_notify_until_tomorrow": "Wyłącz ochronę do jutra",
"enable_protection_timer": "Ochrona zostanie włączona za {{time}}",
"custom_retention_input": "Wprowadź retencję w godzinach",
"custom_rotation_input": "Wprowadź rotację w godzinach",
"protection_section_label": "Ochrona",
"log_and_stats_section_label": "Dziennik zapytań i statystyki",
"ignore_query_log": "Zignoruj tego klienta w dzienniku zapytań",
"ignore_statistics": "Ignoruj tego klienta w statystykach"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "O registro de consulta foi limpo com sucesso", "query_log_cleared": "O registro de consulta foi limpo com sucesso",
"query_log_updated": "O registro da consulta foi atualizado com sucesso", "query_log_updated": "O registro da consulta foi atualizado com sucesso",
"query_log_clear": "Limpar registros de consulta", "query_log_clear": "Limpar registros de consulta",
"query_log_retention": "Arquivamento de registros de consultas", "query_log_retention": "Rotação de registros de consulta",
"query_log_enable": "Ativar registro", "query_log_enable": "Ativar registro",
"query_log_configuration": "Configuração de registros", "query_log_configuration": "Configuração de registros",
"query_log_disabled": "O registro de consulta está desativado e pode ser configurado em <0>configurações</0>", "query_log_disabled": "O registro de consulta está desativado e pode ser configurado em <0>configurações</0>",
"query_log_strict_search": "Use aspas duplas para uma pesquisa mais criteriosa", "query_log_strict_search": "Use aspas duplas para uma pesquisa mais criteriosa",
"query_log_retention_confirm": "Você tem certeza de que deseja alterar o arquivamento do registro de consulta? Se diminuir o valor de intervalo, alguns dados serão perdidos", "query_log_retention_confirm": "Tem a certeza de que quer alterar a rotação do registo de consulta? Se diminuir o valor do intervalo, alguns dados serão perdidos",
"anonymize_client_ip": "Tornar anônimo o IP do cliente", "anonymize_client_ip": "Tornar anônimo o IP do cliente",
"anonymize_client_ip_desc": "Não salva o endereço de IP completo do cliente em registros ou estatísticas", "anonymize_client_ip_desc": "Não salva o endereço de IP completo do cliente em registros ou estatísticas",
"dns_config": "Configuração do servidor DNS", "dns_config": "Configuração do servidor DNS",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Desativar proteção por {{count}} hora", "disable_notify_for_hours": "Desativar proteção por {{count}} hora",
"disable_notify_for_hours_plural": "Desativar proteção por {{count}} horas", "disable_notify_for_hours_plural": "Desativar proteção por {{count}} horas",
"disable_notify_until_tomorrow": "Desativar a proteção até amanhã", "disable_notify_until_tomorrow": "Desativar a proteção até amanhã",
"enable_protection_timer": "A proteção será ativada em {{time}}" "enable_protection_timer": "A proteção será ativada em {{time}}",
"custom_retention_input": "Insira a retenção em horas",
"custom_rotation_input": "Insira a rotação em horas",
"protection_section_label": "Proteção",
"log_and_stats_section_label": "Registro de consultas e estatísticas",
"ignore_query_log": "Ignorar este cliente no registo de consultas",
"ignore_statistics": "Ignorar este cliente nas estatísticas"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "O registo de consulta foi limpo com sucesso", "query_log_cleared": "O registo de consulta foi limpo com sucesso",
"query_log_updated": "O registo da consulta foi atualizado com sucesso", "query_log_updated": "O registo da consulta foi atualizado com sucesso",
"query_log_clear": "Limpar registos de consulta", "query_log_clear": "Limpar registos de consulta",
"query_log_retention": "Retenção de registos de consulta", "query_log_retention": "Rotação de registros de consulta",
"query_log_enable": "Ativar registo", "query_log_enable": "Ativar registo",
"query_log_configuration": "Definições do registo", "query_log_configuration": "Definições do registo",
"query_log_disabled": "O registo de consulta está desativado e pode ser configurado em <0>definições</0>", "query_log_disabled": "O registo de consulta está desativado e pode ser configurado em <0>definições</0>",
"query_log_strict_search": "Usar aspas duplas para uma pesquisa rigorosa", "query_log_strict_search": "Usar aspas duplas para uma pesquisa rigorosa",
"query_log_retention_confirm": "Tem a certeza de que deseja alterar a retenção do registo de consulta? Se diminuir o valor do intervalo, alguns dados serão perdidos", "query_log_retention_confirm": "Tem a certeza de que quer alterar a rotação do registo de consulta? Se diminuir o valor do intervalo, alguns dados serão perdidos",
"anonymize_client_ip": "Tornar anónimo o IP do cliente", "anonymize_client_ip": "Tornar anónimo o IP do cliente",
"anonymize_client_ip_desc": "Não gurda o endereço de IP completo do cliente em registo ou estatísticas", "anonymize_client_ip_desc": "Não gurda o endereço de IP completo do cliente em registo ou estatísticas",
"dns_config": "Definição do servidor DNS", "dns_config": "Definição do servidor DNS",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Desativar proteção por {{count}} hora", "disable_notify_for_hours": "Desativar proteção por {{count}} hora",
"disable_notify_for_hours_plural": "Desativar proteção por {{count}} horas", "disable_notify_for_hours_plural": "Desativar proteção por {{count}} horas",
"disable_notify_until_tomorrow": "Desativar a proteção até amanhã", "disable_notify_until_tomorrow": "Desativar a proteção até amanhã",
"enable_protection_timer": "A proteção será habilitada em {{time}}" "enable_protection_timer": "A proteção será habilitada em {{time}}",
"custom_retention_input": "Insira a retenção em horas",
"custom_rotation_input": "Insira a rotação em horas",
"protection_section_label": "Proteção",
"log_and_stats_section_label": "Log de consulta e estatísticas",
"ignore_query_log": "Ignorar este cliente no log de consulta",
"ignore_statistics": "Ignorar este cliente nas estatísticas"
} }

View File

@@ -642,5 +642,6 @@
"anonymizer_notification": "<0>Nota:</0> Anonimizarea IP este activată. Puteți să o dezactivați în <1>Setări generale</1>.", "anonymizer_notification": "<0>Nota:</0> Anonimizarea IP este activată. Puteți să o dezactivați în <1>Setări generale</1>.",
"confirm_dns_cache_clear": "Sunteți sigur că doriți să ștergeți memoria cache DNS?", "confirm_dns_cache_clear": "Sunteți sigur că doriți să ștergeți memoria cache DNS?",
"cache_cleared": "Cache-ul DNS a fost golit cu succes", "cache_cleared": "Cache-ul DNS a fost golit cu succes",
"clear_cache": "Goliți memoria cache" "clear_cache": "Goliți memoria cache",
"protection_section_label": "Protecție"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Журнал запросов успешно очищен", "query_log_cleared": "Журнал запросов успешно очищен",
"query_log_updated": "Журнал запросов успешно обновлён", "query_log_updated": "Журнал запросов успешно обновлён",
"query_log_clear": "Очистить журнал запросов", "query_log_clear": "Очистить журнал запросов",
"query_log_retention": "Сохранение журнала запросов", "query_log_retention": "Частота ротации журнала запросов",
"query_log_enable": "Включить журнал", "query_log_enable": "Включить журнал",
"query_log_configuration": "Настройка журнала", "query_log_configuration": "Настройка журнала",
"query_log_disabled": "Журнал запросов выключен, его можно включить в <0>настройках</0>", "query_log_disabled": "Журнал запросов выключен, его можно включить в <0>настройках</0>",
"query_log_strict_search": "Используйте двойные кавычки для строгого поиска", "query_log_strict_search": "Используйте двойные кавычки для строгого поиска",
"query_log_retention_confirm": "Вы уверены, что хотите изменить срок хранения запросов? При сокращении интервала данные могут быть утеряны", "query_log_retention_confirm": "Вы уверены, что хотите изменить частоту ротации журнала запросов? При сокращении срока данные могут быть утеряны",
"anonymize_client_ip": "Анонимизировать IP-адрес клиента", "anonymize_client_ip": "Анонимизировать IP-адрес клиента",
"anonymize_client_ip_desc": "Не сохранять полный IP-адрес клиента в журналах и статистике", "anonymize_client_ip_desc": "Не сохранять полный IP-адрес клиента в журналах и статистике",
"dns_config": "Настройки DNS-сервера", "dns_config": "Настройки DNS-сервера",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Отключить защиту на {{count}} час", "disable_notify_for_hours": "Отключить защиту на {{count}} час",
"disable_notify_for_hours_plural": "Отключить защиту на {{count}} часов", "disable_notify_for_hours_plural": "Отключить защиту на {{count}} часов",
"disable_notify_until_tomorrow": "Отключить защиту до завтра", "disable_notify_until_tomorrow": "Отключить защиту до завтра",
"enable_protection_timer": "Защита будет включена в {{time}}" "enable_protection_timer": "Защита будет включена в {{time}}",
"custom_retention_input": "Введите срок хранения в часах",
"custom_rotation_input": "Введите частоту ротации в часах",
"protection_section_label": "Защита",
"log_and_stats_section_label": "Журнал запросов и статистика",
"ignore_query_log": "Игнорировать этого клиента в журнале запросов",
"ignore_statistics": "Игнорировать этого клиента в статистике"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Denník dopytov bol úspešne vymazaný", "query_log_cleared": "Denník dopytov bol úspešne vymazaný",
"query_log_updated": "Denník dopytov bol úspešne aktualizovaný", "query_log_updated": "Denník dopytov bol úspešne aktualizovaný",
"query_log_clear": "Vymazať denníky dopytov", "query_log_clear": "Vymazať denníky dopytov",
"query_log_retention": "Obdobie záznamu denníka dopytov", "query_log_retention": "Rotácia denníkov dopytov",
"query_log_enable": "Zapnúť denník", "query_log_enable": "Zapnúť denník",
"query_log_configuration": "Konfigurácia denníka", "query_log_configuration": "Konfigurácia denníka",
"query_log_disabled": "Protokol dopytov je vypnutý a možno ho nakonfigurovať v <0>nastaveniach</0>", "query_log_disabled": "Protokol dopytov je vypnutý a možno ho nakonfigurovať v <0>nastaveniach</0>",
"query_log_strict_search": "Na prísne vyhľadávanie použite dvojité úvodzovky", "query_log_strict_search": "Na prísne vyhľadávanie použite dvojité úvodzovky",
"query_log_retention_confirm": "Naozaj chcete zmeniť uchovávanie denníku dopytov? Ak znížite hodnotu intervalu, niektoré údaje sa stratia", "query_log_retention_confirm": "Naozaj chcete zmeniť rotáciu denníka dopytov? Ak znížite hodnotu intervalu, niektoré údaje sa stratia",
"anonymize_client_ip": "Anonymizujte IP klienta", "anonymize_client_ip": "Anonymizujte IP klienta",
"anonymize_client_ip_desc": "Neukladať úplnú IP adresu klienta do protokolov a štatistík", "anonymize_client_ip_desc": "Neukladať úplnú IP adresu klienta do protokolov a štatistík",
"dns_config": "Konfigurácia DNS servera", "dns_config": "Konfigurácia DNS servera",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Vypnite ochranu na {{count}} hodinu", "disable_notify_for_hours": "Vypnite ochranu na {{count}} hodinu",
"disable_notify_for_hours_plural": "Vypnite ochranu na {{count}} hodín", "disable_notify_for_hours_plural": "Vypnite ochranu na {{count}} hodín",
"disable_notify_until_tomorrow": "Vypnúť ochranu do zajtra", "disable_notify_until_tomorrow": "Vypnúť ochranu do zajtra",
"enable_protection_timer": "Ochrana bude zapnutá o {{time}}" "enable_protection_timer": "Ochrana bude zapnutá o {{time}}",
"custom_retention_input": "Zadajte retenciu v hodinách",
"custom_rotation_input": "Zadajte rotáciu v hodinách",
"protection_section_label": "Ochrana",
"log_and_stats_section_label": "Protokol dopytov a štatistiky",
"ignore_query_log": "Ignorovať tohto klienta v denníku dopytov",
"ignore_statistics": "Ignorovanie tohto klienta v štatistikách"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Dnevnik poizvedb je uspešno izbrisan", "query_log_cleared": "Dnevnik poizvedb je uspešno izbrisan",
"query_log_updated": "Dnevnik poizvedb je bil uspešno posodobljen", "query_log_updated": "Dnevnik poizvedb je bil uspešno posodobljen",
"query_log_clear": "Počisti dnevnike poizvedb", "query_log_clear": "Počisti dnevnike poizvedb",
"query_log_retention": "Zadrževanje dnevnikov poizvedb", "query_log_retention": "Rotacija dnevnikov poizvedb",
"query_log_enable": "Omogoči dnevni", "query_log_enable": "Omogoči dnevni",
"query_log_configuration": "Konfiguracija dnevnikov", "query_log_configuration": "Konfiguracija dnevnikov",
"query_log_disabled": "Dnevnik poizvedb je onemogočen in ga je mogoče konfigurirati v <0>nastavitvah</0>", "query_log_disabled": "Dnevnik poizvedb je onemogočen in ga je mogoče konfigurirati v <0>nastavitvah</0>",
"query_log_strict_search": "Za strogo iskanje uporabite dvojne narekovaje", "query_log_strict_search": "Za strogo iskanje uporabite dvojne narekovaje",
"query_log_retention_confirm": "Ali ste prepričani, da želite spremeniti zadrževanje dnevnika poizvedb? Če zmanjšate vrednost intervala, bodo nekateri podatki izgubljeni", "query_log_retention_confirm": "Ali ste prepričani, da želite spremeniti rotacijo dnevnika poizvedb? Če zmanjšate vrednost intervala, bodo nekateri podatki izgubljeni",
"anonymize_client_ip": "Anonimiziraj odjemalca IP", "anonymize_client_ip": "Anonimiziraj odjemalca IP",
"anonymize_client_ip_desc": "Ne shrani celotnega naslova IP odjemalca v dnevnikih ali statistiki", "anonymize_client_ip_desc": "Ne shrani celotnega naslova IP odjemalca v dnevnikih ali statistiki",
"dns_config": "Konfiguracija strežnika DNS", "dns_config": "Konfiguracija strežnika DNS",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Onemogoči zaščito za {{count}} uro", "disable_notify_for_hours": "Onemogoči zaščito za {{count}} uro",
"disable_notify_for_hours_plural": "Onemogoči zaščito za {{count}} ur", "disable_notify_for_hours_plural": "Onemogoči zaščito za {{count}} ur",
"disable_notify_until_tomorrow": "Onemogoči zaščito do jutri", "disable_notify_until_tomorrow": "Onemogoči zaščito do jutri",
"enable_protection_timer": "Zaščita bo omogočena ob {{time}}" "enable_protection_timer": "Zaščita bo omogočena ob {{time}}",
"custom_retention_input": "Vnesite zadrževanje v urah",
"custom_rotation_input": "Vnesite rotacijo v urah",
"protection_section_label": "Zaščita",
"log_and_stats_section_label": "Dnevnik poizvedb in statistika",
"ignore_query_log": "Ignorirajte tega odjemalca v dnevniku poizvedb",
"ignore_statistics": "Ignoriranje tega odjemalca v statistiki"
} }

View File

@@ -642,5 +642,6 @@
"anonymizer_notification": "<0>Nota:</0> IP prepoznavanje je omogućeno. Možete ga onemogućiti u opštim <1>postavkama</1>.", "anonymizer_notification": "<0>Nota:</0> IP prepoznavanje je omogućeno. Možete ga onemogućiti u opštim <1>postavkama</1>.",
"confirm_dns_cache_clear": "Želite li zaista da obrišite DNS keš?", "confirm_dns_cache_clear": "Želite li zaista da obrišite DNS keš?",
"cache_cleared": "DNS keš je uspešno očišćen", "cache_cleared": "DNS keš je uspešno očišćen",
"clear_cache": "Obriši keš memoriju" "clear_cache": "Obriši keš memoriju",
"protection_section_label": "Zaštita"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "Sorgu günlüğü başarıyla temizlendi", "query_log_cleared": "Sorgu günlüğü başarıyla temizlendi",
"query_log_updated": "Sorgu günlüğü başarıyla güncellendi", "query_log_updated": "Sorgu günlüğü başarıyla güncellendi",
"query_log_clear": "Sorgu günlüklerini temizle", "query_log_clear": "Sorgu günlüklerini temizle",
"query_log_retention": "Sorgu günlüklerini sakla", "query_log_retention": "Sorgu günlükleri rotasyonu",
"query_log_enable": "Günlüğü etkinleştir", "query_log_enable": "Günlüğü etkinleştir",
"query_log_configuration": "Günlük yapılandırması", "query_log_configuration": "Günlük yapılandırması",
"query_log_disabled": "Sorgu günlüğü devre dışı bırakıldı, bunu <0>ayarlar</0> kısmından yapılandırılabilirsiniz", "query_log_disabled": "Sorgu günlüğü devre dışı bırakıldı, bunu <0>ayarlar</0> kısmından yapılandırılabilirsiniz",
"query_log_strict_search": "Tam arama için çift tırnak işareti kullanın", "query_log_strict_search": "Tam arama için çift tırnak işareti kullanın",
"query_log_retention_confirm": "Sorgu günlüğü saklama süresini değiştirmek istediğinize emin misiniz? Aralık değerini azaltırsanız, bazı veriler kaybolacaktır", "query_log_retention_confirm": "Sorgu günlüğü rotasyonunu değiştirmek istediğinizden emin misiniz? Aralık değerini düşürürseniz, bazı veriler kaybolacaktır.",
"anonymize_client_ip": "İstemcinin IP adresini gizle", "anonymize_client_ip": "İstemcinin IP adresini gizle",
"anonymize_client_ip_desc": "İstemcinin tam IP adresini günlüklere veya istatistiklere kaydetmeyin", "anonymize_client_ip_desc": "İstemcinin tam IP adresini günlüklere veya istatistiklere kaydetmeyin",
"dns_config": "DNS sunucu yapılandırması", "dns_config": "DNS sunucu yapılandırması",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "Korumayı {{count}} saatliğine devre dışı bırak", "disable_notify_for_hours": "Korumayı {{count}} saatliğine devre dışı bırak",
"disable_notify_for_hours_plural": "Korumayı {{count}} saatliğine devre dışı bırak", "disable_notify_for_hours_plural": "Korumayı {{count}} saatliğine devre dışı bırak",
"disable_notify_until_tomorrow": "Korumayı yarına kadar devre dışı bırak", "disable_notify_until_tomorrow": "Korumayı yarına kadar devre dışı bırak",
"enable_protection_timer": "Koruma {{time}} içinde etkinleştirilecektir" "enable_protection_timer": "Koruma {{time}} içinde etkinleştirilecektir",
"custom_retention_input": "Saklama süresini saat olarak girin",
"custom_rotation_input": "Rotasyonu saat cinsinden girin",
"protection_section_label": "Koruma",
"log_and_stats_section_label": "Sorgu günlüğü ve istatistikler",
"ignore_query_log": "Sorgu günlüğünde bu istemciyi yoksay",
"ignore_statistics": "İstatistiklerde bu istemciyi yoksay"
} }

View File

@@ -642,5 +642,6 @@
"anonymizer_notification": "<0>Примітка:</0> IP-анонімізацію ввімкнено. Ви можете вимкнути його в <1>Загальні налаштування</1> .", "anonymizer_notification": "<0>Примітка:</0> IP-анонімізацію ввімкнено. Ви можете вимкнути його в <1>Загальні налаштування</1> .",
"confirm_dns_cache_clear": "Ви впевнені, що бажаєте очистити кеш DNS?", "confirm_dns_cache_clear": "Ви впевнені, що бажаєте очистити кеш DNS?",
"cache_cleared": "Кеш DNS успішно очищено", "cache_cleared": "Кеш DNS успішно очищено",
"clear_cache": "Очистити кеш" "clear_cache": "Очистити кеш",
"protection_section_label": "Захист"
} }

View File

@@ -642,5 +642,6 @@
"anonymizer_notification": "<0> Lưu ý:</0> Tính năng ẩn danh IP được bật. Bạn có thể tắt nó trong <1> Cài đặt chung</1>.", "anonymizer_notification": "<0> Lưu ý:</0> Tính năng ẩn danh IP được bật. Bạn có thể tắt nó trong <1> Cài đặt chung</1>.",
"confirm_dns_cache_clear": "Bạn có chắc chắn muốn xóa bộ đệm ẩn DNS không?", "confirm_dns_cache_clear": "Bạn có chắc chắn muốn xóa bộ đệm ẩn DNS không?",
"cache_cleared": "Đã xóa thành công bộ đệm DNS", "cache_cleared": "Đã xóa thành công bộ đệm DNS",
"clear_cache": "Xóa bộ nhớ cache" "clear_cache": "Xóa bộ nhớ cache",
"protection_section_label": "Sự bảo vệ"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "查询日志已成功清除", "query_log_cleared": "查询日志已成功清除",
"query_log_updated": "已成功更新查询日志", "query_log_updated": "已成功更新查询日志",
"query_log_clear": "清除查询日志", "query_log_clear": "清除查询日志",
"query_log_retention": "查询记录保留时间", "query_log_retention": "查询日志保留时间",
"query_log_enable": "启用日志", "query_log_enable": "启用日志",
"query_log_configuration": "日志配置", "query_log_configuration": "日志配置",
"query_log_disabled": "查询日志已禁用,在<0>这些设置</0>中能配置它们", "query_log_disabled": "查询日志已禁用,在<0>这些设置</0>中能配置它们",
"query_log_strict_search": "使用双引号进行严谨搜索", "query_log_strict_search": "使用双引号进行严谨搜索",
"query_log_retention_confirm": "您确定要更改查询记录保留时间吗? 如果减少间隔时间的值, 某些数据可能会丢失", "query_log_retention_confirm": "您确定要更改查询记录保留时间吗?如果减少时间间隔数值,某些数据可能会丢失",
"anonymize_client_ip": "匿名化客户端IP", "anonymize_client_ip": "匿名化客户端IP",
"anonymize_client_ip_desc": "不要在日志和统计信息中保存客户端的完整 IP 地址", "anonymize_client_ip_desc": "不要在日志和统计信息中保存客户端的完整 IP 地址",
"dns_config": "DNS 服务配置", "dns_config": "DNS 服务配置",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "禁用保护 {{count}} 小时", "disable_notify_for_hours": "禁用保护 {{count}} 小时",
"disable_notify_for_hours_plural": "禁用保护 {{count}} 小时", "disable_notify_for_hours_plural": "禁用保护 {{count}} 小时",
"disable_notify_until_tomorrow": "禁用保护直到明天", "disable_notify_until_tomorrow": "禁用保护直到明天",
"enable_protection_timer": "保护将于 {{time}} 启用" "enable_protection_timer": "保护将于 {{time}} 启用",
"custom_retention_input": "输入保留时间(小时)",
"custom_rotation_input": "输入旋转时间(小时)",
"protection_section_label": "防护",
"log_and_stats_section_label": "查询日志和统计数据",
"ignore_query_log": "在查询日志中忽略此客户端",
"ignore_statistics": "在统计数据中忽略此客户端"
} }

View File

@@ -257,12 +257,12 @@
"query_log_cleared": "該查詢記錄已被成功地清除", "query_log_cleared": "該查詢記錄已被成功地清除",
"query_log_updated": "該查詢記錄已被成功地更新", "query_log_updated": "該查詢記錄已被成功地更新",
"query_log_clear": "清除查詢記錄", "query_log_clear": "清除查詢記錄",
"query_log_retention": "查詢記錄保留", "query_log_retention": "查詢記錄保留時間",
"query_log_enable": "啟用記錄", "query_log_enable": "啟用記錄",
"query_log_configuration": "記錄配置", "query_log_configuration": "記錄配置",
"query_log_disabled": "查詢記錄被禁用並可在<0>設定</0>中被配置", "query_log_disabled": "查詢記錄被禁用並可在<0>設定</0>中被配置",
"query_log_strict_search": "使用雙引號於嚴謹的搜尋", "query_log_strict_search": "使用雙引號於嚴謹的搜尋",
"query_log_retention_confirm": "您確定您想要更改查詢記錄保留嗎?如果您減少該間隔值,某些資料將被丟失", "query_log_retention_confirm": "您確定要更改記錄檔保存期限嗎?如果您縮短期限部分資料可能將會遺失",
"anonymize_client_ip": "將用戶端 IP 匿名", "anonymize_client_ip": "將用戶端 IP 匿名",
"anonymize_client_ip_desc": "不要儲存用戶端之完整的 IP 位址到記錄或統計資料裡", "anonymize_client_ip_desc": "不要儲存用戶端之完整的 IP 位址到記錄或統計資料裡",
"dns_config": "DNS 伺服器配置", "dns_config": "DNS 伺服器配置",
@@ -668,5 +668,11 @@
"disable_notify_for_hours": "計 {{count}} 小時禁用防護", "disable_notify_for_hours": "計 {{count}} 小時禁用防護",
"disable_notify_for_hours_plural": "計 {{count}} 小時禁用防護", "disable_notify_for_hours_plural": "計 {{count}} 小時禁用防護",
"disable_notify_until_tomorrow": "禁用防護直到明天", "disable_notify_until_tomorrow": "禁用防護直到明天",
"enable_protection_timer": "防護將於 {{time}} 被啟用" "enable_protection_timer": "防護將於 {{time}} 被啟用",
"custom_retention_input": "輸入保留時間(小時)",
"custom_rotation_input": "輸入旋轉時間(小時)",
"protection_section_label": "防護",
"log_and_stats_section_label": "查詢記錄和統計資料",
"ignore_query_log": "在查詢記錄中忽略此用戶端",
"ignore_statistics": "在統計資料中忽略此用戶端"
} }

View File

@@ -7,11 +7,10 @@
addrs[$2] = true addrs[$2] = true
prev_line = FNR prev_line = FNR
if ($2 == "0.0.0.0" || $2 == "::") { if ($2 == "0.0.0.0" || $2 == "'::'") {
delete addrs
addrs["localhost"] = true
# Drop all the other addresses. # Drop all the other addresses.
delete addrs
addrs[""] = true
prev_line = -1 prev_line = -1
} }
} }

View File

@@ -61,8 +61,11 @@ then
error_exit "no DNS bindings could be retrieved from $filename" error_exit "no DNS bindings could be retrieved from $filename"
fi fi
first_dns="$( echo "$dns_hosts" | head -n 1 )"
readonly first_dns
# TODO(e.burkov): Deal with 0 port. # TODO(e.burkov): Deal with 0 port.
case "$( echo "$dns_hosts" | head -n 1 )" case "$first_dns"
in in
(*':0') (*':0')
error_exit '0 in DNS port is not supported by healthcheck' error_exit '0 in DNS port is not supported by healthcheck'
@@ -82,8 +85,23 @@ esac
# See https://github.com/AdguardTeam/AdGuardHome/issues/5642. # See https://github.com/AdguardTeam/AdGuardHome/issues/5642.
wget --no-check-certificate "$web_url" -O /dev/null -q || exit 1 wget --no-check-certificate "$web_url" -O /dev/null -q || exit 1
echo "$dns_hosts" | while read -r host test_fqdn="healthcheck.adguardhome.test."
do readonly test_fqdn
nslookup -type=a healthcheck.adguardhome.test. "$host" > /dev/null ||\
# The awk script currently returns only port prefixed with colon in case of
# unspecified address.
case "$first_dns"
in
(':'*)
nslookup -type=a "$test_fqdn" "127.0.0.1${first_dns}" > /dev/null ||\
nslookup -type=a "$test_fqdn" "[::1]${first_dns}" > /dev/null ||\
error_exit "nslookup failed for $host" error_exit "nslookup failed for $host"
done ;;
(*)
echo "$dns_hosts" | while read -r host
do
nslookup -type=a "$test_fqdn" "$host" > /dev/null ||\
error_exit "nslookup failed for $host"
done
;;
esac

2
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
go 1.19 go 1.19
require ( require (
github.com/AdguardTeam/dnsproxy v0.48.3 github.com/AdguardTeam/dnsproxy v0.49.1
github.com/AdguardTeam/golibs v0.13.2 github.com/AdguardTeam/golibs v0.13.2
github.com/AdguardTeam/urlfilter v0.16.1 github.com/AdguardTeam/urlfilter v0.16.1
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1

4
go.sum
View File

@@ -1,5 +1,5 @@
github.com/AdguardTeam/dnsproxy v0.48.3 h1:h9xgDSmd1MqsPFNApyaPVXolmSTtzOWOcfWvPeDEP6s= github.com/AdguardTeam/dnsproxy v0.49.1 h1:JpStBK05uCgA3ldleaNLRmIwE9V7vRg7/kVJQSdnQYg=
github.com/AdguardTeam/dnsproxy v0.48.3/go.mod h1:Y7g7jRTd/u7+KJ/QvnGI2PCE8vnisp6EsW47/Sz0DZw= github.com/AdguardTeam/dnsproxy v0.49.1/go.mod h1:Y7g7jRTd/u7+KJ/QvnGI2PCE8vnisp6EsW47/Sz0DZw=
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw= github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.13.2 h1:BPASsyQKmb+b8VnvsNOHp7bKfcZl9Z+Z2UhPjOiupSc= github.com/AdguardTeam/golibs v0.13.2 h1:BPASsyQKmb+b8VnvsNOHp7bKfcZl9Z+Z2UhPjOiupSc=

View File

@@ -72,8 +72,8 @@ func WriteJSONResponse(w http.ResponseWriter, r *http.Request, resp any) (err er
// WriteJSONResponseCode is like [WriteJSONResponse] but adds the ability to // WriteJSONResponseCode is like [WriteJSONResponse] but adds the ability to
// redefine the status code. // redefine the status code.
func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) { func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) {
w.WriteHeader(code)
w.Header().Set(httphdr.ContentType, HdrValApplicationJSON) w.Header().Set(httphdr.ContentType, HdrValApplicationJSON)
w.WriteHeader(code)
err = json.NewEncoder(w).Encode(resp) err = json.NewEncoder(w).Encode(resp)
if err != nil { if err != nil {
Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err) Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err)

View File

@@ -0,0 +1,6 @@
package aghos
// PreCheckActionStart performs the service start action pre-check.
func PreCheckActionStart() (err error) {
return preCheckActionStart()
}

View File

@@ -0,0 +1,32 @@
//go:build darwin
package aghos
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AdguardTeam/golibs/log"
)
// preCheckActionStart performs the service start action pre-check. It warns
// user that the service should be installed into Applications directory.
func preCheckActionStart() (err error) {
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("getting executable path: %v", err)
}
exe, err = filepath.EvalSymlinks(exe)
if err != nil {
return fmt.Errorf("evaluating executable symlinks: %v", err)
}
if !strings.HasPrefix(exe, "/Applications/") {
log.Info("warning: service must be started from within the /Applications directory")
}
return err
}

View File

@@ -0,0 +1,8 @@
//go:build !darwin
package aghos
// preCheckActionStart performs the service start action pre-check.
func preCheckActionStart() (err error) {
return nil
}

View File

@@ -31,8 +31,16 @@ type ServerConfig struct {
Conf4 V4ServerConf `yaml:"dhcpv4"` Conf4 V4ServerConf `yaml:"dhcpv4"`
Conf6 V6ServerConf `yaml:"dhcpv6"` Conf6 V6ServerConf `yaml:"dhcpv6"`
WorkDir string `yaml:"-"` // WorkDir is used to store DHCP leases.
DBFilePath string `yaml:"-"` //
// Deprecated: Remove it when migration of DHCP leases will not be needed.
WorkDir string `yaml:"-"`
// DataDir is used to store DHCP leases.
DataDir string `yaml:"-"`
// dbFilePath is the path to the file with stored DHCP leases.
dbFilePath string `yaml:"-"`
} }
// DHCPServer - DHCP server interface // DHCPServer - DHCP server interface

View File

@@ -5,43 +5,34 @@ package dhcpd
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"net/netip"
"os" "os"
"time"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/google/renameio/maybe" "github.com/google/renameio/maybe"
"golang.org/x/exp/slices"
) )
const dbFilename = "leases.db" const (
// dataFilename contains saved leases.
dataFilename = "leases.json"
type leaseJSON struct { // dataVersion is the current version of the stored DHCP leases structure.
HWAddr []byte `json:"mac"` dataVersion = 1
IP []byte `json:"ip"` )
Hostname string `json:"host"`
Expiry int64 `json:"exp"` // dataLeases is the structure of the stored DHCP leases.
type dataLeases struct {
// Version is the current version of the structure.
Version int `json:"version"`
// Leases is the list containing stored DHCP leases.
Leases []*Lease `json:"leases"`
} }
func normalizeIP(ip net.IP) net.IP { // dbLoad loads stored leases.
ip4 := ip.To4()
if ip4 != nil {
return ip4
}
return ip
}
// Load lease table from DB
//
// TODO(s.chzhen): Decrease complexity.
func (s *server) dbLoad() (err error) { func (s *server) dbLoad() (err error) {
dynLeases := []*Lease{} data, err := os.ReadFile(s.conf.dbFilePath)
staticLeases := []*Lease{}
v6StaticLeases := []*Lease{}
v6DynLeases := []*Lease{}
data, err := os.ReadFile(s.conf.DBFilePath)
if err != nil { if err != nil {
if !errors.Is(err, os.ErrNotExist) { if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("reading db: %w", err) return fmt.Errorf("reading db: %w", err)
@@ -50,52 +41,30 @@ func (s *server) dbLoad() (err error) {
return nil return nil
} }
obj := []leaseJSON{} dl := &dataLeases{}
err = json.Unmarshal(data, &obj) err = json.Unmarshal(data, dl)
if err != nil { if err != nil {
return fmt.Errorf("decoding db: %w", err) return fmt.Errorf("decoding db: %w", err)
} }
numLeases := len(obj) leases := dl.Leases
for i := range obj {
obj[i].IP = normalizeIP(obj[i].IP)
ip, ok := netip.AddrFromSlice(obj[i].IP) leases4 := []*Lease{}
if !ok { leases6 := []*Lease{}
log.Info("dhcp: invalid IP: %s", obj[i].IP)
continue
}
lease := Lease{ for _, l := range leases {
HWAddr: obj[i].HWAddr, if l.IP.Is4() {
IP: ip, leases4 = append(leases4, l)
Hostname: obj[i].Hostname,
Expiry: time.Unix(obj[i].Expiry, 0),
IsStatic: obj[i].Expiry == leaseExpireStatic,
}
if len(obj[i].IP) == 16 {
if lease.IsStatic {
v6StaticLeases = append(v6StaticLeases, &lease)
} else {
v6DynLeases = append(v6DynLeases, &lease)
}
} else { } else {
if lease.IsStatic { leases6 = append(leases6, l)
staticLeases = append(staticLeases, &lease)
} else {
dynLeases = append(dynLeases, &lease)
}
} }
} }
leases4 := normalizeLeases(staticLeases, dynLeases)
err = s.srv4.ResetLeases(leases4) err = s.srv4.ResetLeases(leases4)
if err != nil { if err != nil {
return fmt.Errorf("resetting dhcpv4 leases: %w", err) return fmt.Errorf("resetting dhcpv4 leases: %w", err)
} }
leases6 := normalizeLeases(v6StaticLeases, v6DynLeases)
if s.srv6 != nil { if s.srv6 != nil {
err = s.srv6.ResetLeases(leases6) err = s.srv6.ResetLeases(leases6)
if err != nil { if err != nil {
@@ -104,90 +73,54 @@ func (s *server) dbLoad() (err error) {
} }
log.Info("dhcp: loaded leases v4:%d v6:%d total-read:%d from DB", log.Info("dhcp: loaded leases v4:%d v6:%d total-read:%d from DB",
len(leases4), len(leases6), numLeases) len(leases4), len(leases6), len(leases))
return nil return nil
} }
// Skip duplicate leases // dbStore stores DHCP leases.
// Static leases have a priority over dynamic leases
func normalizeLeases(staticLeases, dynLeases []*Lease) []*Lease {
leases := []*Lease{}
index := map[string]int{}
for i, lease := range staticLeases {
_, ok := index[lease.HWAddr.String()]
if ok {
continue // skip the lease with the same HW address
}
index[lease.HWAddr.String()] = i
leases = append(leases, lease)
}
for i, lease := range dynLeases {
_, ok := index[lease.HWAddr.String()]
if ok {
continue // skip the lease with the same HW address
}
index[lease.HWAddr.String()] = i
leases = append(leases, lease)
}
return leases
}
// 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 // Use an empty slice here as opposed to nil so that it doesn't write
// "null" into the database file if leases are empty. // "null" into the database file if leases are empty.
leases := []leaseJSON{} leases := []*Lease{}
leases4 := s.srv4.getLeasesRef() leases4 := s.srv4.getLeasesRef()
for _, l := range leases4 { leases = append(leases, leases4...)
if l.Expiry.Unix() == 0 {
continue
}
lease := leaseJSON{
HWAddr: l.HWAddr,
IP: l.IP.AsSlice(),
Hostname: l.Hostname,
Expiry: l.Expiry.Unix(),
}
leases = append(leases, lease)
}
if s.srv6 != nil { if s.srv6 != nil {
leases6 := s.srv6.getLeasesRef() leases6 := s.srv6.getLeasesRef()
for _, l := range leases6 { leases = append(leases, leases6...)
if l.Expiry.Unix() == 0 {
continue
}
lease := leaseJSON{
HWAddr: l.HWAddr,
IP: l.IP.AsSlice(),
Hostname: l.Hostname,
Expiry: l.Expiry.Unix(),
}
leases = append(leases, lease)
}
} }
var data []byte return writeDB(s.conf.dbFilePath, leases)
data, err = json.Marshal(leases) }
// writeDB writes leases to file at path.
func writeDB(path string, leases []*Lease) (err error) {
defer func() { err = errors.Annotate(err, "writing db: %w") }()
slices.SortFunc(leases, func(a, b *Lease) bool {
return a.Hostname < b.Hostname
})
dl := &dataLeases{
Version: dataVersion,
Leases: leases,
}
buf, err := json.Marshal(dl)
if err != nil { if err != nil {
return fmt.Errorf("encoding db: %w", err) // Don't wrap the error since it's informative enough as is.
return err
} }
err = maybe.WriteFile(s.conf.DBFilePath, data, 0o644) err = maybe.WriteFile(path, buf, 0o644)
if err != nil { if err != nil {
return fmt.Errorf("writing db: %w", err) // Don't wrap the error since it's informative enough as is.
return err
} }
log.Info("dhcp: stored %d leases in db", len(leases)) log.Info("dhcp: stored %d leases in %q", len(leases), path)
return nil return nil
} }

View File

@@ -15,13 +15,6 @@ import (
) )
const ( const (
// leaseExpireStatic is used to define the Expiry field for static
// leases.
//
// 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 is the default time-to-live for leases.
DefaultDHCPLeaseTTL = uint32(timeutil.Day / time.Second) DefaultDHCPLeaseTTL = uint32(timeutil.Day / time.Second)
@@ -35,10 +28,10 @@ const (
defaultBackoff time.Duration = 500 * time.Millisecond defaultBackoff time.Duration = 500 * time.Millisecond
) )
// Lease contains the necessary information about a DHCP lease // Lease contains the necessary information about a DHCP lease. It's used in
// various places. So don't change it without good reason.
type Lease struct { type Lease struct {
// Expiry is the expiration time of the lease. The unix timestamp value // Expiry is the expiration time of the lease.
// of 1 means that this is a static lease.
Expiry time.Time `json:"expires"` Expiry time.Time `json:"expires"`
// Hostname of the client. // Hostname of the client.
@@ -238,7 +231,7 @@ func Create(conf *ServerConfig) (s *server, err error) {
LocalDomainName: conf.LocalDomainName, LocalDomainName: conf.LocalDomainName,
DBFilePath: filepath.Join(conf.WorkDir, dbFilename), dbFilePath: filepath.Join(conf.DataDir, dataFilename),
}, },
} }
@@ -279,6 +272,13 @@ func Create(conf *ServerConfig) (s *server, err error) {
return nil, fmt.Errorf("neither dhcpv4 nor dhcpv6 srv is configured") return nil, fmt.Errorf("neither dhcpv4 nor dhcpv6 srv is configured")
} }
// Migrate leases db if needed.
err = migrateDB(conf)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
// Don't delay database loading until the DHCP server is started, // Don't delay database loading until the DHCP server is started,
// because we need static leases functionality available beforehand. // because we need static leases functionality available beforehand.
err = s.dbLoad() err = s.dbLoad()

View File

@@ -5,7 +5,7 @@ package dhcpd
import ( import (
"net" "net"
"net/netip" "net/netip"
"os" "path/filepath"
"testing" "testing"
"time" "time"
@@ -27,7 +27,7 @@ func TestDB(t *testing.T) {
var err error var err error
s := server{ s := server{
conf: &ServerConfig{ conf: &ServerConfig{
DBFilePath: dbFilename, dbFilePath: filepath.Join(t.TempDir(), dataFilename),
}, },
} }
@@ -67,8 +67,6 @@ func TestDB(t *testing.T) {
err = s.dbStore() err = s.dbStore()
require.NoError(t, err) require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, func() (err error) { return os.Remove(dbFilename) })
err = s.srv4.ResetLeases(nil) err = s.srv4.ResetLeases(nil)
require.NoError(t, err) require.NoError(t, err)
@@ -78,36 +76,13 @@ func TestDB(t *testing.T) {
ll := s.srv4.GetLeases(LeasesAll) ll := s.srv4.GetLeases(LeasesAll)
require.Len(t, ll, len(leases)) require.Len(t, ll, len(leases))
assert.Equal(t, leases[1].HWAddr, ll[0].HWAddr) assert.Equal(t, leases[0].HWAddr, ll[0].HWAddr)
assert.Equal(t, leases[1].IP, ll[0].IP) assert.Equal(t, leases[0].IP, ll[0].IP)
assert.True(t, ll[0].IsStatic) assert.Equal(t, leases[0].Expiry.Unix(), ll[0].Expiry.Unix())
assert.Equal(t, leases[0].HWAddr, ll[1].HWAddr) assert.Equal(t, leases[1].HWAddr, ll[1].HWAddr)
assert.Equal(t, leases[0].IP, ll[1].IP) assert.Equal(t, leases[1].IP, ll[1].IP)
assert.Equal(t, leases[0].Expiry.Unix(), ll[1].Expiry.Unix()) assert.True(t, ll[1].IsStatic)
}
func TestNormalizeLeases(t *testing.T) {
dynLeases := []*Lease{{
HWAddr: net.HardwareAddr{1, 2, 3, 4},
}, {
HWAddr: net.HardwareAddr{1, 2, 3, 5},
}}
staticLeases := []*Lease{{
HWAddr: net.HardwareAddr{1, 2, 3, 4},
IP: netip.MustParseAddr("0.2.3.4"),
}, {
HWAddr: net.HardwareAddr{2, 2, 3, 4},
}}
leases := normalizeLeases(staticLeases, dynLeases)
require.Len(t, leases, 3)
assert.Equal(t, leases[0].HWAddr, dynLeases[0].HWAddr)
assert.Equal(t, leases[0].IP, staticLeases[0].IP)
assert.Equal(t, leases[1].HWAddr, staticLeases[1].HWAddr)
assert.Equal(t, leases[2].HWAddr, dynLeases[1].HWAddr)
} }
func TestV4Server_badRange(t *testing.T) { func TestV4Server_badRange(t *testing.T) {

View File

@@ -350,8 +350,10 @@ type netInterfaceJSON struct {
Addrs6 []netip.Addr `json:"ipv6_addresses"` Addrs6 []netip.Addr `json:"ipv6_addresses"`
} }
// handleDHCPInterfaces is the handler for the GET /control/dhcp/interfaces HTTP
// API.
func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
response := map[string]netInterfaceJSON{} resp := map[string]netInterfaceJSON{}
ifaces, err := net.Interfaces() ifaces, err := net.Interfaces()
if err != nil { if err != nil {
@@ -424,20 +426,11 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
} }
if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 { if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 {
jsonIface.GatewayIP = aghnet.GatewayIP(iface.Name) jsonIface.GatewayIP = aghnet.GatewayIP(iface.Name)
response[iface.Name] = jsonIface resp[iface.Name] = jsonIface
} }
} }
err = json.NewEncoder(w).Encode(response) _ = aghhttp.WriteJSONResponse(w, r, resp)
if err != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Failed to marshal json with available interfaces: %s",
err,
)
}
} }
// dhcpSearchOtherResult contains information about other DHCP server for // dhcpSearchOtherResult contains information about other DHCP server for
@@ -639,7 +632,7 @@ func (s *server) handleReset(w http.ResponseWriter, r *http.Request) {
return return
} }
err = os.Remove(s.conf.DBFilePath) err = os.Remove(s.conf.dbFilePath)
if err != nil && !errors.Is(err, os.ErrNotExist) { if err != nil && !errors.Is(err, os.ErrNotExist) {
log.Error("dhcp: removing db: %s", err) log.Error("dhcp: removing db: %s", err)
} }
@@ -651,8 +644,8 @@ func (s *server) handleReset(w http.ResponseWriter, r *http.Request) {
LocalDomainName: s.conf.LocalDomainName, LocalDomainName: s.conf.LocalDomainName,
WorkDir: s.conf.WorkDir, DataDir: s.conf.DataDir,
DBFilePath: s.conf.DBFilePath, dbFilePath: s.conf.dbFilePath,
} }
v4conf := &V4ServerConf{ v4conf := &V4ServerConf{

View File

@@ -31,8 +31,7 @@ func TestServer_handleDHCPStatus(t *testing.T) {
s, err := Create(&ServerConfig{ s, err := Create(&ServerConfig{
Enabled: true, Enabled: true,
Conf4: *defaultV4ServerConf(), Conf4: *defaultV4ServerConf(),
WorkDir: t.TempDir(), DataDir: t.TempDir(),
DBFilePath: dbFilename,
ConfigModified: func() {}, ConfigModified: func() {},
}) })
require.NoError(t, err) require.NoError(t, err)

106
internal/dhcpd/migrate.go Normal file
View File

@@ -0,0 +1,106 @@
package dhcpd
import (
"encoding/json"
"net"
"net/netip"
"os"
"path/filepath"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
)
const (
// leaseExpireStatic is used to define the Expiry field for static
// leases.
//
// Deprecated: Remove it when migration of DHCP leases will be not needed.
leaseExpireStatic = 1
// dbFilename contains saved leases.
//
// Deprecated: Use dataFilename.
dbFilename = "leases.db"
)
// leaseJSON is the structure of stored lease.
//
// Deprecated: Use [Lease].
type leaseJSON struct {
HWAddr []byte `json:"mac"`
IP []byte `json:"ip"`
Hostname string `json:"host"`
Expiry int64 `json:"exp"`
}
func normalizeIP(ip net.IP) net.IP {
ip4 := ip.To4()
if ip4 != nil {
return ip4
}
return ip
}
// migrateDB migrates stored leases if necessary.
func migrateDB(conf *ServerConfig) (err error) {
defer func() { err = errors.Annotate(err, "migrating db: %w") }()
oldLeasesPath := filepath.Join(conf.WorkDir, dbFilename)
dataDirPath := filepath.Join(conf.DataDir, dataFilename)
file, err := os.Open(oldLeasesPath)
if errors.Is(err, os.ErrNotExist) {
// Nothing to migrate.
return nil
} else if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
ljs := []leaseJSON{}
err = json.NewDecoder(file).Decode(&ljs)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
err = file.Close()
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
leases := []*Lease{}
for _, lj := range ljs {
lj.IP = normalizeIP(lj.IP)
ip, ok := netip.AddrFromSlice(lj.IP)
if !ok {
log.Info("dhcp: invalid IP: %s", lj.IP)
continue
}
lease := &Lease{
Expiry: time.Unix(lj.Expiry, 0),
Hostname: lj.Hostname,
HWAddr: lj.HWAddr,
IP: ip,
IsStatic: lj.Expiry == leaseExpireStatic,
}
leases = append(leases, lease)
}
err = writeDB(dataDirPath, leases)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
return os.Remove(oldLeasesPath)
}

View File

@@ -0,0 +1,73 @@
package dhcpd
import (
"encoding/json"
"net"
"net/netip"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testData = `[
{"mac":"ESIzRFVm","ip":"AQIDBA==","host":"test1","exp":1},
{"mac":"ZlVEMyIR","ip":"BAMCAQ==","host":"test2","exp":1231231231}
]`
func TestMigrateDB(t *testing.T) {
dir := t.TempDir()
oldLeasesPath := filepath.Join(dir, dbFilename)
dataDirPath := filepath.Join(dir, dataFilename)
err := os.WriteFile(oldLeasesPath, []byte(testData), 0o644)
require.NoError(t, err)
wantLeases := []*Lease{{
Expiry: time.Time{},
Hostname: "test1",
HWAddr: net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
IP: netip.MustParseAddr("1.2.3.4"),
IsStatic: true,
}, {
Expiry: time.Unix(1231231231, 0),
Hostname: "test2",
HWAddr: net.HardwareAddr{0x66, 0x55, 0x44, 0x33, 0x22, 0x11},
IP: netip.MustParseAddr("4.3.2.1"),
IsStatic: false,
}}
conf := &ServerConfig{
WorkDir: dir,
DataDir: dir,
}
err = migrateDB(conf)
require.NoError(t, err)
_, err = os.Stat(oldLeasesPath)
require.ErrorIs(t, err, os.ErrNotExist)
var data []byte
data, err = os.ReadFile(dataDirPath)
require.NoError(t, err)
dl := &dataLeases{}
err = json.Unmarshal(data, dl)
require.NoError(t, err)
leases := dl.Leases
for i, wl := range wantLeases {
assert.Equal(t, wl.Hostname, leases[i].Hostname)
assert.Equal(t, wl.HWAddr, leases[i].HWAddr)
assert.Equal(t, wl.IP, leases[i].IP)
assert.Equal(t, wl.IsStatic, leases[i].IsStatic)
require.True(t, wl.Expiry.Equal(leases[i].Expiry))
}
}

View File

@@ -256,6 +256,8 @@ func (s *v4Server) rmLeaseByIndex(i int) {
// Remove a dynamic lease with the same properties // Remove a dynamic lease with the same properties
// Return error if a static lease is found // Return error if a static lease is found
//
// TODO(s.chzhen): Refactor the code.
func (s *v4Server) rmDynamicLease(lease *Lease) (err error) { func (s *v4Server) rmDynamicLease(lease *Lease) (err error) {
for i, l := range s.leases { for i, l := range s.leases {
isStatic := l.IsStatic isStatic := l.IsStatic
@@ -357,7 +359,6 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
return fmt.Errorf("can't assign the gateway IP %s to the lease", gwIP) return fmt.Errorf("can't assign the gateway IP %s to the lease", gwIP)
} }
l.Expiry = time.Unix(leaseExpireStatic, 0)
l.IsStatic = true l.IsStatic = true
err = netutil.ValidateMAC(l.HWAddr) err = netutil.ValidateMAC(l.HWAddr)

View File

@@ -68,7 +68,6 @@ func TestV4Server_leasing(t *testing.T) {
t.Run("add_static", func(t *testing.T) { t.Run("add_static", func(t *testing.T) {
err := s.AddStaticLease(&Lease{ err := s.AddStaticLease(&Lease{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName, Hostname: staticName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: staticIP, IP: staticIP,
@@ -78,7 +77,6 @@ func TestV4Server_leasing(t *testing.T) {
t.Run("same_name", func(t *testing.T) { t.Run("same_name", func(t *testing.T) {
err = s.AddStaticLease(&Lease{ err = s.AddStaticLease(&Lease{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName, Hostname: staticName,
HWAddr: anotherMAC, HWAddr: anotherMAC,
IP: anotherIP, IP: anotherIP,
@@ -93,7 +91,6 @@ func TestV4Server_leasing(t *testing.T) {
" (" + staticMAC.String() + "): static lease already exists" " (" + staticMAC.String() + "): static lease already exists"
err = s.AddStaticLease(&Lease{ err = s.AddStaticLease(&Lease{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: anotherName, Hostname: anotherName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: anotherIP, IP: anotherIP,
@@ -108,7 +105,6 @@ func TestV4Server_leasing(t *testing.T) {
" (" + anotherMAC.String() + "): static lease already exists" " (" + anotherMAC.String() + "): static lease already exists"
err = s.AddStaticLease(&Lease{ err = s.AddStaticLease(&Lease{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: anotherName, Hostname: anotherName,
HWAddr: anotherMAC, HWAddr: anotherMAC,
IP: staticIP, IP: staticIP,
@@ -784,7 +780,6 @@ func TestV4Server_FindMACbyIP(t *testing.T) {
s := &v4Server{ s := &v4Server{
leases: []*Lease{{ leases: []*Lease{{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName, Hostname: staticName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: staticIP, IP: staticIP,

View File

@@ -66,8 +66,7 @@ func (s *v6Server) ResetLeases(leases []*Lease) (err error) {
s.leases = nil s.leases = nil
for _, l := range leases { for _, l := range leases {
ip := net.IP(l.IP.AsSlice()) ip := net.IP(l.IP.AsSlice())
if l.Expiry.Unix() != leaseExpireStatic && if !l.IsStatic && !ip6InRange(s.conf.ipStart, ip) {
!ip6InRange(s.conf.ipStart, ip) {
log.Debug("dhcpv6: skipping a lease with IP %v: not within current IP range", l.IP) log.Debug("dhcpv6: skipping a lease with IP %v: not within current IP range", l.IP)
@@ -89,7 +88,7 @@ func (s *v6Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) {
leases = []*Lease{} leases = []*Lease{}
s.leasesLock.Lock() s.leasesLock.Lock()
for _, l := range s.leases { for _, l := range s.leases {
if l.Expiry.Unix() == leaseExpireStatic { if l.IsStatic {
if (flags & LeasesStatic) != 0 { if (flags & LeasesStatic) != 0 {
leases = append(leases, l.Clone()) leases = append(leases, l.Clone())
} }
@@ -150,7 +149,7 @@ func (s *v6Server) rmDynamicLease(lease *Lease) (err error) {
l := s.leases[i] l := s.leases[i]
if bytes.Equal(l.HWAddr, lease.HWAddr) { if bytes.Equal(l.HWAddr, lease.HWAddr) {
if l.Expiry.Unix() == leaseExpireStatic { if l.IsStatic {
return fmt.Errorf("static lease already exists") return fmt.Errorf("static lease already exists")
} }
@@ -163,7 +162,7 @@ func (s *v6Server) rmDynamicLease(lease *Lease) (err error) {
} }
if l.IP == lease.IP { if l.IP == lease.IP {
if l.Expiry.Unix() == leaseExpireStatic { if l.IsStatic {
return fmt.Errorf("static lease already exists") return fmt.Errorf("static lease already exists")
} }
@@ -187,7 +186,7 @@ func (s *v6Server) AddStaticLease(l *Lease) (err error) {
return fmt.Errorf("validating lease: %w", err) return fmt.Errorf("validating lease: %w", err)
} }
l.Expiry = time.Unix(leaseExpireStatic, 0) l.IsStatic = true
s.leasesLock.Lock() s.leasesLock.Lock()
err = s.rmDynamicLease(l) err = s.rmDynamicLease(l)
@@ -274,8 +273,7 @@ func (s *v6Server) findLease(mac net.HardwareAddr) *Lease {
func (s *v6Server) findExpiredLease() int { func (s *v6Server) findExpiredLease() int {
now := time.Now().Unix() now := time.Now().Unix()
for i, lease := range s.leases { for i, lease := range s.leases {
if lease.Expiry.Unix() != leaseExpireStatic && if !lease.IsStatic && lease.Expiry.Unix() <= now {
lease.Expiry.Unix() <= now {
return i return i
} }
} }
@@ -421,7 +419,7 @@ func (s *v6Server) commitLease(msg *dhcpv6.Message, lease *Lease) time.Duration
dhcpv6.MessageTypeRenew, dhcpv6.MessageTypeRenew,
dhcpv6.MessageTypeRebind: dhcpv6.MessageTypeRebind:
if lease.Expiry.Unix() != leaseExpireStatic { if !lease.IsStatic {
s.commitDynamicLease(lease) s.commitDynamicLease(lease)
} }
} }

View File

@@ -44,7 +44,7 @@ func TestV6_AddRemove_static(t *testing.T) {
assert.Equal(t, l.IP, ls[0].IP) assert.Equal(t, l.IP, ls[0].IP)
assert.Equal(t, l.HWAddr, ls[0].HWAddr) assert.Equal(t, l.HWAddr, ls[0].HWAddr)
assert.EqualValues(t, leaseExpireStatic, ls[0].Expiry.Unix()) assert.True(t, ls[0].IsStatic)
// Try to remove non-existent static lease. // Try to remove non-existent static lease.
err = s.RemoveStaticLease(&Lease{ err = s.RemoveStaticLease(&Lease{
@@ -103,7 +103,7 @@ func TestV6_AddReplace(t *testing.T) {
for i, l := range ls { for i, l := range ls {
assert.Equal(t, stLeases[i].IP, l.IP) assert.Equal(t, stLeases[i].IP, l.IP)
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr) assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
assert.EqualValues(t, leaseExpireStatic, l.Expiry.Unix()) assert.True(t, l.IsStatic)
} }
} }
@@ -327,7 +327,6 @@ func TestV6_FindMACbyIP(t *testing.T) {
s := &v6Server{ s := &v6Server{
leases: []*Lease{{ leases: []*Lease{{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName, Hostname: staticName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: staticIP, IP: staticIP,
@@ -341,7 +340,6 @@ func TestV6_FindMACbyIP(t *testing.T) {
} }
s.leases = []*Lease{{ s.leases = []*Lease{{
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: staticName, Hostname: staticName,
HWAddr: staticMAC, HWAddr: staticMAC,
IP: staticIP, IP: staticIP,

View File

@@ -23,6 +23,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/hashprefix"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch" "github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
@@ -915,13 +916,23 @@ func TestBlockedByHosts(t *testing.T) {
} }
func TestBlockedBySafeBrowsing(t *testing.T) { func TestBlockedBySafeBrowsing(t *testing.T) {
const hostname = "wmconvirus.narod.ru" const (
hostname = "wmconvirus.narod.ru"
cacheTime = 10 * time.Minute
cacheSize = 10000
)
sbChecker := hashprefix.New(&hashprefix.Config{
CacheTime: cacheTime,
CacheSize: cacheSize,
Upstream: aghtest.NewBlockUpstream(hostname, true),
})
sbUps := aghtest.NewBlockUpstream(hostname, true)
ans4, _ := (&aghtest.TestResolver{}).HostToIPs(hostname) ans4, _ := (&aghtest.TestResolver{}).HostToIPs(hostname)
filterConf := &filtering.Config{ filterConf := &filtering.Config{
SafeBrowsingEnabled: true, SafeBrowsingEnabled: true,
SafeBrowsingChecker: sbChecker,
} }
forwardConf := ServerConfig{ forwardConf := ServerConfig{
UDPListenAddrs: []*net.UDPAddr{{}}, UDPListenAddrs: []*net.UDPAddr{{}},
@@ -935,7 +946,6 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
}, },
} }
s := createTestServer(t, filterConf, forwardConf, nil) s := createTestServer(t, filterConf, forwardConf, nil)
s.dnsFilter.SetSafeBrowsingUpstream(sbUps)
startDeferStop(t, s) startDeferStop(t, s)
addr := s.dnsProxy.Addr(proxy.ProtoUDP) addr := s.dnsProxy.Addr(proxy.ProtoUDP)

View File

@@ -205,8 +205,8 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
wantSet: `validating upstream servers: validating upstream "!!!": not an ip:port`, wantSet: `validating upstream servers: validating upstream "!!!": not an ip:port`,
}, { }, {
name: "bootstraps_bad", name: "bootstraps_bad",
wantSet: `checking bootstrap a: invalid address: ` + wantSet: `checking bootstrap a: invalid address: bootstrap a:53: ` +
`Resolver a is not eligible to be a bootstrap DNS server`, `ParseAddr("a"): unable to parse IP`,
}, { }, {
name: "cache_bad_ttl", name: "cache_bad_ttl",
wantSet: `cache_ttl_min must be less or equal than cache_ttl_max`, wantSet: `cache_ttl_min must be less or equal than cache_ttl_max`,

View File

@@ -3,6 +3,7 @@ package filtering
import ( import (
"github.com/AdguardTeam/urlfilter/rules" "github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/exp/slices"
) )
// DNSRewriteResult is the result of application of $dnsrewrite rules. // DNSRewriteResult is the result of application of $dnsrewrite rules.
@@ -24,7 +25,13 @@ func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) {
Response: DNSRewriteResultResponse{}, Response: DNSRewriteResultResponse{},
} }
for _, nr := range dnsr { slices.SortFunc(dnsr, rewriteSortsBefore)
for i, nr := range dnsr {
if i > 0 && containsWildcard(nr) {
break
}
dr := nr.DNSRewrite dr := nr.DNSRewrite
if dr.NewCNAME != "" { if dr.NewCNAME != "" {
// NewCNAME rules have a higher priority than other rules. // NewCNAME rules have a higher priority than other rules.
@@ -73,3 +80,19 @@ func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) {
Reason: RewrittenRule, Reason: RewrittenRule,
} }
} }
func rewriteSortsBefore(a, b *rules.NetworkRule) (sortsBefore bool) {
return len(a.Shortcut) > len(b.Shortcut)
}
func containsWildcard(r *rules.NetworkRule) (ok bool) {
for _, c := range r.RuleText {
if c == '*' {
return true
} else if c == '^' {
break
}
}
return false
}

View File

@@ -5,6 +5,7 @@ import (
"path" "path"
"testing" "testing"
"github.com/AdguardTeam/urlfilter"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -202,3 +203,32 @@ func TestDNSFilter_CheckHostRules_dnsrewrite(t *testing.T) {
assert.Equal(t, "new-ptr-with-dot.", ptr) assert.Equal(t, "new-ptr-with-dot.", ptr)
}) })
} }
func TestDNSFilter_ProcessDNSRewrites(t *testing.T) {
const text = `
|www.example.com^$dnsrewrite=127.0.0.1
|*.example.com^$dnsrewrite=127.0.0.2
`
host := "www.example.com"
rrtype := dns.TypeA
f, _ := newForTest(t, nil, []Filter{{ID: 0, Data: []byte(text)}})
setts := &Settings{
FilteringEnabled: true,
}
ufReq := &urlfilter.DNSRequest{
Hostname: host,
SortedClientTags: setts.ClientTags,
ClientIP: setts.ClientIP.String(),
ClientName: setts.ClientName,
DNSType: rrtype,
}
dres, matched := f.filteringEngine.MatchRequest(ufReq)
require.False(t, matched)
res := f.processDNSResultRewrites(dres, host)
assert.Len(t, res.Rules, 1)
}

View File

@@ -18,8 +18,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/mathutil" "github.com/AdguardTeam/golibs/mathutil"
@@ -75,6 +73,12 @@ type Resolver interface {
// Config allows you to configure DNS filtering with New() or just change variables directly. // Config allows you to configure DNS filtering with New() or just change variables directly.
type Config struct { type Config struct {
// SafeBrowsingChecker is the safe browsing hash-prefix checker.
SafeBrowsingChecker Checker `yaml:"-"`
// ParentControl is the parental control hash-prefix checker.
ParentalControlChecker Checker `yaml:"-"`
// enabled is used to be returned within Settings. // enabled is used to be returned within Settings.
// //
// It is of type uint32 to be accessed by atomic. // It is of type uint32 to be accessed by atomic.
@@ -158,8 +162,22 @@ type hostChecker struct {
name string name string
} }
// Checker is used for safe browsing or parental control hash-prefix filtering.
type Checker interface {
// Check returns true if request for the host should be blocked.
Check(host string) (block bool, err error)
}
// DNSFilter matches hostnames and DNS requests against filtering rules. // DNSFilter matches hostnames and DNS requests against filtering rules.
type DNSFilter struct { type DNSFilter struct {
safeSearch SafeSearch
// safeBrowsingChecker is the safe browsing hash-prefix checker.
safeBrowsingChecker Checker
// parentalControl is the parental control hash-prefix checker.
parentalControlChecker Checker
rulesStorage *filterlist.RuleStorage rulesStorage *filterlist.RuleStorage
filteringEngine *urlfilter.DNSEngine filteringEngine *urlfilter.DNSEngine
@@ -168,14 +186,6 @@ type DNSFilter struct {
engineLock sync.RWMutex engineLock sync.RWMutex
parentalServer string // access via methods
safeBrowsingServer string // access via methods
parentalUpstream upstream.Upstream
safeBrowsingUpstream upstream.Upstream
safebrowsingCache cache.Cache
parentalCache cache.Cache
Config // for direct access by library users, even a = assignment Config // for direct access by library users, even a = assignment
// confLock protects Config. // confLock protects Config.
confLock sync.RWMutex confLock sync.RWMutex
@@ -192,7 +202,6 @@ type DNSFilter struct {
// TODO(e.burkov): Don't use regexp for such a simple text processing task. // TODO(e.burkov): Don't use regexp for such a simple text processing task.
filterTitleRegexp *regexp.Regexp filterTitleRegexp *regexp.Regexp
safeSearch SafeSearch
hostCheckers []hostChecker hostCheckers []hostChecker
} }
@@ -940,19 +949,12 @@ func InitModule() {
// be non-nil. // be non-nil.
func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) { func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
d = &DNSFilter{ d = &DNSFilter{
refreshLock: &sync.Mutex{}, refreshLock: &sync.Mutex{},
filterTitleRegexp: regexp.MustCompile(`^! Title: +(.*)$`), filterTitleRegexp: regexp.MustCompile(`^! Title: +(.*)$`),
safeBrowsingChecker: c.SafeBrowsingChecker,
parentalControlChecker: c.ParentalControlChecker,
} }
d.safebrowsingCache = cache.New(cache.Config{
EnableLRU: true,
MaxSize: c.SafeBrowsingCacheSize,
})
d.parentalCache = cache.New(cache.Config{
EnableLRU: true,
MaxSize: c.ParentalCacheSize,
})
d.safeSearch = c.SafeSearch d.safeSearch = c.SafeSearch
d.hostCheckers = []hostChecker{{ d.hostCheckers = []hostChecker{{
@@ -977,11 +979,6 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
defer func() { err = errors.Annotate(err, "filtering: %w") }() defer func() { err = errors.Annotate(err, "filtering: %w") }()
err = d.initSecurityServices()
if err != nil {
return nil, fmt.Errorf("initializing services: %s", err)
}
d.Config = *c d.Config = *c
d.filtersMu = &sync.RWMutex{} d.filtersMu = &sync.RWMutex{}
@@ -1038,3 +1035,69 @@ func (d *DNSFilter) Start() {
// So for now we just start this periodic task from here. // So for now we just start this periodic task from here.
go d.periodicallyRefreshFilters() go d.periodicallyRefreshFilters()
} }
// Safe browsing and parental control methods.
// TODO(a.garipov): Unify with checkParental.
func (d *DNSFilter) checkSafeBrowsing(
host string,
_ uint16,
setts *Settings,
) (res Result, err error) {
if !setts.ProtectionEnabled || !setts.SafeBrowsingEnabled {
return Result{}, nil
}
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("safebrowsing lookup for %q", host)
}
res = Result{
Rules: []*ResultRule{{
Text: "adguard-malware-shavar",
FilterListID: SafeBrowsingListID,
}},
Reason: FilteredSafeBrowsing,
IsFiltered: true,
}
block, err := d.safeBrowsingChecker.Check(host)
if !block || err != nil {
return Result{}, err
}
return res, nil
}
// TODO(a.garipov): Unify with checkSafeBrowsing.
func (d *DNSFilter) checkParental(
host string,
_ uint16,
setts *Settings,
) (res Result, err error) {
if !setts.ProtectionEnabled || !setts.ParentalEnabled {
return Result{}, nil
}
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("parental lookup for %q", host)
}
res = Result{
Rules: []*ResultRule{{
Text: "parental CATEGORY_BLACKLISTED",
FilterListID: ParentalListID,
}},
Reason: FilteredParental,
IsFiltered: true,
}
block, err := d.parentalControlChecker.Check(host)
if !block || err != nil {
return Result{}, err
}
return res, nil
}

View File

@@ -7,7 +7,7 @@ import (
"testing" "testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/cache" "github.com/AdguardTeam/AdGuardHome/internal/filtering/hashprefix"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/urlfilter/rules" "github.com/AdguardTeam/urlfilter/rules"
@@ -27,17 +27,6 @@ const (
// Helpers. // Helpers.
func purgeCaches(d *DNSFilter) {
for _, c := range []cache.Cache{
d.safebrowsingCache,
d.parentalCache,
} {
if c != nil {
c.Clear()
}
}
}
func newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts *Settings) { func newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts *Settings) {
setts = &Settings{ setts = &Settings{
ProtectionEnabled: true, ProtectionEnabled: true,
@@ -58,11 +47,17 @@ func newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts
f, err := New(c, filters) f, err := New(c, filters)
require.NoError(t, err) require.NoError(t, err)
purgeCaches(f)
return f, setts return f, setts
} }
func newChecker(host string) Checker {
return hashprefix.New(&hashprefix.Config{
CacheTime: 10,
CacheSize: 100000,
Upstream: aghtest.NewBlockUpstream(host, true),
})
}
func (d *DNSFilter) checkMatch(t *testing.T, hostname string, setts *Settings) { func (d *DNSFilter) checkMatch(t *testing.T, hostname string, setts *Settings) {
t.Helper() t.Helper()
@@ -175,10 +170,14 @@ func TestSafeBrowsing(t *testing.T) {
aghtest.ReplaceLogWriter(t, logOutput) aghtest.ReplaceLogWriter(t, logOutput)
aghtest.ReplaceLogLevel(t, log.DEBUG) aghtest.ReplaceLogLevel(t, log.DEBUG)
d, setts := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil) sbChecker := newChecker(sbBlocked)
d, setts := newForTest(t, &Config{
SafeBrowsingEnabled: true,
SafeBrowsingChecker: sbChecker,
}, nil)
t.Cleanup(d.Close) t.Cleanup(d.Close)
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
d.checkMatch(t, sbBlocked, setts) d.checkMatch(t, sbBlocked, setts)
require.Contains(t, logOutput.String(), fmt.Sprintf("safebrowsing lookup for %q", sbBlocked)) require.Contains(t, logOutput.String(), fmt.Sprintf("safebrowsing lookup for %q", sbBlocked))
@@ -188,18 +187,17 @@ func TestSafeBrowsing(t *testing.T) {
d.checkMatchEmpty(t, pcBlocked, setts) d.checkMatchEmpty(t, pcBlocked, setts)
// Cached result. // Cached result.
d.safeBrowsingServer = "127.0.0.1"
d.checkMatch(t, sbBlocked, setts) d.checkMatch(t, sbBlocked, setts)
d.checkMatchEmpty(t, pcBlocked, setts) d.checkMatchEmpty(t, pcBlocked, setts)
d.safeBrowsingServer = defaultSafebrowsingServer
} }
func TestParallelSB(t *testing.T) { func TestParallelSB(t *testing.T) {
d, setts := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil) d, setts := newForTest(t, &Config{
SafeBrowsingEnabled: true,
SafeBrowsingChecker: newChecker(sbBlocked),
}, nil)
t.Cleanup(d.Close) t.Cleanup(d.Close)
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
t.Run("group", func(t *testing.T) { t.Run("group", func(t *testing.T) {
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
t.Run(fmt.Sprintf("aaa%d", i), func(t *testing.T) { t.Run(fmt.Sprintf("aaa%d", i), func(t *testing.T) {
@@ -220,10 +218,12 @@ func TestParentalControl(t *testing.T) {
aghtest.ReplaceLogWriter(t, logOutput) aghtest.ReplaceLogWriter(t, logOutput)
aghtest.ReplaceLogLevel(t, log.DEBUG) aghtest.ReplaceLogLevel(t, log.DEBUG)
d, setts := newForTest(t, &Config{ParentalEnabled: true}, nil) d, setts := newForTest(t, &Config{
ParentalEnabled: true,
ParentalControlChecker: newChecker(pcBlocked),
}, nil)
t.Cleanup(d.Close) t.Cleanup(d.Close)
d.SetParentalUpstream(aghtest.NewBlockUpstream(pcBlocked, true))
d.checkMatch(t, pcBlocked, setts) d.checkMatch(t, pcBlocked, setts)
require.Contains(t, logOutput.String(), fmt.Sprintf("parental lookup for %q", pcBlocked)) require.Contains(t, logOutput.String(), fmt.Sprintf("parental lookup for %q", pcBlocked))
@@ -233,7 +233,6 @@ func TestParentalControl(t *testing.T) {
d.checkMatchEmpty(t, "api.jquery.com", setts) d.checkMatchEmpty(t, "api.jquery.com", setts)
// Test cached result. // Test cached result.
d.parentalServer = "127.0.0.1"
d.checkMatch(t, pcBlocked, setts) d.checkMatch(t, pcBlocked, setts)
d.checkMatchEmpty(t, "yandex.ru", setts) d.checkMatchEmpty(t, "yandex.ru", setts)
} }
@@ -593,8 +592,10 @@ func applyClientSettings(setts *Settings) {
func TestClientSettings(t *testing.T) { func TestClientSettings(t *testing.T) {
d, setts := newForTest(t, d, setts := newForTest(t,
&Config{ &Config{
ParentalEnabled: true, ParentalEnabled: true,
SafeBrowsingEnabled: false, SafeBrowsingEnabled: false,
SafeBrowsingChecker: newChecker(sbBlocked),
ParentalControlChecker: newChecker(pcBlocked),
}, },
[]Filter{{ []Filter{{
ID: 0, Data: []byte("||example.org^\n"), ID: 0, Data: []byte("||example.org^\n"),
@@ -602,9 +603,6 @@ func TestClientSettings(t *testing.T) {
) )
t.Cleanup(d.Close) t.Cleanup(d.Close)
d.SetParentalUpstream(aghtest.NewBlockUpstream(pcBlocked, true))
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
type testCase struct { type testCase struct {
name string name string
host string host string
@@ -665,11 +663,12 @@ func TestClientSettings(t *testing.T) {
// Benchmarks. // Benchmarks.
func BenchmarkSafeBrowsing(b *testing.B) { func BenchmarkSafeBrowsing(b *testing.B) {
d, setts := newForTest(b, &Config{SafeBrowsingEnabled: true}, nil) d, setts := newForTest(b, &Config{
SafeBrowsingEnabled: true,
SafeBrowsingChecker: newChecker(sbBlocked),
}, nil)
b.Cleanup(d.Close) b.Cleanup(d.Close)
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
res, err := d.CheckHost(sbBlocked, dns.TypeA, setts) res, err := d.CheckHost(sbBlocked, dns.TypeA, setts)
require.NoError(b, err) require.NoError(b, err)
@@ -679,11 +678,12 @@ func BenchmarkSafeBrowsing(b *testing.B) {
} }
func BenchmarkSafeBrowsingParallel(b *testing.B) { func BenchmarkSafeBrowsingParallel(b *testing.B) {
d, setts := newForTest(b, &Config{SafeBrowsingEnabled: true}, nil) d, setts := newForTest(b, &Config{
SafeBrowsingEnabled: true,
SafeBrowsingChecker: newChecker(sbBlocked),
}, nil)
b.Cleanup(d.Close) b.Cleanup(d.Close)
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
res, err := d.CheckHost(sbBlocked, dns.TypeA, setts) res, err := d.CheckHost(sbBlocked, dns.TypeA, setts)

View File

@@ -0,0 +1,130 @@
package hashprefix
import (
"encoding/binary"
"time"
"github.com/AdguardTeam/golibs/log"
)
// expirySize is the size of expiry in cacheItem.
const expirySize = 8
// cacheItem represents an item that we will store in the cache.
type cacheItem struct {
// expiry is the time when cacheItem will expire.
expiry time.Time
// hashes is the hashed hostnames.
hashes []hostnameHash
}
// toCacheItem decodes cacheItem from data. data must be at least equal to
// expiry size.
func toCacheItem(data []byte) *cacheItem {
t := time.Unix(int64(binary.BigEndian.Uint64(data)), 0)
data = data[expirySize:]
hashes := make([]hostnameHash, len(data)/hashSize)
for i := 0; i < len(data); i += hashSize {
var hash hostnameHash
copy(hash[:], data[i:i+hashSize])
hashes = append(hashes, hash)
}
return &cacheItem{
expiry: t,
hashes: hashes,
}
}
// fromCacheItem encodes cacheItem into data.
func fromCacheItem(item *cacheItem) (data []byte) {
data = make([]byte, len(item.hashes)*hashSize+expirySize)
expiry := item.expiry.Unix()
binary.BigEndian.PutUint64(data[:expirySize], uint64(expiry))
for _, v := range item.hashes {
// nolint:looppointer // The subsilce is used for a copy.
data = append(data, v[:]...)
}
return data
}
// findInCache finds hashes in the cache. If nothing found returns list of
// hashes, prefixes of which will be sent to upstream.
func (c *Checker) findInCache(
hashes []hostnameHash,
) (found, blocked bool, hashesToRequest []hostnameHash) {
now := time.Now()
i := 0
for _, hash := range hashes {
// nolint:looppointer // The subsilce is used for a safe cache lookup.
data := c.cache.Get(hash[:prefixLen])
if data == nil {
hashes[i] = hash
i++
continue
}
item := toCacheItem(data)
if now.After(item.expiry) {
hashes[i] = hash
i++
continue
}
if ok := findMatch(hashes, item.hashes); ok {
return true, true, nil
}
}
if i == 0 {
return true, false, nil
}
return false, false, hashes[:i]
}
// storeInCache caches hashes.
func (c *Checker) storeInCache(hashesToRequest, respHashes []hostnameHash) {
hashToStore := make(map[prefix][]hostnameHash)
for _, hash := range respHashes {
var pref prefix
// nolint:looppointer // The subsilce is used for a copy.
copy(pref[:], hash[:])
hashToStore[pref] = append(hashToStore[pref], hash)
}
for pref, hash := range hashToStore {
// nolint:looppointer // The subsilce is used for a safe cache lookup.
c.setCache(pref[:], hash)
}
for _, hash := range hashesToRequest {
// nolint:looppointer // The subsilce is used for a safe cache lookup.
pref := hash[:prefixLen]
val := c.cache.Get(pref)
if val == nil {
c.setCache(pref, nil)
}
}
}
// setCache stores hash in cache.
func (c *Checker) setCache(pref []byte, hashes []hostnameHash) {
item := &cacheItem{
expiry: time.Now().Add(c.cacheTime),
hashes: hashes,
}
c.cache.Set(pref, fromCacheItem(item))
log.Debug("%s: stored in cache: %v", c.svc, pref)
}

View File

@@ -0,0 +1,245 @@
// Package hashprefix used for safe browsing and parent control.
package hashprefix
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/miekg/dns"
"golang.org/x/exp/slices"
"golang.org/x/net/publicsuffix"
)
const (
// prefixLen is the length of the hash prefix of the filtered hostname.
prefixLen = 2
// hashSize is the size of hashed hostname.
hashSize = sha256.Size
// hexSize is the size of hexadecimal representation of hashed hostname.
hexSize = hashSize * 2
)
// prefix is the type of the SHA256 hash prefix used to match against the
// domain-name database.
type prefix [prefixLen]byte
// hostnameHash is the hashed hostname.
//
// TODO(s.chzhen): Split into prefix and suffix.
type hostnameHash [hashSize]byte
// findMatch returns true if one of the a hostnames matches one of the b.
func findMatch(a, b []hostnameHash) (matched bool) {
for _, hash := range a {
if slices.Contains(b, hash) {
return true
}
}
return false
}
// Config is the configuration structure for safe browsing and parental
// control.
type Config struct {
// Upstream is the upstream DNS server.
Upstream upstream.Upstream
// ServiceName is the name of the service.
ServiceName string
// TXTSuffix is the TXT suffix for DNS request.
TXTSuffix string
// CacheTime is the time period to store hash.
CacheTime time.Duration
// CacheSize is the maximum size of the cache. If it's zero, cache size is
// unlimited.
CacheSize uint
}
type Checker struct {
// upstream is the upstream DNS server.
upstream upstream.Upstream
// cache stores hostname hashes.
cache cache.Cache
// svc is the name of the service.
svc string
// txtSuffix is the TXT suffix for DNS request.
txtSuffix string
// cacheTime is the time period to store hash.
cacheTime time.Duration
}
// New returns Checker.
func New(conf *Config) (c *Checker) {
return &Checker{
upstream: conf.Upstream,
cache: cache.New(cache.Config{
EnableLRU: true,
MaxSize: conf.CacheSize,
}),
svc: conf.ServiceName,
txtSuffix: conf.TXTSuffix,
cacheTime: conf.CacheTime,
}
}
// Check returns true if request for the host should be blocked.
func (c *Checker) Check(host string) (ok bool, err error) {
hashes := hostnameToHashes(host)
found, blocked, hashesToRequest := c.findInCache(hashes)
if found {
log.Debug("%s: found %q in cache, blocked: %t", c.svc, host, blocked)
return blocked, nil
}
question := c.getQuestion(hashesToRequest)
log.Debug("%s: checking %s: %s", c.svc, host, question)
req := (&dns.Msg{}).SetQuestion(question, dns.TypeTXT)
resp, err := c.upstream.Exchange(req)
if err != nil {
return false, fmt.Errorf("getting hashes: %w", err)
}
matched, receivedHashes := c.processAnswer(hashesToRequest, resp, host)
c.storeInCache(hashesToRequest, receivedHashes)
return matched, nil
}
// hostnameToHashes returns hashes that should be checked by the hash prefix
// filter.
func hostnameToHashes(host string) (hashes []hostnameHash) {
// subDomainNum defines how many labels should be hashed to match against a
// hash prefix filter.
const subDomainNum = 4
pubSuf, icann := publicsuffix.PublicSuffix(host)
if !icann {
// Check the full private domain space.
pubSuf = ""
}
nDots := 0
i := strings.LastIndexFunc(host, func(r rune) (ok bool) {
if r == '.' {
nDots++
}
return nDots == subDomainNum
})
if i != -1 {
host = host[i+1:]
}
sub := netutil.Subdomains(host)
for _, s := range sub {
if s == pubSuf {
break
}
sum := sha256.Sum256([]byte(s))
hashes = append(hashes, sum)
}
return hashes
}
// getQuestion combines hexadecimal encoded prefixes of hashed hostnames into
// string.
func (c *Checker) getQuestion(hashes []hostnameHash) (q string) {
b := &strings.Builder{}
for _, hash := range hashes {
// nolint:looppointer // The subsilce is used for safe hex encoding.
stringutil.WriteToBuilder(b, hex.EncodeToString(hash[:prefixLen]), ".")
}
stringutil.WriteToBuilder(b, c.txtSuffix)
return b.String()
}
// processAnswer returns true if DNS response matches the hash, and received
// hashed hostnames from the upstream.
func (c *Checker) processAnswer(
hashesToRequest []hostnameHash,
resp *dns.Msg,
host string,
) (matched bool, receivedHashes []hostnameHash) {
txtCount := 0
for _, a := range resp.Answer {
txt, ok := a.(*dns.TXT)
if !ok {
continue
}
txtCount++
receivedHashes = c.appendHashesFromTXT(receivedHashes, txt, host)
}
log.Debug("%s: received answer for %s with %d TXT count", c.svc, host, txtCount)
matched = findMatch(hashesToRequest, receivedHashes)
if matched {
log.Debug("%s: matched %s", c.svc, host)
return true, receivedHashes
}
return false, receivedHashes
}
// appendHashesFromTXT appends received hashed hostnames.
func (c *Checker) appendHashesFromTXT(
hashes []hostnameHash,
txt *dns.TXT,
host string,
) (receivedHashes []hostnameHash) {
log.Debug("%s: received hashes for %s: %v", c.svc, host, txt.Txt)
for _, t := range txt.Txt {
if len(t) != hexSize {
log.Debug("%s: wrong hex size %d for %s %s", c.svc, len(t), host, t)
continue
}
buf, err := hex.DecodeString(t)
if err != nil {
log.Debug("%s: decoding hex string %s: %s", c.svc, t, err)
continue
}
var hash hostnameHash
copy(hash[:], buf)
hashes = append(hashes, hash)
}
return hashes
}

View File

@@ -0,0 +1,248 @@
package hashprefix
import (
"crypto/sha256"
"encoding/hex"
"strings"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/cache"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
)
const (
cacheTime = 10 * time.Minute
cacheSize = 10000
)
func TestChcker_getQuestion(t *testing.T) {
const suf = "sb.dns.adguard.com."
// test hostnameToHashes()
hashes := hostnameToHashes("1.2.3.sub.host.com")
assert.Len(t, hashes, 3)
hash := sha256.Sum256([]byte("3.sub.host.com"))
hexPref1 := hex.EncodeToString(hash[:prefixLen])
assert.True(t, slices.Contains(hashes, hash))
hash = sha256.Sum256([]byte("sub.host.com"))
hexPref2 := hex.EncodeToString(hash[:prefixLen])
assert.True(t, slices.Contains(hashes, hash))
hash = sha256.Sum256([]byte("host.com"))
hexPref3 := hex.EncodeToString(hash[:prefixLen])
assert.True(t, slices.Contains(hashes, hash))
hash = sha256.Sum256([]byte("com"))
assert.False(t, slices.Contains(hashes, hash))
c := &Checker{
svc: "SafeBrowsing",
txtSuffix: suf,
}
q := c.getQuestion(hashes)
assert.Contains(t, q, hexPref1)
assert.Contains(t, q, hexPref2)
assert.Contains(t, q, hexPref3)
assert.True(t, strings.HasSuffix(q, suf))
}
func TestHostnameToHashes(t *testing.T) {
testCases := []struct {
name string
host string
wantLen int
}{{
name: "basic",
host: "example.com",
wantLen: 1,
}, {
name: "sub_basic",
host: "www.example.com",
wantLen: 2,
}, {
name: "private_domain",
host: "foo.co.uk",
wantLen: 1,
}, {
name: "sub_private_domain",
host: "bar.foo.co.uk",
wantLen: 2,
}, {
name: "private_domain_v2",
host: "foo.blogspot.co.uk",
wantLen: 4,
}, {
name: "sub_private_domain_v2",
host: "bar.foo.blogspot.co.uk",
wantLen: 4,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
hashes := hostnameToHashes(tc.host)
assert.Len(t, hashes, tc.wantLen)
})
}
}
func TestChecker_storeInCache(t *testing.T) {
c := &Checker{
svc: "SafeBrowsing",
cacheTime: cacheTime,
}
conf := cache.Config{}
c.cache = cache.New(conf)
// store in cache hashes for "3.sub.host.com" and "host.com"
// and empty data for hash-prefix for "sub.host.com"
hashes := []hostnameHash{}
hash := sha256.Sum256([]byte("sub.host.com"))
hashes = append(hashes, hash)
var hashesArray []hostnameHash
hash4 := sha256.Sum256([]byte("3.sub.host.com"))
hashesArray = append(hashesArray, hash4)
hash2 := sha256.Sum256([]byte("host.com"))
hashesArray = append(hashesArray, hash2)
c.storeInCache(hashes, hashesArray)
// match "3.sub.host.com" or "host.com" from cache
hashes = []hostnameHash{}
hash = sha256.Sum256([]byte("3.sub.host.com"))
hashes = append(hashes, hash)
hash = sha256.Sum256([]byte("sub.host.com"))
hashes = append(hashes, hash)
hash = sha256.Sum256([]byte("host.com"))
hashes = append(hashes, hash)
found, blocked, _ := c.findInCache(hashes)
assert.True(t, found)
assert.True(t, blocked)
// match "sub.host.com" from cache
hashes = []hostnameHash{}
hash = sha256.Sum256([]byte("sub.host.com"))
hashes = append(hashes, hash)
found, blocked, _ = c.findInCache(hashes)
assert.True(t, found)
assert.False(t, blocked)
// Match "sub.host.com" from cache. Another hash for "host.example" is not
// in the cache, so get data for it from the server.
hashes = []hostnameHash{}
hash = sha256.Sum256([]byte("sub.host.com"))
hashes = append(hashes, hash)
hash = sha256.Sum256([]byte("host.example"))
hashes = append(hashes, hash)
found, _, hashesToRequest := c.findInCache(hashes)
assert.False(t, found)
hash = sha256.Sum256([]byte("sub.host.com"))
ok := slices.Contains(hashesToRequest, hash)
assert.False(t, ok)
hash = sha256.Sum256([]byte("host.example"))
ok = slices.Contains(hashesToRequest, hash)
assert.True(t, ok)
c = &Checker{
svc: "SafeBrowsing",
cacheTime: cacheTime,
}
c.cache = cache.New(cache.Config{})
hashes = []hostnameHash{}
hash = sha256.Sum256([]byte("sub.host.com"))
hashes = append(hashes, hash)
c.cache.Set(hash[:prefixLen], make([]byte, expirySize+hashSize))
found, _, _ = c.findInCache(hashes)
assert.False(t, found)
}
func TestChecker_Check(t *testing.T) {
const hostname = "example.org"
testCases := []struct {
name string
wantBlock bool
}{{
name: "sb_no_block",
wantBlock: false,
}, {
name: "sb_block",
wantBlock: true,
}, {
name: "pc_no_block",
wantBlock: false,
}, {
name: "pc_block",
wantBlock: true,
}}
for _, tc := range testCases {
c := New(&Config{
CacheTime: cacheTime,
CacheSize: cacheSize,
})
// Prepare the upstream.
ups := aghtest.NewBlockUpstream(hostname, tc.wantBlock)
var numReq int
onExchange := ups.OnExchange
ups.OnExchange = func(req *dns.Msg) (resp *dns.Msg, err error) {
numReq++
return onExchange(req)
}
c.upstream = ups
t.Run(tc.name, func(t *testing.T) {
// Firstly, check the request blocking.
hits := 0
res := false
res, err := c.Check(hostname)
require.NoError(t, err)
if tc.wantBlock {
assert.True(t, res)
hits++
} else {
require.False(t, res)
}
// Check the cache state, check the response is now cached.
assert.Equal(t, 1, c.cache.Stats().Count)
assert.Equal(t, hits, c.cache.Stats().Hit)
// There was one request to an upstream.
assert.Equal(t, 1, numReq)
// Now make the same request to check the cache was used.
res, err = c.Check(hostname)
require.NoError(t, err)
if tc.wantBlock {
assert.True(t, res)
} else {
require.False(t, res)
}
// Check the cache state, it should've been used.
assert.Equal(t, 1, c.cache.Stats().Count)
assert.Equal(t, hits+1, c.cache.Stats().Hit)
// Check that there were no additional requests.
assert.Equal(t, 1, numReq)
})
}
}

View File

@@ -8,6 +8,7 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@@ -458,6 +459,80 @@ func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
_ = aghhttp.WriteJSONResponse(w, r, resp) _ = aghhttp.WriteJSONResponse(w, r, resp)
} }
// setProtectedBool sets the value of a boolean pointer under a lock. l must
// protect the value under ptr.
//
// TODO(e.burkov): Make it generic?
func setProtectedBool(mu *sync.RWMutex, ptr *bool, val bool) {
mu.Lock()
defer mu.Unlock()
*ptr = val
}
// protectedBool gets the value of a boolean pointer under a read lock. l must
// protect the value under ptr.
//
// TODO(e.burkov): Make it generic?
func protectedBool(mu *sync.RWMutex, ptr *bool) (val bool) {
mu.RLock()
defer mu.RUnlock()
return *ptr
}
// handleSafeBrowsingEnable is the handler for the POST
// /control/safebrowsing/enable HTTP API.
func (d *DNSFilter) handleSafeBrowsingEnable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled, true)
d.Config.ConfigModified()
}
// handleSafeBrowsingDisable is the handler for the POST
// /control/safebrowsing/disable HTTP API.
func (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled, false)
d.Config.ConfigModified()
}
// handleSafeBrowsingStatus is the handler for the GET
// /control/safebrowsing/status HTTP API.
func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
resp := &struct {
Enabled bool `json:"enabled"`
}{
Enabled: protectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled),
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
// handleParentalEnable is the handler for the POST /control/parental/enable
// HTTP API.
func (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.ParentalEnabled, true)
d.Config.ConfigModified()
}
// handleParentalDisable is the handler for the POST /control/parental/disable
// HTTP API.
func (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.ParentalEnabled, false)
d.Config.ConfigModified()
}
// handleParentalStatus is the handler for the GET /control/parental/status
// HTTP API.
func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) {
resp := &struct {
Enabled bool `json:"enabled"`
}{
Enabled: protectedBool(&d.confLock, &d.Config.ParentalEnabled),
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
// RegisterFilteringHandlers - register handlers // RegisterFilteringHandlers - register handlers
func (d *DNSFilter) RegisterFilteringHandlers() { func (d *DNSFilter) RegisterFilteringHandlers() {
registerHTTP := d.HTTPRegister registerHTTP := d.HTTPRegister

View File

@@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -136,3 +137,171 @@ func TestDNSFilter_handleFilteringSetURL(t *testing.T) {
}) })
} }
} }
func TestDNSFilter_handleSafeBrowsingStatus(t *testing.T) {
const (
testTimeout = time.Second
statusURL = "/control/safebrowsing/status"
)
confModCh := make(chan struct{})
filtersDir := t.TempDir()
testCases := []struct {
name string
url string
enabled bool
wantStatus assert.BoolAssertionFunc
}{{
name: "enable_off",
url: "/control/safebrowsing/enable",
enabled: false,
wantStatus: assert.True,
}, {
name: "enable_on",
url: "/control/safebrowsing/enable",
enabled: true,
wantStatus: assert.True,
}, {
name: "disable_on",
url: "/control/safebrowsing/disable",
enabled: true,
wantStatus: assert.False,
}, {
name: "disable_off",
url: "/control/safebrowsing/disable",
enabled: false,
wantStatus: assert.False,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
handlers := make(map[string]http.Handler)
d, err := New(&Config{
ConfigModified: func() {
testutil.RequireSend(testutil.PanicT{}, confModCh, struct{}{}, testTimeout)
},
DataDir: filtersDir,
HTTPRegister: func(_, url string, handler http.HandlerFunc) {
handlers[url] = handler
},
SafeBrowsingEnabled: tc.enabled,
}, nil)
require.NoError(t, err)
t.Cleanup(d.Close)
d.RegisterFilteringHandlers()
require.NotEmpty(t, handlers)
require.Contains(t, handlers, statusURL)
r := httptest.NewRequest(http.MethodPost, tc.url, nil)
w := httptest.NewRecorder()
go handlers[tc.url].ServeHTTP(w, r)
testutil.RequireReceive(t, confModCh, testTimeout)
r = httptest.NewRequest(http.MethodGet, statusURL, nil)
w = httptest.NewRecorder()
handlers[statusURL].ServeHTTP(w, r)
require.Equal(t, http.StatusOK, w.Code)
status := struct {
Enabled bool `json:"enabled"`
}{
Enabled: false,
}
err = json.NewDecoder(w.Body).Decode(&status)
require.NoError(t, err)
tc.wantStatus(t, status.Enabled)
})
}
}
func TestDNSFilter_handleParentalStatus(t *testing.T) {
const (
testTimeout = time.Second
statusURL = "/control/parental/status"
)
confModCh := make(chan struct{})
filtersDir := t.TempDir()
testCases := []struct {
name string
url string
enabled bool
wantStatus assert.BoolAssertionFunc
}{{
name: "enable_off",
url: "/control/parental/enable",
enabled: false,
wantStatus: assert.True,
}, {
name: "enable_on",
url: "/control/parental/enable",
enabled: true,
wantStatus: assert.True,
}, {
name: "disable_on",
url: "/control/parental/disable",
enabled: true,
wantStatus: assert.False,
}, {
name: "disable_off",
url: "/control/parental/disable",
enabled: false,
wantStatus: assert.False,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
handlers := make(map[string]http.Handler)
d, err := New(&Config{
ConfigModified: func() {
testutil.RequireSend(testutil.PanicT{}, confModCh, struct{}{}, testTimeout)
},
DataDir: filtersDir,
HTTPRegister: func(_, url string, handler http.HandlerFunc) {
handlers[url] = handler
},
ParentalEnabled: tc.enabled,
}, nil)
require.NoError(t, err)
t.Cleanup(d.Close)
d.RegisterFilteringHandlers()
require.NotEmpty(t, handlers)
require.Contains(t, handlers, statusURL)
r := httptest.NewRequest(http.MethodPost, tc.url, nil)
w := httptest.NewRecorder()
go handlers[tc.url].ServeHTTP(w, r)
testutil.RequireReceive(t, confModCh, testTimeout)
r = httptest.NewRequest(http.MethodGet, statusURL, nil)
w = httptest.NewRecorder()
handlers[statusURL].ServeHTTP(w, r)
require.Equal(t, http.StatusOK, w.Code)
status := struct {
Enabled bool `json:"enabled"`
}{
Enabled: false,
}
err = json.NewDecoder(w.Body).Decode(&status)
require.NoError(t, err)
tc.wantStatus(t, status.Enabled)
})
}
}

View File

@@ -1,433 +0,0 @@
package filtering
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/miekg/dns"
"golang.org/x/exp/slices"
"golang.org/x/net/publicsuffix"
)
// Safe browsing and parental control methods.
// TODO(a.garipov): Make configurable.
const (
dnsTimeout = 3 * time.Second
defaultSafebrowsingServer = `https://family.adguard-dns.com/dns-query`
defaultParentalServer = `https://family.adguard-dns.com/dns-query`
sbTXTSuffix = `sb.dns.adguard.com.`
pcTXTSuffix = `pc.dns.adguard.com.`
)
// SetParentalUpstream sets the parental upstream for *DNSFilter.
//
// TODO(e.burkov): Remove this in v1 API to forbid the direct access.
func (d *DNSFilter) SetParentalUpstream(u upstream.Upstream) {
d.parentalUpstream = u
}
// SetSafeBrowsingUpstream sets the safe browsing upstream for *DNSFilter.
//
// TODO(e.burkov): Remove this in v1 API to forbid the direct access.
func (d *DNSFilter) SetSafeBrowsingUpstream(u upstream.Upstream) {
d.safeBrowsingUpstream = u
}
func (d *DNSFilter) initSecurityServices() error {
var err error
d.safeBrowsingServer = defaultSafebrowsingServer
d.parentalServer = defaultParentalServer
opts := &upstream.Options{
Timeout: dnsTimeout,
ServerIPAddrs: []net.IP{
{94, 140, 14, 15},
{94, 140, 15, 16},
net.ParseIP("2a10:50c0::bad1:ff"),
net.ParseIP("2a10:50c0::bad2:ff"),
},
}
parUps, err := upstream.AddressToUpstream(d.parentalServer, opts)
if err != nil {
return fmt.Errorf("converting parental server: %w", err)
}
d.SetParentalUpstream(parUps)
sbUps, err := upstream.AddressToUpstream(d.safeBrowsingServer, opts)
if err != nil {
return fmt.Errorf("converting safe browsing server: %w", err)
}
d.SetSafeBrowsingUpstream(sbUps)
return nil
}
/*
expire byte[4]
hash byte[32]
...
*/
func (c *sbCtx) setCache(prefix, hashes []byte) {
d := make([]byte, 4+len(hashes))
expire := uint(time.Now().Unix()) + c.cacheTime*60
binary.BigEndian.PutUint32(d[:4], uint32(expire))
copy(d[4:], hashes)
c.cache.Set(prefix, d)
log.Debug("%s: stored in cache: %v", c.svc, prefix)
}
// findInHash returns 32-byte hash if it's found in hashToHost.
func (c *sbCtx) findInHash(val []byte) (hash32 [32]byte, found bool) {
for i := 4; i < len(val); i += 32 {
hash := val[i : i+32]
copy(hash32[:], hash[0:32])
_, found = c.hashToHost[hash32]
if found {
return hash32, found
}
}
return [32]byte{}, false
}
func (c *sbCtx) getCached() int {
now := time.Now().Unix()
hashesToRequest := map[[32]byte]string{}
for k, v := range c.hashToHost {
// nolint:looppointer // The subsilce is used for a safe cache lookup.
val := c.cache.Get(k[0:2])
if val == nil || now >= int64(binary.BigEndian.Uint32(val)) {
hashesToRequest[k] = v
continue
}
if hash32, found := c.findInHash(val); found {
log.Debug("%s: found in cache: %s: blocked by %v", c.svc, c.host, hash32)
return 1
}
}
if len(hashesToRequest) == 0 {
log.Debug("%s: found in cache: %s: not blocked", c.svc, c.host)
return -1
}
c.hashToHost = hashesToRequest
return 0
}
type sbCtx struct {
host string
svc string
hashToHost map[[32]byte]string
cache cache.Cache
cacheTime uint
}
func hostnameToHashes(host string) map[[32]byte]string {
hashes := map[[32]byte]string{}
tld, icann := publicsuffix.PublicSuffix(host)
if !icann {
// private suffixes like cloudfront.net
tld = ""
}
curhost := host
nDots := 0
for i := len(curhost) - 1; i >= 0; i-- {
if curhost[i] == '.' {
nDots++
if nDots == 4 {
curhost = curhost[i+1:] // "xxx.a.b.c.d" -> "a.b.c.d"
break
}
}
}
for {
if curhost == "" {
// we've reached end of string
break
}
if tld != "" && curhost == tld {
// we've reached the TLD, don't hash it
break
}
sum := sha256.Sum256([]byte(curhost))
hashes[sum] = curhost
pos := strings.IndexByte(curhost, byte('.'))
if pos < 0 {
break
}
curhost = curhost[pos+1:]
}
return hashes
}
// convert hash array to string
func (c *sbCtx) getQuestion() string {
b := &strings.Builder{}
for hash := range c.hashToHost {
// nolint:looppointer // The subsilce is used for safe hex encoding.
stringutil.WriteToBuilder(b, hex.EncodeToString(hash[0:2]), ".")
}
if c.svc == "SafeBrowsing" {
stringutil.WriteToBuilder(b, sbTXTSuffix)
return b.String()
}
stringutil.WriteToBuilder(b, pcTXTSuffix)
return b.String()
}
// Find the target hash in TXT response
func (c *sbCtx) processTXT(resp *dns.Msg) (bool, [][]byte) {
matched := false
hashes := [][]byte{}
for _, a := range resp.Answer {
txt, ok := a.(*dns.TXT)
if !ok {
continue
}
log.Debug("%s: received hashes for %s: %v", c.svc, c.host, txt.Txt)
for _, t := range txt.Txt {
if len(t) != 32*2 {
continue
}
hash, err := hex.DecodeString(t)
if err != nil {
continue
}
hashes = append(hashes, hash)
if !matched {
var hash32 [32]byte
copy(hash32[:], hash)
var hashHost string
hashHost, ok = c.hashToHost[hash32]
if ok {
log.Debug("%s: matched %s by %s/%s", c.svc, c.host, hashHost, t)
matched = true
}
}
}
}
return matched, hashes
}
func (c *sbCtx) storeCache(hashes [][]byte) {
slices.SortFunc(hashes, func(a, b []byte) (sortsBefore bool) {
return bytes.Compare(a, b) == -1
})
var curData []byte
var prevPrefix []byte
for i, hash := range hashes {
// nolint:looppointer // The subsilce is used for a safe comparison.
if !bytes.Equal(hash[0:2], prevPrefix) {
if i != 0 {
c.setCache(prevPrefix, curData)
curData = nil
}
prevPrefix = hashes[i][0:2]
}
curData = append(curData, hash...)
}
if len(prevPrefix) != 0 {
c.setCache(prevPrefix, curData)
}
for hash := range c.hashToHost {
// nolint:looppointer // The subsilce is used for a safe cache lookup.
prefix := hash[0:2]
val := c.cache.Get(prefix)
if val == nil {
c.setCache(prefix, nil)
}
}
}
func check(c *sbCtx, r Result, u upstream.Upstream) (Result, error) {
c.hashToHost = hostnameToHashes(c.host)
switch c.getCached() {
case -1:
return Result{}, nil
case 1:
return r, nil
}
question := c.getQuestion()
log.Tracef("%s: checking %s: %s", c.svc, c.host, question)
req := (&dns.Msg{}).SetQuestion(question, dns.TypeTXT)
resp, err := u.Exchange(req)
if err != nil {
return Result{}, err
}
matched, receivedHashes := c.processTXT(resp)
c.storeCache(receivedHashes)
if matched {
return r, nil
}
return Result{}, nil
}
// TODO(a.garipov): Unify with checkParental.
func (d *DNSFilter) checkSafeBrowsing(
host string,
_ uint16,
setts *Settings,
) (res Result, err error) {
if !setts.ProtectionEnabled || !setts.SafeBrowsingEnabled {
return Result{}, nil
}
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("safebrowsing lookup for %q", host)
}
sctx := &sbCtx{
host: host,
svc: "SafeBrowsing",
cache: d.safebrowsingCache,
cacheTime: d.Config.CacheTime,
}
res = Result{
Rules: []*ResultRule{{
Text: "adguard-malware-shavar",
FilterListID: SafeBrowsingListID,
}},
Reason: FilteredSafeBrowsing,
IsFiltered: true,
}
return check(sctx, res, d.safeBrowsingUpstream)
}
// TODO(a.garipov): Unify with checkSafeBrowsing.
func (d *DNSFilter) checkParental(
host string,
_ uint16,
setts *Settings,
) (res Result, err error) {
if !setts.ProtectionEnabled || !setts.ParentalEnabled {
return Result{}, nil
}
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("parental lookup for %q", host)
}
sctx := &sbCtx{
host: host,
svc: "Parental",
cache: d.parentalCache,
cacheTime: d.Config.CacheTime,
}
res = Result{
Rules: []*ResultRule{{
Text: "parental CATEGORY_BLACKLISTED",
FilterListID: ParentalListID,
}},
Reason: FilteredParental,
IsFiltered: true,
}
return check(sctx, res, d.parentalUpstream)
}
// setProtectedBool sets the value of a boolean pointer under a lock. l must
// protect the value under ptr.
//
// TODO(e.burkov): Make it generic?
func setProtectedBool(mu *sync.RWMutex, ptr *bool, val bool) {
mu.Lock()
defer mu.Unlock()
*ptr = val
}
// protectedBool gets the value of a boolean pointer under a read lock. l must
// protect the value under ptr.
//
// TODO(e.burkov): Make it generic?
func protectedBool(mu *sync.RWMutex, ptr *bool) (val bool) {
mu.RLock()
defer mu.RUnlock()
return *ptr
}
func (d *DNSFilter) handleSafeBrowsingEnable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled, true)
d.Config.ConfigModified()
}
func (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled, false)
d.Config.ConfigModified()
}
func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
resp := &struct {
Enabled bool `json:"enabled"`
}{
Enabled: protectedBool(&d.confLock, &d.Config.SafeBrowsingEnabled),
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}
func (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.ParentalEnabled, true)
d.Config.ConfigModified()
}
func (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request) {
setProtectedBool(&d.confLock, &d.Config.ParentalEnabled, false)
d.Config.ConfigModified()
}
func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) {
resp := &struct {
Enabled bool `json:"enabled"`
}{
Enabled: protectedBool(&d.confLock, &d.Config.ParentalEnabled),
}
_ = aghhttp.WriteJSONResponse(w, r, resp)
}

View File

@@ -1,226 +0,0 @@
package filtering
import (
"crypto/sha256"
"strings"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/golibs/cache"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSafeBrowsingHash(t *testing.T) {
// test hostnameToHashes()
hashes := hostnameToHashes("1.2.3.sub.host.com")
assert.Len(t, hashes, 3)
_, ok := hashes[sha256.Sum256([]byte("3.sub.host.com"))]
assert.True(t, ok)
_, ok = hashes[sha256.Sum256([]byte("sub.host.com"))]
assert.True(t, ok)
_, ok = hashes[sha256.Sum256([]byte("host.com"))]
assert.True(t, ok)
_, ok = hashes[sha256.Sum256([]byte("com"))]
assert.False(t, ok)
c := &sbCtx{
svc: "SafeBrowsing",
hashToHost: hashes,
}
q := c.getQuestion()
assert.Contains(t, q, "7a1b.")
assert.Contains(t, q, "af5a.")
assert.Contains(t, q, "eb11.")
assert.True(t, strings.HasSuffix(q, "sb.dns.adguard.com."))
}
func TestSafeBrowsingCache(t *testing.T) {
c := &sbCtx{
svc: "SafeBrowsing",
cacheTime: 100,
}
conf := cache.Config{}
c.cache = cache.New(conf)
// store in cache hashes for "3.sub.host.com" and "host.com"
// and empty data for hash-prefix for "sub.host.com"
hash := sha256.Sum256([]byte("sub.host.com"))
c.hashToHost = make(map[[32]byte]string)
c.hashToHost[hash] = "sub.host.com"
var hashesArray [][]byte
hash4 := sha256.Sum256([]byte("3.sub.host.com"))
hashesArray = append(hashesArray, hash4[:])
hash2 := sha256.Sum256([]byte("host.com"))
hashesArray = append(hashesArray, hash2[:])
c.storeCache(hashesArray)
// match "3.sub.host.com" or "host.com" from cache
c.hashToHost = make(map[[32]byte]string)
hash = sha256.Sum256([]byte("3.sub.host.com"))
c.hashToHost[hash] = "3.sub.host.com"
hash = sha256.Sum256([]byte("sub.host.com"))
c.hashToHost[hash] = "sub.host.com"
hash = sha256.Sum256([]byte("host.com"))
c.hashToHost[hash] = "host.com"
assert.Equal(t, 1, c.getCached())
// match "sub.host.com" from cache
c.hashToHost = make(map[[32]byte]string)
hash = sha256.Sum256([]byte("sub.host.com"))
c.hashToHost[hash] = "sub.host.com"
assert.Equal(t, -1, c.getCached())
// Match "sub.host.com" from cache. Another hash for "host.example" is not
// in the cache, so get data for it from the server.
c.hashToHost = make(map[[32]byte]string)
hash = sha256.Sum256([]byte("sub.host.com"))
c.hashToHost[hash] = "sub.host.com"
hash = sha256.Sum256([]byte("host.example"))
c.hashToHost[hash] = "host.example"
assert.Empty(t, c.getCached())
hash = sha256.Sum256([]byte("sub.host.com"))
_, ok := c.hashToHost[hash]
assert.False(t, ok)
hash = sha256.Sum256([]byte("host.example"))
_, ok = c.hashToHost[hash]
assert.True(t, ok)
c = &sbCtx{
svc: "SafeBrowsing",
cacheTime: 100,
}
conf = cache.Config{}
c.cache = cache.New(conf)
hash = sha256.Sum256([]byte("sub.host.com"))
c.hashToHost = make(map[[32]byte]string)
c.hashToHost[hash] = "sub.host.com"
c.cache.Set(hash[0:2], make([]byte, 32))
assert.Empty(t, c.getCached())
}
func TestSBPC_checkErrorUpstream(t *testing.T) {
d, _ := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
t.Cleanup(d.Close)
ups := aghtest.NewErrorUpstream()
d.SetSafeBrowsingUpstream(ups)
d.SetParentalUpstream(ups)
setts := &Settings{
ProtectionEnabled: true,
SafeBrowsingEnabled: true,
ParentalEnabled: true,
}
_, err := d.checkSafeBrowsing("smthng.com", dns.TypeA, setts)
assert.Error(t, err)
_, err = d.checkParental("smthng.com", dns.TypeA, setts)
assert.Error(t, err)
}
func TestSBPC(t *testing.T) {
d, _ := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
t.Cleanup(d.Close)
const hostname = "example.org"
setts := &Settings{
ProtectionEnabled: true,
SafeBrowsingEnabled: true,
ParentalEnabled: true,
}
testCases := []struct {
testCache cache.Cache
testFunc func(host string, _ uint16, _ *Settings) (res Result, err error)
name string
block bool
}{{
testCache: d.safebrowsingCache,
testFunc: d.checkSafeBrowsing,
name: "sb_no_block",
block: false,
}, {
testCache: d.safebrowsingCache,
testFunc: d.checkSafeBrowsing,
name: "sb_block",
block: true,
}, {
testCache: d.parentalCache,
testFunc: d.checkParental,
name: "pc_no_block",
block: false,
}, {
testCache: d.parentalCache,
testFunc: d.checkParental,
name: "pc_block",
block: true,
}}
for _, tc := range testCases {
// Prepare the upstream.
ups := aghtest.NewBlockUpstream(hostname, tc.block)
var numReq int
onExchange := ups.OnExchange
ups.OnExchange = func(req *dns.Msg) (resp *dns.Msg, err error) {
numReq++
return onExchange(req)
}
d.SetSafeBrowsingUpstream(ups)
d.SetParentalUpstream(ups)
t.Run(tc.name, func(t *testing.T) {
// Firstly, check the request blocking.
hits := 0
res, err := tc.testFunc(hostname, dns.TypeA, setts)
require.NoError(t, err)
if tc.block {
assert.True(t, res.IsFiltered)
require.Len(t, res.Rules, 1)
hits++
} else {
require.False(t, res.IsFiltered)
}
// Check the cache state, check the response is now cached.
assert.Equal(t, 1, tc.testCache.Stats().Count)
assert.Equal(t, hits, tc.testCache.Stats().Hit)
// There was one request to an upstream.
assert.Equal(t, 1, numReq)
// Now make the same request to check the cache was used.
res, err = tc.testFunc(hostname, dns.TypeA, setts)
require.NoError(t, err)
if tc.block {
assert.True(t, res.IsFiltered)
require.Len(t, res.Rules, 1)
} else {
require.False(t, res.IsFiltered)
}
// Check the cache state, it should've been used.
assert.Equal(t, 1, tc.testCache.Stats().Count)
assert.Equal(t, hits+1, tc.testCache.Stats().Hit)
// Check that there were no additional requests.
assert.Equal(t, 1, numReq)
})
purgeCaches(d)
}
}

View File

@@ -1438,6 +1438,8 @@ var blockedServices = []blockedService{{
"||mindly.social^", "||mindly.social^",
"||mstdn.ca^", "||mstdn.ca^",
"||mstdn.jp^", "||mstdn.jp^",
"||mstdn.party^",
"||mstdn.plus^",
"||mstdn.social^", "||mstdn.social^",
"||muenchen.social^", "||muenchen.social^",
"||muenster.im^", "||muenster.im^",
@@ -1447,7 +1449,6 @@ var blockedServices = []blockedService{{
"||nrw.social^", "||nrw.social^",
"||o3o.ca^", "||o3o.ca^",
"||ohai.social^", "||ohai.social^",
"||pewtix.com^",
"||piaille.fr^", "||piaille.fr^",
"||pol.social^", "||pol.social^",
"||ravenation.club^", "||ravenation.club^",
@@ -1469,7 +1470,6 @@ var blockedServices = []blockedService{{
"||techhub.social^", "||techhub.social^",
"||theblower.au^", "||theblower.au^",
"||tkz.one^", "||tkz.one^",
"||todon.eu^",
"||toot.aquilenet.fr^", "||toot.aquilenet.fr^",
"||toot.community^", "||toot.community^",
"||toot.funami.tech^", "||toot.funami.tech^",

View File

@@ -3,14 +3,12 @@ package home
import ( import (
"net" "net"
"net/netip" "net/netip"
"os"
"runtime" "runtime"
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -283,8 +281,8 @@ func TestClientsAddExisting(t *testing.T) {
// First, init a DHCP server with a single static lease. // First, init a DHCP server with a single static lease.
config := &dhcpd.ServerConfig{ config := &dhcpd.ServerConfig{
Enabled: true, Enabled: true,
DBFilePath: "leases.db", DataDir: t.TempDir(),
Conf4: dhcpd.V4ServerConf{ Conf4: dhcpd.V4ServerConf{
Enabled: true, Enabled: true,
GatewayIP: netip.MustParseAddr("1.2.3.1"), GatewayIP: netip.MustParseAddr("1.2.3.1"),
@@ -296,9 +294,6 @@ func TestClientsAddExisting(t *testing.T) {
dhcpServer, err := dhcpd.Create(config) dhcpServer, err := dhcpd.Create(config)
require.NoError(t, err) require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, func() (err error) {
return os.Remove("leases.db")
})
clients.dhcpServer = dhcpServer clients.dhcpServer = dhcpServer

View File

@@ -399,19 +399,39 @@ func (c *configuration) getConfigFilename() string {
return configFile return configFile
} }
// getLogSettings reads logging settings from the config file. // readLogSettings reads logging settings from the config file. We do it in a
// we do it in a separate method in order to configure logger before the actual configuration is parsed and applied. // separate method in order to configure logger before the actual configuration
func getLogSettings() logSettings { // is parsed and applied.
l := logSettings{} func readLogSettings() (ls *logSettings) {
ls = &logSettings{}
yamlFile, err := readConfigFile() yamlFile, err := readConfigFile()
if err != nil { if err != nil {
return l return ls
} }
err = yaml.Unmarshal(yamlFile, &l)
err = yaml.Unmarshal(yamlFile, ls)
if err != nil { if err != nil {
log.Error("Couldn't get logging settings from the configuration: %s", err) log.Error("Couldn't get logging settings from the configuration: %s", err)
} }
return l
return ls
}
// validateBindHosts returns error if any of binding hosts from configuration is
// not a valid IP address.
func validateBindHosts(conf *configuration) (err error) {
if !conf.BindHost.IsValid() {
return errors.Error("bind_host is not a valid ip address")
}
for i, addr := range conf.DNS.BindHosts {
if !addr.IsValid() {
return fmt.Errorf("dns.bind_hosts at index %d is not a valid ip address", i)
}
}
return nil
} }
// parseConfig loads configuration from the YAML file // parseConfig loads configuration from the YAML file
@@ -425,6 +445,13 @@ func parseConfig() (err error) {
config.fileData = nil config.fileData = nil
err = yaml.Unmarshal(fileData, &config) err = yaml.Unmarshal(fileData, &config)
if err != nil { if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
err = validateBindHosts(config)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err return err
} }

View File

@@ -180,7 +180,7 @@ func registerControlHandlers() {
httpRegister(http.MethodGet, "/control/status", handleStatus) httpRegister(http.MethodGet, "/control/status", handleStatus)
httpRegister(http.MethodPost, "/control/i18n/change_language", handleI18nChangeLanguage) httpRegister(http.MethodPost, "/control/i18n/change_language", handleI18nChangeLanguage)
httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage) httpRegister(http.MethodGet, "/control/i18n/current_language", handleI18nCurrentLanguage)
Context.mux.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON))) Context.mux.HandleFunc("/control/version.json", postInstall(optionalAuth(handleVersionJSON)))
httpRegister(http.MethodPost, "/control/update", handleUpdate) httpRegister(http.MethodPost, "/control/update", handleUpdate)
httpRegister(http.MethodGet, "/control/profile", handleGetProfile) httpRegister(http.MethodGet, "/control/profile", handleGetProfile)
httpRegister(http.MethodPut, "/control/profile/update", handlePutProfile) httpRegister(http.MethodPut, "/control/profile/update", handlePutProfile)

View File

@@ -26,15 +26,14 @@ type temporaryError interface {
Temporary() (ok bool) Temporary() (ok bool)
} }
// Get the latest available version from the Internet // handleVersionJSON is the handler for the POST /control/version.json HTTP API.
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) { //
// TODO(a.garipov): Find out if this API used with a GET method by anyone.
func handleVersionJSON(w http.ResponseWriter, r *http.Request) {
resp := &versionResponse{} resp := &versionResponse{}
if Context.disableUpdate { if Context.disableUpdate {
resp.Disabled = true resp.Disabled = true
err := json.NewEncoder(w).Encode(resp) _ = aghhttp.WriteJSONResponse(w, r, resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "writing body: %s", err)
}
return return
} }

View File

@@ -27,14 +27,17 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/hashprefix"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch" "github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/stats" "github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/AdGuardHome/internal/updater" "github.com/AdguardTeam/AdGuardHome/internal/updater"
"github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/AdGuardHome/internal/version"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
) )
@@ -143,7 +146,9 @@ func Main(clientBuildFS fs.FS) {
run(opts, clientBuildFS) run(opts, clientBuildFS)
} }
func setupContext(opts options) { // setupContext initializes [Context] fields. It also reads and upgrades
// config file if necessary.
func setupContext(opts options) (err error) {
setupContextFlags(opts) setupContextFlags(opts)
Context.tlsRoots = aghtls.SystemRootCAs() Context.tlsRoots = aghtls.SystemRootCAs()
@@ -160,10 +165,15 @@ func setupContext(opts options) {
}, },
} }
Context.mux = http.NewServeMux()
if !Context.firstRun { if !Context.firstRun {
// Do the upgrade if necessary. // Do the upgrade if necessary.
err := upgradeConfig() err = upgradeConfig()
fatalOnError(err) if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
if err = parseConfig(); err != nil { if err = parseConfig(); err != nil {
log.Error("parsing configuration file: %s", err) log.Error("parsing configuration file: %s", err)
@@ -179,11 +189,14 @@ func setupContext(opts options) {
if !opts.noEtcHosts && config.Clients.Sources.HostsFile { if !opts.noEtcHosts && config.Clients.Sources.HostsFile {
err = setupHostsContainer() err = setupHostsContainer()
fatalOnError(err) if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
} }
} }
Context.mux = http.NewServeMux() return nil
} }
// setupContextFlags sets global flags and prints their status to the log. // setupContextFlags sets global flags and prints their status to the log.
@@ -285,28 +298,32 @@ func setupHostsContainer() (err error) {
return nil return nil
} }
func setupConfig(opts options) (err error) { // setupOpts sets up command-line options.
config.DNS.DnsfilterConf.EtcHosts = Context.etcHosts func setupOpts(opts options) (err error) {
config.DNS.DnsfilterConf.ConfigModified = onConfigModified err = setupBindOpts(opts)
config.DNS.DnsfilterConf.HTTPRegister = httpRegister
config.DNS.DnsfilterConf.DataDir = Context.getDataDir()
config.DNS.DnsfilterConf.Filters = slices.Clone(config.Filters)
config.DNS.DnsfilterConf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
config.DNS.DnsfilterConf.UserRules = slices.Clone(config.UserRules)
config.DNS.DnsfilterConf.HTTPClient = Context.client
config.DNS.DnsfilterConf.SafeSearchConf.CustomResolver = safeSearchResolver{}
config.DNS.DnsfilterConf.SafeSearch, err = safesearch.NewDefault(
config.DNS.DnsfilterConf.SafeSearchConf,
"default",
config.DNS.DnsfilterConf.SafeSearchCacheSize,
time.Minute*time.Duration(config.DNS.DnsfilterConf.CacheTime),
)
if err != nil { if err != nil {
return fmt.Errorf("initializing safesearch: %w", err) // Don't wrap the error, because it's informative enough as is.
return err
} }
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
Context.pidFileName = opts.pidFile
}
return nil
}
// initContextClients initializes Context clients and related fields.
func initContextClients() (err error) {
err = setupDNSFilteringConf(config.DNS.DnsfilterConf)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
//lint:ignore SA1019 Migration is not over.
config.DHCP.WorkDir = Context.workDir config.DHCP.WorkDir = Context.workDir
config.DHCP.DataDir = Context.getDataDir()
config.DHCP.HTTPRegister = httpRegister config.DHCP.HTTPRegister = httpRegister
config.DHCP.ConfigModified = onConfigModified config.DHCP.ConfigModified = onConfigModified
@@ -336,8 +353,19 @@ func setupConfig(opts options) (err error) {
arpdb = aghnet.NewARPDB() arpdb = aghnet.NewARPDB()
} }
Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb, config.DNS.DnsfilterConf) Context.clients.Init(
config.Clients.Persistent,
Context.dhcpServer,
Context.etcHosts,
arpdb,
config.DNS.DnsfilterConf,
)
return nil
}
// setupBindOpts overrides bind host/port from the opts.
func setupBindOpts(opts options) (err error) {
if opts.bindPort != 0 { if opts.bindPort != 0 {
config.BindPort = opts.bindPort config.BindPort = opts.bindPort
@@ -348,12 +376,83 @@ func setupConfig(opts options) (err error) {
} }
} }
// override bind host/port from the console
if opts.bindHost.IsValid() { if opts.bindHost.IsValid() {
config.BindHost = opts.bindHost config.BindHost = opts.bindHost
} }
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
Context.pidFileName = opts.pidFile return nil
}
// setupDNSFilteringConf sets up DNS filtering configuration settings.
func setupDNSFilteringConf(conf *filtering.Config) (err error) {
const (
dnsTimeout = 3 * time.Second
sbService = "safe browsing"
defaultSafeBrowsingServer = `https://family.adguard-dns.com/dns-query`
sbTXTSuffix = `sb.dns.adguard.com.`
pcService = "parental control"
defaultParentalServer = `https://family.adguard-dns.com/dns-query`
pcTXTSuffix = `pc.dns.adguard.com.`
)
conf.EtcHosts = Context.etcHosts
conf.ConfigModified = onConfigModified
conf.HTTPRegister = httpRegister
conf.DataDir = Context.getDataDir()
conf.Filters = slices.Clone(config.Filters)
conf.WhitelistFilters = slices.Clone(config.WhitelistFilters)
conf.UserRules = slices.Clone(config.UserRules)
conf.HTTPClient = Context.client
cacheTime := time.Duration(conf.CacheTime) * time.Minute
upsOpts := &upstream.Options{
Timeout: dnsTimeout,
ServerIPAddrs: []net.IP{
{94, 140, 14, 15},
{94, 140, 15, 16},
net.ParseIP("2a10:50c0::bad1:ff"),
net.ParseIP("2a10:50c0::bad2:ff"),
},
}
sbUps, err := upstream.AddressToUpstream(defaultSafeBrowsingServer, upsOpts)
if err != nil {
return fmt.Errorf("converting safe browsing server: %w", err)
}
conf.SafeBrowsingChecker = hashprefix.New(&hashprefix.Config{
Upstream: sbUps,
ServiceName: sbService,
TXTSuffix: sbTXTSuffix,
CacheTime: cacheTime,
CacheSize: conf.SafeBrowsingCacheSize,
})
parUps, err := upstream.AddressToUpstream(defaultParentalServer, upsOpts)
if err != nil {
return fmt.Errorf("converting parental server: %w", err)
}
conf.ParentalControlChecker = hashprefix.New(&hashprefix.Config{
Upstream: parUps,
ServiceName: pcService,
TXTSuffix: pcTXTSuffix,
CacheTime: cacheTime,
CacheSize: conf.SafeBrowsingCacheSize,
})
conf.SafeSearchConf.CustomResolver = safeSearchResolver{}
conf.SafeSearch, err = safesearch.NewDefault(
conf.SafeSearchConf,
"default",
conf.SafeSearchCacheSize,
cacheTime,
)
if err != nil {
return fmt.Errorf("initializing safesearch: %w", err)
} }
return nil return nil
@@ -430,14 +529,16 @@ func fatalOnError(err error) {
// run configures and starts AdGuard Home. // run configures and starts AdGuard Home.
func run(opts options, clientBuildFS fs.FS) { func run(opts options, clientBuildFS fs.FS) {
// configure config filename // Configure config filename.
initConfigFilename(opts) initConfigFilename(opts)
// configure working dir and config path // Configure working dir and config path.
initWorkingDir(opts) err := initWorkingDir(opts)
fatalOnError(err)
// configure log level and output // Configure log level and output.
configureLogger(opts) err = configureLogger(opts)
fatalOnError(err)
// Print the first message after logger is configured. // Print the first message after logger is configured.
log.Info(version.Full()) log.Info(version.Full())
@@ -446,25 +547,29 @@ func run(opts options, clientBuildFS fs.FS) {
log.Info("AdGuard Home is running as a service") log.Info("AdGuard Home is running as a service")
} }
setupContext(opts) err = setupContext(opts)
err := configureOS(config)
fatalOnError(err) fatalOnError(err)
// clients package uses filtering package's static data (filtering.BlockedSvcKnown()), err = configureOS(config)
// so we have to initialize filtering's static data first, fatalOnError(err)
// but also avoid relying on automatic Go init() function
// Clients package uses filtering package's static data
// (filtering.BlockedSvcKnown()), so we have to initialize filtering static
// data first, but also to avoid relying on automatic Go init() function.
filtering.InitModule() filtering.InitModule()
err = setupConfig(opts) err = initContextClients()
fatalOnError(err) fatalOnError(err)
// TODO(e.burkov): This could be made earlier, probably as the option's err = setupOpts(opts)
fatalOnError(err)
// TODO(e.burkov): This could be made earlier, probably as the option's
// effect. // effect.
cmdlineUpdate(opts) cmdlineUpdate(opts)
if !Context.firstRun { if !Context.firstRun {
// Save the updated config // Save the updated config.
err = config.write() err = config.write()
fatalOnError(err) fatalOnError(err)
@@ -474,33 +579,15 @@ func run(opts options, clientBuildFS fs.FS) {
} }
} }
err = os.MkdirAll(Context.getDataDir(), 0o755) dir := Context.getDataDir()
if err != nil { err = os.MkdirAll(dir, 0o755)
log.Fatalf("Cannot create DNS data dir at %s: %s", Context.getDataDir(), err) fatalOnError(errors.Annotate(err, "creating DNS data dir at %s: %w", dir))
}
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
GLMode = opts.glinetMode GLMode = opts.glinetMode
var rateLimiter *authRateLimiter
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
rateLimiter = newAuthRateLimiter(
time.Duration(config.AuthBlockMin)*time.Minute,
config.AuthAttempts,
)
} else {
log.Info("authratelimiter is disabled")
}
Context.auth = InitAuth( // Init auth module.
sessFilename, Context.auth, err = initUsers()
config.Users, fatalOnError(err)
config.WebSessionTTLHours*60*60,
rateLimiter,
)
if Context.auth == nil {
log.Fatalf("Couldn't initialize Auth module")
}
config.Users = nil
Context.tls, err = newTLSManager(config.TLS) Context.tls, err = newTLSManager(config.TLS)
if err != nil { if err != nil {
@@ -518,10 +605,10 @@ func run(opts options, clientBuildFS fs.FS) {
Context.tls.start() Context.tls.start()
go func() { go func() {
serr := startDNSServer() sErr := startDNSServer()
if serr != nil { if sErr != nil {
closeDNSServer() closeDNSServer()
fatalOnError(serr) fatalOnError(sErr)
} }
}() }()
@@ -535,10 +622,33 @@ func run(opts options, clientBuildFS fs.FS) {
Context.web.start() Context.web.start()
// wait indefinitely for other go-routines to complete their job // Wait indefinitely for other goroutines to complete their job.
select {} select {}
} }
// initUsers initializes context auth module. Clears config users field.
func initUsers() (auth *Auth, err error) {
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
var rateLimiter *authRateLimiter
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
blockDur := time.Duration(config.AuthBlockMin) * time.Minute
rateLimiter = newAuthRateLimiter(blockDur, config.AuthAttempts)
} else {
log.Info("authratelimiter is disabled")
}
sessionTTL := config.WebSessionTTLHours * 60 * 60
auth = InitAuth(sessFilename, config.Users, sessionTTL, rateLimiter)
if auth == nil {
return nil, errors.Error("initializing auth module failed")
}
config.Users = nil
return auth, nil
}
func (c *configuration) anonymizer() (ipmut *aghnet.IPMut) { func (c *configuration) anonymizer() (ipmut *aghnet.IPMut) {
var anonFunc aghnet.IPMutFunc var anonFunc aghnet.IPMutFunc
if c.DNS.AnonymizeClientIP { if c.DNS.AnonymizeClientIP {
@@ -611,22 +721,19 @@ func writePIDFile(fn string) bool {
return true return true
} }
// initConfigFilename sets up context config file path. This file path can be
// overridden by command-line arguments, or is set to default.
func initConfigFilename(opts options) { func initConfigFilename(opts options) {
// config file path can be overridden by command-line arguments: Context.configFilename = stringutil.Coalesce(opts.confFilename, "AdGuardHome.yaml")
if opts.confFilename != "" {
Context.configFilename = opts.confFilename
} else {
// Default config file name
Context.configFilename = "AdGuardHome.yaml"
}
} }
// initWorkingDir initializes the workDir // initWorkingDir initializes the workDir. If no command-line arguments are
// if no command-line arguments specified, we use the directory where our binary file is located // specified, the directory with the binary file is used.
func initWorkingDir(opts options) { func initWorkingDir(opts options) (err error) {
execPath, err := os.Executable() execPath, err := os.Executable()
if err != nil { if err != nil {
panic(err) // Don't wrap the error, because it's informative enough as is.
return err
} }
if opts.workDir != "" { if opts.workDir != "" {
@@ -638,34 +745,20 @@ func initWorkingDir(opts options) {
workDir, err := filepath.EvalSymlinks(Context.workDir) workDir, err := filepath.EvalSymlinks(Context.workDir)
if err != nil { if err != nil {
panic(err) // Don't wrap the error, because it's informative enough as is.
return err
} }
Context.workDir = workDir Context.workDir = workDir
return nil
} }
// configureLogger configures logger level and output // configureLogger configures logger level and output.
func configureLogger(opts options) { func configureLogger(opts options) (err error) {
ls := getLogSettings() ls := getLogSettings(opts)
// command-line arguments can override config settings // Configure logger level.
if opts.verbose || config.Verbose {
ls.Verbose = true
}
if opts.logFile != "" {
ls.File = opts.logFile
} else if config.File != "" {
ls.File = config.File
}
// Handle default log settings overrides
ls.Compress = config.Compress
ls.LocalTime = config.LocalTime
ls.MaxBackups = config.MaxBackups
ls.MaxSize = config.MaxSize
ls.MaxAge = config.MaxAge
// log.SetLevel(log.INFO) - default
if ls.Verbose { if ls.Verbose {
log.SetLevel(log.DEBUG) log.SetLevel(log.DEBUG)
} }
@@ -674,38 +767,63 @@ func configureLogger(opts options) {
// happen pretty quickly. // happen pretty quickly.
log.SetFlags(log.LstdFlags | log.Lmicroseconds) log.SetFlags(log.LstdFlags | log.Lmicroseconds)
if opts.runningAsService && ls.File == "" && runtime.GOOS == "windows" { // Write logs to stdout by default.
// When running as a Windows service, use eventlog by default if nothing
// else is configured. Otherwise, we'll simply lose the log output.
ls.File = configSyslog
}
// logs are written to stdout (default)
if ls.File == "" { if ls.File == "" {
return return nil
} }
if ls.File == configSyslog { if ls.File == configSyslog {
// Use syslog where it is possible and eventlog on Windows // Use syslog where it is possible and eventlog on Windows.
err := aghos.ConfigureSyslog(serviceName) err = aghos.ConfigureSyslog(serviceName)
if err != nil { if err != nil {
log.Fatalf("cannot initialize syslog: %s", err) return fmt.Errorf("cannot initialize syslog: %w", err)
}
} else {
logFilePath := ls.File
if !filepath.IsAbs(logFilePath) {
logFilePath = filepath.Join(Context.workDir, logFilePath)
} }
log.SetOutput(&lumberjack.Logger{ return nil
Filename: logFilePath,
Compress: ls.Compress, // disabled by default
LocalTime: ls.LocalTime,
MaxBackups: ls.MaxBackups,
MaxSize: ls.MaxSize, // megabytes
MaxAge: ls.MaxAge, // days
})
} }
logFilePath := ls.File
if !filepath.IsAbs(logFilePath) {
logFilePath = filepath.Join(Context.workDir, logFilePath)
}
log.SetOutput(&lumberjack.Logger{
Filename: logFilePath,
Compress: ls.Compress,
LocalTime: ls.LocalTime,
MaxBackups: ls.MaxBackups,
MaxSize: ls.MaxSize,
MaxAge: ls.MaxAge,
})
return nil
}
// getLogSettings returns a log settings object properly initialized from opts.
func getLogSettings(opts options) (ls *logSettings) {
ls = readLogSettings()
// Command-line arguments can override config settings.
if opts.verbose || config.Verbose {
ls.Verbose = true
}
ls.File = stringutil.Coalesce(opts.logFile, config.File, ls.File)
// Handle default log settings overrides.
ls.Compress = config.Compress
ls.LocalTime = config.LocalTime
ls.MaxBackups = config.MaxBackups
ls.MaxSize = config.MaxSize
ls.MaxAge = config.MaxAge
if opts.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
// When running as a Windows service, use eventlog by default if
// nothing else is configured. Otherwise, we'll lose the log output.
ls.File = configSyslog
}
return ls
} }
// cleanup stops and resets all the modules. // cleanup stops and resets all the modules.

View File

@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"os" "os"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@@ -84,14 +83,9 @@ func svcStatus(s service.Service) (status service.Status, err error) {
// On OpenWrt, the service utility may not exist. We use our service script // On OpenWrt, the service utility may not exist. We use our service script
// directly in this case. // directly in this case.
func svcAction(s service.Service, action string) (err error) { func svcAction(s service.Service, action string) (err error) {
if runtime.GOOS == "darwin" && action == "start" { if action == "start" {
var exe string if err = aghos.PreCheckActionStart(); err != nil {
if exe, err = os.Executable(); err != nil { log.Error("starting service: %s", err)
log.Error("starting service: getting executable path: %s", err)
} else if exe, err = filepath.EvalSymlinks(exe); err != nil {
log.Error("starting service: evaluating executable symlinks: %s", err)
} else if !strings.HasPrefix(exe, "/Applications/") {
log.Info("warning: service must be started from within the /Applications directory")
} }
} }
@@ -99,8 +93,6 @@ func svcAction(s service.Service, action string) (err error) {
if err != nil && service.Platform() == "unix-systemv" && if err != nil && service.Platform() == "unix-systemv" &&
(action == "start" || action == "stop" || action == "restart") { (action == "start" || action == "stop" || action == "restart") {
_, err = runInitdCommand(action) _, err = runInitdCommand(action)
return err
} }
return err return err
@@ -224,6 +216,7 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
runOpts := opts runOpts := opts
runOpts.serviceControlAction = "run" runOpts.serviceControlAction = "run"
svcConfig := &service.Config{ svcConfig := &service.Config{
Name: serviceName, Name: serviceName,
DisplayName: serviceDisplayName, DisplayName: serviceDisplayName,
@@ -233,35 +226,48 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
} }
configureService(svcConfig) configureService(svcConfig)
prg := &program{ s, err := service.New(&program{clientBuildFS: clientBuildFS, opts: runOpts}, svcConfig)
clientBuildFS: clientBuildFS, if err != nil {
opts: runOpts,
}
var s service.Service
if s, err = service.New(prg, svcConfig); err != nil {
log.Fatalf("service: initializing service: %s", err) log.Fatalf("service: initializing service: %s", err)
} }
err = handleServiceCommand(s, action, opts)
if err != nil {
log.Fatalf("service: %s", err)
}
log.Printf(
"service: action %s has been done successfully on %s",
action,
service.ChosenSystem(),
)
}
// handleServiceCommand handles service command.
func handleServiceCommand(s service.Service, action string, opts options) (err error) {
switch action { switch action {
case "status": case "status":
handleServiceStatusCommand(s) handleServiceStatusCommand(s)
case "run": case "run":
if err = s.Run(); err != nil { if err = s.Run(); err != nil {
log.Fatalf("service: failed to run service: %s", err) return fmt.Errorf("failed to run service: %w", err)
} }
case "install": case "install":
initConfigFilename(opts) initConfigFilename(opts)
initWorkingDir(opts) if err = initWorkingDir(opts); err != nil {
return fmt.Errorf("failed to init working dir: %w", err)
}
handleServiceInstallCommand(s) handleServiceInstallCommand(s)
case "uninstall": case "uninstall":
handleServiceUninstallCommand(s) handleServiceUninstallCommand(s)
default: default:
if err = svcAction(s, action); err != nil { if err = svcAction(s, action); err != nil {
log.Fatalf("service: executing action %q: %s", action, err) return fmt.Errorf("executing action %q: %w", action, err)
} }
} }
log.Printf("service: action %s has been done successfully on %s", action, service.ChosenSystem()) return nil
} }
// handleServiceStatusCommand handles service "status" command. // handleServiceStatusCommand handles service "status" command.

View File

@@ -172,9 +172,32 @@ func loadTLSConf(tlsConf *tlsConfigSettings, status *tlsConfigStatus) (err error
} }
}() }()
tlsConf.CertificateChainData = []byte(tlsConf.CertificateChain) err = loadCertificateChainData(tlsConf, status)
tlsConf.PrivateKeyData = []byte(tlsConf.PrivateKey) if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
err = loadPrivateKeyData(tlsConf, status)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
err = validateCertificates(
status,
tlsConf.CertificateChainData,
tlsConf.PrivateKeyData,
tlsConf.ServerName,
)
return errors.Annotate(err, "validating certificate pair: %w")
}
// loadCertificateChainData loads PEM-encoded certificates chain data to the
// TLS configuration.
func loadCertificateChainData(tlsConf *tlsConfigSettings, status *tlsConfigStatus) (err error) {
tlsConf.CertificateChainData = []byte(tlsConf.CertificateChain)
if tlsConf.CertificatePath != "" { if tlsConf.CertificatePath != "" {
if tlsConf.CertificateChain != "" { if tlsConf.CertificateChain != "" {
return errors.Error("certificate data and file can't be set together") return errors.Error("certificate data and file can't be set together")
@@ -190,6 +213,13 @@ func loadTLSConf(tlsConf *tlsConfigSettings, status *tlsConfigStatus) (err error
status.ValidCert = true status.ValidCert = true
} }
return nil
}
// loadPrivateKeyData loads PEM-encoded private key data to the TLS
// configuration.
func loadPrivateKeyData(tlsConf *tlsConfigSettings, status *tlsConfigStatus) (err error) {
tlsConf.PrivateKeyData = []byte(tlsConf.PrivateKey)
if tlsConf.PrivateKeyPath != "" { if tlsConf.PrivateKeyPath != "" {
if tlsConf.PrivateKey != "" { if tlsConf.PrivateKey != "" {
return errors.Error("private key data and file can't be set together") return errors.Error("private key data and file can't be set together")
@@ -203,16 +233,6 @@ func loadTLSConf(tlsConf *tlsConfigSettings, status *tlsConfigStatus) (err error
status.ValidKey = true status.ValidKey = true
} }
err = validateCertificates(
status,
tlsConf.CertificateChainData,
tlsConf.PrivateKeyData,
tlsConf.ServerName,
)
if err != nil {
return fmt.Errorf("validating certificate pair: %w", err)
}
return nil return nil
} }

View File

@@ -41,7 +41,8 @@ func upgradeConfig() error {
err = yaml.Unmarshal(body, &diskConf) err = yaml.Unmarshal(body, &diskConf)
if err != nil { if err != nil {
log.Printf("Couldn't parse config file: %s", err) log.Printf("parsing config file for upgrade: %s", err)
return err return err
} }
@@ -293,71 +294,61 @@ func upgradeSchema4to5(diskConf yobj) error {
return nil return nil
} }
// clients: // upgradeSchema5to6 performs the following changes:
// ...
// //
// ip: 127.0.0.1 // # BEFORE:
// mac: ... // 'clients':
// ...
// 'ip': 127.0.0.1
// 'mac': ...
// //
// -> // # AFTER:
// // 'clients':
// clients: // ...
// ... // 'ids':
// // - 127.0.0.1
// ids: // - ...
// - 127.0.0.1
// - ...
func upgradeSchema5to6(diskConf yobj) error { func upgradeSchema5to6(diskConf yobj) error {
log.Printf("%s(): called", funcName()) log.Printf("Upgrade yaml: 5 to 6")
diskConf["schema_version"] = 6 diskConf["schema_version"] = 6
clients, ok := diskConf["clients"] clientsVal, ok := diskConf["clients"]
if !ok { if !ok {
return nil return nil
} }
switch arr := clients.(type) { clients, ok := clientsVal.([]yobj)
case []any: if !ok {
for i := range arr { return fmt.Errorf("unexpected type of clients: %T", clientsVal)
switch c := arr[i].(type) { }
case map[any]any:
var ipVal any
ipVal, ok = c["ip"]
ids := []string{}
if ok {
var ip string
ip, ok = ipVal.(string)
if !ok {
log.Fatalf("client.ip is not a string: %v", ipVal)
return nil
}
if len(ip) != 0 {
ids = append(ids, ip)
}
}
var macVal any for i := range clients {
macVal, ok = c["mac"] c := clients[i]
if ok { var ids []string
var mac string
mac, ok = macVal.(string)
if !ok {
log.Fatalf("client.mac is not a string: %v", macVal)
return nil
}
if len(mac) != 0 {
ids = append(ids, mac)
}
}
c["ids"] = ids if ipVal, hasIP := c["ip"]; hasIP {
default: var ip string
continue if ip, ok = ipVal.(string); !ok {
return fmt.Errorf("client.ip is not a string: %v", ipVal)
}
if ip != "" {
ids = append(ids, ip)
} }
} }
default:
return nil if macVal, hasMac := c["mac"]; hasMac {
var mac string
if mac, ok = macVal.(string); !ok {
return fmt.Errorf("client.mac is not a string: %v", macVal)
}
if mac != "" {
ids = append(ids, mac)
}
}
c["ids"] = ids
} }
return nil return nil

View File

@@ -68,6 +68,95 @@ func TestUpgradeSchema2to3(t *testing.T) {
assertEqualExcept(t, oldDiskConf, diskConf, excludedEntries, excludedEntries) assertEqualExcept(t, oldDiskConf, diskConf, excludedEntries, excludedEntries)
} }
func TestUpgradeSchema5to6(t *testing.T) {
const newSchemaVer = 6
testCases := []struct {
in yobj
want yobj
wantErr string
name string
}{{
in: yobj{
"clients": []yobj{},
},
want: yobj{
"clients": []yobj{},
"schema_version": newSchemaVer,
},
wantErr: "",
name: "no_clients",
}, {
in: yobj{
"clients": []yobj{{"ip": "127.0.0.1"}},
},
want: yobj{
"clients": []yobj{{
"ids": []string{"127.0.0.1"},
"ip": "127.0.0.1",
}},
"schema_version": newSchemaVer,
},
wantErr: "",
name: "client_ip",
}, {
in: yobj{
"clients": []yobj{{"mac": "mac"}},
},
want: yobj{
"clients": []yobj{{
"ids": []string{"mac"},
"mac": "mac",
}},
"schema_version": newSchemaVer,
},
wantErr: "",
name: "client_mac",
}, {
in: yobj{
"clients": []yobj{{"ip": "127.0.0.1", "mac": "mac"}},
},
want: yobj{
"clients": []yobj{{
"ids": []string{"127.0.0.1", "mac"},
"ip": "127.0.0.1",
"mac": "mac",
}},
"schema_version": newSchemaVer,
},
wantErr: "",
name: "client_ip_mac",
}, {
in: yobj{
"clients": []yobj{{"ip": 1, "mac": "mac"}},
},
want: yobj{
"clients": []yobj{{"ip": 1, "mac": "mac"}},
"schema_version": newSchemaVer,
},
wantErr: "client.ip is not a string: 1",
name: "inv_client_ip",
}, {
in: yobj{
"clients": []yobj{{"ip": "127.0.0.1", "mac": 1}},
},
want: yobj{
"clients": []yobj{{"ip": "127.0.0.1", "mac": 1}},
"schema_version": newSchemaVer,
},
wantErr: "client.mac is not a string: 1",
name: "inv_client_mac",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema5to6(tc.in)
testutil.AssertErrorMsg(t, tc.wantErr, err)
assert.Equal(t, tc.want, tc.in)
})
}
}
func TestUpgradeSchema7to8(t *testing.T) { func TestUpgradeSchema7to8(t *testing.T) {
const host = "1.2.3.4" const host = "1.2.3.4"
oldConf := yobj{ oldConf := yobj{

View File

@@ -182,8 +182,7 @@ func TestDecodeLogEntry(t *testing.T) {
if tc.want == "" { if tc.want == "" {
assert.Empty(t, s) assert.Empty(t, s)
} else { } else {
assert.True(t, strings.HasSuffix(s, tc.want), assert.True(t, strings.HasSuffix(s, tc.want), "got %q", s)
"got %q", s)
} }
logOutput.Reset() logOutput.Reset()

View File

@@ -6,7 +6,6 @@ import (
"testing" "testing"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxyutil"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
@@ -367,6 +366,6 @@ func assertLogEntry(t *testing.T, entry *logEntry, host string, answer, client n
require.NoError(t, msg.Unpack(entry.Answer)) require.NoError(t, msg.Unpack(entry.Answer))
require.Len(t, msg.Answer, 1) require.Len(t, msg.Answer, 1)
ip := proxyutil.IPFromRR(msg.Answer[0]).To16() a := testutil.RequireTypeAssert[*dns.A](t, msg.Answer[0])
assert.Equal(t, answer, ip) assert.Equal(t, answer, a.A.To16())
} }

View File

@@ -10,7 +10,7 @@ require (
github.com/kyoh86/looppointer v0.2.1 github.com/kyoh86/looppointer v0.2.1
github.com/securego/gosec/v2 v2.15.0 github.com/securego/gosec/v2 v2.15.0
golang.org/x/tools v0.8.0 golang.org/x/tools v0.8.0
golang.org/x/vuln v0.0.0-20230412133542-76c53851972b golang.org/x/vuln v0.0.0-20230418010118-28ba02ac73db
honnef.co/go/tools v0.4.3 honnef.co/go/tools v0.4.3
mvdan.cc/gofumpt v0.5.0 mvdan.cc/gofumpt v0.5.0
mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8 mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8

View File

@@ -92,8 +92,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
golang.org/x/vuln v0.0.0-20230412133542-76c53851972b h1:o/Q5x3MPiGccB6ViFm9Df21OWQz5qAUtEVsFaNXFR6o= golang.org/x/vuln v0.0.0-20230418010118-28ba02ac73db h1:tLxfII6jPR3mfwEMkyOakawu+Lldo9hIA7vliXnDZYg=
golang.org/x/vuln v0.0.0-20230412133542-76c53851972b/go.mod h1:64LpnL2PuSMzFYeCmJjYiRbroOUG9aCZYznINnF5PHE= golang.org/x/vuln v0.0.0-20230418010118-28ba02ac73db/go.mod h1:64LpnL2PuSMzFYeCmJjYiRbroOUG9aCZYznINnF5PHE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -4,6 +4,16 @@
## v0.108.0: API changes ## v0.108.0: API changes
## v0.107.30: API changes
### `POST /control/version.json` and `GET /control/dhcp/interfaces` content type
* The value of the `Content-Type` header in the `POST /control/version.json` and
`GET /control/dhcp/interfaces` HTTP APIs is now correctly set to
`application/json` as opposed to `text/plain`.
## v0.107.29: API changes ## v0.107.29: API changes
### `GET /control/clients` And `GET /control/clients/find` ### `GET /control/clients` And `GET /control/clients/find`
@@ -16,6 +26,8 @@
set AdGuard Home will use default value (false). It can be changed in the set AdGuard Home will use default value (false). It can be changed in the
future versions. future versions.
## v0.107.27: API changes ## v0.107.27: API changes
### The new optional fields `"edns_cs_use_custom"` and `"edns_cs_custom_ip"` in `DNSConfig` ### The new optional fields `"edns_cs_use_custom"` and `"edns_cs_custom_ip"` in `DNSConfig`

View File

@@ -161,11 +161,8 @@ run_linter "$GO" vet ./...
run_linter govulncheck ./... run_linter govulncheck ./...
# Apply more lax standards to the code we haven't properly refactored yet. # Apply more lax standards to the code we haven't properly refactored yet.
run_linter gocyclo --over 13\ run_linter gocyclo --over 13 ./internal/querylog
./internal/dhcpd\ run_linter gocyclo --over 12 ./internal/dhcpd
./internal/home/\
./internal/querylog/\
;
# Apply the normal standards to new or somewhat refactored code. # Apply the normal standards to new or somewhat refactored code.
run_linter gocyclo --over 10\ run_linter gocyclo --over 10\
@@ -175,6 +172,7 @@ run_linter gocyclo --over 10\
./internal/aghtest/\ ./internal/aghtest/\
./internal/dnsforward/\ ./internal/dnsforward/\
./internal/filtering/\ ./internal/filtering/\
./internal/home/\
./internal/stats/\ ./internal/stats/\
./internal/tools/\ ./internal/tools/\
./internal/updater/\ ./internal/updater/\