Compare commits

..

14 Commits

Author SHA1 Message Date
Ainar Garipov
9200163f85 all: sync with master 2022-08-17 18:23:30 +03:00
Ainar Garipov
3c17853344 cherry-pick: 4844-snap-core22
Closes #4843.
Updates #4844.

* commit '385a873b0f006f26832e73744845fdbc2864aad0':
  all: chlog
  Update Snap to Ubuntu Core 22 #4843
2022-08-17 18:16:57 +03:00
Eugene Burkov
993a3fc42c cherry-pick: 4358 stats races
Merge in DNS/adguard-home from 4358-stats-races to master

Updates #4358

Squashed commit of the following:

commit 162d17b04d95adad21fb9b3c5a6fb64df2e037ec
Merge: 17732cfa d4c3a43b
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Aug 17 14:04:20 2022 +0300

    Merge branch 'master' into 4358-stats-races

commit 17732cfa0f3b2589bf2c252697eee1d6b358a66c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Aug 17 13:53:42 2022 +0300

    stats: imp docs, locking

commit 4ee090869af0fa2b777c12027c3b77d5acd6e4de
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Aug 16 20:26:19 2022 +0300

    stats: revert const

commit a7681a1b882cef04511fcd5d569f5abe2f955239
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Aug 16 20:23:00 2022 +0300

    stats: imp concurrency

commit a6c6c1a0572e4201cd24644fd3f86f51fc27f633
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Aug 16 19:51:30 2022 +0300

    stats: imp code, tests, docs

commit 954196b49f5ad91d91f445ff656e63c318e4124c
Merge: 281e00da 6e63757f
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Aug 16 13:07:32 2022 +0300

    Merge branch 'master' into 4358-stats-races

commit 281e00daf781d045269584ce0158eed1d77918df
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Aug 12 16:22:18 2022 +0300

    stats: imp closing

commit ed036d9aa7e25498869edfb866b6e923538970eb
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Aug 12 16:11:12 2022 +0300

    stats: imp tests more

commit f848a12487ecd2afc8416e800510090cc1be7330
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Aug 12 13:54:19 2022 +0300

    stats: imp tests, code

commit 60e11f042d51ec68850143129e61c701c5e4f3a4
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Aug 11 16:36:07 2022 +0300

    stats: fix test

commit 6d97f1db093b5ce0d37984ff96a9ef6f4e02dba1
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Aug 11 14:53:21 2022 +0300

    stats: imp code, docs

commit 20c70c2847b0de6c7f9271a8d9a831175ed0c499
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Aug 10 20:53:36 2022 +0300

    stats: imp shared memory safety

commit 8b3945670a190bab070171e6b4976edab1e3e2a2
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Aug 10 17:22:55 2022 +0300

    stats: imp code
2022-08-17 18:15:41 +03:00
Ainar Garipov
7bb9b2416b cherry-pick: upd-specs
Merge in DNS/adguard-home from upd-specs to master

Squashed commit of the following:

commit d7ac1dc1ef305098ff741d557c13db8a60ffe1f9
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Aug 15 19:16:51 2022 +0300

    bamboo-specs: allow larger keys
2022-08-17 18:15:16 +03:00
Eugene Burkov
2de321ce24 cherry-pick: Fix frontend CI build
Merge in DNS/adguard-home from fix-bamboo-specs to master

Squashed commit of the following:

commit e59b75ab9528bbe8fbf5e15054d848abffbae312
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Aug 15 18:52:10 2022 +0300

    all: fix ci frontend build
2022-08-17 18:14:59 +03:00
Eugene Burkov
30b2b85ff1 cherry-pick: Separate front- and back- end builds
Merge in DNS/adguard-home from imp-bamboo-specs to master

Squashed commit of the following:

commit 3415b650e48aefef3ad16030be3d797de4015403
Merge: e37c0a2b f58265ec
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Aug 15 18:42:42 2022 +0300

    Merge branch 'master' into imp-bamboo-specs

commit e37c0a2bb52fab98e133332e8f54d500d0f96b06
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Aug 15 18:30:33 2022 +0300

    scripts: replace find with loop

commit 826a02f6a11000cce4b3205229d6bbb050c8dd73
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Aug 15 18:00:41 2022 +0300

    all: ...again

commit 54aebf5d4aeba35e3dc320436236759f4d1ccdad
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Aug 15 17:59:24 2022 +0300

    all: fix spec yaml

commit 87b92b30504f2427c40303265354afba4855e0bb
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Aug 15 17:48:19 2022 +0300

    all: separate front- and back-end builds
2022-08-17 18:14:44 +03:00
Ainar Garipov
6ea4788f56 cherry-pick: 4836-revert-dhcp-upd
Updates #4836.

Squashed commit of the following:

commit 6fe1721d44be1c23e524d477e28b5f7cc5dd2dc6
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Aug 15 17:48:41 2022 +0300

    dhcpd: reverd mod upd
2022-08-17 18:13:27 +03:00
Ainar Garipov
3c52a021b9 cherry-pick: add-ar-i18n
Merge in DNS/adguard-home from add-ar-i18n to master

Squashed commit of the following:

commit 6ef7c70bceb6f6ebabd81011154022a75fc91bd3
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Aug 10 20:55:39 2022 +0300

    client: add ar locale
2022-08-17 18:12:15 +03:00
Ainar Garipov
0ceea9af5f cherry-pick: upd-yaml
Merge in DNS/adguard-home from upd-yaml to master

Squashed commit of the following:

commit f0c3a1896e7eba73b1c8a02533637cdabc89909b
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Aug 8 15:28:02 2022 +0300

    home: restore indent lvl

commit b52c124d2e786e8575c58e75efa7d2cd2b70b67f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Mon Aug 8 15:06:41 2022 +0300

    all: upd tools, yaml mod
2022-08-17 18:10:49 +03:00
Eugene Burkov
39b404be19 cherry-pick: 4358 fix stats
Merge in DNS/adguard-home from 4358-fix-stats to master

Updates #4358.
Updates #4342.

Squashed commit of the following:

commit 5683cb304688ea639e5ba7f219a7bf12370211a4
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Aug 4 18:20:54 2022 +0300

    stats: rm races test

commit 63dd67650ed64eaf9685b955a4fdf3c0067a7f8c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Aug 4 17:13:36 2022 +0300

    stats: try to imp test

commit 59a0f249fc00566872db62e362c87bc0c201b333
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Aug 4 16:38:57 2022 +0300

    stats: fix nil ptr deref

commit 7fc3ff18a34a1d0e0fec3ca83a33f499ac752572
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Apr 7 16:02:51 2022 +0300

    stats: fix races finally, imp tests

commit c63f5f4e7929819fe79b3a1e392f6b91cd630846
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Aug 4 00:56:49 2022 +0300

    aghhttp: add register func

commit 61adc7f0e95279c1b7f4a0c0af5ab387ee461411
Merge: edbdb2d4 9b3adac1
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Aug 4 00:36:01 2022 +0300

    Merge branch 'master' into 4358-fix-stats

commit edbdb2d4c6a06dcbf8107a28c4c3a61ba394e907
Merge: a91e4d7a a481ff4c
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Aug 3 21:00:42 2022 +0300

    Merge branch 'master' into 4358-fix-stats

commit a91e4d7af13591eeef45cb7980d1ebc1650a5cb7
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Aug 3 18:46:19 2022 +0300

    stats: imp code, docs

commit c5f3814c5c1a734ca8ff6726cc9ffc1177a055cf
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Aug 3 18:16:13 2022 +0300

    all: log changes

commit 5e6caafc771dddc4c6be07c34658de359106fbe5
Merge: 091ba756 eb8e8166
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Aug 3 18:09:10 2022 +0300

    Merge branch 'master' into 4358-fix-stats

commit 091ba75618d3689b9c04f05431283417c8cc52f9
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Aug 3 18:07:39 2022 +0300

    stats: imp docs, code

commit f2b2de77ce5f0448d6df9232a614a3710f1e2e8a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Aug 2 17:09:30 2022 +0300

    all: refactor stats & add mutexes

commit b3f11c455ceaa3738ec20eefc46f866ff36ed046
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Apr 27 15:30:09 2022 +0300

    WIP
2022-08-17 18:10:16 +03:00
Ainar Garipov
56dc3eab02 cherry-pick: 4801-hassio-link
Updates #4801.

* commit '73f935f3f370ad7e1dfb2495fe71d1dc5e415672':
  Update Hass.io AdGuard Home integration link
2022-08-17 18:07:06 +03:00
Ainar Garipov
554a38eeb1 cherry-pick: 4800-upd-link
Updates #4800.

* commit 'bbccd616148f63240afee6ccf643179ff322c6f4':
  Update RFC 9250 link
2022-08-17 18:06:29 +03:00
Ainar Garipov
c8d3afe869 cherry-pick: 4670-invalid-arg-cap-check
Updates #4670.

Squashed commit of the following:

commit 9c32739eb92ef57c78a4dc3ec3c0f280aebf7182
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Aug 3 20:04:54 2022 +0300

    aghnet: imp port check for older linuxes
2022-08-17 18:05:28 +03:00
Ainar Garipov
44222c604c all: upd chlog 2022-08-17 18:05:16 +03:00
63 changed files with 2643 additions and 1237 deletions

View File

@@ -2,8 +2,11 @@
{
"project_id": "home",
"base_locale": "en",
"localizable_files": ["client/src/__locales/en.json"],
"localizable_files": [
"client/src/__locales/en.json"
],
"languages": {
"ar": "العربية",
"be": "Беларуская",
"bg": "Български",
"cs": "Český",

View File

@@ -20,27 +20,57 @@ and this project adheres to
- Weaker cipher suites that use the CBC (cipher block chaining) mode of
operation have been disabled ([#2993]).
### Added
- Support for Discovery of Designated Resolvers (DDR) according to the [RFC
draft][ddr-draft] ([#4463]).
### Deprecated
- Go 1.18 support. v0.109.0 will require at least Go 1.19 to build.
[#2993]: https://github.com/AdguardTeam/AdGuardHome/issues/2993
[ddr-draft]: https://datatracker.ietf.org/doc/html/draft-ietf-add-ddr-08
<!--
## [v0.107.10] - 2022-09-06 (APPROX.)
## [v0.107.11] - 2022-09-28 (APPROX.)
See also the [v0.107.11 GitHub milestone][ms-v0.107.11].
[ms-v0.107.11]: https://github.com/AdguardTeam/AdGuardHome/milestone/47?closed=1
-->
## [v0.107.10] - 2022-08-17
See also the [v0.107.10 GitHub milestone][ms-v0.107.10].
[ms-v0.107.10]: https://github.com/AdguardTeam/AdGuardHome/milestone/46?closed=1
### Added
- Arabic localization.
- Support for Discovery of Designated Resolvers (DDR) according to the [RFC
draft][ddr-draft] ([#4463]).
### Changed
- Our snap package now uses the `core22` image as its base ([#4843]).
### Fixed
- DHCP not working on most OSes ([#4836]).
- `invalid argument` errors during update checks on older Linux kernels
([#4670]).
- Data races and concurrent map access in statistics module ([#4358], [#4342]).
[#4342]: https://github.com/AdguardTeam/AdGuardHome/issues/4342
[#4358]: https://github.com/AdguardTeam/AdGuardHome/issues/4358
[#4670]: https://github.com/AdguardTeam/AdGuardHome/issues/4670
[#4836]: https://github.com/AdguardTeam/AdGuardHome/issues/4836
[#4843]: https://github.com/AdguardTeam/AdGuardHome/issues/4843
[ddr-draft]: https://datatracker.ietf.org/doc/html/draft-ietf-add-ddr-08
## [v0.107.9] - 2022-08-03
See also the [v0.107.9 GitHub milestone][ms-v0.107.9].
@@ -54,8 +84,8 @@ See also the [v0.107.9 GitHub milestone][ms-v0.107.9].
### Added
- Domain-specific upstream servers test. Such test fails with an appropriate
warning message ([#4517]).
- Domain-specific upstream servers test. If such test fails, a warning message
is shown ([#4517]).
- `windows/arm64` support ([#3057]).
### Changed
@@ -65,6 +95,7 @@ See also the [v0.107.9 GitHub milestone][ms-v0.107.9].
### Fixed
- DHCP not working on most OSes ([#4836]).
- Several UI issues ([#4775], [#4776], [#4782]).
### Removed
@@ -76,6 +107,7 @@ See also the [v0.107.9 GitHub milestone][ms-v0.107.9].
[#4775]: https://github.com/AdguardTeam/AdGuardHome/issues/4775
[#4776]: https://github.com/AdguardTeam/AdGuardHome/issues/4776
[#4782]: https://github.com/AdguardTeam/AdGuardHome/issues/4782
[#4836]: https://github.com/AdguardTeam/AdGuardHome/issues/4836
[go-1.18.5]: https://groups.google.com/g/golang-announce/c/YqYYG87xB10
[ms-v0.107.9]: https://github.com/AdguardTeam/AdGuardHome/milestone/45?closed=1
@@ -450,7 +482,7 @@ See also the [v0.107.0 GitHub milestone][ms-v0.107.0].
- Upstream server information for responses from cache ([#3772]). Note that old
log entries concerning cached responses won't include that information.
- Finnish and Ukrainian translations.
- Finnish and Ukrainian localizations.
- Setting the timeout for IP address pinging in the "Fastest IP address" mode
through the new `fastest_timeout` field in the configuration file ([#1992]).
- Static IP address detection on FreeBSD ([#3289]).
@@ -1085,11 +1117,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
<!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.10...HEAD
[v0.107.9]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.9...v0.107.10
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.11...HEAD
[v0.107.11]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.10...v0.107.11
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.9...HEAD
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.10...HEAD
[v0.107.10]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.9...v0.107.10
[v0.107.9]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.8...v0.107.9
[v0.107.8]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.7...v0.107.8
[v0.107.7]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.6...v0.107.7

View File

@@ -39,7 +39,7 @@ YARN_INSTALL_FLAGS = $(YARN_FLAGS) --network-timeout 120000 --silent\
# into BUILD_RELEASE_DEPS_0, and so both frontend and backend
# dependencies are fetched and the frontend is built. Otherwise, if
# FRONTEND_PREBUILT is 1, only backend dependencies are fetched and the
# frontend isn't reuilt.
# frontend isn't rebuilt.
#
# TODO(a.garipov): We could probably do that from .../build-release.sh,
# but that would mean either calling make from inside make or

View File

@@ -116,7 +116,7 @@ If you're running **Linux**, there's a secure and easy way to install AdGuard Ho
### API
If you want to integrate with AdGuard Home, you can use our [REST API](https://github.com/AdguardTeam/AdGuardHome/tree/master/openapi).
Alternatively, you can use this [python client](https://pypi.org/project/adguardhome/), which is used to build the [AdGuard Home Hass.io Add-on](https://community.home-assistant.io/t/community-hass-io-add-on-adguard-home).
Alternatively, you can use this [python client](https://pypi.org/project/adguardhome/), which is used to build the [AdGuard Home Hass.io Add-on](https://www.home-assistant.io/integrations/adguard/).
<a id="comparison"></a>
## Comparing AdGuard Home to other solutions

View File

@@ -10,6 +10,12 @@
'dockerGo': 'adguard/golang-ubuntu:5.0'
'stages':
- 'Build frontend':
'manual': false
'final': false
'jobs':
- 'Build frontend'
- 'Make release':
'manual': false
'final': false
@@ -40,11 +46,41 @@
'jobs':
- 'Publish to GitHub Releases'
'Make release':
'Build frontend':
'docker':
'image': '${bamboo.dockerGo}'
'volumes':
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
'key': 'BF'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
# Explicitly checkout the revision that we need.
git checkout "${bamboo.repository.revision.number}"
make js-deps js-build
'artifacts':
- 'name': 'AdGuardHome frontend'
'pattern': 'build*/**'
'shared': true
'required': true
'requirements':
- 'adg-docker': 'true'
'Make release':
'docker':
'image': '${bamboo.dockerGo}'
'volumes':
'${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
'${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
'key': 'MR'
@@ -65,13 +101,14 @@
git checkout "${bamboo.repository.revision.number}"
# Run the build with the specified channel.
echo "${bamboo.gpgSecretKey}"\
echo "${bamboo.gpgSecretKeyPart1}${bamboo.gpgSecretKeyPart2}"\
| awk '{ gsub(/\\n/, "\n"); print; }'\
| gpg --import --batch --yes
make\
CHANNEL=${bamboo.channel}\
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
FRONTEND_PREBUILT=1\
VERBOSE=1\
build-release
# TODO(a.garipov): Use more fine-grained artifact rules.

View File

@@ -0,0 +1,634 @@
{
"client_settings": "الإعدادات",
"example_upstream_reserved": "يمكنك تحديد <0> DNS upstream لنطاق معين (نطاقات) </0>",
"example_upstream_comment": "يمكنك تحديد تعليق",
"upstream_parallel": "استخدام الاستعلامات المتوازية لتسريع الحل عن طريق الاستعلام في وقت واحد عن جميع خوادم المنبع",
"parallel_requests": "طلبات موازية",
"load_balancing": "توزيع الحمل",
"load_balancing_desc": "الاستعلام عن خادم واحد في كل مرة سيستخدم AdGuard الرئيسية الخوارزمية العشوائية الموزونة لاختيار الخادم بحيث يتم استخدام أسرع خادم في كثير من الأحيان",
"bootstrap_dns": "خوادم Bootstrap DNS",
"bootstrap_dns_desc": "يتم استخدام خوادم Bootstrap DNS لحل عناوين IP الخاصة بمحللات DoH / DoT التي تحددها على هيئة تدفقات.",
"local_ptr_title": "خوادم DNS العكسية الخاصة",
"local_ptr_desc": "خوادم DNS التي يستخدمها AdGuard Home لاستعلامات PTR المحلية. تُستخدم هذه الخوادم لحل أسماء المضيفين للعملاء بعناوين IP خاصة ، على سبيل المثال \"192.168.12.34\" ، باستخدام DNS العكسي. في حالة عدم التعيين ، يستخدم AdGuard Home عناوين محللات DNS الافتراضية لنظام التشغيل الخاص بك باستثناء عناوين AdGuard Home نفسها.",
"local_ptr_default_resolver": "بشكل افتراضي ، يستخدم AdGuard Home محللات DNS العكسية التالية: {{ip}}.",
"local_ptr_no_default_resolver": "لم يتمكن AdGuard Home من تحديد محللات DNS العكسية المناسبة لهذا النظام.",
"local_ptr_placeholder": "أدخل عنوان خادم واحد لكل سطر",
"resolve_clients_title": "تفعيل التحليل العكسي لعناوين IP للعملاء",
"resolve_clients_desc": "حل عكسيًا لعناوين IP للعملاء في أسماء مضيفيهم عن طريق إرسال استعلامات PTR إلى أدوات الحل المقابلة (خوادم DNS الخاصة للعملاء المحليين ، والخوادم الأولية للعملاء الذين لديهم عناوين IP عامة).",
"use_private_ptr_resolvers_title": "استخدم محللات DNS العكسية الخاصة",
"use_private_ptr_resolvers_desc": "قم بإجراء عمليات بحث DNS عكسية عن العناوين التي يتم تقديمها محليًا باستخدام هذه الخوادم الأولية. في حالة التعطيل ، يستجيب AdGuard Home مع NXDOMAIN لجميع طلبات PTR هذه باستثناء العملاء المعروفين من DHCP و / etc / hosts وما إلى ذلك.",
"check_dhcp_servers": "تحقق من خوادم DHCP",
"save_config": "حفظ الإعدادات",
"enabled_dhcp": "خادم DHCP مفعل",
"disabled_dhcp": "خادم DHCP غير مفعل",
"unavailable_dhcp": "DHCP غير متوفر",
"unavailable_dhcp_desc": "لا يمكن لـ AdGuard Home تشغيل خادم DHCP على نظام التشغيل الخاص بك",
"dhcp_title": "خادم DHCP (تجريبي!)",
"dhcp_description": "إذا كان جهاز الراوتر الخاص بك لا يوفر إعدادات DHCP ، يمكنك استخدام خادم DHCP المدمج في AdGuard.",
"dhcp_enable": "فعل خادم DHCP",
"dhcp_disable": "عطل خادم DHCP",
"dhcp_not_found": "من الآمن تمكين خادم DHCP المدمج - لم نعثر على أي خوادم DHCP نشطة على الشبكة. ومع ذلك ، نشجعك على إعادة التحقق يدويًا لأن اختبارنا التلقائي في الوقت الحالي لا يوفر ضمانًا بنسبة 100٪.",
"dhcp_found": "تم العثور على خادم DHCP نشط على الشبكة. وبالتالي لا ينصح بتفعيل خادم DHCP المدمج.",
"dhcp_leases": "عقود إيجار DHCP",
"dhcp_static_leases": "إيجارات DHCP الثابتة",
"dhcp_leases_not_found": "لم يتم العثور على عقود إيجار DHCP",
"dhcp_config_saved": "الإعدادات محفوظة لخادم DHCP",
"dhcp_ipv4_settings": "DHCP IPv4 إعدادات",
"dhcp_ipv6_settings": "DHCP IPv6 إعدادات",
"form_error_required": "الحقل مطلوب",
"form_error_ip4_format": "عنوان IPv4 غير صالح",
"form_error_ip4_range_start_format": "عناوين البداية لـIPv4 غير صالحة للنطاق",
"form_error_ip4_range_end_format": "عناوين IPv4 غير صالحة لنطاق النهاية",
"form_error_ip4_gateway_format": "عنوان IPv4 غير صالح للبوابة",
"form_error_ip6_format": "عنوان IPv6 غير صالح",
"form_error_ip_format": "عنوان IP غير صحيح",
"form_error_mac_format": "عنوان MAC غير صالح",
"form_error_client_id_format": "يجب أن يحتوي معرف العميل على الأرقام والأحرف الصغيرة والواصلات فقط",
"form_error_server_name": "اسم الخادم غير صالح",
"form_error_subnet": "لا تحتوي الشبكة الفرعية \"{{cidr}}\" على عنوان IP \"{{ip}}\"",
"form_error_positive": "يجب أن يكون أكبر من 0",
"out_of_range_error": "يجب أن يكون خارج النطاق \"{{start}}\" - \"{{end}}\"",
"lower_range_start_error": "يجب أن يكون أقل من نطاق البداية",
"greater_range_start_error": "يجب أن يكون أكبر من نطاق البداية",
"greater_range_end_error": "يجب أن يكون أكبر من نطاق النهاية",
"subnet_error": "يجب أن تكون العناوين في شبكة فرعية واحدة",
"gateway_or_subnet_invalid": "قناع الشبكة الفرعية غير صالح",
"dhcp_form_gateway_input": "IP البوابة",
"dhcp_form_subnet_input": "قناع الشبكة الفرعية",
"dhcp_form_range_title": "مجموعة عناوين IP",
"dhcp_form_range_start": "نطاق البداية",
"dhcp_form_range_end": "نطاق النهاية",
"dhcp_form_lease_title": "مدة تأجير DHCP (بالثواني)",
"dhcp_form_lease_input": "مدة الإيجار",
"dhcp_interface_select": "حدد واجهة DHCP",
"dhcp_hardware_address": "عناوين الأجهزة",
"dhcp_ip_addresses": "عناوين الـIP",
"ip": "IP",
"dhcp_table_hostname": "اسم المضيف",
"dhcp_table_expires": "يتنهي في",
"dhcp_warning": "إذا كنت تريد تمكين خادم DHCP على أي حال ، فتأكد من عدم وجود خادم DHCP نشط آخر في شبكتك. خلاف ذلك ، يمكن أن يعطل خدمة الإنترنت للأجهزة المتصلة!",
"dhcp_error": "لم نتمكن من تحديد ما إذا كان هناك خادم DHCP آخر في الشبكة.",
"dhcp_static_ip_error": "من أجل استخدام خادم DHCP ، يجب تعيين عنوان IP ثابت. فشلنا في تحديد ما إذا تم تكوين واجهة الشبكة هذه باستخدام عنوان IP ثابت. يرجى تعيين عنوان IP ثابت يدويًا.",
"dhcp_dynamic_ip_found": "يستخدم نظامك عنوان IP الديناميكي للواجهة <0>{{interfaceName}}</0>. من أجل استعمال خادم DHCP ، يجب تعيين عنوان IP ثابت. عنوان IP الحالي الخاص بك هو <0>{{ipAddress}}</0>. إذا ضغطت على زر تفعيل DHCP سنقوم تلقائيًا بتعيين عنوان الIP هذا على أنه ثابت.",
"dhcp_lease_added": "تمت أضافة مدة الايجار \"{{key}}\" بنجاح",
"dhcp_lease_deleted": "تمت ازالة مدة الايجار \"{{key}}\" بنجاح",
"dhcp_new_static_lease": "عقد إيجار ثابت جديد",
"dhcp_static_leases_not_found": "لم يتم العثور على عقود إيجار ثابتة DHCP",
"dhcp_add_static_lease": "إضافة عقد إيجار ثابت",
"dhcp_reset_leases": "إعادة تعيين كافة عقود الإيجار",
"dhcp_reset_leases_confirm": "هل أنت متأكد أنك تريد إعادة تعيين كافة عقود الإيجار؟",
"dhcp_reset_leases_success": "إعادة تعيين تأجير DHCP بنجاح",
"dhcp_reset": "هل أنت متأكد من أنك تريد إعادة تعيين تكوين DHCP؟",
"country": "الدولة",
"city": "المدينة",
"delete_confirm": "هل أنت متأكد من أنك تريد حذف \"{{key}}\"؟",
"form_enter_hostname": "أدخل اسم الhostname",
"error_details": "مزيد من التفاصيل حول الخطأ",
"response_details": "تفاصيل الاستجابة",
"request_details": "تفاصيل الطلب",
"client_details": "تفاصيل العميل",
"details": "التفاصيل",
"back": "رجوع",
"dashboard": "لوحة القيادة",
"settings": "الإعدادات",
"filters": "الفلاتر",
"filter": "فلتر",
"query_log": "سجل الQuery",
"compact": "المدمج",
"nothing_found": "لم يتم العثور علي شيء...",
"faq": "أسئلة مكررة",
"version": "الإصدار",
"address": "العناوين",
"protocol": "البروتوكول",
"on": "ON",
"off": "OFF",
"copyright": "حقوق النشر",
"homepage": "الصفحة الرئيسية",
"report_an_issue": "الإبلاغ عن مشكلة",
"privacy_policy": "سياسة الخصوصية",
"enable_protection": "تفعيل الحماية",
"enabled_protection": "الحماية مفعلة",
"disable_protection": "تعطيل الحماية",
"disabled_protection": "الحماية غير مفعلة",
"refresh_statics": "تحيين الإحصائيات",
"dns_query": "DNS Queries",
"blocked_by": "<0>تم حظره بواسطة الفلاتر</0>",
"stats_malware_phishing": "حسر البرامج الضارة / والتصيّد",
"stats_adult": "حظر مواقع الويب الخاصة بالبالغين",
"stats_query_domain": "اعلى النطاقات التي تم الاستعلام عنها",
"for_last_24_hours": "لأخر 24 ساعة",
"for_last_days": "لآخر {{value}} يوم",
"for_last_days_plural": "لآخر {{count}} ايام",
"stats_disabled": "تم تعطيل الإحصائيات. يمكنك تشغيله من <0> صفحة الإعدادات </0>.",
"stats_disabled_short": "تم تعطيل الإحصائيات",
"no_domains_found": "لم يتم العثور على النطاق",
"requests_count": "عدد الطلبات",
"top_blocked_domains": "اعلى الدومينات المحظورة",
"top_clients": "كبار العملاء",
"no_clients_found": "لم يتم العثور على عملاء",
"general_statistics": "الإحصاءات العامة",
"number_of_dns_query_days": "عدد استعلامات DNS التي تمت معالجتها لآخر {{count}} يوم",
"number_of_dns_query_days_plural": "عدد استعلامات DNS التي تمت معالجتها لآخر {{count}} أيام",
"number_of_dns_query_24_hours": "عدد استعلامات DNS التي تمت معالجتها لآخر 24 ساعة",
"number_of_dns_query_blocked_24_hours": "عدد طلبات DNS المحظورة بواسطة فلاتر adblock وقوائم حظر المضيفين",
"number_of_dns_query_blocked_24_hours_by_sec": "عدد طلبات DNS التي تم حظرها من قبل وحدة أمان التصفح AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "عدد من المواقع (الإباحية) للبالغين تم حجبها",
"enforced_save_search": "فرض البحث الآمن",
"number_of_dns_query_to_safe_search": "عدد طلبات DNS لمحركات البحث التي تم فرض البحث الآمن عنها",
"average_processing_time": "متوسط وقت المعالجة",
"average_processing_time_hint": "متوسط الوقت بالمللي ثانية عند معالجة طلب DNS",
"block_domain_use_filters_and_hosts": "حظر النطاقات باستخدام عوامل التصفية وملفات المضيفين",
"filters_block_toggle_hint": "يمكنك إعداد قواعد حظر في <a>المرشحات</a> اعدادات.",
"use_adguard_browsing_sec": "استخدم خدمة الويب الأمنية لتصفح AdGuard",
"use_adguard_browsing_sec_hint": "سيتحقق AdGuard Home مما إذا كان النطاق محظورًا بواسطة خدمة الويب الخاصة بأمان التصفح. سيستخدم واجهة برمجة تطبيقات بحث صديقة للخصوصية لإجراء الفحص: يتم إرسال بادئة قصيرة فقط من تجزئة اسم المجال SHA256 إلى الخادم.",
"use_adguard_parental": "استخدام خدمة AdGuard للرقابة الأبوية على الويب",
"use_adguard_parental_hint": "سيتحقق AdGuard Home مما إذا كان النطاق يحتوي على محتوى للبالغين. إنه يستخدم نفس واجهة برمجة التطبيقات الصديقة للخصوصية مثل خدمة الويب الأمنية للتصفح.",
"enforce_safe_search": "استخدم البحث الآمن",
"enforce_save_search_hint": "سيفرض AdGuard Home البحث الآمن في محركات البحث التالية: Google وYouTube وBing وDuckDuckGo وYandex وPixabay.",
"no_servers_specified": "لم يتم تحديد خوادم",
"general_settings": "الإعدادات العامة",
"dns_settings": "إعدادات الـ DNS",
"dns_blocklists": "قوائم حظر DNS",
"dns_allowlists": "قوائم السماح لـ DNS",
"dns_blocklists_desc": "سيقوم AdGuard Home بحظر النطاقات المطابقة لقوائم الحظر",
"dns_allowlists_desc": "سيتم السماح بالنطاقات من قوائم DNS المسموحة حتى لو كانت في أي من قوائم الحظر",
"custom_filtering_rules": "قواعد التصفية المخصصة",
"encryption_settings": "إعدادات التعمية",
"dhcp_settings": "إعدادات DHCP",
"upstream_dns": "خادم DNS لـ Upstream",
"upstream_dns_help": "أدخل عنوان خادم واحد في كل سطر. <a>تعرف على المزيد</a> حول تكوين خوادم DNS الأولية.",
"upstream_dns_configured_in_file": "تم اعداده في {{path}}",
"test_upstream_btn": "اختبار upstream",
"upstreams": "Upstreams",
"apply_btn": "تطبيق",
"disabled_filtering_toast": "تم تعطيل الفلترة",
"enabled_filtering_toast": "تم تمكين الفلترة",
"disabled_safe_browsing_toast": "تم تعطيل التصفح الآمن",
"enabled_safe_browsing_toast": "تم تمكين التصفح الآمن",
"disabled_parental_toast": "تعطيل الرقابة الأبوية",
"enabled_parental_toast": "تفعيل الرقابة الأبوية",
"disabled_safe_search_toast": "تعطيل البحث الآمن",
"enabled_save_search_toast": "تفعيل البحث الآمن",
"enabled_table_header": "تمكين",
"name_table_header": "الاسم",
"list_url_table_header": "قائمة الروابط",
"rules_count_table_header": "عدد القواعد",
"last_time_updated_table_header": "آخر تحديث",
"actions_table_header": "الإجراءات",
"request_table_header": "طلب",
"edit_table_action": "تحرير",
"delete_table_action": "حذف",
"elapsed": "المنقضي",
"filters_and_hosts_hint": "يفهم AdGuard Home قواعد حظر الإعلانات الاساسية وملفات الهوست.",
"no_blocklist_added": "لم يتم إضافة قوائم الحظر",
"no_whitelist_added": "لم تتم إضافة قوائم السماح",
"add_blocklist": "إضافة قائمة الحظر",
"add_allowlist": "إضافة قائمة السماح",
"cancel_btn": "إلغاء",
"enter_name_hint": "أدخل الاسم",
"enter_url_or_path_hint": "إدخال عنوان URL أو مسار مطلق للقائمة",
"check_updates_btn": "تحقق من وجود تحديثات",
"new_blocklist": "قائمة حظر جديدة",
"new_allowlist": "قائمة السماح الجديدة",
"edit_blocklist": "تحرير قائمة الحظر",
"edit_allowlist": "تحرير قائمة السماح",
"choose_blocklist": "اختر قوائم الحظر",
"choose_allowlist": "اختر قوائم السماح",
"enter_valid_blocklist": "إدخال عنوان URL صالح إلى قائمة الحظر",
"enter_valid_allowlist": "أدخل عنوان URL صالحًا لقائمة السماح",
"form_error_url_format": "تنسيق رابط غير صالح",
"form_error_url_or_path_format": "عنوان URL أو المسار المطلق للقائمة غير صالح",
"custom_filter_rules": "قواعد التصفية المخصصة",
"custom_filter_rules_hint": "أدخل قاعدة واحدة على السطر يمكنك استخدام قواعد adblock أو بناء جملة ملفات المضيفين",
"system_host_files": "ملفات الهوست للنظام",
"examples_title": "أمثلة",
"example_meaning_filter_block": "منع الوصول إلى نطاق example.org وجميع نطاقاته الفرعية",
"example_meaning_filter_whitelist": "إلغاء حظر الوصول إلى نطاق example.org وجميع نطاقاته الفرعية",
"example_meaning_host_block": "الرد ب 127.0.0.1 على example.org (ولكن ليس لنطاقاته الفرعية);",
"example_comment": "! ها هو التعليق.",
"example_comment_meaning": "فقط تعليق;",
"example_comment_hash": "# تعليق أيضًا",
"example_regex_meaning": "منع الوصول إلى النطاقات المطابقة للتعبير العادي المحدد.",
"example_upstream_regular": "regular DNS (over UDP);",
"example_upstream_udp": "regular DNS (over UDP, hostname);",
"example_upstream_dot": "مشفر<0>DNS-over-TLS</0>;",
"example_upstream_doh": "مشفر <0>DNS-over-HTTPS</0>;",
"example_upstream_doq": "encrypted <0>DNS-over-QUIC</0>;",
"example_upstream_sdns": "<0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> resolvers;",
"example_upstream_tcp": "regular DNS (over TCP);",
"example_upstream_tcp_hostname": "regular DNS (over TCP, hostname);",
"all_lists_up_to_date_toast": "جميع القوائم محدثة بالفعل",
"updated_upstream_dns_toast": "تم حفظ خوادم Upstream بنجاح",
"dns_test_ok_toast": "تعمل خوادم DNS المحددة بشكل صحيح",
"dns_test_not_ok_toast": "خادم \"{{key}}\": لا يمكن استخدامه يرجى التحقق من كتابته بشكل صحيح",
"unblock": "إلغاء الحظر",
"block": "حظر",
"disallow_this_client": "منع هذا العميل",
"allow_this_client": "السماح لهذا العميل",
"block_for_this_client_only": "احجب هذا العميل فقط",
"unblock_for_this_client_only": "إلغاء حجب هذا العميل فقط",
"time_table_header": "وقت",
"date": "التاريخ",
"domain_name_table_header": "اسم النطاق",
"domain_or_client": "الدومين أو العميل",
"type_table_header": "النوع",
"response_table_header": "استجابة",
"response_code": "كود الاستجابة",
"client_table_header": "عميل",
"empty_response_status": "فارغ",
"show_all_filter_type": "إظهار الكل",
"show_filtered_type": "إظهار ماتمت تصفيته",
"no_logs_found": "لم يتم العثور على سجلات",
"refresh_btn": "تحديث",
"previous_btn": "السابق",
"next_btn": "التالي",
"loading_table_status": "جار التحميل...",
"page_table_footer_text": "الصفحة",
"rows_table_footer_text": "صفوف",
"updated_custom_filtering_toast": "تحديث قواعد الفلترة المخصصة",
"rule_removed_from_custom_filtering_toast": "تم إزالة قاعدة من قواعد الفلترة المخصصة: {{rule}}",
"rule_added_to_custom_filtering_toast": "تم إضافة إلى قواعد الفلترة المخصصة: {{rule}}",
"query_log_response_status": "الحالات: {{value}}",
"query_log_filtered": "تم الفلترة بواسطة {{filter}}",
"query_log_confirm_clear": "هل أنت متأكد من أنك تريد محو كامل سجل التصفية؟",
"query_log_cleared": "تم مسح سجل الاستعلام بنجاح",
"query_log_updated": "تم تحديث سجل الاستعلام بنجاح",
"query_log_clear": "مسح سجلات الاستعلام",
"query_log_retention": "الاحتفاظ بسجلات الاستعلام",
"query_log_enable": "تمكين السجل",
"query_log_configuration": "تكوين السجلات",
"query_log_disabled": "سجل الاستعلام معطل ويمكن تهيئته من<0>الاعدادات</0>",
"query_log_strict_search": "استخدم علامات الاقتباس المزدوجة للبحث الدقيق",
"query_log_retention_confirm": "هل أنت متأكد من أنك تريد تغيير الاحتفاظ بسجل الاستعلام؟ إذا قمت بتقليل قيمة الفاصل الزمني سيتم فقدان بعض البيانات",
"anonymize_client_ip": "إخفاء عنوان IP العميل",
"anonymize_client_ip_desc": "لا تقم بحفظ كامل عنوان IP العميل في السجلات والإحصائيات",
"dns_config": "إعداد خادم DNS",
"dns_cache_config": "ضبط الملفات المؤقتة لـ DNS",
"dns_cache_config_desc": "هنا تستطيع ضبط اعدادات الـ DNS وملفاته",
"blocking_mode": "وضع الحجب",
"default": "إفتراضي",
"nxdomain": "NXDOMAIN",
"refused": "مرفوض",
"null_ip": "عنوان IP فارغ",
"custom_ip": "عنوان IP مخصص",
"blocking_ipv4": "حجب عنوان IPv4",
"blocking_ipv6": "حجب عنوان IPv6",
"dnscrypt": "DNSCrypt",
"dns_over_https": "DNS-over-HTTPS",
"dns_over_tls": "DNS-over-TLS",
"dns_over_quic": "DNS-over-QUIC",
"client_id": "عنوان العميل الشخصي",
"client_id_placeholder": "ادخل عنوان العميل الشخصي",
"client_id_desc": "يمكن تحديد هوية العميل. اعرف المزيد عن كيفية تحديد هوية العملاء <a> هنا</a>.",
"download_mobileconfig_doh": "حمّل .mobileconfig for DNS-over-HTTPS",
"download_mobileconfig_dot": "حمل .mobileconfig for DNS-over-TLS",
"download_mobileconfig": "حمّل ملف الإعدادات",
"plain_dns": "عنوان DNS العادي",
"form_enter_rate_limit": "ادخل حد التقييم",
"rate_limit": "حدود التقييم",
"edns_enable": "فعل EDNS client subnet",
"edns_cs_desc": "أضف EDNS الشبكة الفرعية للعميل (ECS) إلى الطلبات الأولية وقم بتسجيل القيم المرسلة من قبل العملاء في سجل الاستعلام.",
"rate_limit_desc": "عدد الطلبات في الثانية المسموح بها لكل عميل. جعله على 0 يعني عدم وجود حد.",
"blocking_ipv4_desc": "سيتم إرجاع عنوان IP لطلب محظور",
"blocking_ipv6_desc": "سيتم إرجاع عنوان IP لطلب AAAA محظور",
"blocking_mode_default": "الافتراضي: الرد بعنوان IP صفري (0.0.0.0 لـ A ؛ :: لـ AAAA) عند حظره بواسطة قاعدة نمط Adblock ؛ الرد بعنوان IP المحدد في القاعدة عند حظره بواسطة / etc / hosts-style rule",
"blocking_mode_refused": "مرفوض: رد برمز مرفوض",
"blocking_mode_nxdomain": "NXDOMAIN: الرد باستخدام رمز NXDOMAIN",
"blocking_mode_null_ip": "IP Null: الاستجابة بعنوان IP صفري (0.0.0.0 لـ A ؛ :: لـ AAAA)",
"blocking_mode_custom_ip": "استجابة IP مخصصة بعنوان IP تم تعيينه يدويًا",
"upstream_dns_client_desc": "إذا احتفظت بهذا الحقل فارغًا ، فسيستخدم AdGuard Home الخوادم التي تم تكوينها في<0>DNS إعدادات</0>.",
"tracker_source": "مصدر المتعقب",
"source_label": "المصدر",
"found_in_known_domain_db": "تم العثور عليه في قاعدة بيانات دومينات معروفة.",
"category_label": "الفئة",
"rule_label": "قواعد",
"list_label": "قائمه",
"unknown_filter": "فلتر غير معروف {{filterId}}",
"known_tracker": "متعقب معروف",
"install_welcome_title": "مرحبًا بك في AdGuard Home!",
"install_welcome_desc": "AdGuard Home هو إعلان ومتتبع على مستوى الشبكة يمنع خادم DNS. الغرض منه هو السماح لك بالتحكم في شبكتك بأكملها وجميع أجهزتك، ولا يتطلب استخدام برنامج من جانب العميل.",
"install_settings_title": "واجهة ويب المسؤول",
"install_settings_listen": "واجهة الاستماع",
"install_settings_port": "المنفذ",
"install_settings_interface_link": "ستكون واجهة الويب الخاصة بمسؤول AdGuard Home متاحة على العناوين التالية:",
"form_error_port": "أدخل رقم منفذ صالح",
"install_settings_dns": "خادم DNS",
"install_settings_dns_desc": "ستحتاج إلى ضبط أجهزتك أو جهاز التوجيه الخاص بك لاستخدام خادم DNS على العناوين التالية:",
"install_settings_all_interfaces": "جميع الواجهات",
"install_auth_title": "المصادقة",
"install_auth_desc": "يجب إعداد مصادقة كلمة المرور لواجهة ويب مسؤول AdGuard Home. في حال كان AdGuard Home لا يمكن الوصول إليه إلا في شبكتك المحلية ، فلا يزال من المهم حمايته من الوصول غير المقيد.",
"install_auth_username": "اسم المستخدم",
"install_auth_password": "الكلمة السرية",
"install_auth_confirm": "تاكيد كلمه المرور",
"install_auth_username_enter": "أدخل اسم المستخدم",
"install_auth_password_enter": "أدخل كلمة المرور",
"install_step": "خطوة",
"install_devices_title": "قم بإعداد أجهزتك",
"install_devices_desc": "لبدء استخدام AdGuard Home، تحتاج إلى إعداد أجهزتك لاستخدامها.",
"install_submit_title": "تهانينا!",
"install_submit_desc": "انتهى إجراء الإعداد وأنت على استعداد لبدء استخدام AdGuard Home",
"install_devices_router": "راوتر",
"install_devices_router_desc": "يغطي هذا الإعداد تلقائيا جميع الأجهزة المتصلة بجهاز التوجيه المنزلي، دون الحاجة إلى تكوين كل منها يدويا.",
"install_devices_address": "يستمع خادم AdGuard Home DNS إلى العناوين التالية",
"install_devices_router_list_1": "افتح تفضيلات جهاز التوجيه الخاص بك. عادة، يمكنك الوصول إليه من متصفحك عبر عنوان URL، مثل http://192.168.0.1/ أو http://192.168.1.1/. قد يطلب منك إدخال كلمة مرور. إذا كنت لا تتذكر ذلك، يمكنك في كثير من الأحيان إعادة تعيين كلمة المرور عن طريق الضغط على زر في جهاز التوجيه نفسه، ولكن كن على علم بأنه إذا تم اختيار هذا الإجراء، فمن المحتمل أن تفقد إعدادات جهاز التوجيه بأكمله. إذا كان جهاز التوجيه الخاص بك يتطلب تطبيقا لإعداده، فيرجى تثبيت التطبيق على هاتفك أو الكمبيوتر الشخصي واستخدامه للوصول إلى إعدادات جهاز التوجيه.",
"install_devices_router_list_2": "ابحث عن إعدادات DHCP / DNS. ابحث عن أحرف DNS بجوار الحقل الذي يسمح بمجموعتين أو ثلاث مجموعات من الأرقام ، كل واحدة مقسمة إلى أربع مجموعات من واحد إلى ثلاثة أرقام.",
"install_devices_router_list_3": "أدخل عناوين خادم AdGuard Home هناك.",
"install_devices_router_list_4": "في بعض أنواع أجهزة التوجيه ، لا يمكن إعداد خادم DNS مخصص. في هذه الحالة ، قد يساعد إعداد AdGuard Home باعتباره <0>خادم DHCP</0>. بخلاف ذلك ، يجب عليك التحقق من دليل جهاز التوجيه حول كيفية تخصيص خوادم DNS على طراز جهاز التوجيه المحدد الخاص بك.",
"install_devices_windows_list_1": "افتح لوحة التحكم من خلال قائمة ابدأ أو بحث Windows.",
"install_devices_windows_list_2": "انتقل إلى فئة الشبكة والإنترنت ثم إلى مركز الشبكة والمشاركة.",
"install_devices_windows_list_3": "على الجانب الأيسر من الشاشة ، ابحث عن \"تغيير إعدادات المحول\" وانقر عليها.",
"install_devices_windows_list_4": "حدد اتصالك النشط ، وانقر فوقه بزر الماوس الأيمن واختر خصائص.",
"install_devices_windows_list_5": "ابحث عن \"Internet Protocol Version 4 (TCP / IPv4)\" (أو ، لـ IPv6 ، \"Internet Protocol Version 6 (TCP / IPv6)\") في القائمة ، حدده ثم انقر فوق خصائص مرة أخرى.",
"install_devices_windows_list_6": "اختر \"استخدام عناوين خادم DNS التالية\" وأدخل عناوين خادم AdGuard Home.",
"install_devices_macos_list_1": "انقر فوق أيقونة Apple وانتقل إلى تفضيلات النظام.",
"install_devices_macos_list_2": "اضغط على الشبكة.",
"install_devices_macos_list_3": "حدد الاتصال الأول في قائمتك وانقر فوق خيارات متقدمة.",
"install_devices_macos_list_4": "حدد علامة التبويب DNS وأدخل عناوين خادم AdGuard Home.",
"install_devices_android_list_1": "من الشاشة الرئيسية لقائمة Android ، انقر فوق الإعدادات.",
"install_devices_android_list_2": "اضغط على Wi-Fi في القائمة. ستظهر الشاشة التي تسرد جميع الشبكات المتاحة (من المستحيل تعيين DNS مخصص لاتصال المحمول).",
"install_devices_android_list_3": "اضغط لفترة طويلة على الشبكة التي تتصل بها ثم اضغط على تعديل الشبكة",
"install_devices_android_list_4": "في بعض الأجهزة قد تحتاج إلى تحديد المربع المتقدم لرؤية المزيد من الإعدادات لضبط إعدادات DNS لنظام اندرويد ستحتاج إلى تبديل إعدادات IP من DHCP إلى ثابت.",
"install_devices_android_list_5": "قم بتغيير قيم DNS 1 و DNS 2 المعينة لعناوين خادم AdGuard Home",
"install_devices_ios_list_1": "من الشاشة الرئيسية انقر فوق الإعدادات",
"install_devices_ios_list_2": "اختر Wi-Fi في القائمة اليسرى (من المستحيل ضبط الـ DNS لشبكات الجوال).",
"install_devices_ios_list_3": "اضغط على اسم الشبكة النشطة حاليًا.",
"install_devices_ios_list_4": "في حقل DNS ، أدخل عناوين خادم AdGuard Home.",
"get_started": "أبدأ",
"next": "التالي",
"open_dashboard": "افتح لوحة التحكم",
"install_saved": "تم الحفظ بنجاح",
"encryption_title": "التعمية",
"encryption_desc": "دعم التشفير (HTTPS / TLS) لكل من DNS وواجهة ويب المسؤول",
"encryption_config_saved": "تم حفظ اعدادات التشفير",
"encryption_server": "اسم الخادم",
"encryption_server_enter": "ادخل عنوان النطاق الخاص بك",
"encryption_redirect": "إعادة التوجيه إلى HTTPS تلقائيًا",
"encryption_redirect_desc": "إذا تم تحديده ، فسيقوم AdGuard Home بإعادة توجيهك تلقائيًا من عناوين HTTP إلى عناوين HTTPS.",
"encryption_https": "منفذ HTTPS",
"encryption_https_desc": "إذا تم تكوين منفذ HTTPS ، فسيتم الوصول إلى واجهة مشرف AdGuard Home عبر HTTPS ، وستوفر أيضًا DNS-over-HTTPS على موقع '/dns-query'.",
"encryption_dot": "منفذ DNS-over-TLS",
"encryption_dot_desc": "إذا تم ضبط هذا المنفذ ، فسيقوم AdGuard Home بتشغيل خادم DNS-over-TLS على هذا المنفذ.",
"encryption_doq": "DNS-over-QUIC port",
"encryption_doq_desc": "إذا تم ضبط هذا المنفذ، فسيقوم AdGuard Home بتشغيل خادم DNS-over-QUIC على هذا المنفذ.",
"encryption_certificates": "الشهادات",
"encryption_certificates_desc": "من أجل استخدام التشفير ، تحتاج إلى تقديم سلسلة شهادات SSL صالحة لنطاقك. يمكنك الحصول على شهادة مجانية على <0>{{link}}</0> أو يمكنك شرائها من أحد المراجع المصدقة الموثوقة.",
"encryption_certificates_input": "انسخ / الصق الشهادات المشفرة PEM هنا.",
"encryption_status": "الحالة",
"encryption_expire": "يتنهي في",
"encryption_key": "مفتاح خاص",
"encryption_key_input": "انسخ / الصق مفتاحك الخاص المشفر بـ PEM لشهادتك هنا",
"encryption_enable": "تمكين التشفير (HTTPS و DNS-over-HTTPS و DNS-over-TLS)",
"encryption_enable_desc": "إذا تم تمكين التشفير فستعمل واجهة مسؤول AdGuard Home عبر HTTPS وسيستمع خادم DNS للطلبات عبر DNS-over-HTTPS و DNS-over-TLS.",
"encryption_chain_valid": "سلسلة الشهادات صالحة",
"encryption_chain_invalid": "سلسلة الشهادات غير صالحة",
"encryption_key_valid": "هذا مفتاح خاص {{type}} صالح",
"encryption_key_invalid": "هذا مفتاح خاص {{type}} غير صالح",
"encryption_subject": "الموضوع",
"encryption_issuer": "المصدر",
"encryption_hostnames": "اسم المستضيف",
"encryption_reset": "هل أنت متأكد أنك تريد إعادة تعيين إعدادات التشفير؟",
"topline_expiring_certificate": "شهادة SSL الخاصة بك على وشك الانتهاء. قم بتحديث <0>إعدادات التشفير</0>.",
"topline_expired_certificate": "انتهت صلاحية شهادة SSL الخاصة بك. قم بتحديث <0>إعدادات التشفير</0>.",
"form_error_port_range": "أدخل رقم المنفذ في النطاق 80-65535",
"form_error_port_unsafe": "منفذ غير آمن",
"form_error_equal": "يجب ألا تكون متساوية",
"form_error_password": "كلمة السر غير مطابقة",
"reset_settings": "إعادة ضبط الإعدادات",
"update_announcement": "AdGuard Home {{version}} متوفر الآن! <0>انقر هنا</0> لمزيد من المعلومات.",
"setup_guide": "دليل الإعداد",
"dns_addresses": "عناوين DNS",
"dns_start": "خادم DNS قيد التشغيل",
"dns_status_error": "خطأ في التحقق من حالة خادم الـ DNS",
"down": "تحت",
"fix": "يصلح",
"dns_providers": "فيما يلي قائمة <0> بموفري DNS المعروفين </0> للاختيار من بينها.",
"update_now": "تحديث الآن",
"update_failed": "فشل التحديث التلقائي. الرجاء <a> اتباع هذه الخطوات </a> للتحديث يدويًا.",
"manual_update": "الرجاء <a> اتباع هذه الخطوات </a> للتحديث يدويًا.",
"processing_update": "يُرجى الانتظار ، يتم تحديث صفحة AdGuard الرئيسية",
"clients_title": "العملاء الدائمين",
"clients_desc": "قم بضبط سجلات العميل الدائمة للأجهزة المتصلة بـ AdGuard Home",
"settings_global": "عالمي",
"settings_custom": "مخصص",
"table_client": "العميل",
"table_name": "الاسم",
"save_btn": "حفظ",
"client_add": "إضافة عميل",
"client_new": "عميل جديد",
"client_edit": "تعديل العميل",
"client_identifier": "المعّرف",
"ip_address": "عنوان IP",
"client_identifier_desc": "يمكن التعرف على العملاء من خلال عنوان IP أو CIDR أو عنوان MAC أو ClientID (يمكن استخدامه في DoT / DoH / DoQ). تعرف على المزيد حول كيفية تحديد العملاء <0> هنا </0>.",
"form_enter_ip": "ادخل عنوان IP",
"form_enter_subnet_ip": "أدخل عنوان IP في الشبكة الفرعية \"{{cidr}}\"",
"form_enter_mac": "ادخل MAC",
"form_enter_id": "ادخل المعّرف",
"form_add_id": "أضافة معّرف",
"form_client_name": "ادخل اسم العميل",
"name": "اسم",
"client_global_settings": "استخدم إعدادات عالمية",
"client_deleted": "تم حذف العميل \"{{key}}\" بنجاح",
"client_added": "تم اضافة العميل \"{{key}}\" بنجاح",
"client_updated": "تم تحديث العميل \"{{key}}\" بنجاح",
"clients_not_found": "لم يتم العثور على عملاء",
"client_confirm_delete": "هل أنت متأكد من أنك تريد حذف العميل \"{{key}}\"?",
"list_confirm_delete": "هل أنت متأكد أنك تريد حذف هذه القائمة؟",
"auto_clients_title": "Runtime clients",
"auto_clients_desc": "الأجهزة غير المدرجة في قائمة العملاء الدائمين الذين قد لا يزالون يستخدمون AdGuard Home",
"access_title": "إعدادات الوصول",
"access_desc": "هنا يمكنك ضبط قواعد الوصول لخادم AdGuard Home DNS",
"access_allowed_title": "العملاء المسموحين",
"access_allowed_desc": "قائمة CIDRs أو عناوين IP أو <a> ClientIDs </a>. إذا كانت هذه القائمة تحتوي على إدخالات ، فسيقبل AdGuard Home الطلبات من هؤلاء العملاء فقط.",
"access_disallowed_title": "العملاء غير المسموحين",
"access_disallowed_desc": "قائمة CIDRs أو عناوين IP أو <a> ClientIDs </a>. إذا كانت هذه القائمة تحتوي على إدخالات ، فسيقوم AdGuard Home بإسقاط الطلبات من هؤلاء العملاء. يتم تجاهل هذا الحقل إذا كانت هناك إدخالات في العملاء المسموح لهم.",
"access_blocked_title": "النطاقات غير المسموح بها",
"access_blocked_desc": "لا ينبغي الخلط بينه وبين المرشحات. يسقط AdGuard Home استعلامات DNS المطابقة لهذه المجالات ، ولا تظهر هذه الاستعلامات حتى في سجل الاستعلام. يمكنك تحديد أسماء النطاقات الدقيقة أو أحرف البدل أو قواعد تصفية عناوين URL ، على سبيل المثال \"example.org\" أو \"*.example.org\" أو \"|| example.org ^\" في المقابل.",
"access_settings_saved": "تم حفظ إعدادات الوصول بنجاح",
"updates_checked": "يتوفر إصدار جديد من AdGuard Home",
"updates_version_equal": "AdGuard Home محدث",
"check_updates_now": "تحقق من وجود تحديثات الآن",
"dns_privacy": "خصوصية DNS",
"setup_dns_privacy_1": "<0> DNS-over-TLS: </0> استخدم سلسلة <1> {{address}} </1>.",
"setup_dns_privacy_2": "<0> DNS-over-HTTPS: </0> استخدم سلسلة <1> {{address}} </1>.",
"setup_dns_privacy_3": "<0> فيما يلي قائمة بالبرامج التي يمكنك استخدامها. </0>",
"setup_dns_privacy_4": "على جهاز iOS 14 أو macOS Big Sur ، يمكنك تنزيل ملف \".mobileconfig\" خاص يضيف خوادم <highlight> DNS-over-HTTPS </highlight> أو <highlight> DNS-over-TLS </highlight> إلى إعدادات DNS.",
"setup_dns_privacy_android_1": "يدعم Android 9 DNS-over-TLS أصلاً. لتكوينه ، انتقل إلى الإعدادات → الشبكة والإنترنت → متقدم → DNS الخاص وأدخل اسم المجال الخاص بك هناك.",
"setup_dns_privacy_android_2": "<0> AdGuard لنظام Android </0> يدعم <1> DNS-over-HTTPS </1> و <1> DNS-over-TLS </1>.",
"setup_dns_privacy_android_3": "<0> Intra </0> يضيف دعم <1> DNS-over-HTTPS </1> إلى Android.",
"setup_dns_privacy_ios_1": "<0> DNSCloak </0> يدعم <1> DNS-over-HTTPS </1> ، ولكن من أجل تكوينه لاستخدام الخادم الخاص بك ، ستحتاج إلى إنشاء <2> DNS Stamp </2> لذلك.",
"setup_dns_privacy_ios_2": "<0> AdGuard لنظام iOS </0> يدعم إعداد <1> DNS-over-HTTPS </1> و <1> DNS-over-TLS </1> الإعداد.",
"setup_dns_privacy_other_title": "تطبيقات أخرى",
"setup_dns_privacy_other_1": "يمكن أن يكون AdGuard Home نفسه عميل DNS آمنًا على أي نظام أساسي.",
"setup_dns_privacy_other_2": "يدعم <0> dnsproxy </0> جميع بروتوكولات DNS الآمنة المعروفة.",
"setup_dns_privacy_other_3": "<0> dnscrypt-proxy </0> يدعم <1> DNS-over-HTTPS </1>.",
"setup_dns_privacy_other_4": "يدعم <0> Mozilla Firefox </0> <1> DNS-over-HTTPS </1>.",
"setup_dns_privacy_other_5": "ستجد المزيد من التطبيقات <0> هنا </0> و <1> هنا </1>.",
"setup_dns_privacy_ioc_mac": "اعدادات iOS و macOS",
"setup_dns_notice": "من أجل استخدام <0> DNS-over-HTTPS </0> أو <1> DNS-over-TLS </1> ، تحتاج إلى <1> تكوين التشفير </1> في إعدادات AdGuard Home.",
"rewrite_added": "تمت إضافة إعادة كتابة DNS لـ \"{{key}}\" بنجاح",
"rewrite_deleted": "تم حذف إعادة كتابة DNS لـ \"{{key}}\" بنجاح",
"rewrite_add": "إضافة إعادة كتابة DNS",
"rewrite_not_found": "لم يتم العثور على إعادة كتابة DNS",
"rewrite_confirm_delete": "هل أنت متأكد من أنك تريد حذف إعادة كتابة DNS لـ \"{{key}}\"؟",
"rewrite_desc": "يسمح بتكوين استجابة DNS المخصصة بسهولة لاسم نطاق معين.",
"rewrite_applied": "يتم تطبيق قاعدة إعادة الكتابة",
"rewrite_hosts_applied": "أعيد كتابتها بواسطة قاعدة ملف المضيفين",
"dns_rewrites": "إعادة كتابة DNS",
"form_domain": "أدخل اسم النطاق أو حرف البدل",
"form_answer": "أدخل عنوان IP أو اسم النطاق",
"form_error_domain_format": "تنسيق النطاق غير صالح",
"form_error_answer_format": "تنسيق إجابة غير صالح",
"configure": "ضبط",
"main_settings": "الاعدادات الرئيسية",
"block_services": "حظر خدمات معينة",
"blocked_services": "الخوادم المحجوبة",
"blocked_services_desc": "يسمح بحجب المواقع والخدمات الشعبية بسرعة.",
"blocked_services_saved": "تم حفظ الخوادم المحجوبة بنجاح",
"blocked_services_global": "استخدام خدمات الحظر العالمية",
"blocked_service": "الخدمات المحجوبة",
"block_all": "حجب الكل",
"unblock_all": "إلغاء حجب الكل",
"encryption_certificate_path": "مسار الشهادة",
"encryption_private_key_path": "مسار المفتاح الخاص",
"encryption_certificates_source_path": "قم بتعيين مسار ملف الشهادات",
"encryption_certificates_source_content": "الصق محتويات الشهادات",
"encryption_key_source_path": "قم بتعيين ملف مفتاح خاص",
"encryption_key_source_content": "الصق محتويات المفتاح الخاص",
"stats_params": "ضبط الاحصائيات",
"config_successfully_saved": "تم حفظ الاعدادات بنجاح",
"interval_6_hour": اعات6",
"interval_24_hour": "24 ساعة",
"interval_days": "{{count}} يوم",
"interval_days_plural": "{{count}} الأيام",
"domain": "النطاق",
"ecs": "ECS",
"punycode": "Punycode",
"answer": "الإجابة",
"filter_added_successfully": "تم إضافة القائمة بنجاح",
"filter_removed_successfully": "تم ازالته من القائمة بنجاح",
"filter_updated": "تم تحديث القائمة بنجاح",
"statistics_configuration": "ضبط الاحصائيات",
"statistics_retention": "الاحتفاظ بالإحصاءات",
"statistics_retention_desc": "إذا قمت بتقليل قيمة الفاصل الزمني ، فستفقد بعض البيانات",
"statistics_clear": "إعادة تعيين الإحصائيات",
"statistics_clear_confirm": "هل أنت متأكد من أنك تريد مسح الإحصاءات؟",
"statistics_retention_confirm": "هل أنت متأكد أنك تريد تغيير الاحتفاظ بالإحصاءات؟ إذا قمت بتقليل قيمة الفاصل الزمني ، فستفقد بعض البيانات",
"statistics_cleared": "تم مسح الإحصائيات بنجاح",
"statistics_enable": "تفعيل الاحصائيات",
"interval_hours": "{{count}} ساعة",
"interval_hours_plural": "{{count}} ساعات",
"filters_configuration": "اضبط الفلاتر",
"filters_enable": "تفعيل الفلاتر",
"filters_interval": "الفاصل الزمني لتحديث الفلاتر",
"disabled": "معطلة",
"username_label": "اسم المستخدم",
"username_placeholder": "ادخل اسم المستخدم",
"password_label": "كلمة المرور",
"password_placeholder": "ادخل كلمة المرور",
"sign_in": "تسجيل الدخول",
"sign_out": "تسجيل الخروج",
"forgot_password": "نسيت كلمة المرور؟",
"forgot_password_desc": "يرجى اتباع <0> هذه الخطوات </0> لإنشاء كلمة مرور جديدة لحساب المستخدم الخاص بك.",
"location": "الموقع",
"orgname": "اسم المنظمة",
"netname": "اسم الشبكة",
"network": "الشبكة",
"descr": "الوصف",
"whois": "WHOIS",
"filtering_rules_learn_more": "<0> اعرف المزيد </0> حول إنشاء قوائم المضيفين الخاصة بك.",
"blocked_by_response": "حظر بواسطة CNAME or IP in response",
"blocked_by_cname_or_ip": "حظر بواسطة CNAME or IP",
"try_again": "حاول مرة أخرى",
"domain_desc": "أدخل اسم النطاق أو حرف البدل الذي تريد إعادة كتابته.",
"example_rewrite_domain": "أعد كتابة الردود لاسم النطاق هذا فقط.",
"example_rewrite_wildcard": "أعد كتابة الردود لجميع النطاقات الفرعية <0> example.org </0>.",
"rewrite_ip_address": "عنوان IP: استخدم عنوان IP هذا في استجابة A أو AAAA",
"rewrite_domain_name": "اسم النطاق: أضف سجل CNAME",
"rewrite_A": "<0> A </0>: قيمة خاصة ، احتفظ بسجلات <0> A </0> من upstream",
"rewrite_AAAA": "<0> AAAA </0>: قيمة خاصة ، احتفظ بسجلات <0> AAAA </0> من upstream",
"disable_ipv6": "قم بتعطيل تحليل عناوين IPv6",
"disable_ipv6_desc": "قم بإسقاط جميع استعلامات DNS لعناوين IPv6 (اكتب AAAA).",
"fastest_addr": "أسرع عنوان IP",
"fastest_addr_desc": "استعلم عن جميع خوادم DNS وأعد عنوان IP الأسرع بين جميع الاستجابات. يؤدي هذا إلى إبطاء استعلامات DNS حيث يتعين على AdGuard Home انتظار الاستجابات من جميع خوادم DNS ، ولكنه يحسن الاتصال الكلي.",
"autofix_warning_text": "إذا قمت بالنقر فوق \"إصلاح\" ، فسيقوم AdGuard Home بتهيئة نظامك لاستخدام خادم AdGuard Home DNS.",
"autofix_warning_list": "سيقوم بتنفيذ هذه المهام: <0> إلغاء تنشيط نظام DNSStubListener </0> <0> تعيين عنوان خادم DNS إلى 127.0.0.1 </0> <0> استبدال هدف الارتباط الرمزي لـ /etc/resolv.conf بـ / run / systemd /resolve/resolv.conf </0> <0> إيقاف DNSStubListener (إعادة تحميل خدمة حل نظام d) </0>",
"autofix_warning_result": "نتيجة لذلك ، ستتم معالجة جميع طلبات DNS من نظامك بواسطة AdGuard Home افتراضيًا.",
"tags_title": "وسوم",
"tags_desc": "يمكنك تحديد العلامات التي تتوافق مع العميل. قم بتضمين العلامات في قواعد التصفية لتطبيقها بدقة أكبر. <0> معرفة المزيد </0>.",
"form_select_tags": "حدد علامات العميل",
"check_title": "تحقق من الفلترة",
"check_desc": "تحقق مما إذا تم فلترة اسم المضيف.",
"check": "تحقق",
"form_enter_host": "ادخل اسم المضيف",
"filtered_custom_rules": "تمت تصفيتها حسب قواعد التصفية المخصصة",
"choose_from_list": "اختر من القائمة",
"add_custom_list": "أضف قائمة مخصصة",
"host_whitelisted": "المضيف مسموح به",
"check_ip": "عناوين الـ IP: {{ip}}",
"check_cname": "CNAME: {{cname}}",
"check_reason": "سبب: {{reason}}",
"check_service": "أسم الخدمة: {{service}}",
"service_name": "أسم الخدمة",
"check_not_found": "غير موجود في قوائم التصفية الخاصة بك",
"client_confirm_block": "هل أنت متأكد من أنك تريد منع العميل \"{{ip}}\"؟",
"client_confirm_unblock": "هل تريد بالتأكيد إلغاء حظر العميل \"{{ip}}\"؟",
"client_blocked": "تم حظر العميل \"{{ip}}\" بنجاح",
"client_unblocked": "تم إلغاء حظر العميل \"{{ip}}\" بنجاح",
"static_ip": "عنوان IP ثابت",
"static_ip_desc": "AdGuard Home هو خادم لذلك يحتاج إلى عنوان IP ثابت ليعمل بشكل صحيح. خلاف ذلك ، في مرحلة ما ، قد يقوم جهاز التوجيه الخاص بك بتعيين عنوان IP مختلف لهذا الجهاز.",
"set_static_ip": "قم بتعيين عنوان IP ثابت",
"install_static_ok": "أخبار جيدة! تم ضبط عنوان IP الثابت بالفعل",
"install_static_error": "لا يمكن لـ AdGuard Home تكوينه تلقائيًا لواجهة الشبكة هذه. الرجاء البحث عن تعليمات حول كيفية القيام بذلك يدويًا.",
"install_static_configure": "اكتشف AdGuard Home استخدام عنوان IP الديناميكي <0> {{ip}} </0>. هل تريد تعيينه كعنوان ثابت؟",
"confirm_static_ip": "سيقوم AdGuard Home بتهيئة {{ip}} ليكون عنوان IP الثابت الخاص بك. هل تريد المتابعة؟",
"list_updated": "قائمة {{count}} محدثة",
"list_updated_plural": "قوائم {{count}} محدثة",
"dnssec_enable": "تفعيل DNSSEC",
"dnssec_enable_desc": "قم بتعيين علامة DNSSEC في استعلامات DNS الواردة وتحقق من النتيجة (مطلوب محلل يدعم DNSSEC).",
"validated_with_dnssec": "تم التحقق من صحتها باستخدام DNSSEC",
"all_queries": "كافة الاستفسارات",
"show_blocked_responses": "حظر",
"show_whitelisted_responses": "القائمة البيضاء",
"show_processed_responses": "المعالجة",
"blocked_safebrowsing": "محظور بواسطة التصفح الآمن",
"blocked_adult_websites": "محظور بواسطة الرقابة الأبوية",
"blocked_threats": "التهديدات المحظورة",
"allowed": "القائمة البيضاء",
"filtered": "تمت الفلترة",
"rewritten": "أعيدت كتابته",
"safe_search": "البحث الأمن",
"blocklist": "قائمة الحظر",
"milliseconds_abbreviation": "ms",
"cache_size": "حجم ذاكرة التخزين المؤقت",
"cache_size_desc": "حجم ذاكرة التخزين المؤقت لنظام أسماء النطاقات (بالبايت).",
"cache_ttl_min_override": "تجاوز الحد الأدنى من مدة البقاء TTL",
"cache_ttl_max_override": "تجاوز الحد الاقصى من مدة البقاء TTL",
"enter_cache_size": "أدخل حجم ذاكرة التخزين المؤقت (بايت)",
"enter_cache_ttl_min_override": "أدخل الحد الأدنى من مدة البقاء (بالثواني)",
"enter_cache_ttl_max_override": "أدخل الحد الاقصى من مدة البقاء (بالثواني)",
"cache_ttl_min_override_desc": "قم بتمديد قيم فترة البقاء القصيرة (بالثواني) المستلمة من الخادم الرئيسي عند تخزين استجابات DNS مؤقتًا.",
"cache_ttl_max_override_desc": "قم بتعيين الحد الأقصى لقيمة الوقت للعيش (بالثواني) للإدخالات في ذاكرة التخزين المؤقت لنظام أسماء النطاقات.",
"ttl_cache_validation": "يجب أن يكون الحد الأدنى لتجاوز TTL لذاكرة التخزين المؤقت أقل من أو يساوي الحد الأقصى",
"cache_optimistic": "متفائل التخزين المؤقت",
"cache_optimistic_desc": "اجعل AdGuard Home يستجيب من ذاكرة التخزين المؤقت حتى عندما تنتهي صلاحية الإدخالات وحاول أيضًا تحديثها.",
"filter_category_general": "General",
"filter_category_security": "الامان",
"filter_category_regional": "إقليمي",
"filter_category_other": "أخرى",
"filter_category_general_desc": "القوائم التي تمنع التتبع والإعلان على معظم الأجهزة",
"filter_category_security_desc": "القوائم المصممة خصيصًا لحظر النطاقات الخبيثة والتصيد الاحتيالي والخداع",
"filter_category_regional_desc": "القوائم التي تركز على الإعلانات الإقليمية وخوادم التتبع",
"filter_category_other_desc": "قوائم حظر أخرى",
"setup_config_to_enable_dhcp_server": "أضبط الاعدادات لتمكين خادم DHCP",
"original_response": "الرد الأصلي",
"click_to_view_queries": "انقر لعرض الـ queries",
"port_53_faq_link": "غالبًا ما يتم احتلال المنفذ 53 بواسطة خدمات \"DNSStubListener\" أو \"حل النظام\". يرجى قراءة <0> هذه التعليمات </0> حول كيفية حل هذه المشكلة.",
"adg_will_drop_dns_queries": "سيقوم AdGuard Home بإسقاط جميع استعلامات DNS من هذا العميل.",
"filter_allowlist": "تحذير: سيؤدي هذا الإجراء أيضًا إلى استبعاد القاعدة \"{{disallowed_rule}}\" من قائمة العملاء المسموح لهم.",
"last_rule_in_allowlist": "لا يمكن منع هذا العميل لأن استبعاد القاعدة \"{{disallowed_rule}}\" سيؤدي إلى تعطيل قائمة \"العملاء المسموح لهم\".",
"use_saved_key": "استخدم المفتاح المحفوظ مسبقًا",
"parental_control": "الرقابة الابويه",
"safe_browsing": "تصفح آمن",
"served_from_cache": "{{value}} <i>(يتم تقديمه من ذاكرة التخزين المؤقت)</i>",
"form_error_password_length": "يجب أن تتكون كلمة المرور من {{value}} من الأحرف على الأقل"
}

View File

@@ -222,6 +222,7 @@
"updated_upstream_dns_toast": "Upstream-servere er gemt",
"dns_test_ok_toast": "Angivne DNS-servere fungerer korrekt",
"dns_test_not_ok_toast": "Server \"{{key}}\": Kunne ikke bruges. Tjek, at du har angivet den korrekt",
"dns_test_warning_toast": "Upstream \"{{key}}\" svarer ikke på testforespørgsler og fungerer muligvis ikke korrekt",
"unblock": "Afblokering",
"block": "Blokering",
"disallow_this_client": "Afvis denne klient",

View File

@@ -47,7 +47,7 @@
"form_error_server_name": "Nombre de servidor no válido",
"form_error_subnet": "La subred \"{{cidr}}\" no contiene la dirección IP \"{{ip}}\"",
"form_error_positive": "Debe ser mayor que 0",
"form_error_gateway_ip": "Asignación no puede tener la dirección IP de la puerta de enlace",
"form_error_gateway_ip": "La asignación no puede tener la dirección IP de la puerta de enlace",
"out_of_range_error": "Debe estar fuera del rango \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Debe ser inferior que el inicio de rango",
"greater_range_start_error": "Debe ser mayor que el inicio de rango",
@@ -222,7 +222,7 @@
"updated_upstream_dns_toast": "Servidores DNS de subida guardados correctamente",
"dns_test_ok_toast": "Los servidores DNS especificados funcionan correctamente",
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no se puede utilizar, por favor revisa si lo has escrito correctamente",
"dns_test_warning_toast": "Upstream \"{{key}}\" no responde a las peticiones de prueba y es posible que no funcione correctamente",
"dns_test_warning_toast": "DNS de subida \"{{key}}\" no responde a las peticiones de prueba y es posible que no funcione correctamente",
"unblock": "Desbloquear",
"block": "Bloquear",
"disallow_this_client": "No permitir a este cliente",
@@ -364,7 +364,7 @@
"encryption_config_saved": "Configuración de cifrado guardado",
"encryption_server": "Nombre del servidor",
"encryption_server_enter": "Ingresa el nombre del dominio",
"encryption_server_desc": "Si se configura, AdGuard Home detecta los ClientID, responde a las consultas DDR y realiza validaciones de conexión adicionales. Si no se configura, estas funciones están deshabilitadas. Debe coincidir con uno de los nombres DNS del certificado.",
"encryption_server_desc": "Si se configura, AdGuard Home detecta los ID de clientes, responde a las consultas DDR y realiza validaciones de conexión adicionales. Si no se configura, estas funciones se deshabilitarán. Debe coincidir con uno de los nombres DNS del certificado.",
"encryption_redirect": "Redireccionar a HTTPS automáticamente",
"encryption_redirect_desc": "Si está marcado, AdGuard Home redireccionará automáticamente de HTTP a las direcciones HTTPS.",
"encryption_https": "Puerto HTTPS",

View File

@@ -63,7 +63,7 @@ const Examples = (props) => (
<Trans
components={[
<a
href="https://tools.ietf.org/html/draft-huitema-quic-dnsoquic-07"
href="https://datatracker.ietf.org/doc/html/rfc9250"
target="_blank"
rel="noopener noreferrer"
key="0"

View File

@@ -4,6 +4,7 @@ import langDetect from 'i18next-browser-languagedetector';
import { LANGUAGES, BASE_LOCALE } from './helpers/twosky';
import ar from './__locales/ar.json';
import be from './__locales/be.json';
import bg from './__locales/bg.json';
import cs from './__locales/cs.json';
@@ -42,6 +43,7 @@ import zhTW from './__locales/zh-tw.json';
import { setHtmlLangAttr } from './helpers/helpers';
const resources = {
ar: { translation: ar },
be: { translation: be },
bg: { translation: bg },
cs: { translation: cs },

14
go.mod
View File

@@ -22,25 +22,21 @@ require (
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
github.com/mdlayher/netlink v1.6.0
// TODO(a.garipov): This package is deprecated; find a new one or use
// our own code for that.
github.com/mdlayher/raw v0.1.0 // indirect
// our own code for that. Perhaps, use gopacket.
github.com/mdlayher/raw v0.1.0
github.com/miekg/dns v1.1.50
github.com/stretchr/testify v1.7.1
github.com/ti-mo/netfilter v0.4.0
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462
golang.org/x/sys v0.0.0-20220731174439-a90be440212d
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.0
)
require (
github.com/mdlayher/packet v1.0.0
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
)
require (
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
@@ -55,6 +51,7 @@ require (
github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect
github.com/mdlayher/packet v1.0.0 // indirect
github.com/mdlayher/socket v0.2.3 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
@@ -68,5 +65,4 @@ require (
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

4
go.sum
View File

@@ -465,8 +465,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -10,6 +10,20 @@ import (
"golang.org/x/exp/slices"
)
// Coalesce returns the first non-zero value. It is named after the function
// COALESCE in SQL. If values or all its elements are empty, it returns a zero
// value.
func Coalesce[T comparable](values ...T) (res T) {
var zero T
for _, v := range values {
if v != zero {
return v
}
}
return zero
}
// UniqChecker allows validating uniqueness of comparable items.
//
// TODO(a.garipov): The Ordered constraint is only really necessary in Validate.

View File

@@ -9,6 +9,12 @@ import (
"github.com/AdguardTeam/golibs/log"
)
// RegisterFunc is the function that sets the handler to handle the URL for the
// method.
//
// TODO(e.burkov, a.garipov): Get rid of it.
type RegisterFunc func(method, url string, handler http.HandlerFunc)
// OK responds with word OK.
func OK(w http.ResponseWriter) {
if _, err := io.WriteString(w, "OK\n"); err != nil {

View File

@@ -470,7 +470,7 @@ func TestHostsContainer(t *testing.T) {
}},
}, {
req: &urlfilter.DNSRequest{
Hostname: "nonexisting",
Hostname: "nonexistent.example",
DNSType: dns.TypeA,
},
name: "non-existing",

View File

@@ -154,10 +154,13 @@ func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) {
return netIfaces, nil
}
// GetInterfaceByIP returns the name of interface containing provided ip.
// InterfaceByIP returns the name of the interface bound to ip.
//
// TODO(e.burkov): See TODO on GetValidInterfacesForWeb.
func GetInterfaceByIP(ip net.IP) string {
// TODO(a.garipov, e.burkov): This function is technically incorrect, since one
// IP address can be shared by multiple interfaces in some configurations.
//
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
func InterfaceByIP(ip net.IP) (ifaceName string) {
ifaces, err := GetValidNetInterfacesForWeb()
if err != nil {
return ""
@@ -177,7 +180,7 @@ func GetInterfaceByIP(ip net.IP) string {
// GetSubnet returns pointer to net.IPNet for the specified interface or nil if
// the search fails.
//
// TODO(e.burkov): See TODO on GetValidInterfacesForWeb.
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
func GetSubnet(ifaceName string) *net.IPNet {
netIfaces, err := GetValidNetInterfacesForWeb()
if err != nil {

View File

@@ -13,6 +13,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/google/renameio/maybe"
"golang.org/x/sys/unix"
@@ -22,17 +23,27 @@ import (
const dhcpcdConf = "etc/dhcpcd.conf"
func canBindPrivilegedPorts() (can bool, err error) {
cnbs, err := unix.PrctlRetInt(
res, err := unix.PrctlRetInt(
unix.PR_CAP_AMBIENT,
unix.PR_CAP_AMBIENT_IS_SET,
unix.CAP_NET_BIND_SERVICE,
0,
0,
)
if err != nil {
if errors.Is(err, unix.EINVAL) {
// Older versions of Linux kernel do not support this. Print a
// warning and check admin rights.
log.Info("warning: cannot check capability cap_net_bind_service: %s", err)
} else {
return false, err
}
}
// Don't check the error because it's always nil on Linux.
adm, _ := aghos.HaveAdminRights()
return cnbs == 1 || adm, err
return res == 1 || adm, nil
}
// dhcpcdStaticConfig checks if interface is configured by /etc/dhcpcd.conf to

View File

@@ -132,7 +132,7 @@ func TestGatewayIP(t *testing.T) {
}
}
func TestGetInterfaceByIP(t *testing.T) {
func TestInterfaceByIP(t *testing.T) {
ifaces, err := GetValidNetInterfacesForWeb()
require.NoError(t, err)
require.NotEmpty(t, ifaces)
@@ -142,7 +142,7 @@ func TestGetInterfaceByIP(t *testing.T) {
require.NotEmpty(t, iface.Addresses)
for _, ip := range iface.Addresses {
ifaceName := GetInterfaceByIP(ip)
ifaceName := InterfaceByIP(ip)
require.Equal(t, iface.Name, ifaceName)
}
})

View File

@@ -1,4 +1,4 @@
package aghos
package aghos_test
import (
"testing"

View File

@@ -0,0 +1,57 @@
package aghos
import (
"io/fs"
"path"
"testing"
"testing/fstest"
"github.com/AdguardTeam/golibs/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// errFS is an fs.FS implementation, method Open of which always returns
// errFSOpen.
type errFS struct{}
// errFSOpen is returned from errGlobFS.Open.
const errFSOpen errors.Error = "test open error"
// Open implements the fs.FS interface for *errGlobFS. fsys is always nil and
// err is always errFSOpen.
func (efs *errFS) Open(name string) (fsys fs.File, err error) {
return nil, errFSOpen
}
func TestWalkerFunc_CheckFile(t *testing.T) {
emptyFS := fstest.MapFS{}
t.Run("non-existing", func(t *testing.T) {
_, ok, err := checkFile(emptyFS, nil, "lol")
require.NoError(t, err)
assert.True(t, ok)
})
t.Run("invalid_argument", func(t *testing.T) {
_, ok, err := checkFile(&errFS{}, nil, "")
require.ErrorIs(t, err, errFSOpen)
assert.False(t, ok)
})
t.Run("ignore_dirs", func(t *testing.T) {
const dirName = "dir"
testFS := fstest.MapFS{
path.Join(dirName, "file"): &fstest.MapFile{Data: []byte{}},
}
patterns, ok, err := checkFile(testFS, nil, dirName)
require.NoError(t, err)
assert.Empty(t, patterns)
assert.True(t, ok)
})
}

View File

@@ -1,13 +1,13 @@
package aghos
package aghos_test
import (
"bufio"
"io"
"io/fs"
"path"
"testing"
"testing/fstest"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -16,7 +16,7 @@ import (
func TestFileWalker_Walk(t *testing.T) {
const attribute = `000`
makeFileWalker := func(_ string) (fw FileWalker) {
makeFileWalker := func(_ string) (fw aghos.FileWalker) {
return func(r io.Reader) (patterns []string, cont bool, err error) {
s := bufio.NewScanner(r)
for s.Scan() {
@@ -113,7 +113,7 @@ func TestFileWalker_Walk(t *testing.T) {
f := fstest.MapFS{
filename: &fstest.MapFile{Data: []byte("[]")},
}
ok, err := FileWalker(func(r io.Reader) (patterns []string, cont bool, err error) {
ok, err := aghos.FileWalker(func(r io.Reader) (patterns []string, cont bool, err error) {
s := bufio.NewScanner(r)
for s.Scan() {
patterns = append(patterns, s.Text())
@@ -134,7 +134,7 @@ func TestFileWalker_Walk(t *testing.T) {
"mockfile.txt": &fstest.MapFile{Data: []byte(`mockdata`)},
}
ok, err := FileWalker(func(r io.Reader) (patterns []string, ok bool, err error) {
ok, err := aghos.FileWalker(func(r io.Reader) (patterns []string, ok bool, err error) {
return nil, true, rerr
}).Walk(f, "*")
require.ErrorIs(t, err, rerr)
@@ -142,45 +142,3 @@ func TestFileWalker_Walk(t *testing.T) {
assert.False(t, ok)
})
}
type errFS struct {
fs.GlobFS
}
const errErrFSOpen errors.Error = "this error is always returned"
func (efs *errFS) Open(name string) (fs.File, error) {
return nil, errErrFSOpen
}
func TestWalkerFunc_CheckFile(t *testing.T) {
emptyFS := fstest.MapFS{}
t.Run("non-existing", func(t *testing.T) {
_, ok, err := checkFile(emptyFS, nil, "lol")
require.NoError(t, err)
assert.True(t, ok)
})
t.Run("invalid_argument", func(t *testing.T) {
_, ok, err := checkFile(&errFS{}, nil, "")
require.ErrorIs(t, err, errErrFSOpen)
assert.False(t, ok)
})
t.Run("ignore_dirs", func(t *testing.T) {
const dirName = "dir"
testFS := fstest.MapFS{
path.Join(dirName, "file"): &fstest.MapFile{Data: []byte{}},
}
patterns, ok, err := checkFile(testFS, nil, dirName)
require.NoError(t, err)
assert.Empty(t, patterns)
assert.True(t, ok)
})
}

View File

@@ -1,20 +0,0 @@
package aghtest
import (
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/miekg/dns"
)
// Exchanger is a mock aghnet.Exchanger implementation for tests.
type Exchanger struct {
Ups upstream.Upstream
}
// Exchange implements aghnet.Exchanger interface for *Exchanger.
func (e *Exchanger) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
if e.Ups == nil {
e.Ups = &TestErrUpstream{}
}
return e.Ups.Exchange(req)
}

View File

@@ -1,23 +0,0 @@
package aghtest
// FSWatcher is a mock aghos.FSWatcher implementation to use in tests.
type FSWatcher struct {
OnEvents func() (e <-chan struct{})
OnAdd func(name string) (err error)
OnClose func() (err error)
}
// Events implements the aghos.FSWatcher interface for *FSWatcher.
func (w *FSWatcher) Events() (e <-chan struct{}) {
return w.OnEvents()
}
// Add implements the aghos.FSWatcher interface for *FSWatcher.
func (w *FSWatcher) Add(name string) (err error) {
return w.OnAdd(name)
}
// Close implements the aghos.FSWatcher interface for *FSWatcher.
func (w *FSWatcher) Close() (err error) {
return w.OnClose()
}

View File

@@ -0,0 +1,135 @@
package aghtest
import (
"io/fs"
"net"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/miekg/dns"
)
// Interface Mocks
//
// Keep entities in this file in alphabetic order.
// Standard Library
// type check
var _ fs.FS = &FS{}
// FS is a mock [fs.FS] implementation for tests.
type FS struct {
OnOpen func(name string) (fs.File, error)
}
// Open implements the [fs.FS] interface for *FS.
func (fsys *FS) Open(name string) (fs.File, error) {
return fsys.OnOpen(name)
}
// type check
var _ fs.GlobFS = &GlobFS{}
// GlobFS is a mock [fs.GlobFS] implementation for tests.
type GlobFS struct {
// FS is embedded here to avoid implementing all it's methods.
FS
OnGlob func(pattern string) ([]string, error)
}
// Glob implements the [fs.GlobFS] interface for *GlobFS.
func (fsys *GlobFS) Glob(pattern string) ([]string, error) {
return fsys.OnGlob(pattern)
}
// type check
var _ fs.StatFS = &StatFS{}
// StatFS is a mock [fs.StatFS] implementation for tests.
type StatFS struct {
// FS is embedded here to avoid implementing all it's methods.
FS
OnStat func(name string) (fs.FileInfo, error)
}
// Stat implements the [fs.StatFS] interface for *StatFS.
func (fsys *StatFS) Stat(name string) (fs.FileInfo, error) {
return fsys.OnStat(name)
}
// type check
var _ net.Listener = (*Listener)(nil)
// Listener is a mock [net.Listener] implementation for tests.
type Listener struct {
OnAccept func() (conn net.Conn, err error)
OnAddr func() (addr net.Addr)
OnClose func() (err error)
}
// Accept implements the [net.Listener] interface for *Listener.
func (l *Listener) Accept() (conn net.Conn, err error) {
return l.OnAccept()
}
// Addr implements the [net.Listener] interface for *Listener.
func (l *Listener) Addr() (addr net.Addr) {
return l.OnAddr()
}
// Close implements the [net.Listener] interface for *Listener.
func (l *Listener) Close() (err error) {
return l.OnClose()
}
// Module dnsproxy
// type check
var _ upstream.Upstream = (*UpstreamMock)(nil)
// UpstreamMock is a mock [upstream.Upstream] implementation for tests.
//
// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and
// rename it to just Upstream.
type UpstreamMock struct {
OnAddress func() (addr string)
OnExchange func(req *dns.Msg) (resp *dns.Msg, err error)
}
// Address implements the [upstream.Upstream] interface for *UpstreamMock.
func (u *UpstreamMock) Address() (addr string) {
return u.OnAddress()
}
// Exchange implements the [upstream.Upstream] interface for *UpstreamMock.
func (u *UpstreamMock) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
return u.OnExchange(req)
}
// Module AdGuardHome
// type check
var _ aghos.FSWatcher = (*FSWatcher)(nil)
// FSWatcher is a mock [aghos.FSWatcher] implementation for tests.
type FSWatcher struct {
OnEvents func() (e <-chan struct{})
OnAdd func(name string) (err error)
OnClose func() (err error)
}
// Events implements the [aghos.FSWatcher] interface for *FSWatcher.
func (w *FSWatcher) Events() (e <-chan struct{}) {
return w.OnEvents()
}
// Add implements the [aghos.FSWatcher] interface for *FSWatcher.
func (w *FSWatcher) Add(name string) (err error) {
return w.OnAdd(name)
}
// Close implements the [aghos.FSWatcher] interface for *FSWatcher.
func (w *FSWatcher) Close() (err error) {
return w.OnClose()
}

View File

@@ -0,0 +1,9 @@
package aghtest_test
import (
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
)
// type check
var _ aghos.FSWatcher = (*aghtest.FSWatcher)(nil)

View File

@@ -1,46 +0,0 @@
package aghtest
import "io/fs"
// type check
var _ fs.FS = &FS{}
// FS is a mock fs.FS implementation to use in tests.
type FS struct {
OnOpen func(name string) (fs.File, error)
}
// Open implements the fs.FS interface for *FS.
func (fsys *FS) Open(name string) (fs.File, error) {
return fsys.OnOpen(name)
}
// type check
var _ fs.StatFS = &StatFS{}
// StatFS is a mock fs.StatFS implementation to use in tests.
type StatFS struct {
// FS is embedded here to avoid implementing all it's methods.
FS
OnStat func(name string) (fs.FileInfo, error)
}
// Stat implements the fs.StatFS interface for *StatFS.
func (fsys *StatFS) Stat(name string) (fs.FileInfo, error) {
return fsys.OnStat(name)
}
// type check
var _ fs.GlobFS = &GlobFS{}
// GlobFS is a mock fs.GlobFS implementation to use in tests.
type GlobFS struct {
// FS is embedded here to avoid implementing all it's methods.
FS
OnGlob func(pattern string) ([]string, error)
}
// Glob implements the fs.GlobFS interface for *GlobFS.
func (fsys *GlobFS) Glob(pattern string) ([]string, error) {
return fsys.OnGlob(pattern)
}

View File

@@ -6,12 +6,18 @@ import (
"fmt"
"net"
"strings"
"sync"
"testing"
"github.com/AdguardTeam/golibs/errors"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
)
// Additional Upstream Testing Utilities
// Upstream is a mock implementation of upstream.Upstream.
//
// TODO(a.garipov): Replace with UpstreamMock and rename it to just Upstream.
type Upstream struct {
// CName is a map of hostname to canonical name.
CName map[string][]string
@@ -25,6 +31,43 @@ type Upstream struct {
Addr string
}
// RespondTo returns a response with answer if req has class cl, question type
// qt, and target targ.
func RespondTo(t testing.TB, req *dns.Msg, cl, qt uint16, targ, answer string) (resp *dns.Msg) {
t.Helper()
require.NotNil(t, req)
require.Len(t, req.Question, 1)
q := req.Question[0]
targ = dns.Fqdn(targ)
if q.Qclass != cl || q.Qtype != qt || q.Name != targ {
return nil
}
respHdr := dns.RR_Header{
Name: targ,
Rrtype: qt,
Class: cl,
Ttl: 60,
}
resp = new(dns.Msg).SetReply(req)
switch qt {
case dns.TypePTR:
resp.Answer = []dns.RR{
&dns.PTR{
Hdr: respHdr,
Ptr: answer,
},
}
default:
t.Fatalf("unsupported question type: %s", dns.Type(qt))
}
return resp
}
// Exchange implements the upstream.Upstream interface for *Upstream.
//
// TODO(a.garipov): Split further into handlers.
@@ -76,74 +119,57 @@ func (u *Upstream) Address() string {
return u.Addr
}
// TestBlockUpstream implements upstream.Upstream interface for replacing real
// upstream in tests.
type TestBlockUpstream struct {
Hostname string
// lock protects reqNum.
lock sync.RWMutex
reqNum int
Block bool
}
// Exchange returns a message unique for TestBlockUpstream's Hostname-Block
// pair.
func (u *TestBlockUpstream) Exchange(r *dns.Msg) (*dns.Msg, error) {
u.lock.Lock()
defer u.lock.Unlock()
u.reqNum++
hash := sha256.Sum256([]byte(u.Hostname))
hashToReturn := hex.EncodeToString(hash[:])
if !u.Block {
hashToReturn = hex.EncodeToString(hash[:])[:2] + strings.Repeat("ab", 28)
// NewBlockUpstream returns an [*UpstreamMock] that works like an upstream that
// supports hash-based safe-browsing/adult-blocking feature. If shouldBlock is
// true, hostname's actual hash is returned, blocking it. Otherwise, it returns
// a different hash.
func NewBlockUpstream(hostname string, shouldBlock bool) (u *UpstreamMock) {
hash := sha256.Sum256([]byte(hostname))
hashStr := hex.EncodeToString(hash[:])
if !shouldBlock {
hashStr = hex.EncodeToString(hash[:])[:2] + strings.Repeat("ab", 28)
}
m := &dns.Msg{}
m.SetReply(r)
m.Answer = []dns.RR{
&dns.TXT{
Hdr: dns.RR_Header{
Name: r.Question[0].Name,
},
Txt: []string{
hashToReturn,
},
ans := &dns.TXT{
Hdr: dns.RR_Header{
Name: "",
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: 60,
},
Txt: []string{hashStr},
}
respTmpl := &dns.Msg{
Answer: []dns.RR{ans},
}
return &UpstreamMock{
OnAddress: func() (addr string) {
return "sbpc.upstream.example"
},
OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
resp = respTmpl.Copy()
resp.SetReply(req)
resp.Answer[0].(*dns.TXT).Hdr.Name = req.Question[0].Name
return resp, nil
},
}
return m, nil
}
// Address always returns an empty string.
func (u *TestBlockUpstream) Address() string {
return ""
}
// ErrUpstream is the error returned from the [*UpstreamMock] created by
// [NewErrorUpstream].
const ErrUpstream errors.Error = "test upstream error"
// RequestsCount returns the number of handled requests. It's safe for
// concurrent use.
func (u *TestBlockUpstream) RequestsCount() int {
u.lock.Lock()
defer u.lock.Unlock()
return u.reqNum
}
// TestErrUpstream implements upstream.Upstream interface for replacing real
// upstream in tests.
type TestErrUpstream struct {
// The error returned by Exchange may be unwrapped to the Err.
Err error
}
// Exchange always returns nil Msg and non-nil error.
func (u *TestErrUpstream) Exchange(*dns.Msg) (*dns.Msg, error) {
return nil, fmt.Errorf("errupstream: %w", u.Err)
}
// Address always returns an empty string.
func (u *TestErrUpstream) Address() string {
return ""
// NewErrorUpstream returns an [*UpstreamMock] that returns [ErrUpstream] from
// its Exchange method.
func NewErrorUpstream() (u *UpstreamMock) {
return &UpstreamMock{
OnAddress: func() (addr string) {
return "error.upstream.example"
},
OnExchange: func(_ *dns.Msg) (resp *dns.Msg, err error) {
return nil, errors.Error("test upstream error")
},
}
}

View File

@@ -16,16 +16,18 @@ import (
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/mdlayher/ethernet"
"github.com/mdlayher/packet"
//lint:ignore SA1019 See the TODO in go.mod.
"github.com/mdlayher/raw"
)
// dhcpUnicastAddr is the combination of MAC and IP addresses for responding to
// the unconfigured host.
type dhcpUnicastAddr struct {
// packet.Addr is embedded here to make *dhcpUcastAddr a net.Addr without
// raw.Addr is embedded here to make *dhcpUcastAddr a net.Addr without
// actually implementing all methods. It also contains the client's
// hardware address.
packet.Addr
raw.Addr
// yiaddr is an IP address just allocated by server for the host.
yiaddr net.IP
@@ -51,13 +53,7 @@ type dhcpConn struct {
// newDHCPConn creates the special connection for DHCP server.
func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err error) {
var ucast net.PacketConn
ucast, err = packet.Listen(
iface,
packet.Raw,
int(ethernet.EtherTypeIPv4),
nil,
)
if err != nil {
if ucast, err = raw.ListenPacket(iface, uint16(ethernet.EtherTypeIPv4), nil); err != nil {
return nil, fmt.Errorf("creating raw udp connection: %w", err)
}

View File

@@ -11,9 +11,11 @@ import (
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/mdlayher/packet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
//lint:ignore SA1019 See the TODO in go.mod.
"github.com/mdlayher/raw"
)
func TestDHCPConn_WriteTo_common(t *testing.T) {
@@ -56,7 +58,7 @@ func TestBuildEtherPkt(t *testing.T) {
srcIP: net.IP{1, 2, 3, 4},
}
peer := &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}},
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}},
yiaddr: net.IP{4, 3, 2, 1},
}
payload := (&dhcpv4.DHCPv4{}).ToBytes()
@@ -102,7 +104,7 @@ func TestBuildEtherPkt(t *testing.T) {
t.Run("serializing_error", func(t *testing.T) {
// Create a peer with invalid MAC.
badPeer := &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}},
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}},
yiaddr: net.IP{4, 3, 2, 1},
}

View File

@@ -5,11 +5,11 @@ import (
"encoding/json"
"fmt"
"net"
"net/http"
"path/filepath"
"runtime"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
)
@@ -126,7 +126,7 @@ type ServerConfig struct {
ConfigModified func() `yaml:"-"`
// Register an HTTP handler
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"`
HTTPRegister aghhttp.RegisterFunc `yaml:"-"`
Enabled bool `yaml:"enabled"`
InterfaceName string `yaml:"interface_name"`

View File

@@ -20,7 +20,9 @@ import (
"github.com/go-ping/ping"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/mdlayher/packet"
//lint:ignore SA1019 See the TODO in go.mod.
"github.com/mdlayher/raw"
)
// v4Server is a DHCPv4 server.
@@ -992,7 +994,7 @@ func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DH
// Unicast DHCPOFFER and DHCPACK messages to the client's
// hardware address and yiaddr.
peer = &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: req.ClientHWAddr},
Addr: raw.Addr{HardwareAddr: req.ClientHWAddr},
yiaddr: resp.YourIPAddr,
}
default:

View File

@@ -12,9 +12,11 @@ import (
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/mdlayher/packet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
//lint:ignore SA1019 See the TODO in go.mod.
"github.com/mdlayher/raw"
)
var (
@@ -554,7 +556,7 @@ func TestV4Server_Send(t *testing.T) {
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
want: &dhcpUnicastAddr{
Addr: packet.Addr{HardwareAddr: knownMAC},
Addr: raw.Addr{HardwareAddr: knownMAC},
yiaddr: knownIP,
},
}, {

View File

@@ -5,12 +5,12 @@ import (
"crypto/x509"
"fmt"
"net"
"net/http"
"os"
"sort"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
@@ -121,6 +121,7 @@ type FilteringConfig struct {
EnableDNSSEC bool `yaml:"enable_dnssec"` // Set AD flag in outcoming DNS request
EnableEDNSClientSubnet bool `yaml:"edns_client_subnet"` // Enable EDNS Client Subnet option
MaxGoroutines uint32 `yaml:"max_goroutines"` // Max. number of parallel goroutines for processing incoming requests
HandleDDR bool `yaml:"handle_ddr"` // Handle DDR requests
// IpsetList is the ipset configuration that allows AdGuard Home to add
// IP addresses of the specified domain names to an ipset list. Syntax:
@@ -151,7 +152,7 @@ type TLSConfig struct {
PrivateKeyData []byte `yaml:"-" json:"-"`
// ServerName is the hostname of the server. Currently, it is only being
// used for ClientID checking.
// used for ClientID checking and Discovery of Designated Resolvers (DDR).
ServerName string `yaml:"-" json:"-"`
cert tls.Certificate
@@ -191,7 +192,7 @@ type ServerConfig struct {
ConfigModified func()
// Register an HTTP handler
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request))
HTTPRegister aghhttp.RegisterFunc
// ResolveClients signals if the RDNS should resolve clients' addresses.
ResolveClients bool

View File

@@ -76,6 +76,10 @@ const (
resultCodeError
)
// ddrHostFQDN is the FQDN used in Discovery of Designated Resolvers (DDR) requests.
// See https://www.ietf.org/archive/id/draft-ietf-add-ddr-06.html.
const ddrHostFQDN = "_dns.resolver.arpa."
// handleDNSRequest filters the incoming DNS requests and writes them to the query log
func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error {
ctx := &dnsContext{
@@ -94,6 +98,7 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error {
mods := []modProcessFunc{
s.processRecursion,
s.processInitial,
s.processDDRQuery,
s.processDetermineLocal,
s.processDHCPHosts,
s.processRestrictLocal,
@@ -239,6 +244,98 @@ func (s *Server) onDHCPLeaseChanged(flags int) {
s.setTableIPToHost(ipToHost)
}
// processDDRQuery responds to SVCB query for a special use domain name
// _dns.resolver.arpa. The result contains different types of encryption
// supported by current user configuration.
//
// See https://www.ietf.org/archive/id/draft-ietf-add-ddr-06.html.
func (s *Server) processDDRQuery(ctx *dnsContext) (rc resultCode) {
d := ctx.proxyCtx
question := d.Req.Question[0]
if !s.conf.HandleDDR {
return resultCodeSuccess
}
if question.Name == ddrHostFQDN {
if s.dnsProxy.TLSListenAddr == nil && s.conf.HTTPSListenAddrs == nil &&
s.dnsProxy.QUICListenAddr == nil || question.Qtype != dns.TypeSVCB {
d.Res = s.makeResponse(d.Req)
return resultCodeFinish
}
d.Res = s.makeDDRResponse(d.Req)
return resultCodeFinish
}
return resultCodeSuccess
}
// makeDDRResponse creates DDR answer according to server configuration. The
// contructed SVCB resource records have the priority of 1 for each entry,
// similar to examples provided by https://www.ietf.org/archive/id/draft-ietf-add-ddr-06.html.
//
// TODO(a.meshkov): Consider setting the priority values based on the protocol.
func (s *Server) makeDDRResponse(req *dns.Msg) (resp *dns.Msg) {
resp = s.makeResponse(req)
// TODO(e.burkov): Think about storing the FQDN version of the server's
// name somewhere.
domainName := dns.Fqdn(s.conf.ServerName)
for _, addr := range s.conf.HTTPSListenAddrs {
values := []dns.SVCBKeyValue{
&dns.SVCBAlpn{Alpn: []string{"h2"}},
&dns.SVCBPort{Port: uint16(addr.Port)},
&dns.SVCBDoHPath{Template: "/dns-query?dns"},
}
ans := &dns.SVCB{
Hdr: s.hdr(req, dns.TypeSVCB),
Priority: 1,
Target: domainName,
Value: values,
}
resp.Answer = append(resp.Answer, ans)
}
for _, addr := range s.dnsProxy.TLSListenAddr {
values := []dns.SVCBKeyValue{
&dns.SVCBAlpn{Alpn: []string{"dot"}},
&dns.SVCBPort{Port: uint16(addr.Port)},
}
ans := &dns.SVCB{
Hdr: s.hdr(req, dns.TypeSVCB),
Priority: 1,
Target: domainName,
Value: values,
}
resp.Answer = append(resp.Answer, ans)
}
for _, addr := range s.dnsProxy.QUICListenAddr {
values := []dns.SVCBKeyValue{
&dns.SVCBAlpn{Alpn: []string{"doq"}},
&dns.SVCBPort{Port: uint16(addr.Port)},
}
ans := &dns.SVCB{
Hdr: s.hdr(req, dns.TypeSVCB),
Priority: 1,
Target: domainName,
Value: values,
}
resp.Answer = append(resp.Answer, ans)
}
return resp
}
// processDetermineLocal determines if the client's IP address is from
// locally-served network and saves the result into the context.
func (s *Server) processDetermineLocal(dctx *dnsContext) (rc resultCode) {

View File

@@ -14,6 +14,177 @@ import (
"github.com/stretchr/testify/require"
)
const (
ddrTestDomainName = "dns.example.net"
ddrTestFQDN = ddrTestDomainName + "."
)
func TestServer_ProcessDDRQuery(t *testing.T) {
dohSVCB := &dns.SVCB{
Priority: 1,
Target: ddrTestFQDN,
Value: []dns.SVCBKeyValue{
&dns.SVCBAlpn{Alpn: []string{"h2"}},
&dns.SVCBPort{Port: 8044},
&dns.SVCBDoHPath{Template: "/dns-query?dns"},
},
}
dotSVCB := &dns.SVCB{
Priority: 1,
Target: ddrTestFQDN,
Value: []dns.SVCBKeyValue{
&dns.SVCBAlpn{Alpn: []string{"dot"}},
&dns.SVCBPort{Port: 8043},
},
}
doqSVCB := &dns.SVCB{
Priority: 1,
Target: ddrTestFQDN,
Value: []dns.SVCBKeyValue{
&dns.SVCBAlpn{Alpn: []string{"doq"}},
&dns.SVCBPort{Port: 8042},
},
}
testCases := []struct {
name string
host string
want []*dns.SVCB
wantRes resultCode
portDoH int
portDoT int
portDoQ int
qtype uint16
ddrEnabled bool
}{{
name: "pass_host",
wantRes: resultCodeSuccess,
host: "example.net.",
qtype: dns.TypeSVCB,
ddrEnabled: true,
portDoH: 8043,
}, {
name: "pass_qtype",
wantRes: resultCodeFinish,
host: ddrHostFQDN,
qtype: dns.TypeA,
ddrEnabled: true,
portDoH: 8043,
}, {
name: "pass_disabled_tls",
wantRes: resultCodeFinish,
host: ddrHostFQDN,
qtype: dns.TypeSVCB,
ddrEnabled: true,
}, {
name: "pass_disabled_ddr",
wantRes: resultCodeSuccess,
host: ddrHostFQDN,
qtype: dns.TypeSVCB,
ddrEnabled: false,
portDoH: 8043,
}, {
name: "dot",
wantRes: resultCodeFinish,
want: []*dns.SVCB{dotSVCB},
host: ddrHostFQDN,
qtype: dns.TypeSVCB,
ddrEnabled: true,
portDoT: 8043,
}, {
name: "doh",
wantRes: resultCodeFinish,
want: []*dns.SVCB{dohSVCB},
host: ddrHostFQDN,
qtype: dns.TypeSVCB,
ddrEnabled: true,
portDoH: 8044,
}, {
name: "doq",
wantRes: resultCodeFinish,
want: []*dns.SVCB{doqSVCB},
host: ddrHostFQDN,
qtype: dns.TypeSVCB,
ddrEnabled: true,
portDoQ: 8042,
}, {
name: "dot_doh",
wantRes: resultCodeFinish,
want: []*dns.SVCB{dotSVCB, dohSVCB},
host: ddrHostFQDN,
qtype: dns.TypeSVCB,
ddrEnabled: true,
portDoT: 8043,
portDoH: 8044,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := prepareTestServer(t, tc.portDoH, tc.portDoT, tc.portDoQ, tc.ddrEnabled)
req := createTestMessageWithType(tc.host, tc.qtype)
dctx := &dnsContext{
proxyCtx: &proxy.DNSContext{
Req: req,
},
}
res := s.processDDRQuery(dctx)
require.Equal(t, tc.wantRes, res)
if tc.wantRes != resultCodeFinish {
return
}
msg := dctx.proxyCtx.Res
require.NotNil(t, msg)
for _, v := range tc.want {
v.Hdr = s.hdr(req, dns.TypeSVCB)
}
assert.ElementsMatch(t, tc.want, msg.Answer)
})
}
}
func prepareTestServer(t *testing.T, portDoH, portDoT, portDoQ int, ddrEnabled bool) (s *Server) {
t.Helper()
proxyConf := proxy.Config{}
if portDoT > 0 {
proxyConf.TLSListenAddr = []*net.TCPAddr{{Port: portDoT}}
}
if portDoQ > 0 {
proxyConf.QUICListenAddr = []*net.UDPAddr{{Port: portDoQ}}
}
s = &Server{
dnsProxy: &proxy.Proxy{
Config: proxyConf,
},
conf: ServerConfig{
FilteringConfig: FilteringConfig{
HandleDDR: ddrEnabled,
},
TLSConfig: TLSConfig{
ServerName: ddrTestDomainName,
},
},
}
if portDoH > 0 {
s.conf.TLSConfig.HTTPSListenAddrs = []*net.TCPAddr{{Port: portDoH}}
}
return s
}
func TestServer_ProcessDetermineLocal(t *testing.T) {
s := &Server{
privateNets: netutil.SubnetSetFunc(netutil.IsLocallyServed),

View File

@@ -61,7 +61,7 @@ type Server struct {
dnsFilter *filtering.DNSFilter // DNS filter instance
dhcpServer dhcpd.ServerInterface // DHCP server instance (optional)
queryLog querylog.QueryLog // Query log instance
stats stats.Stats
stats stats.Interface
access *accessCtx
// localDomainSuffix is the suffix used to detect internal hosts. It
@@ -107,7 +107,7 @@ const defaultLocalDomainSuffix = "lan"
// DNSCreateParams are parameters to create a new server.
type DNSCreateParams struct {
DNSFilter *filtering.DNSFilter
Stats stats.Stats
Stats stats.Interface
QueryLog querylog.QueryLog
DHCPServer dhcpd.ServerInterface
PrivateNets netutil.SubnetSet

View File

@@ -17,13 +17,13 @@ import (
"testing/fstest"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil"
@@ -853,10 +853,7 @@ func TestBlockedByHosts(t *testing.T) {
func TestBlockedBySafeBrowsing(t *testing.T) {
const hostname = "wmconvirus.narod.ru"
sbUps := &aghtest.TestBlockUpstream{
Hostname: hostname,
Block: true,
}
sbUps := aghtest.NewBlockUpstream(hostname, true)
ans4, _ := (&aghtest.TestResolver{}).HostToIPs(hostname)
filterConf := &filtering.Config{
@@ -1029,7 +1026,7 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
s.conf.UDPListenAddrs = []*net.UDPAddr{{}}
s.conf.TCPListenAddrs = []*net.TCPAddr{{}}
s.conf.UpstreamDNS = []string{"127.0.0.1:53"}
s.conf.FilteringConfig.ProtectionEnabled = true
s.conf.ProtectionEnabled = true
err = s.Prepare(nil)
require.NoError(t, err)
@@ -1177,25 +1174,48 @@ func TestNewServer(t *testing.T) {
}
func TestServer_Exchange(t *testing.T) {
extUpstream := &aghtest.Upstream{
Reverse: map[string][]string{
"1.1.1.1.in-addr.arpa.": {"one.one.one.one"},
const (
onesHost = "one.one.one.one"
localDomainHost = "local.domain"
)
var (
onesIP = net.IP{1, 1, 1, 1}
localIP = net.IP{192, 168, 1, 1}
)
revExtIPv4, err := netutil.IPToReversedAddr(onesIP)
require.NoError(t, err)
extUpstream := &aghtest.UpstreamMock{
OnAddress: func() (addr string) { return "external.upstream.example" },
OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
resp = aghalg.Coalesce(
aghtest.RespondTo(t, req, dns.ClassINET, dns.TypePTR, revExtIPv4, onesHost),
new(dns.Msg).SetRcode(req, dns.RcodeNameError),
)
return resp, nil
},
}
locUpstream := &aghtest.Upstream{
Reverse: map[string][]string{
"1.1.168.192.in-addr.arpa.": {"local.domain"},
"2.1.168.192.in-addr.arpa.": {},
revLocIPv4, err := netutil.IPToReversedAddr(localIP)
require.NoError(t, err)
locUpstream := &aghtest.UpstreamMock{
OnAddress: func() (addr string) { return "local.upstream.example" },
OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
resp = aghalg.Coalesce(
aghtest.RespondTo(t, req, dns.ClassINET, dns.TypePTR, revLocIPv4, localDomainHost),
new(dns.Msg).SetRcode(req, dns.RcodeNameError),
)
return resp, nil
},
}
upstreamErr := errors.Error("upstream error")
errUpstream := &aghtest.TestErrUpstream{
Err: upstreamErr,
}
nonPtrUpstream := &aghtest.TestBlockUpstream{
Hostname: "some-host",
Block: true,
}
errUpstream := aghtest.NewErrorUpstream()
nonPtrUpstream := aghtest.NewBlockUpstream("some-host", true)
srv := NewCustomServer(&proxy.Proxy{
Config: proxy.Config{
@@ -1209,7 +1229,6 @@ func TestServer_Exchange(t *testing.T) {
srv.privateNets = netutil.SubnetSetFunc(netutil.IsLocallyServed)
localIP := net.IP{192, 168, 1, 1}
testCases := []struct {
name string
want string
@@ -1218,20 +1237,20 @@ func TestServer_Exchange(t *testing.T) {
req net.IP
}{{
name: "external_good",
want: "one.one.one.one",
want: onesHost,
wantErr: nil,
locUpstream: nil,
req: net.IP{1, 1, 1, 1},
req: onesIP,
}, {
name: "local_good",
want: "local.domain",
want: localDomainHost,
wantErr: nil,
locUpstream: locUpstream,
req: localIP,
}, {
name: "upstream_error",
want: "",
wantErr: upstreamErr,
wantErr: aghtest.ErrUpstream,
locUpstream: errUpstream,
req: localIP,
}, {

View File

@@ -34,7 +34,7 @@ func (l *testQueryLog) Add(p *querylog.AddParams) {
type testStats struct {
// Stats is embedded here simply to make testStats a stats.Stats without
// actually implementing all methods.
stats.Stats
stats.Interface
lastEntry stats.Entry
}

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"io/fs"
"net"
"net/http"
"os"
"runtime"
"runtime/debug"
@@ -14,6 +13,7 @@ import (
"sync"
"sync/atomic"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/cache"
@@ -94,7 +94,7 @@ type Config struct {
ConfigModified func() `yaml:"-"`
// Register an HTTP handler
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"`
HTTPRegister aghhttp.RegisterFunc `yaml:"-"`
// CustomResolver is the resolver used by DNSFilter.
CustomResolver Resolver `yaml:"-"`

View File

@@ -21,6 +21,11 @@ func TestMain(m *testing.M) {
aghtest.DiscardLogOutput(m)
}
const (
sbBlocked = "wmconvirus.narod.ru"
pcBlocked = "pornhub.com"
)
var setts = Settings{
ProtectionEnabled: true,
}
@@ -173,43 +178,37 @@ func TestSafeBrowsing(t *testing.T) {
d := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
t.Cleanup(d.Close)
const matching = "wmconvirus.narod.ru"
d.SetSafeBrowsingUpstream(&aghtest.TestBlockUpstream{
Hostname: matching,
Block: true,
})
d.checkMatch(t, matching)
require.Contains(t, logOutput.String(), "SafeBrowsing lookup for "+matching)
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
d.checkMatch(t, sbBlocked)
d.checkMatch(t, "test."+matching)
require.Contains(t, logOutput.String(), fmt.Sprintf("safebrowsing lookup for %q", sbBlocked))
d.checkMatch(t, "test."+sbBlocked)
d.checkMatchEmpty(t, "yandex.ru")
d.checkMatchEmpty(t, "pornhub.com")
d.checkMatchEmpty(t, pcBlocked)
// Cached result.
d.safeBrowsingServer = "127.0.0.1"
d.checkMatch(t, matching)
d.checkMatchEmpty(t, "pornhub.com")
d.checkMatch(t, sbBlocked)
d.checkMatchEmpty(t, pcBlocked)
d.safeBrowsingServer = defaultSafebrowsingServer
}
func TestParallelSB(t *testing.T) {
d := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
t.Cleanup(d.Close)
const matching = "wmconvirus.narod.ru"
d.SetSafeBrowsingUpstream(&aghtest.TestBlockUpstream{
Hostname: matching,
Block: true,
})
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
t.Run("group", func(t *testing.T) {
for i := 0; i < 100; i++ {
t.Run(fmt.Sprintf("aaa%d", i), func(t *testing.T) {
t.Parallel()
d.checkMatch(t, matching)
d.checkMatch(t, "test."+matching)
d.checkMatch(t, sbBlocked)
d.checkMatch(t, "test."+sbBlocked)
d.checkMatchEmpty(t, "yandex.ru")
d.checkMatchEmpty(t, "pornhub.com")
d.checkMatchEmpty(t, pcBlocked)
})
}
})
@@ -382,23 +381,19 @@ func TestParentalControl(t *testing.T) {
d := newForTest(t, &Config{ParentalEnabled: true}, nil)
t.Cleanup(d.Close)
const matching = "pornhub.com"
d.SetParentalUpstream(&aghtest.TestBlockUpstream{
Hostname: matching,
Block: true,
})
d.checkMatch(t, matching)
require.Contains(t, logOutput.String(), "Parental lookup for "+matching)
d.SetParentalUpstream(aghtest.NewBlockUpstream(pcBlocked, true))
d.checkMatch(t, pcBlocked)
require.Contains(t, logOutput.String(), fmt.Sprintf("parental lookup for %q", pcBlocked))
d.checkMatch(t, "www."+matching)
d.checkMatch(t, "www."+pcBlocked)
d.checkMatchEmpty(t, "www.yandex.ru")
d.checkMatchEmpty(t, "yandex.ru")
d.checkMatchEmpty(t, "api.jquery.com")
// Test cached result.
d.parentalServer = "127.0.0.1"
d.checkMatch(t, matching)
d.checkMatch(t, pcBlocked)
d.checkMatchEmpty(t, "yandex.ru")
}
@@ -445,7 +440,7 @@ func TestMatching(t *testing.T) {
}, {
name: "sanity",
rules: "||doubleclick.net^",
host: "wmconvirus.narod.ru",
host: sbBlocked,
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
@@ -765,14 +760,9 @@ func TestClientSettings(t *testing.T) {
}},
)
t.Cleanup(d.Close)
d.SetParentalUpstream(&aghtest.TestBlockUpstream{
Hostname: "pornhub.com",
Block: true,
})
d.SetSafeBrowsingUpstream(&aghtest.TestBlockUpstream{
Hostname: "wmconvirus.narod.ru",
Block: true,
})
d.SetParentalUpstream(aghtest.NewBlockUpstream(pcBlocked, true))
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
type testCase struct {
name string
@@ -787,12 +777,12 @@ func TestClientSettings(t *testing.T) {
wantReason: FilteredBlockList,
}, {
name: "parental",
host: "pornhub.com",
host: pcBlocked,
before: true,
wantReason: FilteredParental,
}, {
name: "safebrowsing",
host: "wmconvirus.narod.ru",
host: sbBlocked,
before: false,
wantReason: FilteredSafeBrowsing,
}, {
@@ -836,33 +826,29 @@ func TestClientSettings(t *testing.T) {
func BenchmarkSafeBrowsing(b *testing.B) {
d := newForTest(b, &Config{SafeBrowsingEnabled: true}, nil)
b.Cleanup(d.Close)
blocked := "wmconvirus.narod.ru"
d.SetSafeBrowsingUpstream(&aghtest.TestBlockUpstream{
Hostname: blocked,
Block: true,
})
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
for n := 0; n < b.N; n++ {
res, err := d.CheckHost(blocked, dns.TypeA, &setts)
res, err := d.CheckHost(sbBlocked, dns.TypeA, &setts)
require.NoError(b, err)
assert.True(b, res.IsFiltered, "Expected hostname %s to match", blocked)
assert.Truef(b, res.IsFiltered, "expected hostname %q to match", sbBlocked)
}
}
func BenchmarkSafeBrowsingParallel(b *testing.B) {
d := newForTest(b, &Config{SafeBrowsingEnabled: true}, nil)
b.Cleanup(d.Close)
blocked := "wmconvirus.narod.ru"
d.SetSafeBrowsingUpstream(&aghtest.TestBlockUpstream{
Hostname: blocked,
Block: true,
})
d.SetSafeBrowsingUpstream(aghtest.NewBlockUpstream(sbBlocked, true))
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
res, err := d.CheckHost(blocked, dns.TypeA, &setts)
res, err := d.CheckHost(sbBlocked, dns.TypeA, &setts)
require.NoError(b, err)
assert.True(b, res.IsFiltered, "Expected hostname %s to match", blocked)
assert.Truef(b, res.IsFiltered, "expected hostname %q to match", sbBlocked)
}
})
}

View File

@@ -314,7 +314,7 @@ func (d *DNSFilter) checkSafeBrowsing(
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("SafeBrowsing lookup for %s", host)
defer timer.LogElapsed("safebrowsing lookup for %q", host)
}
sctx := &sbCtx{
@@ -348,7 +348,7 @@ func (d *DNSFilter) checkParental(
if log.GetLevel() >= log.DEBUG {
timer := log.StartTimer()
defer timer.LogElapsed("Parental lookup for %s", host)
defer timer.LogElapsed("parental lookup for %q", host)
}
sctx := &sbCtx{

View File

@@ -74,21 +74,20 @@ func TestSafeBrowsingCache(t *testing.T) {
c.hashToHost[hash] = "sub.host.com"
assert.Equal(t, -1, c.getCached())
// match "sub.host.com" from cache,
// but another hash for "nonexisting.com" is not in cache
// which means that we must get data from server for it
// 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("nonexisting.com"))
c.hashToHost[hash] = "nonexisting.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("nonexisting.com"))
hash = sha256.Sum256([]byte("host.example"))
_, ok = c.hashToHost[hash]
assert.True(t, ok)
@@ -111,8 +110,7 @@ func TestSBPC_checkErrorUpstream(t *testing.T) {
d := newForTest(t, &Config{SafeBrowsingEnabled: true}, nil)
t.Cleanup(d.Close)
ups := &aghtest.TestErrUpstream{}
ups := aghtest.NewErrorUpstream()
d.SetSafeBrowsingUpstream(ups)
d.SetParentalUpstream(ups)
@@ -170,10 +168,16 @@ func TestSBPC(t *testing.T) {
for _, tc := range testCases {
// Prepare the upstream.
ups := &aghtest.TestBlockUpstream{
Hostname: hostname,
Block: tc.block,
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)
@@ -196,7 +200,7 @@ func TestSBPC(t *testing.T) {
assert.Equal(t, hits, tc.testCache.Stats().Hit)
// There was one request to an upstream.
assert.Equal(t, 1, ups.RequestsCount())
assert.Equal(t, 1, numReq)
// Now make the same request to check the cache was used.
res, err = tc.testFunc(hostname, dns.TypeA, setts)
@@ -214,7 +218,7 @@ func TestSBPC(t *testing.T) {
assert.Equal(t, hits+1, tc.testCache.Stats().Hit)
// Check that there were no additional requests.
assert.Equal(t, 1, ups.RequestsCount())
assert.Equal(t, 1, numReq)
})
purgeCaches(d)

View File

@@ -2,6 +2,7 @@ package home
import (
"bytes"
"encoding"
"fmt"
"net"
"sort"
@@ -60,6 +61,33 @@ const (
ClientSourceHostsFile
)
var _ fmt.Stringer = clientSource(0)
// String returns a human-readable name of cs.
func (cs clientSource) String() (s string) {
switch cs {
case ClientSourceWHOIS:
return "WHOIS"
case ClientSourceARP:
return "ARP"
case ClientSourceRDNS:
return "rDNS"
case ClientSourceDHCP:
return "DHCP"
case ClientSourceHostsFile:
return "etc/hosts"
default:
return ""
}
}
var _ encoding.TextMarshaler = clientSource(0)
// MarshalText implements encoding.TextMarshaler for the clientSource.
func (cs clientSource) MarshalText() (text []byte, err error) {
return []byte(cs.String()), nil
}
// clientSourceConf is used to configure where the runtime clients will be
// obtained from.
type clientSourcesConf struct {
@@ -397,6 +425,7 @@ func (clients *clientsContainer) Find(id string) (c *Client, ok bool) {
c.Tags = stringutil.CloneSlice(c.Tags)
c.BlockedServices = stringutil.CloneSlice(c.BlockedServices)
c.Upstreams = stringutil.CloneSlice(c.Upstreams)
return c, true
}

View File

@@ -47,9 +47,9 @@ type clientJSON struct {
type runtimeClientJSON struct {
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info"`
Name string `json:"name"`
Source string `json:"source"`
IP net.IP `json:"ip"`
Name string `json:"name"`
Source clientSource `json:"source"`
IP net.IP `json:"ip"`
}
type clientListJSON struct {
@@ -81,20 +81,9 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
cj := runtimeClientJSON{
WHOISInfo: rc.WHOISInfo,
Name: rc.Host,
IP: ip,
}
cj.Source = "etc/hosts"
switch rc.Source {
case ClientSourceDHCP:
cj.Source = "DHCP"
case ClientSourceRDNS:
cj.Source = "rDNS"
case ClientSourceARP:
cj.Source = "ARP"
case ClientSourceWHOIS:
cj.Source = "WHOIS"
Name: rc.Host,
Source: rc.Source,
IP: ip,
}
data.RuntimeClients = append(data.RuntimeClients, cj)
@@ -107,13 +96,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w).Encode(data)
if e != nil {
aghhttp.Error(
r,
w,
http.StatusInternalServerError,
"Failed to encode to json: %v",
e,
)
aghhttp.Error(r, w, http.StatusInternalServerError, "failed to encode to json: %v", e)
return
}
@@ -279,9 +262,9 @@ func (clients *clientsContainer) handleFindClient(w http.ResponseWriter, r *http
func (clients *clientsContainer) findRuntime(ip net.IP, idStr string) (cj *clientJSON) {
rc, ok := clients.FindRuntimeClient(ip)
if !ok {
// It is still possible that the IP used to be in the runtime
// clients list, but then the server was reloaded. So, check
// the DNS server's blocked IP list.
// It is still possible that the IP used to be in the runtime clients
// list, but then the server was reloaded. So, check the DNS server's
// blocked IP list.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2428.
disallowed, rule := clients.dnsServer.IsBlockedClient(ip, idStr)

View File

@@ -1,6 +1,7 @@
package home
import (
"bytes"
"fmt"
"net"
"os"
@@ -19,7 +20,7 @@ import (
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/google/renameio/maybe"
yaml "gopkg.in/yaml.v2"
yaml "gopkg.in/yaml.v3"
)
const (
@@ -27,15 +28,36 @@ const (
filterDir = "filters" // cache location for downloaded filters, it's under DataDir
)
// logSettings
// logSettings are the logging settings part of the configuration file.
//
// TODO(a.garipov): Put them into a separate object.
type logSettings struct {
LogCompress bool `yaml:"log_compress"` // Compress determines if the rotated log files should be compressed using gzip (default: false)
LogLocalTime bool `yaml:"log_localtime"` // If the time used for formatting the timestamps in is the computer's local time (default: false [UTC])
LogMaxBackups int `yaml:"log_max_backups"` // Maximum number of old log files to retain (MaxAge may still cause them to get deleted)
LogMaxSize int `yaml:"log_max_size"` // Maximum size in megabytes of the log file before it gets rotated (default 100 MB)
LogMaxAge int `yaml:"log_max_age"` // MaxAge is the maximum number of days to retain old log files
LogFile string `yaml:"log_file"` // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
Verbose bool `yaml:"verbose"` // If true, verbose logging is enabled
// File is the path to the log file. If empty, logs are written to stdout.
// If "syslog", logs are written to syslog.
File string `yaml:"log_file"`
// MaxBackups is the maximum number of old log files to retain.
//
// NOTE: MaxAge may still cause them to get deleted.
MaxBackups int `yaml:"log_max_backups"`
// MaxSize is the maximum size of the log file before it gets rotated, in
// megabytes. The default value is 100 MB.
MaxSize int `yaml:"log_max_size"`
// MaxAge is the maximum duration for retaining old log files, in days.
MaxAge int `yaml:"log_max_age"`
// Compress determines, if the rotated log files should be compressed using
// gzip.
Compress bool `yaml:"log_compress"`
// LocalTime determines, if the time used for formatting the timestamps in
// is the computer's local time.
LocalTime bool `yaml:"log_localtime"`
// Verbose determines, if verbose (aka debug) logging is enabled.
Verbose bool `yaml:"verbose"`
}
// osConfig contains OS-related configuration.
@@ -187,6 +209,7 @@ var config = &configuration{
Ratelimit: 20,
RefuseAny: true,
AllServers: false,
HandleDDR: true,
FastestTimeout: timeutil.Duration{
Duration: fastip.DefaultPingWaitTimeout,
},
@@ -222,11 +245,11 @@ var config = &configuration{
},
},
logSettings: logSettings{
LogCompress: false,
LogLocalTime: false,
LogMaxBackups: 0,
LogMaxSize: 100,
LogMaxAge: 3,
Compress: false,
LocalTime: false,
MaxBackups: 0,
MaxSize: 100,
MaxAge: 3,
},
OSConfig: &osConfig{},
SchemaVersion: currentSchemaVersion,
@@ -365,13 +388,14 @@ func readConfigFile() (fileData []byte, err error) {
}
// Saves configuration to the YAML file and also saves the user filter contents to a file
func (c *configuration) write() error {
func (c *configuration) write() (err error) {
c.Lock()
defer c.Unlock()
if Context.auth != nil {
config.Users = Context.auth.GetUsers()
}
if Context.tls != nil {
tlsConf := tlsConfigSettings{}
Context.tls.WriteDiskConfig(&tlsConf)
@@ -417,19 +441,20 @@ func (c *configuration) write() error {
config.Clients.Persistent = Context.clients.forConfig()
configFile := config.getConfigFilename()
log.Debug("Writing YAML file: %s", configFile)
yamlText, err := yaml.Marshal(&config)
if err != nil {
log.Error("Couldn't generate YAML file: %s", err)
log.Debug("writing config file %q", configFile)
return err
buf := &bytes.Buffer{}
enc := yaml.NewEncoder(buf)
enc.SetIndent(2)
err = enc.Encode(config)
if err != nil {
return fmt.Errorf("generating config file: %w", err)
}
err = maybe.WriteFile(configFile, yamlText, 0o644)
err = maybe.WriteFile(configFile, buf.Bytes(), 0o644)
if err != nil {
log.Error("Couldn't save YAML config: %s", err)
return err
return fmt.Errorf("writing config file: %w", err)
}
return nil

View File

@@ -189,7 +189,7 @@ func registerControlHandlers() {
RegisterAuthHandlers()
}
func httpRegister(method, url string, handler func(http.ResponseWriter, *http.Request)) {
func httpRegister(method, url string, handler http.HandlerFunc) {
if method == "" {
// "/dns-query" handler doesn't need auth, gzip and isn't restricted by 1 HTTP method
Context.mux.HandleFunc(url, postInstall(handler))

View File

@@ -216,7 +216,7 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request)
func handleStaticIP(ip net.IP, set bool) staticIPJSON {
resp := staticIPJSON{}
interfaceName := aghnet.GetInterfaceByIP(ip)
interfaceName := aghnet.InterfaceByIP(ip)
resp.Static = "no"
if len(interfaceName) == 0 {

View File

@@ -17,7 +17,7 @@ import (
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/ameshkov/dnscrypt/v2"
yaml "gopkg.in/yaml.v2"
yaml "gopkg.in/yaml.v3"
)
// Default ports.
@@ -397,7 +397,7 @@ func startDNSServer() error {
Context.queryLog.Start()
const topClientsNumber = 100 // the number of clients to get
for _, ip := range Context.stats.GetTopClientsIP(topClientsNumber) {
for _, ip := range Context.stats.TopClientsIP(topClientsNumber) {
if ip == nil {
continue
}
@@ -456,7 +456,12 @@ func closeDNSServer() {
}
if Context.stats != nil {
Context.stats.Close()
err := Context.stats.Close()
if err != nil {
log.Debug("closing stats: %s", err)
}
// TODO(e.burkov): Find out if it's safe.
Context.stats = nil
}

View File

@@ -46,7 +46,7 @@ type homeContext struct {
// --
clients clientsContainer // per-client-settings module
stats stats.Stats // statistics module
stats stats.Interface // statistics module
queryLog querylog.QueryLog // query log module
dnsServer *dnsforward.Server // DNS module
rdns *RDNS // rDNS module
@@ -602,17 +602,17 @@ func configureLogger(args options) {
ls.Verbose = true
}
if args.logFile != "" {
ls.LogFile = args.logFile
} else if config.LogFile != "" {
ls.LogFile = config.LogFile
ls.File = args.logFile
} else if config.File != "" {
ls.File = config.File
}
// Handle default log settings overrides
ls.LogCompress = config.LogCompress
ls.LogLocalTime = config.LogLocalTime
ls.LogMaxBackups = config.LogMaxBackups
ls.LogMaxSize = config.LogMaxSize
ls.LogMaxAge = config.LogMaxAge
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 {
@@ -623,27 +623,27 @@ func configureLogger(args options) {
// happen pretty quickly.
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
if args.runningAsService && ls.LogFile == "" && runtime.GOOS == "windows" {
if args.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
// When running as a Windows service, use eventlog by default if nothing
// else is configured. Otherwise, we'll simply lose the log output.
ls.LogFile = configSyslog
ls.File = configSyslog
}
// logs are written to stdout (default)
if ls.LogFile == "" {
if ls.File == "" {
return
}
if ls.LogFile == configSyslog {
if ls.File == configSyslog {
// Use syslog where it is possible and eventlog on Windows
err := aghos.ConfigureSyslog(serviceName)
if err != nil {
log.Fatalf("cannot initialize syslog: %s", err)
}
} else {
logFilePath := filepath.Join(Context.workDir, ls.LogFile)
if filepath.IsAbs(ls.LogFile) {
logFilePath = ls.LogFile
logFilePath := filepath.Join(Context.workDir, ls.File)
if filepath.IsAbs(ls.File) {
logFilePath = ls.File
}
_, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
@@ -653,11 +653,11 @@ func configureLogger(args options) {
log.SetOutput(&lumberjack.Logger{
Filename: logFilePath,
Compress: ls.LogCompress, // disabled by default
LocalTime: ls.LogLocalTime,
MaxBackups: ls.LogMaxBackups,
MaxSize: ls.LogMaxSize, // megabytes
MaxAge: ls.LogMaxAge, // days
Compress: ls.Compress, // disabled by default
LocalTime: ls.LocalTime,
MaxBackups: ls.MaxBackups,
MaxSize: ls.MaxSize, // megabytes
MaxAge: ls.MaxAge, // days
})
}
}

View File

@@ -13,6 +13,7 @@ import (
// TODO(a.garipov): Get rid of a global or generate from .twosky.json.
var allowedLanguages = stringutil.NewSet(
"ar",
"be",
"bg",
"cs",
@@ -50,7 +51,7 @@ var allowedLanguages = stringutil.NewSet(
"zh-tw",
)
func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
func handleI18nCurrentLanguage(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain")
log.Printf("config.Language is %s", config.Language)
_, err := fmt.Fprintf(w, "%s\n", config.Language)
@@ -58,6 +59,7 @@ func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {
msg := fmt.Sprintf("Unable to write response json: %s", err)
log.Println(msg)
http.Error(w, msg, http.StatusInternalServerError)
return
}
}
@@ -69,6 +71,7 @@ func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) {
msg := fmt.Sprintf("failed to read request body: %s", err)
log.Println(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}

View File

@@ -3,15 +3,16 @@ package home
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"sync"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
@@ -80,8 +81,10 @@ func TestRDNS_Begin(t *testing.T) {
binary.BigEndian.PutUint64(ttl, uint64(time.Now().Add(100*time.Hour).Unix()))
rdns := &RDNS{
ipCache: ipCache,
exchanger: &rDNSExchanger{},
ipCache: ipCache,
exchanger: &rDNSExchanger{
ex: aghtest.NewErrorUpstream(),
},
clients: &clientsContainer{
list: map[string]*Client{},
idIndex: tc.cliIDIndex,
@@ -108,16 +111,22 @@ func TestRDNS_Begin(t *testing.T) {
// rDNSExchanger is a mock dnsforward.RDNSExchanger implementation for tests.
type rDNSExchanger struct {
ex aghtest.Exchanger
ex upstream.Upstream
usePrivate bool
}
// Exchange implements dnsforward.RDNSExchanger interface for *RDNSExchanger.
func (e *rDNSExchanger) Exchange(ip net.IP) (host string, err error) {
rev, err := netutil.IPToReversedAddr(ip)
if err != nil {
return "", fmt.Errorf("reversing ip: %w", err)
}
req := &dns.Msg{
Question: []dns.Question{{
Name: ip.String(),
Qtype: dns.TypePTR,
Name: dns.Fqdn(rev),
Qclass: dns.ClassINET,
Qtype: dns.TypePTR,
}},
}
@@ -146,7 +155,9 @@ func TestRDNS_ensurePrivateCache(t *testing.T) {
MaxCount: defaultRDNSCacheSize,
})
ex := &rDNSExchanger{}
ex := &rDNSExchanger{
ex: aghtest.NewErrorUpstream(),
}
rdns := &RDNS{
ipCache: ipCache,
@@ -167,15 +178,27 @@ func TestRDNS_WorkerLoop(t *testing.T) {
w := &bytes.Buffer{}
aghtest.ReplaceLogWriter(t, w)
locUpstream := &aghtest.Upstream{
Reverse: map[string][]string{
"192.168.1.1": {"local.domain"},
"2a00:1450:400c:c06::93": {"ipv6.domain"},
localIP := net.IP{192, 168, 1, 1}
revIPv4, err := netutil.IPToReversedAddr(localIP)
require.NoError(t, err)
revIPv6, err := netutil.IPToReversedAddr(net.ParseIP("2a00:1450:400c:c06::93"))
require.NoError(t, err)
locUpstream := &aghtest.UpstreamMock{
OnAddress: func() (addr string) { return "local.upstream.example" },
OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
resp = aghalg.Coalesce(
aghtest.RespondTo(t, req, dns.ClassINET, dns.TypePTR, revIPv4, "local.domain"),
aghtest.RespondTo(t, req, dns.ClassINET, dns.TypePTR, revIPv6, "ipv6.domain"),
new(dns.Msg).SetRcode(req, dns.RcodeNameError),
)
return resp, nil
},
}
errUpstream := &aghtest.TestErrUpstream{
Err: errors.Error("1234"),
}
errUpstream := aghtest.NewErrorUpstream()
testCases := []struct {
ups upstream.Upstream
@@ -186,10 +209,10 @@ func TestRDNS_WorkerLoop(t *testing.T) {
ups: locUpstream,
wantLog: "",
name: "all_good",
cliIP: net.IP{192, 168, 1, 1},
cliIP: localIP,
}, {
ups: errUpstream,
wantLog: `rdns: resolving "192.168.1.2": errupstream: 1234`,
wantLog: `rdns: resolving "192.168.1.2": test upstream error`,
name: "resolve_error",
cliIP: net.IP{192, 168, 1, 2},
}, {
@@ -211,9 +234,7 @@ func TestRDNS_WorkerLoop(t *testing.T) {
ch := make(chan net.IP)
rdns := &RDNS{
exchanger: &rDNSExchanger{
ex: aghtest.Exchanger{
Ups: tc.ups,
},
ex: tc.ups,
},
clients: cc,
ipCh: ch,

View File

@@ -1,6 +1,7 @@
package home
import (
"bytes"
"fmt"
"net/url"
"os"
@@ -17,7 +18,7 @@ import (
"github.com/AdguardTeam/golibs/timeutil"
"github.com/google/renameio/maybe"
"golang.org/x/crypto/bcrypt"
yaml "gopkg.in/yaml.v2"
yaml "gopkg.in/yaml.v3"
)
// currentSchemaVersion is the current schema version.
@@ -104,16 +105,20 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) {
return fmt.Errorf("unknown configuration schema version %d", oldVersion)
}
body, err := yaml.Marshal(diskConf)
buf := &bytes.Buffer{}
enc := yaml.NewEncoder(buf)
enc.SetIndent(2)
err = enc.Encode(diskConf)
if err != nil {
return fmt.Errorf("generating new config: %w", err)
}
config.fileData = body
config.fileData = buf.Bytes()
confFile := config.getConfigFilename()
err = maybe.WriteFile(confFile, body, 0o644)
err = maybe.WriteFile(confFile, config.fileData, 0o644)
if err != nil {
return fmt.Errorf("saving new config: %w", err)
return fmt.Errorf("writing new config: %w", err)
}
return nil

View File

@@ -190,7 +190,7 @@ func testDiskConf(schemaVersion int) (diskConf yobj) {
return diskConf
}
// testDNSConf creates a DNS config for test the way gopkg.in/yaml.v2 would
// testDNSConf creates a DNS config for test the way gopkg.in/yaml.v3 would
// unmarshal it. In YAML, keys aren't guaranteed to always only be strings.
func testDNSConf(schemaVersion int) (dnsConf yobj) {
dnsConf = yobj{

View File

@@ -2,10 +2,10 @@ package querylog
import (
"net"
"net/http"
"path/filepath"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/errors"
@@ -38,7 +38,7 @@ type Config struct {
ConfigModified func()
// HTTPRegister registers an HTTP handler.
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request))
HTTPRegister aghhttp.RegisterFunc
// FindClient returns client information by their IDs.
FindClient func(ids []string) (c *Client, err error)

View File

@@ -5,6 +5,7 @@ package stats
import (
"encoding/json"
"net/http"
"sync/atomic"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@@ -15,18 +16,10 @@ import (
// The key is either a client's address or a requested address.
type topAddrs = map[string]uint64
// statsResponse is a response for getting statistics.
type statsResponse struct {
// StatsResp is a response to the GET /control/stats.
type StatsResp struct {
TimeUnits string `json:"time_units"`
NumDNSQueries uint64 `json:"num_dns_queries"`
NumBlockedFiltering uint64 `json:"num_blocked_filtering"`
NumReplacedSafebrowsing uint64 `json:"num_replaced_safebrowsing"`
NumReplacedSafesearch uint64 `json:"num_replaced_safesearch"`
NumReplacedParental uint64 `json:"num_replaced_parental"`
AvgProcessingTime float64 `json:"avg_processing_time"`
TopQueried []topAddrs `json:"top_queried_domains"`
TopClients []topAddrs `json:"top_clients"`
TopBlocked []topAddrs `json:"top_blocked_domains"`
@@ -36,37 +29,30 @@ type statsResponse struct {
BlockedFiltering []uint64 `json:"blocked_filtering"`
ReplacedSafebrowsing []uint64 `json:"replaced_safebrowsing"`
ReplacedParental []uint64 `json:"replaced_parental"`
NumDNSQueries uint64 `json:"num_dns_queries"`
NumBlockedFiltering uint64 `json:"num_blocked_filtering"`
NumReplacedSafebrowsing uint64 `json:"num_replaced_safebrowsing"`
NumReplacedSafesearch uint64 `json:"num_replaced_safesearch"`
NumReplacedParental uint64 `json:"num_replaced_parental"`
AvgProcessingTime float64 `json:"avg_processing_time"`
}
// handleStats is a handler for getting statistics.
func (s *statsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
// handleStats handles requests to the GET /control/stats endpoint.
func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
limit := atomic.LoadUint32(&s.limitHours)
start := time.Now()
resp, ok := s.getData(limit)
log.Debug("stats: prepared data in %v", time.Since(start))
var resp statsResponse
if s.conf.limit == 0 {
resp = statsResponse{
TimeUnits: "days",
if !ok {
// Don't bring the message to the lower case since it's a part of UI
// text for the moment.
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't get statistics data")
TopBlocked: []topAddrs{},
TopClients: []topAddrs{},
TopQueried: []topAddrs{},
BlockedFiltering: []uint64{},
DNSQueries: []uint64{},
ReplacedParental: []uint64{},
ReplacedSafebrowsing: []uint64{},
}
} else {
var ok bool
resp, ok = s.getData()
log.Debug("stats: prepared data in %v", time.Since(start))
if !ok {
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't get statistics data")
return
}
return
}
w.Header().Set("Content-Type", "application/json")
@@ -74,36 +60,30 @@ func (s *statsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
err := json.NewEncoder(w).Encode(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
return
}
}
type config struct {
// configResp is the response to the GET /control/stats_info.
type configResp struct {
IntervalDays uint32 `json:"interval"`
}
// Get configuration
func (s *statsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
resp := config{}
resp.IntervalDays = s.conf.limit / 24
// handleStatsInfo handles requests to the GET /control/stats_info endpoint.
func (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
resp := configResp{IntervalDays: atomic.LoadUint32(&s.limitHours) / 24}
data, err := json.Marshal(resp)
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(resp)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(data)
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "http write: %s", err)
}
}
// Set configuration
func (s *statsCtx) handleStatsConfig(w http.ResponseWriter, r *http.Request) {
reqData := config{}
// handleStatsConfig handles requests to the POST /control/stats_config
// endpoint.
func (s *StatsCtx) handleStatsConfig(w http.ResponseWriter, r *http.Request) {
reqData := configResp{}
err := json.NewDecoder(r.Body).Decode(&reqData)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
@@ -118,22 +98,25 @@ func (s *statsCtx) handleStatsConfig(w http.ResponseWriter, r *http.Request) {
}
s.setLimit(int(reqData.IntervalDays))
s.conf.ConfigModified()
s.configModified()
}
// Reset data
func (s *statsCtx) handleStatsReset(w http.ResponseWriter, r *http.Request) {
s.clear()
// handleStatsReset handles requests to the POST /control/stats_reset endpoint.
func (s *StatsCtx) handleStatsReset(w http.ResponseWriter, r *http.Request) {
err := s.clear()
if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "stats: %s", err)
}
}
// Register web handlers
func (s *statsCtx) initWeb() {
if s.conf.HTTPRegister == nil {
// initWeb registers the handlers for web endpoints of statistics module.
func (s *StatsCtx) initWeb() {
if s.httpRegister == nil {
return
}
s.conf.HTTPRegister(http.MethodGet, "/control/stats", s.handleStats)
s.conf.HTTPRegister(http.MethodPost, "/control/stats_reset", s.handleStatsReset)
s.conf.HTTPRegister(http.MethodPost, "/control/stats_config", s.handleStatsConfig)
s.conf.HTTPRegister(http.MethodGet, "/control/stats_info", s.handleStatsInfo)
s.httpRegister(http.MethodGet, "/control/stats", s.handleStats)
s.httpRegister(http.MethodPost, "/control/stats_reset", s.handleStatsReset)
s.httpRegister(http.MethodPost, "/control/stats_config", s.handleStatsConfig)
s.httpRegister(http.MethodGet, "/control/stats_info", s.handleStatsInfo)
}

View File

@@ -3,86 +3,541 @@
package stats
import (
"fmt"
"io"
"net"
"net/http"
"os"
"sync"
"sync/atomic"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"go.etcd.io/bbolt"
)
type unitIDCallback func() uint32
// DiskConfig - configuration settings that are stored on disk
// DiskConfig is the configuration structure that is stored in file.
type DiskConfig struct {
Interval uint32 `yaml:"statistics_interval"` // time interval for statistics (in days)
// Interval is the number of days for which the statistics are collected
// before flushing to the database.
Interval uint32 `yaml:"statistics_interval"`
}
// Config - module configuration
type Config struct {
Filename string // database file name
LimitDays uint32 // time limit (in days)
UnitID unitIDCallback // user function to get the current unit ID. If nil, the current time hour is used.
// checkInterval returns true if days is valid to be used as statistics
// retention interval. The valid values are 0, 1, 7, 30 and 90.
func checkInterval(days uint32) (ok bool) {
return days == 0 || days == 1 || days == 7 || days == 30 || days == 90
}
// Called when the configuration is changed by HTTP request
// Config is the configuration structure for the statistics collecting.
type Config struct {
// UnitID is the function to generate the identifier for current unit. If
// nil, the default function is used, see newUnitID.
UnitID UnitIDGenFunc
// ConfigModified will be called each time the configuration changed via web
// interface.
ConfigModified func()
// Register an HTTP handler
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request))
// HTTPRegister is the function that registers handlers for the stats
// endpoints.
HTTPRegister aghhttp.RegisterFunc
limit uint32 // maximum time we need to keep data for (in hours)
// Filename is the name of the database file.
Filename string
// LimitDays is the maximum number of days to collect statistics into the
// current unit.
LimitDays uint32
}
// New - create object
func New(conf Config) (Stats, error) {
return createObject(conf)
}
// Stats - main interface
type Stats interface {
// Interface is the statistics interface to be used by other packages.
type Interface interface {
// Start begins the statistics collecting.
Start()
// Close object.
// This function is not thread safe
// (can't be called in parallel with any other function of this interface).
Close()
io.Closer
// Update counters
// Update collects the incoming statistics data.
Update(e Entry)
// Get IP addresses of the clients with the most number of requests
GetTopClientsIP(limit uint) []net.IP
// GetTopClientIP returns at most limit IP addresses corresponding to the
// clients with the most number of requests.
TopClientsIP(limit uint) []net.IP
// WriteDiskConfig - write configuration
// WriteDiskConfig puts the Interface's configuration to the dc.
WriteDiskConfig(dc *DiskConfig)
}
// TimeUnit - time unit
type TimeUnit int
// Supported time units
const (
Hours TimeUnit = iota
Days
)
// Result of DNS request processing
type Result int
// Supported result values
const (
RNotFiltered Result = iota + 1
RFiltered
RSafeBrowsing
RSafeSearch
RParental
rLast
)
// Entry is a statistics data entry.
type Entry struct {
// Clients is the client's primary ID.
// StatsCtx collects the statistics and flushes it to the database. Its default
// flushing interval is one hour.
//
// TODO(e.burkov): Use atomic.Pointer for accessing db in go1.19.
type StatsCtx struct {
// limitHours is the maximum number of hours to collect statistics into the
// current unit.
//
// TODO(a.garipov): Make this a {net.IP, string} enum?
Client string
// It is of type uint32 to be accessed by atomic. It's arranged at the
// beginning of the structure to keep 64-bit alignment.
limitHours uint32
Domain string
Result Result
Time uint32 // processing time (msec)
// currMu protects curr.
currMu *sync.RWMutex
// curr is the actual statistics collection result.
curr *unit
// dbMu protects db.
dbMu *sync.Mutex
// db is the opened statistics database, if any.
db *bbolt.DB
// unitIDGen is the function that generates an identifier for the current
// unit. It's here for only testing purposes.
unitIDGen UnitIDGenFunc
// httpRegister is used to set HTTP handlers.
httpRegister aghhttp.RegisterFunc
// configModified is called whenever the configuration is modified via web
// interface.
configModified func()
// filename is the name of database file.
filename string
}
var _ Interface = &StatsCtx{}
// New creates s from conf and properly initializes it. Don't use s before
// calling it's Start method.
func New(conf Config) (s *StatsCtx, err error) {
defer withRecovered(&err)
s = &StatsCtx{
currMu: &sync.RWMutex{},
dbMu: &sync.Mutex{},
filename: conf.Filename,
configModified: conf.ConfigModified,
httpRegister: conf.HTTPRegister,
}
if s.limitHours = conf.LimitDays * 24; !checkInterval(conf.LimitDays) {
s.limitHours = 24
}
if s.unitIDGen = newUnitID; conf.UnitID != nil {
s.unitIDGen = conf.UnitID
}
// TODO(e.burkov): Move the code below to the Start method.
err = s.openDB()
if err != nil {
return nil, fmt.Errorf("opening database: %w", err)
}
var udb *unitDB
id := s.unitIDGen()
tx, err := s.db.Begin(true)
if err != nil {
return nil, fmt.Errorf("stats: opening a transaction: %w", err)
}
deleted := deleteOldUnits(tx, id-s.limitHours-1)
udb = loadUnitFromDB(tx, id)
err = finishTxn(tx, deleted > 0)
if err != nil {
log.Error("stats: %s", err)
}
s.curr = newUnit(id)
s.curr.deserialize(udb)
log.Debug("stats: initialized")
return s, nil
}
// withRecovered turns the value recovered from panic if any into an error and
// combines it with the one pointed by orig. orig must be non-nil.
func withRecovered(orig *error) {
p := recover()
if p == nil {
return
}
var err error
switch p := p.(type) {
case error:
err = fmt.Errorf("panic: %w", p)
default:
err = fmt.Errorf("panic: recovered value of type %[1]T: %[1]v", p)
}
*orig = errors.WithDeferred(*orig, err)
}
// Start implements the Interface interface for *StatsCtx.
func (s *StatsCtx) Start() {
s.initWeb()
go s.periodicFlush()
}
// Close implements the io.Closer interface for *StatsCtx.
func (s *StatsCtx) Close() (err error) {
defer func() { err = errors.Annotate(err, "stats: closing: %w") }()
db := s.swapDatabase(nil)
if db == nil {
return nil
}
defer func() {
cerr := db.Close()
if cerr == nil {
log.Debug("stats: database closed")
}
err = errors.WithDeferred(err, cerr)
}()
tx, err := db.Begin(true)
if err != nil {
return fmt.Errorf("opening transaction: %w", err)
}
defer func() { err = errors.WithDeferred(err, finishTxn(tx, err == nil)) }()
s.currMu.RLock()
defer s.currMu.RUnlock()
udb := s.curr.serialize()
return udb.flushUnitToDB(tx, s.curr.id)
}
// Update implements the Interface interface for *StatsCtx.
func (s *StatsCtx) Update(e Entry) {
if atomic.LoadUint32(&s.limitHours) == 0 {
return
}
if e.Result == 0 || e.Result >= resultLast || e.Domain == "" || e.Client == "" {
log.Debug("stats: malformed entry")
return
}
s.currMu.Lock()
defer s.currMu.Unlock()
if s.curr == nil {
log.Error("stats: current unit is nil")
return
}
clientID := e.Client
if ip := net.ParseIP(clientID); ip != nil {
clientID = ip.String()
}
s.curr.add(e.Result, e.Domain, clientID, uint64(e.Time))
}
// WriteDiskConfig implements the Interface interface for *StatsCtx.
func (s *StatsCtx) WriteDiskConfig(dc *DiskConfig) {
dc.Interval = atomic.LoadUint32(&s.limitHours) / 24
}
// TopClientsIP implements the Interface interface for *StatsCtx.
func (s *StatsCtx) TopClientsIP(maxCount uint) (ips []net.IP) {
limit := atomic.LoadUint32(&s.limitHours)
if limit == 0 {
return nil
}
units, _ := s.loadUnits(limit)
if units == nil {
return nil
}
// Collect data for all the clients to sort and crop it afterwards.
m := map[string]uint64{}
for _, u := range units {
for _, it := range u.Clients {
m[it.Name] += it.Count
}
}
a := convertMapToSlice(m, int(maxCount))
ips = []net.IP{}
for _, it := range a {
ip := net.ParseIP(it.Name)
if ip != nil {
ips = append(ips, ip)
}
}
return ips
}
// database returns the database if it's opened. It's safe for concurrent use.
func (s *StatsCtx) database() (db *bbolt.DB) {
s.dbMu.Lock()
defer s.dbMu.Unlock()
return s.db
}
// swapDatabase swaps the database with another one and returns it. It's safe
// for concurrent use.
func (s *StatsCtx) swapDatabase(with *bbolt.DB) (old *bbolt.DB) {
s.dbMu.Lock()
defer s.dbMu.Unlock()
old, s.db = s.db, with
return old
}
// deleteOldUnits walks the buckets available to tx and deletes old units. It
// returns the number of deletions performed.
func deleteOldUnits(tx *bbolt.Tx, firstID uint32) (deleted int) {
log.Debug("stats: deleting old units until id %d", firstID)
// TODO(a.garipov): See if this is actually necessary. Looks like a rather
// bizarre solution.
const errStop errors.Error = "stop iteration"
walk := func(name []byte, _ *bbolt.Bucket) (err error) {
nameID, ok := unitNameToID(name)
if ok && nameID >= firstID {
return errStop
}
err = tx.DeleteBucket(name)
if err != nil {
log.Debug("stats: deleting bucket: %s", err)
return nil
}
log.Debug("stats: deleted unit %d (name %x)", nameID, name)
deleted++
return nil
}
err := tx.ForEach(walk)
if err != nil && !errors.Is(err, errStop) {
log.Debug("stats: deleting units: %s", err)
}
return deleted
}
// openDB returns an error if the database can't be opened from the specified
// file. It's safe for concurrent use.
func (s *StatsCtx) openDB() (err error) {
log.Debug("stats: opening database")
var db *bbolt.DB
db, err = bbolt.Open(s.filename, 0o644, nil)
if err != nil {
if err.Error() == "invalid argument" {
log.Error("AdGuard Home cannot be initialized due to an incompatible file system.\nPlease read the explanation here: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#limitations")
}
return err
}
// Use defer to unlock the mutex as soon as possible.
defer log.Debug("stats: database opened")
s.dbMu.Lock()
defer s.dbMu.Unlock()
s.db = db
return nil
}
func (s *StatsCtx) flush() (cont bool, sleepFor time.Duration) {
id := s.unitIDGen()
s.currMu.Lock()
defer s.currMu.Unlock()
ptr := s.curr
if ptr == nil {
return false, 0
}
limit := atomic.LoadUint32(&s.limitHours)
if limit == 0 || ptr.id == id {
return true, time.Second
}
db := s.database()
if db == nil {
return true, 0
}
tx, err := db.Begin(true)
if err != nil {
log.Error("stats: opening transaction: %s", err)
return true, 0
}
s.curr = newUnit(id)
isCommitable := true
ferr := ptr.serialize().flushUnitToDB(tx, ptr.id)
if ferr != nil {
log.Error("stats: flushing unit: %s", ferr)
isCommitable = false
}
derr := tx.DeleteBucket(idToUnitName(id - limit))
if derr != nil {
log.Error("stats: deleting unit: %s", derr)
if !errors.Is(derr, bbolt.ErrBucketNotFound) {
isCommitable = false
}
}
err = finishTxn(tx, isCommitable)
if err != nil {
log.Error("stats: %s", err)
}
return true, 0
}
// periodicFlush checks and flushes the unit to the database if the freshly
// generated unit ID differs from the current's ID. Flushing process includes:
// - swapping the current unit with the new empty one;
// - writing the current unit to the database;
// - removing the stale unit from the database.
func (s *StatsCtx) periodicFlush() {
for cont, sleepFor := true, time.Duration(0); cont; time.Sleep(sleepFor) {
cont, sleepFor = s.flush()
}
log.Debug("periodic flushing finished")
}
func (s *StatsCtx) setLimit(limitDays int) {
atomic.StoreUint32(&s.limitHours, uint32(24*limitDays))
if limitDays == 0 {
if err := s.clear(); err != nil {
log.Error("stats: %s", err)
}
}
log.Debug("stats: set limit: %d days", limitDays)
}
// Reset counters and clear database
func (s *StatsCtx) clear() (err error) {
defer func() { err = errors.Annotate(err, "clearing: %w") }()
db := s.swapDatabase(nil)
if db != nil {
var tx *bbolt.Tx
tx, err = db.Begin(true)
if err != nil {
log.Error("stats: opening a transaction: %s", err)
} else if err = finishTxn(tx, false); err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
// Active transactions will continue using database, but new ones won't
// be created.
err = db.Close()
if err != nil {
return fmt.Errorf("closing database: %w", err)
}
// All active transactions are now closed.
log.Debug("stats: database closed")
}
err = os.Remove(s.filename)
if err != nil {
log.Error("stats: %s", err)
}
err = s.openDB()
if err != nil {
log.Error("stats: opening database: %s", err)
}
// Use defer to unlock the mutex as soon as possible.
defer log.Debug("stats: cleared")
s.currMu.Lock()
defer s.currMu.Unlock()
s.curr = newUnit(s.unitIDGen())
return nil
}
func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, firstID uint32) {
db := s.database()
if db == nil {
return nil, 0
}
// Use writable transaction to ensure any ongoing writable transaction is
// taken into account.
tx, err := db.Begin(true)
if err != nil {
log.Error("stats: opening transaction: %s", err)
return nil, 0
}
s.currMu.RLock()
defer s.currMu.RUnlock()
cur := s.curr
var curID uint32
if cur != nil {
curID = cur.id
} else {
curID = s.unitIDGen()
}
// Per-hour units.
units = make([]*unitDB, 0, limit)
firstID = curID - limit + 1
for i := firstID; i != curID; i++ {
u := loadUnitFromDB(tx, i)
if u == nil {
u = &unitDB{NResult: make([]uint64, resultLast)}
}
units = append(units, u)
}
err = finishTxn(tx, false)
if err != nil {
log.Error("stats: %s", err)
}
if cur != nil {
units = append(units, cur.serialize())
}
if unitsLen := len(units); unitsLen != int(limit) {
log.Fatalf("loaded %d units whilst the desired number is %d", unitsLen, limit)
}
return units, firstID
}

View File

@@ -0,0 +1,26 @@
package stats
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TODO(e.burkov): Use more realistic data.
func TestStatsCollector(t *testing.T) {
ng := func(_ *unitDB) uint64 { return 0 }
units := make([]*unitDB, 720)
t.Run("hours", func(t *testing.T) {
statsData := statsCollector(units, 0, Hours, ng)
assert.Len(t, statsData, 720)
})
t.Run("days", func(t *testing.T) {
for i := 0; i != 25; i++ {
statsData := statsCollector(units, uint32(i), Days, ng)
require.Lenf(t, statsData, 30, "i=%d", i)
}
})
}

View File

@@ -1,13 +1,17 @@
package stats
package stats_test
import (
"encoding/json"
"fmt"
"net"
"os"
"net/http"
"net/http/httptest"
"path/filepath"
"sync/atomic"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -17,147 +21,176 @@ func TestMain(m *testing.M) {
aghtest.DiscardLogOutput(m)
}
func UIntArrayEquals(a, b []uint64) bool {
if len(a) != len(b) {
return false
// constUnitID is the UnitIDGenFunc which always return 0.
func constUnitID() (id uint32) { return 0 }
func assertSuccessAndUnmarshal(t *testing.T, to any, handler http.Handler, req *http.Request) {
t.Helper()
require.NotNil(t, handler)
rw := httptest.NewRecorder()
handler.ServeHTTP(rw, req)
require.Equal(t, http.StatusOK, rw.Code)
data := rw.Body.Bytes()
if to == nil {
assert.Empty(t, data)
return
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
err := json.Unmarshal(data, to)
require.NoError(t, err)
}
func TestStats(t *testing.T) {
conf := Config{
Filename: "./stats.db",
cliIP := net.IP{127, 0, 0, 1}
cliIPStr := cliIP.String()
handlers := map[string]http.Handler{}
conf := stats.Config{
Filename: filepath.Join(t.TempDir(), "stats.db"),
LimitDays: 1,
UnitID: constUnitID,
HTTPRegister: func(_, url string, handler http.HandlerFunc) {
handlers[url] = handler
},
}
s, err := createObject(conf)
s, err := stats.New(conf)
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, func() (err error) {
s.clear()
s.Close()
return os.Remove(conf.Filename)
s.Start()
testutil.CleanupAndRequireSuccess(t, s.Close)
t.Run("data", func(t *testing.T) {
const reqDomain = "domain"
entries := []stats.Entry{{
Domain: reqDomain,
Client: cliIPStr,
Result: stats.RFiltered,
Time: 123456,
}, {
Domain: reqDomain,
Client: cliIPStr,
Result: stats.RNotFiltered,
Time: 123456,
}}
wantData := &stats.StatsResp{
TimeUnits: "hours",
TopQueried: []map[string]uint64{0: {reqDomain: 1}},
TopClients: []map[string]uint64{0: {cliIPStr: 2}},
TopBlocked: []map[string]uint64{0: {reqDomain: 1}},
DNSQueries: []uint64{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,
},
BlockedFiltering: []uint64{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
},
ReplacedSafebrowsing: []uint64{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
},
ReplacedParental: []uint64{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
},
NumDNSQueries: 2,
NumBlockedFiltering: 1,
NumReplacedSafebrowsing: 0,
NumReplacedSafesearch: 0,
NumReplacedParental: 0,
AvgProcessingTime: 0.123456,
}
for _, e := range entries {
s.Update(e)
}
data := &stats.StatsResp{}
req := httptest.NewRequest(http.MethodGet, "/control/stats", nil)
assertSuccessAndUnmarshal(t, data, handlers["/control/stats"], req)
assert.Equal(t, wantData, data)
})
s.Update(Entry{
Domain: "domain",
Client: "127.0.0.1",
Result: RFiltered,
Time: 123456,
})
s.Update(Entry{
Domain: "domain",
Client: "127.0.0.1",
Result: RNotFiltered,
Time: 123456,
t.Run("tops", func(t *testing.T) {
topClients := s.TopClientsIP(2)
require.NotEmpty(t, topClients)
assert.True(t, cliIP.Equal(topClients[0]))
})
d, ok := s.getData()
require.True(t, ok)
t.Run("reset", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/control/stats_reset", nil)
assertSuccessAndUnmarshal(t, nil, handlers["/control/stats_reset"], req)
a := []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}
assert.True(t, UIntArrayEquals(d.DNSQueries, a))
_24zeroes := [24]uint64{}
emptyData := &stats.StatsResp{
TimeUnits: "hours",
TopQueried: []map[string]uint64{},
TopClients: []map[string]uint64{},
TopBlocked: []map[string]uint64{},
DNSQueries: _24zeroes[:],
BlockedFiltering: _24zeroes[:],
ReplacedSafebrowsing: _24zeroes[:],
ReplacedParental: _24zeroes[:],
}
a = []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
assert.True(t, UIntArrayEquals(d.BlockedFiltering, a))
req = httptest.NewRequest(http.MethodGet, "/control/stats", nil)
data := &stats.StatsResp{}
a = []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
assert.True(t, UIntArrayEquals(d.ReplacedSafebrowsing, a))
a = []uint64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
assert.True(t, UIntArrayEquals(d.ReplacedParental, a))
m := d.TopQueried
require.NotEmpty(t, m)
assert.EqualValues(t, 1, m[0]["domain"])
m = d.TopBlocked
require.NotEmpty(t, m)
assert.EqualValues(t, 1, m[0]["domain"])
m = d.TopClients
require.NotEmpty(t, m)
assert.EqualValues(t, 2, m[0]["127.0.0.1"])
assert.EqualValues(t, 2, d.NumDNSQueries)
assert.EqualValues(t, 1, d.NumBlockedFiltering)
assert.EqualValues(t, 0, d.NumReplacedSafebrowsing)
assert.EqualValues(t, 0, d.NumReplacedSafesearch)
assert.EqualValues(t, 0, d.NumReplacedParental)
assert.EqualValues(t, 0.123456, d.AvgProcessingTime)
topClients := s.GetTopClientsIP(2)
require.NotEmpty(t, topClients)
assert.True(t, net.IP{127, 0, 0, 1}.Equal(topClients[0]))
assertSuccessAndUnmarshal(t, data, handlers["/control/stats"], req)
assert.Equal(t, emptyData, data)
})
}
func TestLargeNumbers(t *testing.T) {
var hour int32 = 0
newID := func() uint32 {
// Use "atomic" to make go race detector happy.
return uint32(atomic.LoadInt32(&hour))
var curHour uint32 = 1
handlers := map[string]http.Handler{}
conf := stats.Config{
Filename: filepath.Join(t.TempDir(), "stats.db"),
LimitDays: 1,
UnitID: func() (id uint32) { return atomic.LoadUint32(&curHour) },
HTTPRegister: func(_, url string, handler http.HandlerFunc) { handlers[url] = handler },
}
conf := Config{
Filename: "./stats.db",
LimitDays: 1,
UnitID: newID,
}
s, err := createObject(conf)
s, err := stats.New(conf)
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, func() (err error) {
s.Close()
return os.Remove(conf.Filename)
})
s.Start()
testutil.CleanupAndRequireSuccess(t, s.Close)
// Number of distinct clients and domains every hour.
const n = 1000
const (
hoursNum = 12
cliNumPerHour = 1000
)
for h := 0; h < 12; h++ {
atomic.AddInt32(&hour, 1)
for i := 0; i < n; i++ {
s.Update(Entry{
Domain: fmt.Sprintf("domain%d", i),
Client: net.IP{
127,
0,
byte((i & 0xff00) >> 8),
byte(i & 0xff),
}.String(),
Result: RNotFiltered,
req := httptest.NewRequest(http.MethodGet, "/control/stats", nil)
for h := 0; h < hoursNum; h++ {
atomic.AddUint32(&curHour, 1)
for i := 0; i < cliNumPerHour; i++ {
ip := net.IP{127, 0, byte((i & 0xff00) >> 8), byte(i & 0xff)}
e := stats.Entry{
Domain: fmt.Sprintf("domain%d.hour%d", i, h),
Client: ip.String(),
Result: stats.RNotFiltered,
Time: 123456,
})
}
s.Update(e)
}
}
d, ok := s.getData()
require.True(t, ok)
assert.EqualValues(t, hour*n, d.NumDNSQueries)
}
func TestStatsCollector(t *testing.T) {
ng := func(_ *unitDB) uint64 {
return 0
}
units := make([]*unitDB, 720)
t.Run("hours", func(t *testing.T) {
statsData := statsCollector(units, 0, Hours, ng)
assert.Len(t, statsData, 720)
})
t.Run("days", func(t *testing.T) {
for i := 0; i != 25; i++ {
statsData := statsCollector(units, uint32(i), Days, ng)
require.Lenf(t, statsData, 30, "i=%d", i)
}
})
data := &stats.StatsResp{}
assertSuccessAndUnmarshal(t, data, handlers["/control/stats"], req)
assert.Equal(t, hoursNum*cliNumPerHour, int(data.NumDNSQueries))
}

View File

@@ -5,253 +5,148 @@ import (
"encoding/binary"
"encoding/gob"
"fmt"
"net"
"os"
"sort"
"sync"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
bolt "go.etcd.io/bbolt"
"go.etcd.io/bbolt"
)
// TODO(a.garipov): Rewrite all of this. Add proper error handling and
// inspection. Improve logging. Decrease complexity.
const (
maxDomains = 100 // max number of top domains to store in file or return via Get()
maxClients = 100 // max number of top clients to store in file or return via Get()
// maxDomains is the max number of top domains to return.
maxDomains = 100
// maxClients is the max number of top clients to return.
maxClients = 100
)
// statsCtx - global context
type statsCtx struct {
// mu protects unit.
mu *sync.Mutex
// current is the actual statistics collection result.
current *unit
// UnitIDGenFunc is the signature of a function that generates a unique ID for
// the statistics unit.
type UnitIDGenFunc func() (id uint32)
db *bolt.DB
conf *Config
// TimeUnit is the unit of measuring time while aggregating the statistics.
type TimeUnit int
// Supported TimeUnit values.
const (
Hours TimeUnit = iota
Days
)
// Result is the resulting code of processing the DNS request.
type Result int
// Supported Result values.
//
// TODO(e.burkov): Think about better naming.
const (
RNotFiltered Result = iota + 1
RFiltered
RSafeBrowsing
RSafeSearch
RParental
resultLast = RParental + 1
)
// Entry is a statistics data entry.
type Entry struct {
// Clients is the client's primary ID.
//
// TODO(a.garipov): Make this a {net.IP, string} enum?
Client string
// Domain is the domain name requested.
Domain string
// Result is the result of processing the request.
Result Result
// Time is the duration of the request processing in milliseconds.
Time uint32
}
// data for 1 time unit
// unit collects the statistics data for a specific period of time.
type unit struct {
id uint32 // unit ID. Default: absolute hour since Jan 1, 1970
// id is the unique unit's identifier. It's set to an absolute hour number
// since the beginning of UNIX time by the default ID generating function.
//
// Must not be rewritten after creating to be accessed concurrently without
// using mu.
id uint32
nTotal uint64 // total requests
nResult []uint64 // number of requests per one result
timeSum uint64 // sum of processing time of all requests (usec)
// nTotal stores the total number of requests.
nTotal uint64
// nResult stores the number of requests grouped by it's result.
nResult []uint64
// timeSum stores the sum of processing time in milliseconds of each request
// written by the unit.
timeSum uint64
// top:
domains map[string]uint64 // number of requests per domain
blockedDomains map[string]uint64 // number of blocked requests per domain
clients map[string]uint64 // number of requests per client
// domains stores the number of requests for each domain.
domains map[string]uint64
// blockedDomains stores the number of requests for each domain that has
// been blocked.
blockedDomains map[string]uint64
// clients stores the number of requests from each client.
clients map[string]uint64
}
// name-count pair
// newUnit allocates the new *unit.
func newUnit(id uint32) (u *unit) {
return &unit{
id: id,
nResult: make([]uint64, resultLast),
domains: make(map[string]uint64),
blockedDomains: make(map[string]uint64),
clients: make(map[string]uint64),
}
}
// countPair is a single name-number pair for deserializing statistics data into
// the database.
type countPair struct {
Name string
Count uint64
}
// structure for storing data in file
// unitDB is the structure for serializing statistics data into the database.
type unitDB struct {
NTotal uint64
// NTotal is the total number of requests.
NTotal uint64
// NResult is the number of requests by the result's kind.
NResult []uint64
Domains []countPair
// Domains is the number of requests for each domain name.
Domains []countPair
// BlockedDomains is the number of requests blocked for each domain name.
BlockedDomains []countPair
Clients []countPair
// Clients is the number of requests from each client.
Clients []countPair
TimeAvg uint32 // usec
// TimeAvg is the average of processing times in milliseconds of all the
// requests in the unit.
TimeAvg uint32
}
// withRecovered turns the value recovered from panic if any into an error and
// combines it with the one pointed by orig. orig must be non-nil.
func withRecovered(orig *error) {
p := recover()
if p == nil {
return
}
// newUnitID is the default UnitIDGenFunc that generates the unique id hourly.
func newUnitID() (id uint32) {
const secsInHour = int64(time.Hour / time.Second)
var err error
switch p := p.(type) {
case error:
err = fmt.Errorf("panic: %w", p)
default:
err = fmt.Errorf("panic: recovered value of type %[1]T: %[1]v", p)
}
*orig = errors.WithDeferred(*orig, err)
return uint32(time.Now().Unix() / secsInHour)
}
// createObject creates s from conf and properly initializes it.
func createObject(conf Config) (s *statsCtx, err error) {
defer withRecovered(&err)
s = &statsCtx{
mu: &sync.Mutex{},
}
if !checkInterval(conf.LimitDays) {
conf.LimitDays = 1
func finishTxn(tx *bbolt.Tx, commit bool) (err error) {
if commit {
err = errors.Annotate(tx.Commit(), "committing: %w")
} else {
err = errors.Annotate(tx.Rollback(), "rolling back: %w")
}
s.conf = &Config{}
*s.conf = conf
s.conf.limit = conf.LimitDays * 24
if conf.UnitID == nil {
s.conf.UnitID = newUnitID
}
if !s.dbOpen() {
return nil, fmt.Errorf("open database")
}
id := s.conf.UnitID()
tx := s.beginTxn(true)
var udb *unitDB
if tx != nil {
log.Tracef("Deleting old units...")
firstID := id - s.conf.limit - 1
unitDel := 0
err = tx.ForEach(newBucketWalker(tx, &unitDel, firstID))
if err != nil && !errors.Is(err, errStop) {
log.Debug("stats: deleting units: %s", err)
}
udb = s.loadUnitFromDB(tx, id)
if unitDel != 0 {
s.commitTxn(tx)
} else {
err = tx.Rollback()
if err != nil {
log.Debug("rolling back: %s", err)
}
}
}
u := unit{}
s.initUnit(&u, id)
if udb != nil {
deserialize(&u, udb)
}
s.current = &u
log.Debug("stats: initialized")
return s, nil
}
// TODO(a.garipov): See if this is actually necessary. Looks like a rather
// bizarre solution.
const errStop errors.Error = "stop iteration"
// newBucketWalker returns a new bucket walker that deletes old units. The
// integer that unitDelPtr points to is incremented for every successful
// deletion. If the bucket isn't deleted, f returns errStop.
func newBucketWalker(
tx *bolt.Tx,
unitDelPtr *int,
firstID uint32,
) (f func(name []byte, b *bolt.Bucket) (err error)) {
return func(name []byte, _ *bolt.Bucket) (err error) {
nameID, ok := unitNameToID(name)
if !ok || nameID < firstID {
err = tx.DeleteBucket(name)
if err != nil {
log.Debug("stats: tx.DeleteBucket: %s", err)
return nil
}
log.Debug("stats: deleted unit %d (name %x)", nameID, name)
*unitDelPtr++
return nil
}
return errStop
}
}
func (s *statsCtx) Start() {
s.initWeb()
go s.periodicFlush()
}
func checkInterval(days uint32) bool {
return days == 0 || days == 1 || days == 7 || days == 30 || days == 90
}
func (s *statsCtx) dbOpen() bool {
var err error
log.Tracef("db.Open...")
s.db, err = bolt.Open(s.conf.Filename, 0o644, nil)
if err != nil {
log.Error("stats: open DB: %s: %s", s.conf.Filename, err)
if err.Error() == "invalid argument" {
log.Error("AdGuard Home cannot be initialized due to an incompatible file system.\nPlease read the explanation here: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#limitations")
}
return false
}
log.Tracef("db.Open")
return true
}
// Atomically swap the currently active unit with a new value
// Return old value
func (s *statsCtx) swapUnit(new *unit) (u *unit) {
s.mu.Lock()
defer s.mu.Unlock()
u = s.current
s.current = new
return u
}
// Get unit ID for the current hour
func newUnitID() uint32 {
return uint32(time.Now().Unix() / (60 * 60))
}
// Initialize a unit
func (s *statsCtx) initUnit(u *unit, id uint32) {
u.id = id
u.nResult = make([]uint64, rLast)
u.domains = make(map[string]uint64)
u.blockedDomains = make(map[string]uint64)
u.clients = make(map[string]uint64)
}
// Open a DB transaction
func (s *statsCtx) beginTxn(wr bool) *bolt.Tx {
db := s.db
if db == nil {
return nil
}
log.Tracef("db.Begin...")
tx, err := db.Begin(wr)
if err != nil {
log.Error("db.Begin: %s", err)
return nil
}
log.Tracef("db.Begin")
return tx
}
func (s *statsCtx) commitTxn(tx *bolt.Tx) {
err := tx.Commit()
if err != nil {
log.Debug("tx.Commit: %s", err)
return
}
log.Tracef("tx.Commit")
return err
}
// bucketNameLen is the length of a bucket, a 64-bit unsigned integer.
@@ -262,10 +157,10 @@ const bucketNameLen = 8
// idToUnitName converts a numerical ID into a database unit name.
func idToUnitName(id uint32) (name []byte) {
name = make([]byte, bucketNameLen)
binary.BigEndian.PutUint64(name, uint64(id))
n := [bucketNameLen]byte{}
binary.BigEndian.PutUint64(n[:], uint64(id))
return name
return n[:]
}
// unitNameToID converts a database unit name into a numerical ID. ok is false
@@ -278,316 +173,131 @@ func unitNameToID(name []byte) (id uint32, ok bool) {
return uint32(binary.BigEndian.Uint64(name)), true
}
func (s *statsCtx) ongoing() (u *unit) {
s.mu.Lock()
defer s.mu.Unlock()
return s.current
}
// Flush the current unit to DB and delete an old unit when a new hour is started
// If a unit must be flushed:
// . lock DB
// . atomically set a new empty unit as the current one and get the old unit
// This is important to do it inside DB lock, so the reader won't get inconsistent results.
// . write the unit to DB
// . remove the stale unit from DB
// . unlock DB
func (s *statsCtx) periodicFlush() {
for {
ptr := s.ongoing()
if ptr == nil {
break
}
id := s.conf.UnitID()
if ptr.id == id || s.conf.limit == 0 {
time.Sleep(time.Second)
continue
}
tx := s.beginTxn(true)
nu := unit{}
s.initUnit(&nu, id)
u := s.swapUnit(&nu)
udb := serialize(u)
if tx == nil {
continue
}
ok1 := s.flushUnitToDB(tx, u.id, udb)
ok2 := s.deleteUnit(tx, id-s.conf.limit)
if ok1 || ok2 {
s.commitTxn(tx)
} else {
_ = tx.Rollback()
}
}
log.Tracef("periodicFlush() exited")
}
// Delete unit's data from file
func (s *statsCtx) deleteUnit(tx *bolt.Tx, id uint32) bool {
err := tx.DeleteBucket(idToUnitName(id))
if err != nil {
log.Tracef("stats: bolt DeleteBucket: %s", err)
return false
}
log.Debug("stats: deleted unit %d", id)
return true
}
func convertMapToSlice(m map[string]uint64, max int) []countPair {
a := []countPair{}
func convertMapToSlice(m map[string]uint64, max int) (s []countPair) {
s = make([]countPair, 0, len(m))
for k, v := range m {
pair := countPair{}
pair.Name = k
pair.Count = v
a = append(a, pair)
s = append(s, countPair{Name: k, Count: v})
}
less := func(i, j int) bool {
return a[j].Count < a[i].Count
sort.Slice(s, func(i, j int) bool {
return s[j].Count < s[i].Count
})
if max > len(s) {
max = len(s)
}
sort.Slice(a, less)
if max > len(a) {
max = len(a)
}
return a[:max]
return s[:max]
}
func convertSliceToMap(a []countPair) map[string]uint64 {
m := map[string]uint64{}
func convertSliceToMap(a []countPair) (m map[string]uint64) {
m = map[string]uint64{}
for _, it := range a {
m[it.Name] = it.Count
}
return m
}
func serialize(u *unit) *unitDB {
udb := unitDB{}
udb.NTotal = u.nTotal
udb.NResult = append(udb.NResult, u.nResult...)
// serialize converts u to the *unitDB. It's safe for concurrent use. u must
// not be nil.
func (u *unit) serialize() (udb *unitDB) {
var timeAvg uint32 = 0
if u.nTotal != 0 {
udb.TimeAvg = uint32(u.timeSum / u.nTotal)
timeAvg = uint32(u.timeSum / u.nTotal)
}
udb.Domains = convertMapToSlice(u.domains, maxDomains)
udb.BlockedDomains = convertMapToSlice(u.blockedDomains, maxDomains)
udb.Clients = convertMapToSlice(u.clients, maxClients)
return &udb
return &unitDB{
NTotal: u.nTotal,
NResult: append([]uint64{}, u.nResult...),
Domains: convertMapToSlice(u.domains, maxDomains),
BlockedDomains: convertMapToSlice(u.blockedDomains, maxDomains),
Clients: convertMapToSlice(u.clients, maxClients),
TimeAvg: timeAvg,
}
}
func deserialize(u *unit, udb *unitDB) {
u.nTotal = udb.NTotal
n := len(udb.NResult)
if n < len(u.nResult) {
n = len(u.nResult) // n = min(len(udb.NResult), len(u.nResult))
}
for i := 1; i < n; i++ {
u.nResult[i] = udb.NResult[i]
}
u.domains = convertSliceToMap(udb.Domains)
u.blockedDomains = convertSliceToMap(udb.BlockedDomains)
u.clients = convertSliceToMap(udb.Clients)
u.timeSum = uint64(udb.TimeAvg) * u.nTotal
}
func (s *statsCtx) flushUnitToDB(tx *bolt.Tx, id uint32, udb *unitDB) bool {
log.Tracef("Flushing unit %d", id)
bkt, err := tx.CreateBucketIfNotExists(idToUnitName(id))
if err != nil {
log.Error("tx.CreateBucketIfNotExists: %s", err)
return false
}
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err = enc.Encode(udb)
if err != nil {
log.Error("gob.Encode: %s", err)
return false
}
err = bkt.Put([]byte{0}, buf.Bytes())
if err != nil {
log.Error("bkt.Put: %s", err)
return false
}
return true
}
func (s *statsCtx) loadUnitFromDB(tx *bolt.Tx, id uint32) *unitDB {
func loadUnitFromDB(tx *bbolt.Tx, id uint32) (udb *unitDB) {
bkt := tx.Bucket(idToUnitName(id))
if bkt == nil {
return nil
}
// log.Tracef("Loading unit %d", id)
log.Tracef("Loading unit %d", id)
var buf bytes.Buffer
buf.Write(bkt.Get([]byte{0}))
dec := gob.NewDecoder(&buf)
udb := unitDB{}
err := dec.Decode(&udb)
udb = &unitDB{}
err := gob.NewDecoder(&buf).Decode(udb)
if err != nil {
log.Error("gob Decode: %s", err)
return nil
}
return &udb
return udb
}
func convertTopSlice(a []countPair) []map[string]uint64 {
m := []map[string]uint64{}
for _, it := range a {
ent := map[string]uint64{}
ent[it.Name] = it.Count
m = append(m, ent)
}
return m
}
func (s *statsCtx) setLimit(limitDays int) {
s.conf.limit = uint32(limitDays) * 24
if limitDays == 0 {
s.clear()
}
log.Debug("stats: set limit: %d", limitDays)
}
func (s *statsCtx) WriteDiskConfig(dc *DiskConfig) {
dc.Interval = s.conf.limit / 24
}
func (s *statsCtx) Close() {
u := s.swapUnit(nil)
udb := serialize(u)
tx := s.beginTxn(true)
if tx != nil {
if s.flushUnitToDB(tx, u.id, udb) {
s.commitTxn(tx)
} else {
_ = tx.Rollback()
}
}
if s.db != nil {
log.Tracef("db.Close...")
_ = s.db.Close()
log.Tracef("db.Close")
}
log.Debug("stats: closed")
}
// Reset counters and clear database
func (s *statsCtx) clear() {
tx := s.beginTxn(true)
if tx != nil {
db := s.db
s.db = nil
_ = tx.Rollback()
// the active transactions can continue using database,
// but no new transactions will be opened
_ = db.Close()
log.Tracef("db.Close")
// all active transactions are now closed
}
u := unit{}
s.initUnit(&u, s.conf.UnitID())
_ = s.swapUnit(&u)
err := os.Remove(s.conf.Filename)
if err != nil {
log.Error("os.Remove: %s", err)
}
_ = s.dbOpen()
log.Debug("stats: cleared")
}
func (s *statsCtx) Update(e Entry) {
if s.conf.limit == 0 {
// deserealize assigns the appropriate values from udb to u. u must not be nil.
// It's safe for concurrent use.
func (u *unit) deserialize(udb *unitDB) {
if udb == nil {
return
}
if e.Result == 0 ||
e.Result >= rLast ||
e.Domain == "" ||
e.Client == "" {
return
}
u.nTotal = udb.NTotal
u.nResult = make([]uint64, resultLast)
copy(u.nResult, udb.NResult)
u.domains = convertSliceToMap(udb.Domains)
u.blockedDomains = convertSliceToMap(udb.BlockedDomains)
u.clients = convertSliceToMap(udb.Clients)
u.timeSum = uint64(udb.TimeAvg) * udb.NTotal
}
clientID := e.Client
if ip := net.ParseIP(clientID); ip != nil {
clientID = ip.String()
}
s.mu.Lock()
defer s.mu.Unlock()
u := s.current
u.nResult[e.Result]++
if e.Result == RNotFiltered {
u.domains[e.Domain]++
// add adds new data to u. It's safe for concurrent use.
func (u *unit) add(res Result, domain, cli string, dur uint64) {
u.nResult[res]++
if res == RNotFiltered {
u.domains[domain]++
} else {
u.blockedDomains[e.Domain]++
u.blockedDomains[domain]++
}
u.clients[clientID]++
u.timeSum += uint64(e.Time)
u.clients[cli]++
u.timeSum += dur
u.nTotal++
}
func (s *statsCtx) loadUnits(limit uint32) ([]*unitDB, uint32) {
tx := s.beginTxn(false)
if tx == nil {
return nil, 0
// flushUnitToDB puts udb to the database at id.
func (udb *unitDB) flushUnitToDB(tx *bbolt.Tx, id uint32) (err error) {
log.Debug("stats: flushing unit with id %d and total of %d", id, udb.NTotal)
bkt, err := tx.CreateBucketIfNotExists(idToUnitName(id))
if err != nil {
return fmt.Errorf("creating bucket: %w", err)
}
cur := s.ongoing()
curID := cur.id
// Per-hour units.
units := []*unitDB{}
firstID := curID - limit + 1
for i := firstID; i != curID; i++ {
u := s.loadUnitFromDB(tx, i)
if u == nil {
u = &unitDB{}
u.NResult = make([]uint64, rLast)
}
units = append(units, u)
buf := &bytes.Buffer{}
err = gob.NewEncoder(buf).Encode(udb)
if err != nil {
return fmt.Errorf("encoding unit: %w", err)
}
_ = tx.Rollback()
units = append(units, serialize(cur))
if len(units) != int(limit) {
log.Fatalf("len(units) != limit: %d %d", len(units), limit)
err = bkt.Put([]byte{0}, buf.Bytes())
if err != nil {
return fmt.Errorf("putting unit to database: %w", err)
}
return units, firstID
return nil
}
func convertTopSlice(a []countPair) (m []map[string]uint64) {
m = make([]map[string]uint64, 0, len(a))
for _, it := range a {
m = append(m, map[string]uint64{it.Name: it.Count})
}
return m
}
// numsGetter is a signature for statsCollector argument.
@@ -597,6 +307,7 @@ type numsGetter func(u *unitDB) (num uint64)
// timeUnit using ng to retrieve data.
func statsCollector(units []*unitDB, firstID uint32, timeUnit TimeUnit, ng numsGetter) (nums []uint64) {
if timeUnit == Hours {
nums = make([]uint64, 0, len(units))
for _, u := range units {
nums = append(nums, ng(u))
}
@@ -628,16 +339,17 @@ func statsCollector(units []*unitDB, firstID uint32, timeUnit TimeUnit, ng numsG
// pairsGetter is a signature for topsCollector argument.
type pairsGetter func(u *unitDB) (pairs []countPair)
// topsCollector collects statistics about highest values fro the given *unitDB
// topsCollector collects statistics about highest values from the given *unitDB
// slice using pg to retrieve data.
func topsCollector(units []*unitDB, max int, pg pairsGetter) []map[string]uint64 {
m := map[string]uint64{}
for _, u := range units {
for _, it := range pg(u) {
m[it.Name] += it.Count
for _, cp := range pg(u) {
m[cp.Name] += cp.Count
}
}
a2 := convertMapToSlice(m, max)
return convertTopSlice(a2)
}
@@ -668,8 +380,21 @@ func topsCollector(units []*unitDB, max int, pg pairsGetter) []map[string]uint64
* parental-blocked
These values are just the sum of data for all units.
*/
func (s *statsCtx) getData() (statsResponse, bool) {
limit := s.conf.limit
func (s *StatsCtx) getData(limit uint32) (StatsResp, bool) {
if limit == 0 {
return StatsResp{
TimeUnits: "days",
TopBlocked: []topAddrs{},
TopClients: []topAddrs{},
TopQueried: []topAddrs{},
BlockedFiltering: []uint64{},
DNSQueries: []uint64{},
ReplacedParental: []uint64{},
ReplacedSafebrowsing: []uint64{},
}, true
}
timeUnit := Hours
if limit/24 > 7 {
@@ -678,7 +403,7 @@ func (s *statsCtx) getData() (statsResponse, bool) {
units, firstID := s.loadUnits(limit)
if units == nil {
return statsResponse{}, false
return StatsResp{}, false
}
dnsQueries := statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NTotal })
@@ -686,7 +411,7 @@ func (s *statsCtx) getData() (statsResponse, bool) {
log.Fatalf("len(dnsQueries) != limit: %d %d", len(dnsQueries), limit)
}
data := statsResponse{
data := StatsResp{
DNSQueries: dnsQueries,
BlockedFiltering: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RFiltered] }),
ReplacedSafebrowsing: statsCollector(units, firstID, timeUnit, func(u *unitDB) (num uint64) { return u.NResult[RSafeBrowsing] }),
@@ -698,7 +423,7 @@ func (s *statsCtx) getData() (statsResponse, bool) {
// Total counters:
sum := unitDB{
NResult: make([]uint64, rLast),
NResult: make([]uint64, resultLast),
}
timeN := 0
for _, u := range units {
@@ -730,31 +455,3 @@ func (s *statsCtx) getData() (statsResponse, bool) {
return data, true
}
func (s *statsCtx) GetTopClientsIP(maxCount uint) []net.IP {
if s.conf.limit == 0 {
return nil
}
units, _ := s.loadUnits(s.conf.limit)
if units == nil {
return nil
}
// top clients
m := map[string]uint64{}
for _, u := range units {
for _, it := range u.Clients {
m[it.Name] += it.Count
}
}
a := convertMapToSlice(m, int(maxCount))
d := []net.IP{}
for _, it := range a {
ip := net.ParseIP(it.Name)
if ip != nil {
d = append(d, ip)
}
}
return d
}

View File

@@ -6,7 +6,7 @@ require (
github.com/fzipp/gocyclo v0.6.0
github.com/golangci/misspell v0.3.5
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8
github.com/kisielk/errcheck v1.6.1
github.com/kisielk/errcheck v1.6.2
github.com/kyoh86/looppointer v0.1.7
github.com/securego/gosec/v2 v2.12.0
golang.org/x/tools v0.1.12
@@ -27,6 +27,6 @@ require (
golang.org/x/exp/typeparams v0.0.0-20220722155223-a9213eeb770e // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

View File

@@ -218,8 +218,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.6.1 h1:cErYo+J4SmEjdXZrVXGwLJCE2sB06s23LpkcyWNrT+s=
github.com/kisielk/errcheck v1.6.1/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw=
github.com/kisielk/errcheck v1.6.2 h1:uGQ9xI8/pgc9iOoCe7kWQgRE6SBTrCGmTSf0LrEtY7c=
github.com/kisielk/errcheck v1.6.2/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -549,8 +549,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220731174439-a90be440212d h1:Sv5ogFZatcgIMMtBSTTAgMYsicp25MXBubjXNDKwm80=
golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 h1:9vYwv7OjYaky/tlAeD7C4oC9EsPTlaFl1H2jS++V+ME=
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

View File

@@ -109,17 +109,17 @@ log "checking tools"
# Make sure we fail gracefully if one of the tools we need is missing. Use
# alternatives when available.
sha256sum_cmd='sha256sum'
for tool in gpg gzip sed "$sha256sum_cmd" snapcraft tar zip
use_shasum='0'
for tool in gpg gzip sed sha256sum snapcraft tar zip
do
if ! command -v "$tool" > /dev/null
then
if [ "$tool" = "$sha256sum_cmd" ] && command -v 'shasum' > /dev/null
if [ "$tool" = 'sha256sum' ] && command -v 'shasum' > /dev/null
then
# macOS doesn't have sha256sum installed by default, but
# it does have shasum.
# macOS doesn't have sha256sum installed by default, but it does
# have shasum.
log 'replacing sha256sum with shasum -a 256'
sha256sum_cmd='shasum -a 256'
use_shasum='1'
else
log "pieces don't fit, '$tool' not found"
@@ -127,7 +127,7 @@ do
fi
fi
done
readonly sha256sum_cmd
readonly use_shasum
# Data section. Arrange data into space-separated tables for read -r to read.
# Use a hyphen for missing values.
@@ -332,15 +332,40 @@ log "$build_archive"
log "calculating checksums"
# calculate_checksums uses the previously detected SHA-256 tool to calculate
# checksums. Do not use find with -exec, since shasum requires arguments.
calculate_checksums() {
if [ "$use_shasum" -eq '0' ]
then
sha256sum "$@"
else
shasum -a 256 "$@"
fi
}
# Calculate the checksums of the files in a subshell with a different working
# directory. Don't use ls, because files matching one of the patterns may be
# absent, which will make ls return with a non-zero status code.
#
# TODO(a.garipov): Consider calculating these as the build goes.
(
set +f
cd "./${dist}"
find . ! -name . -prune \( -name '*.tar.gz' -o -name '*.zip' \)\
-exec "$sha256sum_cmd" {} +\
> ./checksums.txt
: > ./checksums.txt
for archive in ./*.zip ./*.tar.gz
do
# Make sure that we don't try to calculate a checksum for a glob pattern
# that matched no files.
if [ ! -f "$archive" ]
then
continue
fi
calculate_checksums "$archive" >> ./checksums.txt
done
)
log "writing versions"

View File

@@ -1,7 +1,7 @@
# The %VARIABLES% are be replaced by actual values by the build script.
'name': 'adguard-home'
'base': 'core20'
'base': 'core22'
'version': '%VERSION%'
'summary': Network-wide ads & trackers blocking DNS server
'description': |