Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9200163f85 | ||
|
|
3c17853344 | ||
|
|
993a3fc42c | ||
|
|
7bb9b2416b | ||
|
|
2de321ce24 | ||
|
|
30b2b85ff1 | ||
|
|
6ea4788f56 | ||
|
|
3c52a021b9 | ||
|
|
0ceea9af5f | ||
|
|
39b404be19 | ||
|
|
56dc3eab02 | ||
|
|
554a38eeb1 | ||
|
|
c8d3afe869 | ||
|
|
44222c604c |
@@ -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ý",
|
||||
|
||||
61
CHANGELOG.md
61
CHANGELOG.md
@@ -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
|
||||
|
||||
2
Makefile
2
Makefile
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
634
client/src/__locales/ar.json
Normal file
634
client/src/__locales/ar.json
Normal 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}} من الأحرف على الأقل"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
14
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -470,7 +470,7 @@ func TestHostsContainer(t *testing.T) {
|
||||
}},
|
||||
}, {
|
||||
req: &urlfilter.DNSRequest{
|
||||
Hostname: "nonexisting",
|
||||
Hostname: "nonexistent.example",
|
||||
DNSType: dns.TypeA,
|
||||
},
|
||||
name: "non-existing",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package aghos
|
||||
package aghos_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
57
internal/aghos/filewalker_internal_test.go
Normal file
57
internal/aghos/filewalker_internal_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
135
internal/aghtest/interface.go
Normal file
135
internal/aghtest/interface.go
Normal 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()
|
||||
}
|
||||
9
internal/aghtest/interface_test.go
Normal file
9
internal/aghtest/interface_test.go
Normal 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)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}, {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:"-"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
26
internal/stats/stats_internal_test.go
Normal file
26
internal/stats/stats_internal_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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': |
|
||||
|
||||
Reference in New Issue
Block a user