Compare commits
24 Commits
v0.107.0-b
...
v0.107.0-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0b14b4811 | ||
|
|
b1e28a74d1 | ||
|
|
2fc108486d | ||
|
|
1e72960140 | ||
|
|
1e52b309aa | ||
|
|
6bff1d365a | ||
|
|
3ebac37d51 | ||
|
|
ea8950a80d | ||
|
|
7804d97743 | ||
|
|
7d9a1a2f30 | ||
|
|
8388b3d5bc | ||
|
|
b8c4651dec | ||
|
|
d7aafa7dc6 | ||
|
|
b296fa2246 | ||
|
|
49d67a70b6 | ||
|
|
3fa38fb420 | ||
|
|
2796e65468 | ||
|
|
2b635bf689 | ||
|
|
adec2c998b | ||
|
|
da45eabc31 | ||
|
|
08ec3f604e | ||
|
|
da86620288 | ||
|
|
a4e078271c | ||
|
|
e178cb631f |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -10,13 +10,11 @@ and this project adheres to
|
||||
## [Unreleased]
|
||||
|
||||
<!--
|
||||
## [v0.107.0] - 2021-09-28 (APPROX.)
|
||||
## [v0.107.0] - 2021-11-02 (APPROX.)
|
||||
-->
|
||||
|
||||
### Added
|
||||
|
||||
- DNS server IP addresses to the `mobileconfig` API responses ([#3568],
|
||||
[#3607]).
|
||||
- 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]).
|
||||
@@ -48,6 +46,13 @@ and this project adheres to
|
||||
|
||||
### Changed
|
||||
|
||||
- Better error message for ED25519 private keys, which are not widely supported
|
||||
([#3737]).
|
||||
- Cache now follows RFC more closely for negative answers ([#3707]).
|
||||
- `$dnsrewrite` rules and other DNS rewrites will now be applied even when the
|
||||
protection is disabled ([#1558]).
|
||||
- DHCP gateway address, subnet mask, IP address range, and leases validations
|
||||
([#3529]).
|
||||
- The `systemd` service script will now create the `/var/log` directory when it
|
||||
doesn't exist ([#3579]).
|
||||
- Items in allowed clients, disallowed clients, and blocked hosts lists are now
|
||||
@@ -116,7 +121,10 @@ In this release, the schema version has changed from 10 to 12.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Adding an IP into only one of the matching ipsets on Linux ([#3638]).
|
||||
- Incorrect assignment of explicitly configured DHCP options ([#3744]).
|
||||
- Occasional panic during shutdown ([#3655]).
|
||||
- Addition of IPs into only one as opposed to all matching ipsets on Linux
|
||||
([#3638]).
|
||||
- Removal of temporary filter files ([#3567]).
|
||||
- Panic when an upstream server responds with an empty question section
|
||||
([#3551]).
|
||||
@@ -152,6 +160,7 @@ In this release, the schema version has changed from 10 to 12.
|
||||
- Go 1.15 support.
|
||||
|
||||
[#1381]: https://github.com/AdguardTeam/AdGuardHome/issues/1381
|
||||
[#1558]: https://github.com/AdguardTeam/AdGuardHome/issues/1558
|
||||
[#1691]: https://github.com/AdguardTeam/AdGuardHome/issues/1691
|
||||
[#1898]: https://github.com/AdguardTeam/AdGuardHome/issues/1898
|
||||
[#1992]: https://github.com/AdguardTeam/AdGuardHome/issues/1992
|
||||
@@ -195,6 +204,7 @@ In this release, the schema version has changed from 10 to 12.
|
||||
[#3450]: https://github.com/AdguardTeam/AdGuardHome/issues/3450
|
||||
[#3457]: https://github.com/AdguardTeam/AdGuardHome/issues/3457
|
||||
[#3506]: https://github.com/AdguardTeam/AdGuardHome/issues/3506
|
||||
[#3529]: https://github.com/AdguardTeam/AdGuardHome/issues/3529
|
||||
[#3538]: https://github.com/AdguardTeam/AdGuardHome/issues/3538
|
||||
[#3551]: https://github.com/AdguardTeam/AdGuardHome/issues/3551
|
||||
[#3564]: https://github.com/AdguardTeam/AdGuardHome/issues/3564
|
||||
@@ -203,6 +213,9 @@ In this release, the schema version has changed from 10 to 12.
|
||||
[#3579]: https://github.com/AdguardTeam/AdGuardHome/issues/3579
|
||||
[#3607]: https://github.com/AdguardTeam/AdGuardHome/issues/3607
|
||||
[#3638]: https://github.com/AdguardTeam/AdGuardHome/issues/3638
|
||||
[#3655]: https://github.com/AdguardTeam/AdGuardHome/issues/3655
|
||||
[#3707]: https://github.com/AdguardTeam/AdGuardHome/issues/3707
|
||||
[#3744]: https://github.com/AdguardTeam/AdGuardHome/issues/3744
|
||||
|
||||
|
||||
|
||||
|
||||
44
README.md
44
README.md
@@ -280,29 +280,29 @@ Edge:
|
||||
curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c edge
|
||||
```
|
||||
|
||||
* Beta channel builds
|
||||
* Linux: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz)
|
||||
* Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz) (recommended for Raspberry Pi), [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz)
|
||||
* Linux MIPS: [32-bit MIPS](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz), [32-bit MIPSLE](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz), [64-bit MIPS](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz), [64-bit MIPSLE](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz)
|
||||
* Windows: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_386.zip)
|
||||
* macOS: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_386.zip)
|
||||
* macOS ARM: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_arm64.zip)
|
||||
* FreeBSD: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz)
|
||||
* FreeBSD ARM: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz), [32-bit ARMv6](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz)
|
||||
* OpenBSD: (coming soon)
|
||||
* OpenBSD ARM: (coming soon)
|
||||
* Beta channel builds
|
||||
* Linux: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_386.tar.gz)
|
||||
* Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz) (recommended for Raspberry Pi OS stable), [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz)
|
||||
* Linux MIPS: [32-bit MIPS](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz), [32-bit MIPSLE](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz), [64-bit MIPS](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz), [64-bit MIPSLE](https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz)
|
||||
* Windows: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_windows_386.zip)
|
||||
* macOS: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_386.zip)
|
||||
* macOS ARM: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_darwin_arm64.zip)
|
||||
* FreeBSD: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz)
|
||||
* FreeBSD ARM: [64-bit](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz), [32-bit ARMv6](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz)
|
||||
* OpenBSD: (coming soon)
|
||||
* OpenBSD ARM: (coming soon)
|
||||
|
||||
* Edge channel builds
|
||||
* Linux: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_386.tar.gz)
|
||||
* Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv6.tar.gz) (recommended for Raspberry Pi), [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv7.tar.gz)
|
||||
* Linux MIPS: [32-bit MIPS](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips_softfloat.tar.gz), [32-bit MIPSLE](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mipsle_softfloat.tar.gz), [64-bit MIPS](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips64_softfloat.tar.gz), [64-bit MIPSLE](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips64le_softfloat.tar.gz)
|
||||
* Windows: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_windows_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_windows_386.zip)
|
||||
* macOS: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_386.zip)
|
||||
* macOS ARM: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_arm64.zip)
|
||||
* FreeBSD: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_386.tar.gz)
|
||||
* FreeBSD ARM: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv5.tar.gz), [32-bit ARMv6](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv6.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv7.tar.gz)
|
||||
* OpenBSD: [64-bit (experimental)](https://static.adguard.com/adguardhome/edge/AdGuardHome_openbsd_amd64.tar.gz)
|
||||
* OpenBSD ARM: [64-bit (experimental)](https://static.adguard.com/adguardhome/edge/AdGuardHome_openbsd_arm64.tar.gz)
|
||||
* Edge channel builds
|
||||
* Linux: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_386.tar.gz)
|
||||
* Linux ARM: [32-bit ARMv6](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv6.tar.gz) (recommended for Raspberry Pi OS stable), [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_armv7.tar.gz)
|
||||
* Linux MIPS: [32-bit MIPS](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips_softfloat.tar.gz), [32-bit MIPSLE](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mipsle_softfloat.tar.gz), [64-bit MIPS](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips64_softfloat.tar.gz), [64-bit MIPSLE](https://static.adguard.com/adguardhome/edge/AdGuardHome_linux_mips64le_softfloat.tar.gz)
|
||||
* Windows: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_windows_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_windows_386.zip)
|
||||
* macOS: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_amd64.zip), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_386.zip)
|
||||
* macOS ARM: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_darwin_arm64.zip)
|
||||
* FreeBSD: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_amd64.tar.gz), [32-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_386.tar.gz)
|
||||
* FreeBSD ARM: [64-bit](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_arm64.tar.gz), [32-bit ARMv5](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv5.tar.gz), [32-bit ARMv6](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv6.tar.gz), [32-bit ARMv7](https://static.adguard.com/adguardhome/edge/AdGuardHome_freebsd_armv7.tar.gz)
|
||||
* OpenBSD: [64-bit (experimental)](https://static.adguard.com/adguardhome/edge/AdGuardHome_openbsd_amd64.tar.gz)
|
||||
* OpenBSD ARM: [64-bit (experimental)](https://static.adguard.com/adguardhome/edge/AdGuardHome_openbsd_arm64.tar.gz)
|
||||
|
||||
|
||||
<a id="reporting-issues"></a>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerGo': 'adguard/golang-ubuntu:3.3'
|
||||
'dockerGo': 'adguard/golang-ubuntu:3.6'
|
||||
|
||||
'stages':
|
||||
- 'Make release':
|
||||
@@ -266,7 +266,7 @@
|
||||
# need to build a few of these.
|
||||
'variables':
|
||||
'channel': 'beta'
|
||||
'dockerGo': 'adguard/golang-ubuntu:3.3'
|
||||
'dockerGo': 'adguard/golang-ubuntu:3.6'
|
||||
# release-vX.Y.Z branches are the branches from which the actual final release
|
||||
# is built.
|
||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||
@@ -276,4 +276,4 @@
|
||||
# are the ones that actually get released.
|
||||
'variables':
|
||||
'channel': 'release'
|
||||
'dockerGo': 'adguard/golang-ubuntu:3.3'
|
||||
'dockerGo': 'adguard/golang-ubuntu:3.6'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'variables':
|
||||
'dockerGo': 'adguard/golang-ubuntu:3.3'
|
||||
'dockerGo': 'adguard/golang-ubuntu:3.6'
|
||||
|
||||
'stages':
|
||||
- 'Tests':
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"dashboard": "Табло",
|
||||
"settings": "Настройки",
|
||||
"filters": "Филтри",
|
||||
"filter": "Филтър",
|
||||
"query_log": "История на заявките",
|
||||
"faq": "ЧЗВ",
|
||||
"version": "версия",
|
||||
@@ -41,6 +42,7 @@
|
||||
"copyright": "Авторско право",
|
||||
"homepage": "Домашна страница",
|
||||
"report_an_issue": "Съобщи за проблем",
|
||||
"privacy_policy": "Правила за поверителност",
|
||||
"enable_protection": "Разреши защита",
|
||||
"enabled_protection": "Защитата е разрешена",
|
||||
"disable_protection": "Забрани защита",
|
||||
@@ -70,6 +72,7 @@
|
||||
"enforce_safe_search": "Включи Безопасно Търсене",
|
||||
"no_servers_specified": "Няма избрани услуги",
|
||||
"general_settings": "Общи настройки",
|
||||
"custom_filtering_rules": "Местни правила за филтриране",
|
||||
"upstream_dns": "Главен DNS сървър",
|
||||
"test_upstream_btn": "Тествай главния DNS",
|
||||
"apply_btn": "Приложи",
|
||||
@@ -86,6 +89,7 @@
|
||||
"rules_count_table_header": "Правила общо",
|
||||
"last_time_updated_table_header": "Последно обновен",
|
||||
"actions_table_header": "Действия",
|
||||
"edit_table_action": "Редактирай",
|
||||
"delete_table_action": "Изтрий",
|
||||
"filters_and_hosts_hint": "AdGuard Home разбира adblock и host синтаксис.",
|
||||
"cancel_btn": "Откажи",
|
||||
@@ -130,6 +134,9 @@
|
||||
"updated_custom_filtering_toast": "Обновени местни правила за филтриране",
|
||||
"rule_removed_from_custom_filtering_toast": "Премахнато от местни правила за филтриране: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Добавено до местни правила за филтриране: {{rule}}",
|
||||
"default": "По подразбиране",
|
||||
"custom_ip": "Персонализиран IP",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"plain_dns": "Обикновен DNS",
|
||||
"source_label": "Източник",
|
||||
"found_in_known_domain_db": "Намерен в списъците с домейни.",
|
||||
@@ -217,8 +224,26 @@
|
||||
"form_error_password": "Паролата не съвпада",
|
||||
"reset_settings": "Изтрий всички настройки",
|
||||
"update_announcement": "Има нова AdGuard Home {{version}}! <0>Цъкни тук</0> за повече информация.",
|
||||
"disable_ipv6": "Изключете IPv6 протокола",
|
||||
"settings_custom": "Персонализиране",
|
||||
"table_client": "Клиент",
|
||||
"table_name": "Име",
|
||||
"save_btn": "Запази",
|
||||
"name": "Име",
|
||||
"clients_not_found": "Нямa намерени адреси",
|
||||
"check_updates_now": "Провери за актуализации",
|
||||
"domain": "Домейн",
|
||||
"disabled": "Деактивиран",
|
||||
"username_label": "Потребител",
|
||||
"username_placeholder": "Въведете потребител",
|
||||
"password_label": "Парола",
|
||||
"password_placeholder": "Въведете парола",
|
||||
"network": "Мрежа",
|
||||
"descr": "Описание",
|
||||
"show_blocked_responses": "Блокирано",
|
||||
"show_whitelisted_responses": "В белия списък",
|
||||
"show_processed_responses": "Обработен",
|
||||
"allowed": "В белия списък",
|
||||
"filter_category_general": "General",
|
||||
"filter_category_security": "Сигурност",
|
||||
"port_53_faq_link": "Порт 53 често е зает от \"DNSStubListener\" или \"systemd-resolved\" услуги. Моля, прочетете <0>тази инструкция</0> как да решите това."
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"dhcp_ipv6_settings": "DHCP IPv6 Settings",
|
||||
"form_error_required": "Required field",
|
||||
"form_error_ip4_format": "Invalid IPv4 format",
|
||||
"form_error_ip4_range_start_format": "Invalid range start IPv4 format",
|
||||
"form_error_ip4_range_end_format": "Invalid range end IPv4 format",
|
||||
"form_error_ip4_gateway_format": "Invalid gateway IPv4 format",
|
||||
"form_error_ip6_format": "Invalid IPv6 format",
|
||||
"form_error_ip_format": "Invalid IP format",
|
||||
"form_error_mac_format": "Invalid MAC format",
|
||||
@@ -45,7 +48,12 @@
|
||||
"form_error_subnet": "Subnet \"{{cidr}}\" does not contain the IP address \"{{ip}}\"",
|
||||
"form_error_positive": "Must be greater than 0",
|
||||
"form_error_negative": "Must be equal to 0 or greater",
|
||||
"range_end_error": "Must be greater than range start",
|
||||
"out_of_range_error": "Must be out of range \"{{start}}\"-\"{{end}}\"",
|
||||
"lower_range_start_error": "Must be lower than range start",
|
||||
"greater_range_start_error": "Must be greater than range start",
|
||||
"greater_range_end_error": "Must be greater than range end",
|
||||
"subnet_error": "Addresses must be in one subnet",
|
||||
"gateway_or_subnet_invalid": "Subnet mask invalid",
|
||||
"dhcp_form_gateway_input": "Gateway IP",
|
||||
"dhcp_form_subnet_input": "Subnet mask",
|
||||
"dhcp_form_range_title": "Range of IP addresses",
|
||||
|
||||
@@ -206,8 +206,10 @@
|
||||
"custom_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",
|
||||
"form_enter_rate_limit": "میزان محدودیت را وارد کنید",
|
||||
"rate_limit": "میزان محدودیت",
|
||||
"edns_enable": "فعالسازی زیرشبکه کلاینت EDNS",
|
||||
@@ -399,6 +401,7 @@
|
||||
"encryption_key_source_content": "چسباندن محتوای کلید خصوصی",
|
||||
"stats_params": "پیکربندی آمار",
|
||||
"config_successfully_saved": "پیکربندی با موفقیت ذخیره شد",
|
||||
"interval_6_hour": "6 ساعت",
|
||||
"interval_24_hour": "24 ساعت",
|
||||
"interval_days": "{{value}} روز",
|
||||
"interval_days_plural": "{{count}} روز",
|
||||
@@ -409,7 +412,7 @@
|
||||
"statistics_configuration": "پیکربندی آمارها",
|
||||
"statistics_retention": "مدت حفظ آمارها",
|
||||
"statistics_retention_desc": "اگر مقدار فاصله را کاهش دهید،برخی داده ها از بین خواهد رفت",
|
||||
"statistics_clear": " پاکسازی آمار",
|
||||
"statistics_clear": "بازنشانی آمار",
|
||||
"statistics_clear_confirm": "آیا واقعا میخواهید آمار را پاک کنید؟",
|
||||
"statistics_retention_confirm": "آیا واقعا میخواهید مدت حفظ آمار را تغییر دهید؟ اگر فاصله را کاهش دهید، برخی داده ها حذف میشود",
|
||||
"statistics_cleared": "آمارها با موفقیت حذف شد",
|
||||
@@ -439,8 +442,6 @@
|
||||
"domain_desc": "نامه دامنه یا علامت تطبیقی را برای بازنویسی وارد کنید.",
|
||||
"example_rewrite_domain": "فقط بازنویسی پاسخ برای این دامنه.",
|
||||
"example_rewrite_wildcard": "بازنویسی پاسخ ها برای همه زیردامنه های <0>example.org</0>.",
|
||||
"disable_ipv6": "غیرفعالسازی IPv6",
|
||||
"disable_ipv6_desc": "اگر این ویژگی فعال شده، همه جستارهای DNS برای آدرس های IPv6 (نوع AAAA) رها میشود.",
|
||||
"fastest_addr": "سریعترین آدرس آی پی",
|
||||
"autofix_warning_text": "اگر روی \"تعمیر\" کلیک کنید، AdGuardHome سیستم شما را برای استفاده از DNS سرور AdGuardHome پیکربندی می کند.",
|
||||
"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 (بارگیری مجدد سرویس systemd-resolved)</0>",
|
||||
@@ -485,5 +486,8 @@
|
||||
"rewritten": "بازنویسی شده",
|
||||
"safe_search": "جستجوی اَمن",
|
||||
"blocklist": "لیست سیاه",
|
||||
"milliseconds_abbreviation": "هـ ثـ"
|
||||
"milliseconds_abbreviation": "هـ ثـ",
|
||||
"filter_category_general": "General",
|
||||
"filter_category_security": "مسدودسازی بدافزار و فیشینگ",
|
||||
"filter_category_other": "ساير"
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
"example_upstream_sdns": "možete koristiti <0>DNS Stamps</0> za <1>DNSCrypt</1> ili <2>DNS-over-HTTPS</2> rezolvere",
|
||||
"example_upstream_tcp": "zadani DNS (putem TCP)",
|
||||
"all_lists_up_to_date_toast": "Svi popisi su ažurirani",
|
||||
"updated_upstream_dns_toast": "Ažurirani su upstream DNS poslužitelji",
|
||||
"updated_upstream_dns_toast": "Uzvodni poslužitelji uspješno su spremljeni",
|
||||
"dns_test_ok_toast": "Odabrani DNS poslužitelji su trenutno aktivni",
|
||||
"dns_test_not_ok_toast": "\"{{key}}\" poslužitelja: ne može se upotrijebiti, provjerite jeste li to ispravno napisali",
|
||||
"unblock": "Odblokiraj",
|
||||
@@ -235,7 +235,7 @@
|
||||
"loading_table_status": "Učitavanje...",
|
||||
"page_table_footer_text": "Stranica",
|
||||
"rows_table_footer_text": "redova",
|
||||
"updated_custom_filtering_toast": "Ažurirana su prilagođena pravila filtriranja",
|
||||
"updated_custom_filtering_toast": "Prilagođena pravila uspješno su spremljena",
|
||||
"rule_removed_from_custom_filtering_toast": "Pravilo je uklonjeno iz prilagođenih pravila filtriranja: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Pravilo je dodano u prilagođena pravila filtriranja: {{rule}}",
|
||||
"query_log_response_status": "Status: {{value}}",
|
||||
@@ -306,7 +306,7 @@
|
||||
"install_settings_dns_desc": "Potrebno je postaviti uređaj ili router da koristi DNS poslužitelj na sljedećim adresama:",
|
||||
"install_settings_all_interfaces": "Sva sučelja",
|
||||
"install_auth_title": "Autentikacija",
|
||||
"install_auth_desc": "Izrazito se preporučuje postavljanje autentikacije za web administratorsko sučelje AdGuard Home. Iako je dostupna samo u vašoj lokalnoj mreži, važno je zaštititi je od ne dozvoljenog pristupa.",
|
||||
"install_auth_desc": "Provjera autentičnosti lozinke na web-sučelje AdGuard Home admin mora biti konfigurirana. Čak i ako je AdGuard Home dostupan samo u vašoj lokalnoj mreži, i dalje je važno zaštititi ga od neograničenog pristupa.",
|
||||
"install_auth_username": "Korisničko ime",
|
||||
"install_auth_password": "Lozinka",
|
||||
"install_auth_confirm": "Potvrdi lozinku",
|
||||
@@ -503,6 +503,7 @@
|
||||
"statistics_clear_confirm": "Jeste li sigurni da želite poništiti statistiku?",
|
||||
"statistics_retention_confirm": "Jeste li sigurni da želite promijeniti zadržavanje statistike? Ako smanjite vrijednost intervala, neki će podaci biti izgubljeni",
|
||||
"statistics_cleared": "Statistika je uspješno uklonjenja",
|
||||
"statistics_enable": "Omogući statistiku",
|
||||
"interval_hours": "{{count}} sata/i",
|
||||
"interval_hours_plural": "{{count}} sata/i",
|
||||
"filters_configuration": "Postavke filtara",
|
||||
@@ -612,6 +613,8 @@
|
||||
"click_to_view_queries": "Kliknite za pregled upita",
|
||||
"port_53_faq_link": "Port 53 često zauzimaju usluge \"DNSStubListener\" ili \"systemd-resolved\". Molimo pročitajte <0>ove upute</0> o tome kako to riješiti.",
|
||||
"adg_will_drop_dns_queries": "AdGuard Home odbaciti će sve DNS upite od ovog klijenta.",
|
||||
"client_not_in_allowed_clients": "Klijent nije dopušten jer nije na popisu \"Dopuštenih klijenata\".",
|
||||
"experimental": "Eksperimentalno"
|
||||
"filter_allowlist": "UPOZORENJE: Ova akcija će također isključiti pravilo \"{{disallowed_rule}}\" s popisa dopuštenih klijenata.",
|
||||
"last_rule_in_allowlist": "Ovaj klijent nije moguće onemogućiti jer će isključivanje pravila \"{{disallowed_rule}}\" ONEMOGUĆITI popis \"Dopušteni klijenti\".",
|
||||
"experimental": "Eksperimentalno",
|
||||
"use_saved_key": "Korištenje prethodno spremljenog ključa"
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
"example_upstream_doq": "versleutelde <0>DNS-via-QUIC</0>",
|
||||
"example_upstream_sdns": "je kunt <0>DNS Stamps</0> voor <1>DNSCrypt</1> of <2>DNS-via-HTTPS</2> oplossingen gebruiken",
|
||||
"example_upstream_tcp": "standaard DNS (over TCP)",
|
||||
"all_lists_up_to_date_toast": "Alle lijsten zijn reeds up-to-date",
|
||||
"all_lists_up_to_date_toast": "Alle lijsten zijn reeds actueel",
|
||||
"updated_upstream_dns_toast": "Upstream-servers succesvol opgeslagen",
|
||||
"dns_test_ok_toast": "Opgegeven DNS-servers werken correct",
|
||||
"dns_test_not_ok_toast": "Server \"{{key}}\": kon niet worden gebruikt, controleer of je het correct hebt geschreven",
|
||||
@@ -378,8 +378,8 @@
|
||||
"encryption_issuer": "Uitgever",
|
||||
"encryption_hostnames": "Hostnamen",
|
||||
"encryption_reset": "Ben je zeker dat je de encryptie instellingen wil resetten?",
|
||||
"topline_expiring_certificate": "Jouw SSL certificaat vervalt binnenkort. Update <0>Encryptie instellingen</0>.",
|
||||
"topline_expired_certificate": "Jouw SSL certificaat is vervallen. Update <0>Encryptie instellingen</0>.",
|
||||
"topline_expiring_certificate": "Jouw SSL-certificaat vervalt binnenkort. Werk de <0>encryptie-instellingen</0> bij.",
|
||||
"topline_expired_certificate": "Jouw SSL-certificaat is vervallen. Werk de <0>encryptie-instellingen</0> bij.",
|
||||
"form_error_port_range": "Poort nummer invoeren tussen 80 en 65535",
|
||||
"form_error_port_unsafe": "Dit is een onveilige poort",
|
||||
"form_error_equal": "Mag niet gelijk zijn",
|
||||
@@ -394,7 +394,7 @@
|
||||
"fix": "Los op",
|
||||
"dns_providers": "hier is een <0>lijst of gekende DNS providers</0> waarvan je kan kiezen.",
|
||||
"update_now": "Update nu",
|
||||
"update_failed": "Auto-update is mislukt. <a>Volg deze stappen</a> om manueel te updaten.",
|
||||
"update_failed": "Automatisch bijwerken is mislukt. <a>Volg deze stappen</a> om handmatig bij te werken.",
|
||||
"processing_update": "Even geduld, AdGuard Home wordt bijgewerkt",
|
||||
"clients_title": "Gebruikers",
|
||||
"clients_desc": "Configureer apparaten die gebruik maken van AdGuard Home",
|
||||
@@ -435,7 +435,7 @@
|
||||
"access_blocked_desc": "Verwar dit niet met filters. AdGuard Home zal deze DNS-zoekopdrachten niet uitvoeren die deze domeinen in de zoekopdracht bevatten. Hier kan je de exacte domeinnamen, wildcards en URL-filter-regels specifiëren, bijv. \"example.org\", \"*.example.org\" of \"||example.org^\".",
|
||||
"access_settings_saved": "Toegangsinstellingen succesvol opgeslagen",
|
||||
"updates_checked": "Met succes op updates gecontroleerd",
|
||||
"updates_version_equal": "AdGuard Home is up-to-date",
|
||||
"updates_version_equal": "AdGuard Home is actueel",
|
||||
"check_updates_now": "Controleer op updates",
|
||||
"dns_privacy": "DNS Privacy",
|
||||
"setup_dns_privacy_1": "<0>DNS-via-TLS:</0> Gebruik <1>{{address}}</1> string.",
|
||||
|
||||
@@ -436,6 +436,7 @@
|
||||
"encryption_key_source_content": "Lim inn innholdet til den private nøkkelen",
|
||||
"stats_params": "Statistikk-oppsett",
|
||||
"config_successfully_saved": "Oppsettet ble vellykket lagret",
|
||||
"interval_6_hour": "6 timer",
|
||||
"interval_24_hour": "24 timer",
|
||||
"interval_days": "{{count}} dag",
|
||||
"interval_days_plural": "{{count}} dager",
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
"enabled_protection": "Ativar proteção",
|
||||
"disable_protection": "Desativar proteção",
|
||||
"disabled_protection": "Desativar proteção",
|
||||
"refresh_statics": "Repor estatísticas",
|
||||
"refresh_statics": "Actualizar estatísticas",
|
||||
"dns_query": "Consultas de DNS",
|
||||
"blocked_by": "<0>Bloqueado por filtros</0>",
|
||||
"stats_malware_phishing": "Malware/phishing bloqueados",
|
||||
|
||||
@@ -112,6 +112,8 @@
|
||||
"for_last_24_hours": "în ultimele 24 ore",
|
||||
"for_last_days": "în ultima {{count}} zi",
|
||||
"for_last_days_plural": "pentru ultimele {{count}} zile",
|
||||
"stats_disabled": "Statisticile au fost dezactivate. Puteți să le porniți din <0>pagina de setări</0>.",
|
||||
"stats_disabled_short": "Statisticile au fost dezactivate",
|
||||
"no_domains_found": "Nu s-au găsit domenii",
|
||||
"requests_count": "Cont interogări",
|
||||
"top_blocked_domains": "Domeniile blocate cel mai des",
|
||||
@@ -206,7 +208,7 @@
|
||||
"example_upstream_sdns": "puteți utiliza <0>DNS Stamps</0> pentru rezolvere <1>DNSCrypt</1> sau <2>DNS-over-HTTPS</2>",
|
||||
"example_upstream_tcp": "DNS clasic (over TCP)",
|
||||
"all_lists_up_to_date_toast": "Toate listele sunt deja la zi",
|
||||
"updated_upstream_dns_toast": "Serverele DNS în amonte aduse la zi",
|
||||
"updated_upstream_dns_toast": "Serverele din amonte au fost salvate cu succes",
|
||||
"dns_test_ok_toast": "Serverele DNS specificate funcționează corect",
|
||||
"dns_test_not_ok_toast": "Serverul \"{{key}}\": nu a putut fi utilizat, verificați dacă l-ați scris corect",
|
||||
"unblock": "Deblocați",
|
||||
@@ -233,7 +235,7 @@
|
||||
"loading_table_status": "Se încarcă...",
|
||||
"page_table_footer_text": "Pagina",
|
||||
"rows_table_footer_text": "linii",
|
||||
"updated_custom_filtering_toast": "Reguli personalizate de filtrare aduse la zi",
|
||||
"updated_custom_filtering_toast": "Regulile personalizate au fost salvate cu succes",
|
||||
"rule_removed_from_custom_filtering_toast": "Regulă scoasă din regullei personalizate de filtrare: {{rule}}",
|
||||
"rule_added_to_custom_filtering_toast": "Regulă adăugată la regulile de filtrare personalizate: {{rule}}",
|
||||
"query_log_response_status": "Statut: {{value}}",
|
||||
@@ -304,7 +306,7 @@
|
||||
"install_settings_dns_desc": "Va trebui să configurați aparatele sau routerul pentru a utiliza serverul DNS pe următoarele adrese:",
|
||||
"install_settings_all_interfaces": "Toate interfețele",
|
||||
"install_auth_title": "Autentificare",
|
||||
"install_auth_desc": "Este foarte recomandat să configurați o parolă pentru accesul la interfața web de administrare AdGuard Home. Chiar dacă este accesibil numai în rețeaua dvs. locală, este încă important să îl protejați de accesul fără restricții.",
|
||||
"install_auth_desc": "Trebuie configurată autentificarea cu parolă la interfața web AdGuard Home admin. Chiar dacă AdGuard Home este accesibil numai în rețeaua locală, este important să îl protejați de accesul fără restricții.",
|
||||
"install_auth_username": "Nume utilizator",
|
||||
"install_auth_password": "Parola",
|
||||
"install_auth_confirm": "Confirmați parola",
|
||||
@@ -327,7 +329,7 @@
|
||||
"install_devices_windows_list_3": "În partea stângă a ecranului găsiți \"Schimbare setări adaptor\" și clicați pe el.",
|
||||
"install_devices_windows_list_4": "Selectați conexiunea activă, faceți clic dreapta pe ea și alegeți \"Proprietăți\".",
|
||||
"install_devices_windows_list_5": "Găsiți Internet Protocol Versiunea 4 (TCP/IPv4) din listă, selectați-l și apoi clicați din nou pe Proprietăți.",
|
||||
"install_devices_windows_list_6": "Alegeți Utilizați următoarele adrese de server DNS și introduceți adresele de server AdGuard Home.",
|
||||
"install_devices_windows_list_6": "Alegeți „Utilizați următoarele adrese de server DNS” și introduceți adresele serverului dvs. AdGuard Home.",
|
||||
"install_devices_macos_list_1": "Clicați pe icoana Apple și accesați Preferințele Sistemului.",
|
||||
"install_devices_macos_list_2": "Clicați pe Network.",
|
||||
"install_devices_macos_list_3": "Selectați prima conexiune din listă și clicați pe Avansat.",
|
||||
@@ -501,6 +503,7 @@
|
||||
"statistics_clear_confirm": "Sunteți sigur că doriți să ștergeți statisticile?",
|
||||
"statistics_retention_confirm": "Sunteți sigur că doriți să schimbați păstrarea statisticilor? Dacă reduceți valoarea intervalului, unele date vor fi pierdute",
|
||||
"statistics_cleared": "Statisticile au fost șterse cu succes",
|
||||
"statistics_enable": "Activați statisticile",
|
||||
"interval_hours": "{{count}} oră",
|
||||
"interval_hours_plural": "{{count}} ore",
|
||||
"filters_configuration": "Configurația filtrelor",
|
||||
@@ -595,6 +598,8 @@
|
||||
"cache_ttl_min_override_desc": "Extinde valorile timp-de-viață scurte (secunde) primite de la serverul din amonte la stocarea în cache a răspunsurilor DNS",
|
||||
"cache_ttl_max_override_desc": "Setează o valoare maximă a timpului-de-viață (secunde) pentru intrările din memoria cache DNS",
|
||||
"ttl_cache_validation": "Valoarea TTL cache minimă trebuie să fie mai mică sau egală cu valoarea maximă",
|
||||
"cache_optimistic": "Caching optimistic",
|
||||
"cache_optimistic_desc": "Face ca AdGuard Home să răspundă din cache chiar și atunci când intrările au expirate și de asemenea, încearcă să le reîmprospăteze.",
|
||||
"filter_category_general": "General",
|
||||
"filter_category_security": "Securitate",
|
||||
"filter_category_regional": "Regional",
|
||||
@@ -608,6 +613,8 @@
|
||||
"click_to_view_queries": "Clicați pentru a vizualiza interogări",
|
||||
"port_53_faq_link": "Portul 53 este adesea ocupat de serviciile \"DNSStubListener\" sau \"systemd-resolved\". Vă rugăm să citiți <0>această instrucțiune</0> despre cum să rezolvați aceasta.",
|
||||
"adg_will_drop_dns_queries": "AdGuard Home va renunța la toate interogările DNS de la acest client.",
|
||||
"client_not_in_allowed_clients": "Clientul nu este permis deoarece nu este în lista de \"Clienți permiși\".",
|
||||
"experimental": "Experimental"
|
||||
"filter_allowlist": "AVERTISMENT: Această acțiune va exclude și regula „{{disallowed_rule}}” din lista de clienți permiși.",
|
||||
"last_rule_in_allowlist": "Acest client nu poate fi exclus deoarece excluderea regulii „{{disallowed_rule}}” va DEZACTIVA lista „Clienți acceptați”.",
|
||||
"experimental": "Experimental",
|
||||
"use_saved_key": "Folosiți cheia salvată anterior"
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
"no_logs_found": "Логи не найдены",
|
||||
"refresh_btn": "Обновить",
|
||||
"previous_btn": "Назад",
|
||||
"next_btn": "Вперёд",
|
||||
"next_btn": "Далее",
|
||||
"loading_table_status": "Загрузка…",
|
||||
"page_table_footer_text": "Страница",
|
||||
"rows_table_footer_text": "строк",
|
||||
@@ -344,7 +344,7 @@
|
||||
"install_devices_ios_list_3": "Нажмите на название сети, к которой устройство подключено в данный момент.",
|
||||
"install_devices_ios_list_4": "В поле «DNS» введите введите адреса AdGuard Home.",
|
||||
"get_started": "Поехали",
|
||||
"next": "Дальше",
|
||||
"next": "Далее",
|
||||
"open_dashboard": "Открыть Панель управления",
|
||||
"install_saved": "Успешно сохранено",
|
||||
"encryption_title": "Шифрование",
|
||||
|
||||
@@ -226,8 +226,10 @@
|
||||
"custom_ip": "Prilagođeni IP",
|
||||
"blocking_ipv4": "Blokiranje IPv4",
|
||||
"blocking_ipv6": "Blokiranje IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"download_mobileconfig_doh": "Preuzimanja",
|
||||
"download_mobileconfig_dot": "Preuzmi .mobileconfig za DNS-over-TLS",
|
||||
"plain_dns": "Plain DNS",
|
||||
@@ -430,6 +432,7 @@
|
||||
"encryption_key_source_content": "Nalepi sadržaj privatnog ključa",
|
||||
"stats_params": "Konfiguracija statistike",
|
||||
"config_successfully_saved": "Konfiguracija je uspešno sačuvana",
|
||||
"interval_6_hour": "6 sati",
|
||||
"interval_24_hour": "24 časa",
|
||||
"interval_days": "{{count}} dan",
|
||||
"interval_days_plural": "{{count}} dana",
|
||||
|
||||
@@ -39,14 +39,18 @@
|
||||
"delete_confirm": "Är du säker på att du vill ta bort \"{{key}}\"?",
|
||||
"form_enter_hostname": "Skriv in värdnamn",
|
||||
"error_details": "Felinformation",
|
||||
"request_details": "Förfrågningsdetaljer",
|
||||
"details": "Detaljer",
|
||||
"back": "Tiilbaka",
|
||||
"dashboard": "Kontrollpanel",
|
||||
"settings": "Inställningar",
|
||||
"filters": "Filter",
|
||||
"filter": "Filter",
|
||||
"query_log": "Förfrågningslogg",
|
||||
"faq": "FAQ",
|
||||
"version": "version",
|
||||
"address": "Adress",
|
||||
"protocol": "Protokoll",
|
||||
"on": "PÅ",
|
||||
"off": "AV",
|
||||
"copyright": "Copyright",
|
||||
@@ -85,6 +89,7 @@
|
||||
"no_servers_specified": "Inga servrar angivna",
|
||||
"general_settings": "Allmänna inställningar",
|
||||
"dns_settings": "DNS-inställningar",
|
||||
"custom_filtering_rules": "Egna filterregler",
|
||||
"encryption_settings": "Krypteringsinställningar",
|
||||
"dhcp_settings": "DHCP-inställningar",
|
||||
"upstream_dns": "Upstream DNS-servrar",
|
||||
@@ -159,6 +164,12 @@
|
||||
"query_log_disabled": "Förfrågningsloggen är avaktiverad och kan konfigureras i <0>inställningar</0>",
|
||||
"query_log_strict_search": "Använd dubbla citattecken för strikt sökning",
|
||||
"query_log_retention_confirm": "Är du säker på att du vill ändra förfrågningsloggars retentionstid? Om du minskar intervallet kommer viss data att gå förlorad",
|
||||
"default": "Standard",
|
||||
"custom_ip": "Eget IP",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"source_label": "Källa",
|
||||
"found_in_known_domain_db": "Hittad i domändatabas.",
|
||||
"category_label": "Kategori",
|
||||
@@ -333,12 +344,20 @@
|
||||
"location": "Plats",
|
||||
"orgname": "Organisationsnamn",
|
||||
"netname": "Nätverksnamn",
|
||||
"network": "Nätverk",
|
||||
"descr": "Beskrivning",
|
||||
"whois": "Whois",
|
||||
"filtering_rules_learn_more": "<0>Mer info</0> om att skapa dina egna blockeringslistor för värdar.",
|
||||
"try_again": "Försök igen",
|
||||
"show_blocked_responses": "Blockerade",
|
||||
"show_whitelisted_responses": "Vitlistade",
|
||||
"show_processed_responses": "Utförda",
|
||||
"blocked_adult_websites": "Blockerade vuxensajter",
|
||||
"blocked_threats": "Blockerade hot",
|
||||
"allowed": "Vitlistade",
|
||||
"safe_search": "Säker surf",
|
||||
"filter_category_general": "General",
|
||||
"filter_category_security": "säkerhet",
|
||||
"filter_category_other": "Annat",
|
||||
"use_saved_key": "Använd den tidigare sparade nyckeln"
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"delete_confirm": "คุณแน่ใจหรือว่าต้องการลบ \"{{key}}\"?",
|
||||
"form_enter_hostname": "ป้อนชื่อโฮสต์",
|
||||
"error_details": "รายละเอียดข้อผิดพลาด",
|
||||
"request_details": "ขอรายละเอียด",
|
||||
"back": "กลับ",
|
||||
"dashboard": "แผงควบคุม",
|
||||
"settings": "การตั้งค่า",
|
||||
@@ -87,6 +88,7 @@
|
||||
"no_servers_specified": "ไม่ได้ระบุเซิร์ฟเวอร์",
|
||||
"general_settings": "การตั้งค่าทั่วไป",
|
||||
"dns_settings": "การตั้งค่า DNS",
|
||||
"custom_filtering_rules": "กฎการกรองที่กำหนดเอง",
|
||||
"encryption_settings": "การตั้งค่าการเข้ารหัส",
|
||||
"dhcp_settings": "การตั้งค่า DHCP",
|
||||
"upstream_dns": "เซิร์ฟเวอร์ DNS ต้นทาง",
|
||||
@@ -169,6 +171,9 @@
|
||||
"custom_ip": "IP กำหนดเอง",
|
||||
"blocking_ipv4": "ปิดกั้น IPv4",
|
||||
"blocking_ipv6": "ปิดกั้น IPv6",
|
||||
"dnscrypt": "DNSCrypt",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"form_enter_rate_limit": "ป้อนขีดจำกัดอัตรา",
|
||||
"rate_limit": "จำกัดอัตรา",
|
||||
"edns_enable": "เปิดใช้งานซับเน็ตไคลเอ็นต์ EDNS",
|
||||
@@ -333,6 +338,7 @@
|
||||
"encryption_key_source_content": "วางเนื้อหาคีย์ส่วนตัว",
|
||||
"stats_params": "การกำหนดค่าสถิติ",
|
||||
"config_successfully_saved": "บันทึกการตั้งค่าเรีบยร้อยแล้ว",
|
||||
"interval_6_hour": "6 ชั่วโมง",
|
||||
"interval_24_hour": "24 ชั่วโมง",
|
||||
"interval_days": "{{count}} วัน",
|
||||
"interval_days_plural": "{{count}} วัน",
|
||||
@@ -383,5 +389,8 @@
|
||||
"form_select_tags": "เลือกแท็กเครื่อง",
|
||||
"check_title": "ตรวจสอบการกรอง",
|
||||
"check_desc": "ตรวจสอบว่าชื่อโฮสต์ถูกกรอง",
|
||||
"form_enter_host": "ป้อนชื่อโฮสต์"
|
||||
"form_enter_host": "ป้อนชื่อโฮสต์",
|
||||
"show_processed_responses": "การประมวลผล",
|
||||
"safe_search": "ค้นหาอย่างปลอดภัย",
|
||||
"filter_category_other": "อื่น ๆ"
|
||||
}
|
||||
|
||||
@@ -215,8 +215,8 @@
|
||||
"block": "封鎖",
|
||||
"disallow_this_client": "不允許此用戶端",
|
||||
"allow_this_client": "允許此用戶端",
|
||||
"block_for_this_client_only": "僅封鎖此用戶端",
|
||||
"unblock_for_this_client_only": "僅解除封鎖此用戶端",
|
||||
"block_for_this_client_only": "僅對此用戶端封鎖",
|
||||
"unblock_for_this_client_only": "僅對此用戶端解除封鎖",
|
||||
"time_table_header": "時間",
|
||||
"date": "日期",
|
||||
"domain_name_table_header": "域名",
|
||||
@@ -529,7 +529,7 @@
|
||||
"blocked_by_cname_or_ip": "被正規名稱(CNAME)或 IP 封鎖",
|
||||
"try_again": "再次嘗試",
|
||||
"domain_desc": "輸入您想要被改寫的域名或萬用字元(wildcard)。",
|
||||
"example_rewrite_domain": "僅對於此域名改寫回應。",
|
||||
"example_rewrite_domain": "僅對此域名改寫回應。",
|
||||
"example_rewrite_wildcard": "對於所有的 <0>example.org</0> 子網域改寫回應。",
|
||||
"rewrite_ip_address": "IP 位址:在一個 A 或 AAAA 回應中使用此 IP",
|
||||
"rewrite_domain_name": "域名:新增一筆正規名稱(CNAME)記錄",
|
||||
|
||||
@@ -16,18 +16,31 @@ const Row = ({
|
||||
? <LogsSearchLink response_status={response_status}>{formatNumber(count)}</LogsSearchLink>
|
||||
: count;
|
||||
|
||||
return <tr key={label}>
|
||||
<td>
|
||||
<Trans components={translationComponents}>{label}</Trans>
|
||||
<Tooltip content={tooltipTitle} placement="top"
|
||||
className="tooltip-container tooltip-custom--narrow text-center">
|
||||
<svg className="icons icon--20 icon--lightgray ml-2">
|
||||
<use xlinkHref="#question" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td className="text-right"><strong>{content}</strong></td>
|
||||
</tr>;
|
||||
return (
|
||||
<div className="counters__row" key={label}>
|
||||
<div className="counters__column">
|
||||
<span className="counters__title">
|
||||
<Trans components={translationComponents}>
|
||||
{label}
|
||||
</Trans>
|
||||
</span>
|
||||
<span className="counters__tooltip">
|
||||
<Tooltip
|
||||
content={tooltipTitle}
|
||||
placement="top"
|
||||
className="tooltip-container tooltip-custom--narrow text-center"
|
||||
>
|
||||
<svg className="icons icon--20 icon--lightgray ml-2">
|
||||
<use xlinkHref="#question" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<div className="counters__column counters__column--value">
|
||||
<strong>{content}</strong>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Counters = ({ refreshButton, subtitle }) => {
|
||||
@@ -88,9 +101,9 @@ const Counters = ({ refreshButton, subtitle }) => {
|
||||
bodyType="card-table"
|
||||
refresh={refreshButton}
|
||||
>
|
||||
<table className="table card-table">
|
||||
<tbody>{rows.map(Row)}</tbody>
|
||||
</table>
|
||||
<div className="counters">
|
||||
{rows.map(Row)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,3 +49,39 @@
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.counters__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
.counters__column--value {
|
||||
flex-shrink: 0;
|
||||
margin-left: 1.5rem;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.counters__column {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.counters__title {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.counters__tooltip {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
validateIpv4,
|
||||
validateRequiredValue,
|
||||
validateIpv4RangeEnd,
|
||||
validateGatewaySubnetMask,
|
||||
validateIpForGatewaySubnetMask,
|
||||
validateNotInRange,
|
||||
} from '../../../helpers/validators';
|
||||
|
||||
const FormDHCPv4 = ({
|
||||
@@ -54,7 +57,11 @@ const FormDHCPv4 = ({
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t(ipv4placeholders.gateway_ip)}
|
||||
validate={[validateIpv4, validateRequired]}
|
||||
validate={[
|
||||
validateIpv4,
|
||||
validateRequired,
|
||||
validateNotInRange,
|
||||
]}
|
||||
disabled={!isInterfaceIncludesIpv4}
|
||||
/>
|
||||
</div>
|
||||
@@ -66,7 +73,11 @@ const FormDHCPv4 = ({
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t(ipv4placeholders.subnet_mask)}
|
||||
validate={[validateIpv4, validateRequired]}
|
||||
validate={[
|
||||
validateIpv4,
|
||||
validateRequired,
|
||||
validateGatewaySubnetMask,
|
||||
]}
|
||||
disabled={!isInterfaceIncludesIpv4}
|
||||
/>
|
||||
</div>
|
||||
@@ -84,7 +95,11 @@ const FormDHCPv4 = ({
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t(ipv4placeholders.range_start)}
|
||||
validate={[validateIpv4]}
|
||||
validate={[
|
||||
validateIpv4,
|
||||
validateGatewaySubnetMask,
|
||||
validateIpForGatewaySubnetMask,
|
||||
]}
|
||||
disabled={!isInterfaceIncludesIpv4}
|
||||
/>
|
||||
</div>
|
||||
@@ -95,7 +110,12 @@ const FormDHCPv4 = ({
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t(ipv4placeholders.range_end)}
|
||||
validate={[validateIpv4, validateIpv4RangeEnd]}
|
||||
validate={[
|
||||
validateIpv4,
|
||||
validateIpv4RangeEnd,
|
||||
validateGatewaySubnetMask,
|
||||
validateIpForGatewaySubnetMask,
|
||||
]}
|
||||
disabled={!isInterfaceIncludesIpv4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,11 @@ const Form = ({
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={t('form_enter_subnet_ip', { cidr })}
|
||||
validate={[validateRequiredValue, validateIpv4, validateIpv4InCidr]}
|
||||
validate={[
|
||||
validateRequiredValue,
|
||||
validateIpv4,
|
||||
validateIpv4InCidr,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="form__group">
|
||||
|
||||
@@ -11,6 +11,8 @@ const Modal = ({
|
||||
handleSubmit,
|
||||
processingAdding,
|
||||
cidr,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -38,10 +40,14 @@ const Modal = ({
|
||||
ip: '',
|
||||
hostname: '',
|
||||
cidr,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
processingAdding={processingAdding}
|
||||
cidr={cidr}
|
||||
rangeStart={rangeStart}
|
||||
rangeEnd={rangeEnd}
|
||||
/>
|
||||
</div>
|
||||
</ReactModal>
|
||||
@@ -53,6 +59,8 @@ Modal.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
processingAdding: PropTypes.bool.isRequired,
|
||||
cidr: PropTypes.string.isRequired,
|
||||
rangeStart: PropTypes.string,
|
||||
rangeEnd: PropTypes.string,
|
||||
};
|
||||
|
||||
export default withTranslation()(Modal);
|
||||
|
||||
@@ -22,6 +22,8 @@ const StaticLeases = ({
|
||||
processingDeleting,
|
||||
staticLeases,
|
||||
cidr,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
}) => {
|
||||
const [t] = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
@@ -100,6 +102,8 @@ const StaticLeases = ({
|
||||
handleSubmit={handleSubmit}
|
||||
processingAdding={processingAdding}
|
||||
cidr={cidr}
|
||||
rangeStart={rangeStart}
|
||||
rangeEnd={rangeEnd}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -111,6 +115,8 @@ StaticLeases.propTypes = {
|
||||
processingAdding: PropTypes.bool.isRequired,
|
||||
processingDeleting: PropTypes.bool.isRequired,
|
||||
cidr: PropTypes.string.isRequired,
|
||||
rangeStart: PropTypes.string,
|
||||
rangeEnd: PropTypes.string,
|
||||
};
|
||||
|
||||
cellWrap.propTypes = {
|
||||
|
||||
@@ -275,6 +275,8 @@ const Dhcp = () => {
|
||||
processingAdding={processingAdding}
|
||||
processingDeleting={processingDeleting}
|
||||
cidr={cidr}
|
||||
rangeStart={dhcp?.values?.v4?.range_start}
|
||||
rangeEnd={dhcp?.values?.v4?.range_end}
|
||||
/>
|
||||
<div className="btn-list mt-2">
|
||||
<button
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
toNumber,
|
||||
} from '../../../helpers/form';
|
||||
import {
|
||||
validateClientId,
|
||||
validateConfigClientId,
|
||||
validateServerName,
|
||||
validatePort,
|
||||
validateIsSafePort,
|
||||
@@ -132,7 +132,7 @@ const MobileConfigForm = ({ invalid }) => {
|
||||
component={renderInputField}
|
||||
className="form-control"
|
||||
placeholder={i18next.t('client_id_placeholder')}
|
||||
validate={validateClientId}
|
||||
validate={validateConfigClientId}
|
||||
/>
|
||||
</div>
|
||||
<div className="form__group form__group--settings">
|
||||
|
||||
@@ -24,7 +24,7 @@ export const R_UNIX_ABSOLUTE_PATH = /^(\/[^/\x00]+)+$/;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
export const R_WIN_ABSOLUTE_PATH = /^([a-zA-Z]:)?(\\|\/)(?:[^\\/:*?"<>|\x00]+\\)*[^\\/:*?"<>|\x00]*$/;
|
||||
|
||||
export const R_CLIENT_ID = /^[a-z0-9-]{1,64}$/;
|
||||
export const R_CLIENT_ID = /^[a-z0-9-]{1,63}$/;
|
||||
|
||||
export const HTML_PAGES = {
|
||||
INSTALL: '/install.html',
|
||||
|
||||
@@ -60,12 +60,6 @@
|
||||
"homepage": "https://github.com/crazy-max/WindowsSpyBlocker",
|
||||
"source": "https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt"
|
||||
},
|
||||
"spam404": {
|
||||
"name": "Spam404",
|
||||
"categoryId": "security",
|
||||
"homepage": "https://github.com/Spam404/lists",
|
||||
"source": "https://raw.githubusercontent.com/Spam404/lists/master/main-blacklist.txt"
|
||||
},
|
||||
"nocoin-filter-list": {
|
||||
"name": "NoCoin Filter List",
|
||||
"categoryId": "security",
|
||||
@@ -156,11 +150,11 @@
|
||||
"homepage": "https://github.com/ABPindo/indonesianadblockrules/",
|
||||
"source": "https://raw.githubusercontent.com/ABPindo/indonesianadblockrules/master/subscriptions/abpindo.txt"
|
||||
},
|
||||
"barb-block": {
|
||||
"name": "BarbBlock",
|
||||
"categoryId": "other",
|
||||
"homepage": "https://github.com/paulgb/BarbBlock/",
|
||||
"source": "https://paulgb.github.io/BarbBlock/blacklists/hosts-file.txt"
|
||||
"NLD-Easylist": {
|
||||
"name": "NLD: Easylist",
|
||||
"categoryId": "regional",
|
||||
"homepage": "https://forums.lanik.us/viewforum.php?f=100",
|
||||
"source": "https://easylist-downloads.adblockplus.org/easylistdutch.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,6 +552,20 @@ export const isIpInCidr = (ip, cidr) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} subnetMask
|
||||
* @returns {IPv4 | null}
|
||||
*/
|
||||
export const parseSubnetMask = (subnetMask) => {
|
||||
try {
|
||||
return ipaddr.parse(subnetMask).prefixLengthFromSubnetMask();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} subnetMask
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import i18next from 'i18next';
|
||||
|
||||
import {
|
||||
MAX_PORT,
|
||||
R_CIDR,
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
R_DOMAIN,
|
||||
} from './constants';
|
||||
import { ip4ToInt, isValidAbsolutePath } from './form';
|
||||
import { isIpInCidr } from './helpers';
|
||||
import { isIpInCidr, parseSubnetMask } from './helpers';
|
||||
|
||||
// Validation functions
|
||||
// https://redux-form.com/8.3.0/examples/fieldlevelvalidation/
|
||||
@@ -44,7 +45,7 @@ export const validateIpv4RangeEnd = (_, allValues) => {
|
||||
const { range_end, range_start } = allValues.v4;
|
||||
|
||||
if (ip4ToInt(range_end) <= ip4ToInt(range_start)) {
|
||||
return 'range_end_error';
|
||||
return 'greater_range_start_error';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@@ -61,6 +62,85 @@ export const validateIpv4 = (value) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {undefined|string}
|
||||
* @param _
|
||||
* @param allValues
|
||||
*/
|
||||
export const validateNotInRange = (value, allValues) => {
|
||||
const { range_start, range_end } = allValues.v4;
|
||||
|
||||
if (range_start && validateIpv4(range_start)) {
|
||||
return 'form_error_ip4_range_start_format';
|
||||
}
|
||||
|
||||
if (range_end && validateIpv4(range_end)) {
|
||||
return 'form_error_ip4_range_end_format';
|
||||
}
|
||||
|
||||
const isAboveMin = range_start && ip4ToInt(value) >= ip4ToInt(range_start);
|
||||
const isBelowMax = range_end && ip4ToInt(value) <= ip4ToInt(range_end);
|
||||
|
||||
if (isAboveMin && isBelowMax) {
|
||||
return i18next.t('out_of_range_error', {
|
||||
start: range_start,
|
||||
end: range_end,
|
||||
});
|
||||
}
|
||||
|
||||
if (!range_end && isAboveMin) {
|
||||
return 'lower_range_start_error';
|
||||
}
|
||||
|
||||
if (!range_start && isBelowMax) {
|
||||
return 'greater_range_end_error';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {undefined|string}
|
||||
* @param _
|
||||
* @param allValues
|
||||
*/
|
||||
export const validateGatewaySubnetMask = (_, allValues) => {
|
||||
if (!allValues || !allValues.v4 || !allValues.v4.subnet_mask || !allValues.v4.gateway_ip) {
|
||||
return 'gateway_or_subnet_invalid';
|
||||
}
|
||||
|
||||
const { subnet_mask, gateway_ip } = allValues.v4;
|
||||
|
||||
if (validateIpv4(gateway_ip)) {
|
||||
return 'form_error_ip4_gateway_format';
|
||||
}
|
||||
|
||||
return parseSubnetMask(subnet_mask) ? undefined : 'gateway_or_subnet_invalid';
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {undefined|string}
|
||||
* @param value
|
||||
* @param allValues
|
||||
*/
|
||||
export const validateIpForGatewaySubnetMask = (value, allValues) => {
|
||||
if (!allValues || !allValues.v4 || !value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const {
|
||||
gateway_ip, subnet_mask,
|
||||
} = allValues.v4;
|
||||
|
||||
const subnetPrefix = parseSubnetMask(subnet_mask);
|
||||
|
||||
if (!isIpInCidr(value, `${gateway_ip}/${subnetPrefix}`)) {
|
||||
return 'subnet_error';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param value {string}
|
||||
* @returns {undefined|string}
|
||||
@@ -83,6 +163,21 @@ export const validateClientId = (value) => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param value {string}
|
||||
* @returns {undefined|string}
|
||||
*/
|
||||
export const validateConfigClientId = (value) => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const formattedValue = value.trim();
|
||||
if (formattedValue && !R_CLIENT_ID.test(formattedValue)) {
|
||||
return 'form_error_client_id_format';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param value {string}
|
||||
* @returns {undefined|string}
|
||||
|
||||
10
go.mod
10
go.mod
@@ -3,8 +3,8 @@ module github.com/AdguardTeam/AdGuardHome
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.39.8
|
||||
github.com/AdguardTeam/golibs v0.9.3
|
||||
github.com/AdguardTeam/dnsproxy v0.39.9
|
||||
github.com/AdguardTeam/golibs v0.10.2
|
||||
github.com/AdguardTeam/urlfilter v0.14.6
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/ameshkov/dnscrypt/v2 v2.2.2
|
||||
@@ -12,12 +12,14 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/go-ping/ping v0.0.0-20210506233800-ff8be3320020
|
||||
github.com/google/go-cmp v0.5.5
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/google/renameio v1.0.1
|
||||
github.com/insomniacslk/dhcp v0.0.0-20210310193751-cfd4d47082c2
|
||||
github.com/kardianos/service v1.2.0
|
||||
github.com/lucas-clemente/quic-go v0.21.1
|
||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7
|
||||
github.com/mdlayher/netlink v1.4.0
|
||||
github.com/mdlayher/raw v0.0.0-20210412142147-51b895745faf // indirect
|
||||
github.com/mdlayher/raw v0.0.0-20210412142147-51b895745faf
|
||||
github.com/miekg/dns v1.1.43
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/stretchr/objx v0.1.1 // indirect
|
||||
@@ -25,7 +27,7 @@ require (
|
||||
github.com/ti-mo/netfilter v0.4.0
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6
|
||||
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
|
||||
19
go.sum
19
go.sum
@@ -9,13 +9,13 @@ dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/AdguardTeam/dhcp v0.0.0-20210519141215-51808c73c0bf h1:gc042VRSIRSUzZ+Px6xQCRWNJZTaPkomisDfUZmoFNk=
|
||||
github.com/AdguardTeam/dhcp v0.0.0-20210519141215-51808c73c0bf/go.mod h1:TKl4jN3Voofo4UJIicyNhWGp/nlQqQkFxmwIFTvBkKI=
|
||||
github.com/AdguardTeam/dnsproxy v0.39.8 h1:miRhkZBx/19Rs1o10r3QC0D0Zc2J2Id/cqXwfvLOyM0=
|
||||
github.com/AdguardTeam/dnsproxy v0.39.8/go.mod h1:eDpJKAdkHORRwAedjuERv+7SWlcz4cn+5uwrbUAWHRY=
|
||||
github.com/AdguardTeam/dnsproxy v0.39.9 h1:lH4lKA7KHKFJZgzlij1YAVX6v7eIQpUFpYh9qV+WfGw=
|
||||
github.com/AdguardTeam/dnsproxy v0.39.9/go.mod h1:eDpJKAdkHORRwAedjuERv+7SWlcz4cn+5uwrbUAWHRY=
|
||||
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||
github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||
github.com/AdguardTeam/golibs v0.9.2/go.mod h1:fCAMwPBJ8S7YMYbTWvYS+eeTLblP5E04IDtNAo7y7IY=
|
||||
github.com/AdguardTeam/golibs v0.9.3 h1:noeKHJEzrSwxzX0Zi3USM3cXf1qQV99SO772jet/uEY=
|
||||
github.com/AdguardTeam/golibs v0.9.3/go.mod h1:fCAMwPBJ8S7YMYbTWvYS+eeTLblP5E04IDtNAo7y7IY=
|
||||
github.com/AdguardTeam/golibs v0.10.2 h1:TAwnS4Y49sSUa4UX1yz/MWNGbIlXHqafrWr9MxdIh9A=
|
||||
github.com/AdguardTeam/golibs v0.10.2/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
||||
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
|
||||
github.com/AdguardTeam/urlfilter v0.14.6 h1:emqoKZElooHACYehRBYENeKVN1a/rspxiqTIMYLuoIo=
|
||||
github.com/AdguardTeam/urlfilter v0.14.6/go.mod h1:klx4JbOfc4EaNb5lWLqOwfg+pVcyRukmoJRvO55lL5U=
|
||||
@@ -93,6 +93,8 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU=
|
||||
@@ -268,6 +270,7 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -300,8 +303,8 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw=
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6 h1:Z04ewVs7JhXaYkmDhBERPi41gnltfQpMWDnTnQbaCqk=
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -364,8 +367,9 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -377,6 +381,7 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -11,7 +11,7 @@ type LimitReachedError struct {
|
||||
Limit int64
|
||||
}
|
||||
|
||||
// Error implements error interface for LimitReachedError.
|
||||
// Error implements the error interface for LimitReachedError.
|
||||
//
|
||||
// TODO(a.garipov): Think about error string format.
|
||||
func (lre *LimitReachedError) Error() string {
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
package aghio
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLimitReader(t *testing.T) {
|
||||
testCases := []struct {
|
||||
want error
|
||||
name string
|
||||
n int64
|
||||
wantErrMsg string
|
||||
name string
|
||||
n int64
|
||||
}{{
|
||||
want: nil,
|
||||
name: "positive",
|
||||
n: 1,
|
||||
wantErrMsg: "",
|
||||
name: "positive",
|
||||
n: 1,
|
||||
}, {
|
||||
want: nil,
|
||||
name: "zero",
|
||||
n: 0,
|
||||
wantErrMsg: "",
|
||||
name: "zero",
|
||||
n: 0,
|
||||
}, {
|
||||
want: fmt.Errorf("aghio: invalid n in LimitReader: -1"),
|
||||
name: "negative",
|
||||
n: -1,
|
||||
wantErrMsg: "aghio: invalid n in LimitReader: -1",
|
||||
name: "negative",
|
||||
n: -1,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := LimitReader(nil, tc.n)
|
||||
assert.Equal(t, tc.want, err)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -73,36 +73,23 @@ func TestLimitedReader_Read(t *testing.T) {
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
readCloser := io.NopCloser(strings.NewReader(tc.rStr))
|
||||
lreader, err := LimitReader(readCloser, tc.limit)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, lreader)
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
readCloser := io.NopCloser(strings.NewReader(tc.rStr))
|
||||
buf := make([]byte, tc.limit+1)
|
||||
n, rerr := lreader.Read(buf)
|
||||
require.Equal(t, rerr, tc.err)
|
||||
|
||||
lreader, err := LimitReader(readCloser, tc.limit)
|
||||
require.NoError(t, err)
|
||||
|
||||
n, err := lreader.Read(buf)
|
||||
require.Equal(t, tc.err, err)
|
||||
assert.Equal(t, tc.want, n)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitedReader_LimitReachedError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
err error
|
||||
name string
|
||||
want string
|
||||
}{{
|
||||
err: &LimitReachedError{
|
||||
Limit: 0,
|
||||
},
|
||||
name: "simplest",
|
||||
want: "attempted to read more than 0 bytes",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, tc.err.Error())
|
||||
})
|
||||
}
|
||||
testutil.AssertErrorMsg(t, "attempted to read more than 0 bytes", &LimitReachedError{
|
||||
Limit: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,387 +0,0 @@
|
||||
package aghnet
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type onChangedT func()
|
||||
|
||||
// EtcHostsContainer - automatic DNS records
|
||||
//
|
||||
// TODO(e.burkov): Move the logic under interface. Refactor. Probably remove
|
||||
// the resolving logic.
|
||||
type EtcHostsContainer struct {
|
||||
// lock protects table and tableReverse.
|
||||
lock sync.RWMutex
|
||||
// table is the host-to-IPs map.
|
||||
table map[string][]net.IP
|
||||
// tableReverse is the IP-to-hosts map. The type of the values in the
|
||||
// map is []string.
|
||||
tableReverse *netutil.IPMap
|
||||
|
||||
hostsFn string // path to the main hosts-file
|
||||
hostsDirs []string // paths to OS-specific directories with hosts-files
|
||||
watcher *fsnotify.Watcher // file and directory watcher object
|
||||
|
||||
// onlyWritesChan used to contain only writing events from watcher.
|
||||
onlyWritesChan chan fsnotify.Event
|
||||
|
||||
onChanged onChangedT // notification to other modules
|
||||
}
|
||||
|
||||
// SetOnChanged - set callback function that will be called when the data is changed
|
||||
func (ehc *EtcHostsContainer) SetOnChanged(onChanged onChangedT) {
|
||||
ehc.onChanged = onChanged
|
||||
}
|
||||
|
||||
// Notify other modules
|
||||
func (ehc *EtcHostsContainer) notify() {
|
||||
if ehc.onChanged == nil {
|
||||
return
|
||||
}
|
||||
ehc.onChanged()
|
||||
}
|
||||
|
||||
// Init - initialize
|
||||
// hostsFn: Override default name for the hosts-file (optional)
|
||||
func (ehc *EtcHostsContainer) Init(hostsFn string) {
|
||||
ehc.table = make(map[string][]net.IP)
|
||||
ehc.onlyWritesChan = make(chan fsnotify.Event, 2)
|
||||
|
||||
ehc.hostsFn = "/etc/hosts"
|
||||
if runtime.GOOS == "windows" {
|
||||
ehc.hostsFn = os.ExpandEnv("$SystemRoot\\system32\\drivers\\etc\\hosts")
|
||||
}
|
||||
if len(hostsFn) != 0 {
|
||||
ehc.hostsFn = hostsFn
|
||||
}
|
||||
|
||||
if aghos.IsOpenWrt() {
|
||||
// OpenWrt: "/tmp/hosts/dhcp.cfg01411c".
|
||||
ehc.hostsDirs = append(ehc.hostsDirs, "/tmp/hosts")
|
||||
}
|
||||
|
||||
// Load hosts initially
|
||||
ehc.updateHosts()
|
||||
|
||||
var err error
|
||||
ehc.watcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Error("etchosts: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start - start module
|
||||
func (ehc *EtcHostsContainer) Start() {
|
||||
if ehc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("Start etchostscontainer module")
|
||||
|
||||
ehc.updateHosts()
|
||||
|
||||
if ehc.watcher != nil {
|
||||
go ehc.watcherLoop()
|
||||
|
||||
err := ehc.watcher.Add(ehc.hostsFn)
|
||||
if err != nil {
|
||||
log.Error("Error while initializing watcher for a file %s: %s", ehc.hostsFn, err)
|
||||
}
|
||||
|
||||
for _, dir := range ehc.hostsDirs {
|
||||
err = ehc.watcher.Add(dir)
|
||||
if err != nil {
|
||||
log.Error("Error while initializing watcher for a directory %s: %s", dir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close - close module
|
||||
func (ehc *EtcHostsContainer) Close() {
|
||||
if ehc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ehc.watcher != nil {
|
||||
_ = ehc.watcher.Close()
|
||||
}
|
||||
|
||||
// Don't close onlyWritesChan here and let onlyWrites close it after
|
||||
// watcher.Events is closed to prevent close races.
|
||||
}
|
||||
|
||||
// Process returns the list of IP addresses for the hostname or nil if nothing
|
||||
// found.
|
||||
func (ehc *EtcHostsContainer) Process(host string, qtype uint16) []net.IP {
|
||||
if qtype == dns.TypePTR {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ipsCopy []net.IP
|
||||
ehc.lock.RLock()
|
||||
defer ehc.lock.RUnlock()
|
||||
|
||||
if ips, ok := ehc.table[host]; ok {
|
||||
ipsCopy = make([]net.IP, len(ips))
|
||||
copy(ipsCopy, ips)
|
||||
}
|
||||
|
||||
log.Debug("etchosts: answer: %s -> %v", host, ipsCopy)
|
||||
return ipsCopy
|
||||
}
|
||||
|
||||
// ProcessReverse processes a PTR request. It returns nil if nothing is found.
|
||||
func (ehc *EtcHostsContainer) ProcessReverse(addr string, qtype uint16) (hosts []string) {
|
||||
if qtype != dns.TypePTR {
|
||||
return nil
|
||||
}
|
||||
|
||||
ip, err := netutil.IPFromReversedAddr(addr)
|
||||
if err != nil {
|
||||
log.Error("etchosts: reversed addr: %s", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
ehc.lock.RLock()
|
||||
defer ehc.lock.RUnlock()
|
||||
|
||||
v, ok := ehc.tableReverse.Get(ip)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
hosts, ok = v.([]string)
|
||||
if !ok {
|
||||
log.Error("etchosts: bad type %T in tableReverse for %s", v, ip)
|
||||
|
||||
return nil
|
||||
} else if len(hosts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug("etchosts: reverse-lookup: %s -> %s", addr, hosts)
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
// List returns an IP-to-hostnames table. The type of the values in the map is
|
||||
// []string. It is safe for concurrent use.
|
||||
func (ehc *EtcHostsContainer) List() (ipToHosts *netutil.IPMap) {
|
||||
ehc.lock.RLock()
|
||||
defer ehc.lock.RUnlock()
|
||||
|
||||
return ehc.tableReverse.ShallowClone()
|
||||
}
|
||||
|
||||
// update table
|
||||
func (ehc *EtcHostsContainer) updateTable(table map[string][]net.IP, host string, ipAddr net.IP) {
|
||||
ips, ok := table[host]
|
||||
if ok {
|
||||
for _, ip := range ips {
|
||||
if ip.Equal(ipAddr) {
|
||||
// IP already exists: don't add duplicates
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
ips = append(ips, ipAddr)
|
||||
table[host] = ips
|
||||
}
|
||||
} else {
|
||||
table[host] = []net.IP{ipAddr}
|
||||
ok = true
|
||||
}
|
||||
if ok {
|
||||
log.Debug("etchosts: added %s -> %s", ipAddr, host)
|
||||
}
|
||||
}
|
||||
|
||||
// updateTableRev updates the reverse address table.
|
||||
func (ehc *EtcHostsContainer) updateTableRev(tableRev *netutil.IPMap, newHost string, ip net.IP) {
|
||||
v, ok := tableRev.Get(ip)
|
||||
if !ok {
|
||||
tableRev.Set(ip, []string{newHost})
|
||||
log.Debug("etchosts: added reverse-address %s -> %s", ip, newHost)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hosts, _ := v.([]string)
|
||||
for _, host := range hosts {
|
||||
if host == newHost {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hosts = append(hosts, newHost)
|
||||
tableRev.Set(ip, hosts)
|
||||
|
||||
log.Debug("etchosts: added reverse-address %s -> %s", ip, newHost)
|
||||
}
|
||||
|
||||
// parseHostsLine parses hosts from the fields.
|
||||
func parseHostsLine(fields []string) (hosts []string) {
|
||||
for _, f := range fields {
|
||||
hashIdx := strings.IndexByte(f, '#')
|
||||
if hashIdx == 0 {
|
||||
// The rest of the fields are a part of the comment.
|
||||
// Skip immediately.
|
||||
return
|
||||
} else if hashIdx > 0 {
|
||||
// Only a part of the field is a comment.
|
||||
hosts = append(hosts, f[:hashIdx])
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
hosts = append(hosts, f)
|
||||
}
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
// load reads IP-hostname pairs from the hosts file. Multiple hostnames per
|
||||
// line for one IP are supported.
|
||||
func (ehc *EtcHostsContainer) load(
|
||||
table map[string][]net.IP,
|
||||
tableRev *netutil.IPMap,
|
||||
fn string,
|
||||
) {
|
||||
f, err := os.Open(fn)
|
||||
if err != nil {
|
||||
log.Error("etchosts: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
derr := f.Close()
|
||||
if derr != nil {
|
||||
log.Error("etchosts: closing file: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Debug("etchosts: loading hosts from file %s", fn)
|
||||
|
||||
s := bufio.NewScanner(f)
|
||||
for s.Scan() {
|
||||
line := strings.TrimSpace(s.Text())
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
ip := net.ParseIP(fields[0])
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
hosts := parseHostsLine(fields[1:])
|
||||
for _, host := range hosts {
|
||||
ehc.updateTable(table, host, ip)
|
||||
ehc.updateTableRev(tableRev, host, ip)
|
||||
}
|
||||
}
|
||||
|
||||
err = s.Err()
|
||||
if err != nil {
|
||||
log.Error("etchosts: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// onlyWrites is a filter for (*fsnotify.Watcher).Events.
|
||||
func (ehc *EtcHostsContainer) onlyWrites() {
|
||||
for event := range ehc.watcher.Events {
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
ehc.onlyWritesChan <- event
|
||||
}
|
||||
}
|
||||
|
||||
close(ehc.onlyWritesChan)
|
||||
}
|
||||
|
||||
// Receive notifications from fsnotify package
|
||||
func (ehc *EtcHostsContainer) watcherLoop() {
|
||||
go ehc.onlyWrites()
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-ehc.onlyWritesChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Assume that we sometimes have the same event occurred
|
||||
// several times.
|
||||
repeat := true
|
||||
for repeat {
|
||||
select {
|
||||
case _, ok = <-ehc.onlyWritesChan:
|
||||
repeat = ok
|
||||
default:
|
||||
repeat = false
|
||||
}
|
||||
}
|
||||
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
log.Debug("etchosts: modified: %s", event.Name)
|
||||
ehc.updateHosts()
|
||||
}
|
||||
|
||||
case err, ok := <-ehc.watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Error("etchosts: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateHosts - loads system hosts
|
||||
func (ehc *EtcHostsContainer) updateHosts() {
|
||||
table := make(map[string][]net.IP)
|
||||
tableRev := netutil.NewIPMap(0)
|
||||
|
||||
ehc.load(table, tableRev, ehc.hostsFn)
|
||||
|
||||
for _, dir := range ehc.hostsDirs {
|
||||
des, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
log.Error("etchosts: Opening directory: %q: %s", dir, err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, de := range des {
|
||||
ehc.load(table, tableRev, filepath.Join(dir, de.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
func() {
|
||||
ehc.lock.Lock()
|
||||
defer ehc.lock.Unlock()
|
||||
|
||||
ehc.table = table
|
||||
ehc.tableReverse = tableRev
|
||||
}()
|
||||
|
||||
ehc.notify()
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package aghnet
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
aghtest.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
func prepareTestFile(t *testing.T) (f *os.File) {
|
||||
t.Helper()
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
f, err := os.CreateTemp(dir, "")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, f)
|
||||
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, f.Close())
|
||||
})
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func assertWriting(t *testing.T, f *os.File, strs ...string) {
|
||||
t.Helper()
|
||||
|
||||
for _, str := range strs {
|
||||
n, err := f.WriteString(str)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, n, len(str))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtcHostsContainerResolution(t *testing.T) {
|
||||
ehc := &EtcHostsContainer{}
|
||||
|
||||
f := prepareTestFile(t)
|
||||
|
||||
assertWriting(t, f,
|
||||
" 127.0.0.1 host localhost # comment \n",
|
||||
" ::1 localhost#comment \n",
|
||||
)
|
||||
ehc.Init(f.Name())
|
||||
|
||||
t.Run("existing_host", func(t *testing.T) {
|
||||
ips := ehc.Process("localhost", dns.TypeA)
|
||||
require.Len(t, ips, 1)
|
||||
assert.Equal(t, net.IPv4(127, 0, 0, 1), ips[0])
|
||||
})
|
||||
|
||||
t.Run("unknown_host", func(t *testing.T) {
|
||||
ips := ehc.Process("newhost", dns.TypeA)
|
||||
assert.Nil(t, ips)
|
||||
|
||||
// Comment.
|
||||
ips = ehc.Process("comment", dns.TypeA)
|
||||
assert.Nil(t, ips)
|
||||
})
|
||||
|
||||
t.Run("hosts_file", func(t *testing.T) {
|
||||
names, ok := ehc.List().Get(net.IP{127, 0, 0, 1})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []string{"host", "localhost"}, names)
|
||||
})
|
||||
|
||||
t.Run("ptr", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
wantIP string
|
||||
wantHost string
|
||||
wantLen int
|
||||
}{
|
||||
{wantIP: "127.0.0.1", wantHost: "host", wantLen: 2},
|
||||
{wantIP: "::1", wantHost: "localhost", wantLen: 1},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
a, err := dns.ReverseAddr(tc.wantIP)
|
||||
require.NoError(t, err)
|
||||
|
||||
a = strings.TrimSuffix(a, ".")
|
||||
hosts := ehc.ProcessReverse(a, dns.TypePTR)
|
||||
require.Len(t, hosts, tc.wantLen)
|
||||
assert.Equal(t, tc.wantHost, hosts[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEtcHostsContainerFSNotify(t *testing.T) {
|
||||
ehc := &EtcHostsContainer{}
|
||||
|
||||
f := prepareTestFile(t)
|
||||
|
||||
assertWriting(t, f, " 127.0.0.1 host localhost \n")
|
||||
ehc.Init(f.Name())
|
||||
|
||||
t.Run("unknown_host", func(t *testing.T) {
|
||||
ips := ehc.Process("newhost", dns.TypeA)
|
||||
assert.Nil(t, ips)
|
||||
})
|
||||
|
||||
// Start monitoring for changes.
|
||||
ehc.Start()
|
||||
t.Cleanup(ehc.Close)
|
||||
|
||||
assertWriting(t, f, "127.0.0.2 newhost\n")
|
||||
require.NoError(t, f.Sync())
|
||||
|
||||
// Wait until fsnotify has triggered and processed the file-modification
|
||||
// event.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
t.Run("notified", func(t *testing.T) {
|
||||
ips := ehc.Process("newhost", dns.TypeA)
|
||||
assert.NotNil(t, ips)
|
||||
require.Len(t, ips, 1)
|
||||
assert.True(t, net.IP{127, 0, 0, 2}.Equal(ips[0]))
|
||||
})
|
||||
}
|
||||
343
internal/aghnet/hostscontainer.go
Normal file
343
internal/aghnet/hostscontainer.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package aghnet
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/AdguardTeam/urlfilter"
|
||||
"github.com/AdguardTeam/urlfilter/filterlist"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// DefaultHostsPaths returns the slice of paths default for the operating system
|
||||
// to files and directories which are containing the hosts database. The result
|
||||
// is intended to use within fs.FS so the initial slash is omitted.
|
||||
func DefaultHostsPaths() (paths []string) {
|
||||
return defaultHostsPaths()
|
||||
}
|
||||
|
||||
// hostsContainerPref is a prefix for logging and wrapping errors in
|
||||
// HostsContainer's methods.
|
||||
const hostsContainerPref = "hosts container"
|
||||
|
||||
// HostsContainer stores the relevant hosts database provided by the OS and
|
||||
// processes both A/AAAA and PTR DNS requests for those.
|
||||
type HostsContainer struct {
|
||||
// engLock protects rulesStrg and engine.
|
||||
engLock *sync.RWMutex
|
||||
|
||||
// rulesStrg stores the rules obtained from the hosts' file.
|
||||
rulesStrg *filterlist.RuleStorage
|
||||
// engine serves rulesStrg.
|
||||
engine *urlfilter.DNSEngine
|
||||
|
||||
// Updates is the channel for receiving updated hosts. The receivable map's
|
||||
// values has a type of slice of strings.
|
||||
updates chan *netutil.IPMap
|
||||
|
||||
// fsys is the working file system to read hosts files from.
|
||||
fsys fs.FS
|
||||
|
||||
// w tracks the changes in specified files and directories.
|
||||
w aghos.FSWatcher
|
||||
// patterns stores specified paths in the fs.Glob-compatible form.
|
||||
patterns []string
|
||||
}
|
||||
|
||||
// errNoPaths is returned when there are no paths to watch passed to the
|
||||
// HostsContainer.
|
||||
const errNoPaths errors.Error = "hosts paths are empty"
|
||||
|
||||
// NewHostsContainer creates a container of hosts, that watches the paths with
|
||||
// w. paths shouldn't be empty and each of them should locate either a file or
|
||||
// a directory in fsys. fsys and w must be non-nil.
|
||||
func NewHostsContainer(
|
||||
fsys fs.FS,
|
||||
w aghos.FSWatcher,
|
||||
paths ...string,
|
||||
) (hc *HostsContainer, err error) {
|
||||
defer func() { err = errors.Annotate(err, "%s: %w", hostsContainerPref) }()
|
||||
|
||||
if len(paths) == 0 {
|
||||
return nil, errNoPaths
|
||||
}
|
||||
|
||||
patterns, err := pathsToPatterns(fsys, paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hc = &HostsContainer{
|
||||
engLock: &sync.RWMutex{},
|
||||
updates: make(chan *netutil.IPMap, 1),
|
||||
fsys: fsys,
|
||||
w: w,
|
||||
patterns: patterns,
|
||||
}
|
||||
|
||||
log.Debug("%s: starting", hostsContainerPref)
|
||||
|
||||
// Load initially.
|
||||
if err = hc.refresh(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
err = w.Add(p)
|
||||
if err == nil {
|
||||
continue
|
||||
} else if errors.Is(err, fs.ErrNotExist) {
|
||||
log.Debug("%s: file %q expected to exist but doesn't", hostsContainerPref, p)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("adding path: %w", err)
|
||||
}
|
||||
|
||||
go hc.handleEvents()
|
||||
|
||||
return hc, nil
|
||||
}
|
||||
|
||||
// MatchRequest is the request processing method to resolve hostnames and
|
||||
// addresses from the operating system's hosts files. Any request not of A/AAAA
|
||||
// or PTR type will return with an empty result. It's safe for concurrent use.
|
||||
func (hc *HostsContainer) MatchRequest(
|
||||
req urlfilter.DNSRequest,
|
||||
) (res urlfilter.DNSResult, ok bool) {
|
||||
switch req.DNSType {
|
||||
case dns.TypeA, dns.TypeAAAA, dns.TypePTR:
|
||||
log.Debug("%s: handling the request", hostsContainerPref)
|
||||
default:
|
||||
return urlfilter.DNSResult{}, false
|
||||
}
|
||||
|
||||
hc.engLock.RLock()
|
||||
defer hc.engLock.RUnlock()
|
||||
|
||||
return hc.engine.MatchRequest(req)
|
||||
}
|
||||
|
||||
// Close implements the io.Closer interface for *HostsContainer.
|
||||
func (hc *HostsContainer) Close() (err error) {
|
||||
log.Debug("%s: closing hosts container", hostsContainerPref)
|
||||
|
||||
return errors.Annotate(hc.w.Close(), "%s: closing: %w", hostsContainerPref)
|
||||
}
|
||||
|
||||
// Upd returns the channel into which the updates are sent.
|
||||
func (hc *HostsContainer) Upd() (updates <-chan *netutil.IPMap) {
|
||||
return hc.updates
|
||||
}
|
||||
|
||||
// pathsToPatterns converts paths into patterns compatible with fs.Glob.
|
||||
func pathsToPatterns(fsys fs.FS, paths []string) (patterns []string, err error) {
|
||||
for i, p := range paths {
|
||||
var fi fs.FileInfo
|
||||
if fi, err = fs.Stat(fsys, p); err != nil {
|
||||
return nil, fmt.Errorf("%q at index %d: %w", p, i, err)
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
p = path.Join(p, "*")
|
||||
}
|
||||
|
||||
patterns = append(patterns, p)
|
||||
}
|
||||
|
||||
return patterns, nil
|
||||
}
|
||||
|
||||
// handleEvents concurrently handles the events. It closes the update channel
|
||||
// of HostsContainer when finishes. Used to be called within a goroutine.
|
||||
func (hc *HostsContainer) handleEvents() {
|
||||
defer log.OnPanic(fmt.Sprintf("%s: handling events", hostsContainerPref))
|
||||
|
||||
defer close(hc.updates)
|
||||
|
||||
for range hc.w.Events() {
|
||||
if err := hc.refresh(); err != nil {
|
||||
log.Error("%s: %s", hostsContainerPref, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hostsParser is a helper type to parse rules from the operating system's hosts
|
||||
// file.
|
||||
type hostsParser struct {
|
||||
// rules builds the resulting rules list content.
|
||||
rules *strings.Builder
|
||||
|
||||
// table stores only the unique IP-hostname pairs. It's also sent to the
|
||||
// updates channel afterwards.
|
||||
table *netutil.IPMap
|
||||
}
|
||||
|
||||
// parseHostsFile is a aghtest.FileWalker for parsing the files with hosts
|
||||
// syntax. It never signs to stop the walking.
|
||||
//
|
||||
// See man hosts(5).
|
||||
func (hp hostsParser) parseHostsFile(
|
||||
r io.Reader,
|
||||
) (patterns []string, cont bool, err error) {
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
ip, hosts := hp.parseLine(s.Text())
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
hp.addPair(ip, host)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, true, s.Err()
|
||||
}
|
||||
|
||||
// parseLine parses the line having the hosts syntax ignoring invalid ones.
|
||||
func (hp hostsParser) parseLine(line string) (ip net.IP, hosts []string) {
|
||||
line = strings.TrimSpace(line)
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if ip = net.ParseIP(fields[0]); ip == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
loop:
|
||||
for _, f := range fields[1:] {
|
||||
switch hashIdx := strings.IndexByte(f, '#'); hashIdx {
|
||||
case 0:
|
||||
// The rest of the fields are a part of the comment so skip
|
||||
// immediately.
|
||||
break loop
|
||||
case -1:
|
||||
hosts = append(hosts, f)
|
||||
default:
|
||||
// Only a part of the field is a comment.
|
||||
hosts = append(hosts, f[:hashIdx])
|
||||
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
return ip, hosts
|
||||
}
|
||||
|
||||
// add returns true if the pair of ip and host wasn't added to the hp before.
|
||||
func (hp hostsParser) add(ip net.IP, host string) (added bool) {
|
||||
v, ok := hp.table.Get(ip)
|
||||
hosts, _ := v.([]string)
|
||||
if ok && stringutil.InSlice(hosts, host) {
|
||||
return false
|
||||
}
|
||||
|
||||
hp.table.Set(ip, append(hosts, host))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// addPair puts the pair of ip and host to the rules builder if needed.
|
||||
func (hp hostsParser) addPair(ip net.IP, host string) {
|
||||
arpa, err := netutil.IPToReversedAddr(ip)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !hp.add(ip, host) {
|
||||
return
|
||||
}
|
||||
|
||||
qtype := "AAAA"
|
||||
if ip.To4() != nil {
|
||||
// Assume the validation of the IP address is performed already.
|
||||
qtype = "A"
|
||||
}
|
||||
|
||||
stringutil.WriteToBuilder(
|
||||
hp.rules,
|
||||
"||",
|
||||
host,
|
||||
"^$dnsrewrite=NOERROR;",
|
||||
qtype,
|
||||
";",
|
||||
ip.String(),
|
||||
"\n",
|
||||
"||",
|
||||
arpa,
|
||||
"^$dnsrewrite=NOERROR;PTR;",
|
||||
dns.Fqdn(host),
|
||||
"\n",
|
||||
)
|
||||
|
||||
log.Debug("%s: added ip-host pair %q/%q", hostsContainerPref, ip, host)
|
||||
}
|
||||
|
||||
// sendUpd tries to send the parsed data to the ch.
|
||||
func (hp hostsParser) sendUpd(ch chan *netutil.IPMap) {
|
||||
log.Debug("%s: sending upd", hostsContainerPref)
|
||||
select {
|
||||
case ch <- hp.table:
|
||||
// Updates are delivered. Go on.
|
||||
default:
|
||||
log.Debug("%s: the buffer is full", hostsContainerPref)
|
||||
}
|
||||
}
|
||||
|
||||
// newStrg creates a new rules storage from parsed data.
|
||||
func (hp hostsParser) newStrg() (s *filterlist.RuleStorage, err error) {
|
||||
return filterlist.NewRuleStorage([]filterlist.RuleList{&filterlist.StringRuleList{
|
||||
ID: 1,
|
||||
RulesText: hp.rules.String(),
|
||||
IgnoreCosmetic: true,
|
||||
}})
|
||||
}
|
||||
|
||||
// refresh gets the data from specified files and propagates the updates.
|
||||
func (hc *HostsContainer) refresh() (err error) {
|
||||
log.Debug("%s: refreshing", hostsContainerPref)
|
||||
|
||||
hp := hostsParser{
|
||||
rules: &strings.Builder{},
|
||||
table: netutil.NewIPMap(0),
|
||||
}
|
||||
|
||||
_, err = aghos.FileWalker(hp.parseHostsFile).Walk(hc.fsys, hc.patterns...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("updating: %w", err)
|
||||
}
|
||||
|
||||
defer hp.sendUpd(hc.updates)
|
||||
|
||||
var rulesStrg *filterlist.RuleStorage
|
||||
if rulesStrg, err = hp.newStrg(); err != nil {
|
||||
return fmt.Errorf("initializing rules storage: %w", err)
|
||||
}
|
||||
|
||||
hc.resetEng(rulesStrg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hc *HostsContainer) resetEng(rulesStrg *filterlist.RuleStorage) {
|
||||
hc.engLock.Lock()
|
||||
defer hc.engLock.Unlock()
|
||||
|
||||
hc.rulesStrg = rulesStrg
|
||||
hc.engine = urlfilter.NewDNSEngine(hc.rulesStrg)
|
||||
}
|
||||
18
internal/aghnet/hostscontainer_linux.go
Normal file
18
internal/aghnet/hostscontainer_linux.go
Normal file
@@ -0,0 +1,18 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package aghnet
|
||||
|
||||
import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
)
|
||||
|
||||
func defaultHostsPaths() (paths []string) {
|
||||
paths = []string{"etc/hosts"}
|
||||
|
||||
if aghos.IsOpenWrt() {
|
||||
paths = append(paths, "tmp/hosts")
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
8
internal/aghnet/hostscontainer_others.go
Normal file
8
internal/aghnet/hostscontainer_others.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build !(windows || linux)
|
||||
// +build !windows,!linux
|
||||
|
||||
package aghnet
|
||||
|
||||
func defaultHostsPaths() (paths []string) {
|
||||
return []string{"etc/hosts"}
|
||||
}
|
||||
504
internal/aghnet/hostscontainer_test.go
Normal file
504
internal/aghnet/hostscontainer_test.go
Normal file
@@ -0,0 +1,504 @@
|
||||
package aghnet
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net"
|
||||
"path"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/urlfilter"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
nl = "\n"
|
||||
sp = " "
|
||||
)
|
||||
|
||||
const closeCalled errors.Error = "close method called"
|
||||
|
||||
// fsWatcherOnCloseStub is a stub implementation of the Close method of
|
||||
// aghos.FSWatcher.
|
||||
func fsWatcherOnCloseStub() (err error) {
|
||||
return closeCalled
|
||||
}
|
||||
|
||||
func TestNewHostsContainer(t *testing.T) {
|
||||
const dirname = "dir"
|
||||
const filename = "file1"
|
||||
|
||||
p := path.Join(dirname, filename)
|
||||
|
||||
testFS := fstest.MapFS{
|
||||
p: &fstest.MapFile{Data: []byte("127.0.0.1 localhost")},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
paths []string
|
||||
wantErr error
|
||||
wantPatterns []string
|
||||
}{{
|
||||
name: "one_file",
|
||||
paths: []string{p},
|
||||
wantErr: nil,
|
||||
wantPatterns: []string{p},
|
||||
}, {
|
||||
name: "no_files",
|
||||
paths: []string{},
|
||||
wantErr: errNoPaths,
|
||||
wantPatterns: nil,
|
||||
}, {
|
||||
name: "non-existent_file",
|
||||
paths: []string{path.Join(dirname, filename+"2")},
|
||||
wantErr: fs.ErrNotExist,
|
||||
wantPatterns: nil,
|
||||
}, {
|
||||
name: "whole_dir",
|
||||
paths: []string{dirname},
|
||||
wantErr: nil,
|
||||
wantPatterns: []string{path.Join(dirname, "*")},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
onAdd := func(name string) (err error) {
|
||||
assert.Contains(t, tc.paths, name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var eventsCalledCounter uint32
|
||||
eventsCh := make(chan struct{})
|
||||
onEvents := func() (e <-chan struct{}) {
|
||||
assert.Equal(t, uint32(1), atomic.AddUint32(&eventsCalledCounter, 1))
|
||||
|
||||
return eventsCh
|
||||
}
|
||||
|
||||
hc, err := NewHostsContainer(testFS, &aghtest.FSWatcher{
|
||||
OnEvents: onEvents,
|
||||
OnAdd: onAdd,
|
||||
OnClose: fsWatcherOnCloseStub,
|
||||
}, tc.paths...)
|
||||
if tc.wantErr != nil {
|
||||
require.ErrorIs(t, err, tc.wantErr)
|
||||
|
||||
assert.Nil(t, hc)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.ErrorIs(t, hc.Close(), closeCalled)
|
||||
})
|
||||
|
||||
require.NotNil(t, hc)
|
||||
|
||||
assert.Equal(t, tc.wantPatterns, hc.patterns)
|
||||
assert.NotNil(t, <-hc.Upd())
|
||||
|
||||
eventsCh <- struct{}{}
|
||||
assert.Equal(t, uint32(1), atomic.LoadUint32(&eventsCalledCounter))
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("nil_fs", func(t *testing.T) {
|
||||
require.Panics(t, func() {
|
||||
_, _ = NewHostsContainer(nil, &aghtest.FSWatcher{
|
||||
// Those shouldn't panic.
|
||||
OnEvents: func() (e <-chan struct{}) { return nil },
|
||||
OnAdd: func(name string) (err error) { return nil },
|
||||
OnClose: func() (err error) { return nil },
|
||||
}, p)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("nil_watcher", func(t *testing.T) {
|
||||
require.Panics(t, func() {
|
||||
_, _ = NewHostsContainer(testFS, nil, p)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("err_watcher", func(t *testing.T) {
|
||||
const errOnAdd errors.Error = "error"
|
||||
|
||||
errWatcher := &aghtest.FSWatcher{
|
||||
OnEvents: func() (e <-chan struct{}) { panic("not implemented") },
|
||||
OnAdd: func(name string) (err error) { return errOnAdd },
|
||||
OnClose: func() (err error) { panic("not implemented") },
|
||||
}
|
||||
|
||||
hc, err := NewHostsContainer(testFS, errWatcher, p)
|
||||
require.ErrorIs(t, err, errOnAdd)
|
||||
|
||||
assert.Nil(t, hc)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHostsContainer_Refresh(t *testing.T) {
|
||||
knownIP := net.IP{127, 0, 0, 1}
|
||||
|
||||
const knownHost = "localhost"
|
||||
const knownAlias = "hocallost"
|
||||
|
||||
const dirname = "dir"
|
||||
const filename1 = "file1"
|
||||
const filename2 = "file2"
|
||||
|
||||
p1 := path.Join(dirname, filename1)
|
||||
p2 := path.Join(dirname, filename2)
|
||||
|
||||
testFS := fstest.MapFS{
|
||||
p1: &fstest.MapFile{
|
||||
Data: []byte(strings.Join([]string{knownIP.String(), knownHost}, sp) + nl),
|
||||
},
|
||||
}
|
||||
|
||||
eventsCh := make(chan struct{}, 1)
|
||||
t.Cleanup(func() { close(eventsCh) })
|
||||
|
||||
w := &aghtest.FSWatcher{
|
||||
OnEvents: func() (e <-chan struct{}) { return eventsCh },
|
||||
OnAdd: func(name string) (err error) {
|
||||
assert.Equal(t, dirname, name)
|
||||
|
||||
return nil
|
||||
},
|
||||
OnClose: fsWatcherOnCloseStub,
|
||||
}
|
||||
|
||||
hc, err := NewHostsContainer(testFS, w, dirname)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.ErrorIs(t, hc.Close(), closeCalled) })
|
||||
|
||||
checkRefresh := func(t *testing.T, wantHosts []string) {
|
||||
upd, ok := <-hc.Upd()
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, upd)
|
||||
|
||||
assert.Equal(t, 1, upd.Len())
|
||||
|
||||
v, ok := upd.Get(knownIP)
|
||||
require.True(t, ok)
|
||||
|
||||
var hosts []string
|
||||
hosts, ok = v.([]string)
|
||||
require.True(t, ok)
|
||||
require.Len(t, hosts, len(wantHosts))
|
||||
|
||||
assert.Equal(t, wantHosts, hosts)
|
||||
}
|
||||
|
||||
t.Run("initial_refresh", func(t *testing.T) {
|
||||
checkRefresh(t, []string{knownHost})
|
||||
})
|
||||
|
||||
testFS[p2] = &fstest.MapFile{
|
||||
Data: []byte(strings.Join([]string{knownIP.String(), knownAlias}, sp) + nl),
|
||||
}
|
||||
|
||||
eventsCh <- struct{}{}
|
||||
|
||||
t.Run("second_refresh", func(t *testing.T) {
|
||||
checkRefresh(t, []string{knownHost, knownAlias})
|
||||
})
|
||||
}
|
||||
|
||||
func TestHostsContainer_MatchRequest(t *testing.T) {
|
||||
var (
|
||||
ip4 = net.IP{127, 0, 0, 1}
|
||||
ip6 = net.IP{
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 1,
|
||||
}
|
||||
|
||||
hostname4 = "localhost"
|
||||
hostname6 = "localhostv6"
|
||||
hostname4a = "abcd"
|
||||
|
||||
reversed4, _ = netutil.IPToReversedAddr(ip4)
|
||||
reversed6, _ = netutil.IPToReversedAddr(ip6)
|
||||
)
|
||||
|
||||
const filename = "file1"
|
||||
|
||||
gsfs := fstest.MapFS{
|
||||
filename: &fstest.MapFile{Data: []byte(
|
||||
strings.Join([]string{ip4.String(), hostname4, hostname4a}, sp) + nl +
|
||||
strings.Join([]string{ip6.String(), hostname6}, sp) + nl +
|
||||
strings.Join([]string{"256.256.256.256", "fakebroadcast"}, sp) + nl,
|
||||
)},
|
||||
}
|
||||
|
||||
hc, err := NewHostsContainer(gsfs, &aghtest.FSWatcher{
|
||||
OnEvents: func() (e <-chan struct{}) { panic("not implemented") },
|
||||
OnAdd: func(name string) (err error) {
|
||||
assert.Equal(t, filename, name)
|
||||
|
||||
return nil
|
||||
},
|
||||
OnClose: fsWatcherOnCloseStub,
|
||||
}, filename)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.ErrorIs(t, hc.Close(), closeCalled) })
|
||||
|
||||
testCase := []struct {
|
||||
name string
|
||||
want interface{}
|
||||
req urlfilter.DNSRequest
|
||||
}{{
|
||||
name: "a",
|
||||
want: ip4.To16(),
|
||||
req: urlfilter.DNSRequest{
|
||||
Hostname: hostname4,
|
||||
DNSType: dns.TypeA,
|
||||
},
|
||||
}, {
|
||||
name: "aaaa",
|
||||
want: ip6,
|
||||
req: urlfilter.DNSRequest{
|
||||
Hostname: hostname6,
|
||||
DNSType: dns.TypeA,
|
||||
},
|
||||
}, {
|
||||
name: "ptr",
|
||||
want: dns.Fqdn(hostname4),
|
||||
req: urlfilter.DNSRequest{
|
||||
Hostname: reversed4,
|
||||
DNSType: dns.TypePTR,
|
||||
},
|
||||
}, {
|
||||
name: "ptr_v6",
|
||||
want: dns.Fqdn(hostname6),
|
||||
req: urlfilter.DNSRequest{
|
||||
Hostname: reversed6,
|
||||
DNSType: dns.TypePTR,
|
||||
},
|
||||
}, {
|
||||
name: "a_alias",
|
||||
want: ip4.To16(),
|
||||
req: urlfilter.DNSRequest{
|
||||
Hostname: hostname4a,
|
||||
DNSType: dns.TypeA,
|
||||
},
|
||||
}}
|
||||
|
||||
for _, tc := range testCase {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, ok := hc.MatchRequest(tc.req)
|
||||
require.False(t, ok)
|
||||
|
||||
assert.Equal(t, tc.want, res.DNSRewrites()[0].DNSRewrite.Value)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("cname", func(t *testing.T) {
|
||||
res, ok := hc.MatchRequest(urlfilter.DNSRequest{
|
||||
Hostname: hostname4,
|
||||
DNSType: dns.TypeCNAME,
|
||||
})
|
||||
require.False(t, ok)
|
||||
|
||||
assert.Empty(t, res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHostsContainer_PathsToPatterns(t *testing.T) {
|
||||
const (
|
||||
dir0 = "dir"
|
||||
dir1 = "dir_1"
|
||||
fn1 = "file_1"
|
||||
fn2 = "file_2"
|
||||
fn3 = "file_3"
|
||||
fn4 = "file_4"
|
||||
)
|
||||
|
||||
fp1 := path.Join(dir0, fn1)
|
||||
fp2 := path.Join(dir0, fn2)
|
||||
fp3 := path.Join(dir0, dir1, fn3)
|
||||
|
||||
gsfs := fstest.MapFS{
|
||||
fp1: &fstest.MapFile{Data: []byte{1}},
|
||||
fp2: &fstest.MapFile{Data: []byte{2}},
|
||||
fp3: &fstest.MapFile{Data: []byte{3}},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantErr error
|
||||
want []string
|
||||
paths []string
|
||||
}{{
|
||||
name: "no_paths",
|
||||
wantErr: nil,
|
||||
want: nil,
|
||||
paths: nil,
|
||||
}, {
|
||||
name: "single_file",
|
||||
wantErr: nil,
|
||||
want: []string{fp1},
|
||||
paths: []string{fp1},
|
||||
}, {
|
||||
name: "several_files",
|
||||
wantErr: nil,
|
||||
want: []string{fp1, fp2},
|
||||
paths: []string{fp1, fp2},
|
||||
}, {
|
||||
name: "whole_dir",
|
||||
wantErr: nil,
|
||||
want: []string{path.Join(dir0, "*")},
|
||||
paths: []string{dir0},
|
||||
}, {
|
||||
name: "file_and_dir",
|
||||
wantErr: nil,
|
||||
want: []string{fp1, path.Join(dir0, dir1, "*")},
|
||||
paths: []string{fp1, path.Join(dir0, dir1)},
|
||||
}, {
|
||||
name: "non-existing",
|
||||
wantErr: fs.ErrNotExist,
|
||||
want: nil,
|
||||
paths: []string{path.Join(dir0, "file_3")},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
patterns, err := pathsToPatterns(gsfs, tc.paths)
|
||||
if tc.wantErr != nil {
|
||||
assert.ErrorIs(t, err, tc.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.want, patterns)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueRules_AddPair(t *testing.T) {
|
||||
knownIP := net.IP{1, 2, 3, 4}
|
||||
|
||||
const knownHost = "host1"
|
||||
|
||||
ipToHost := netutil.NewIPMap(0)
|
||||
ipToHost.Set(knownIP, []string{knownHost})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
host string
|
||||
wantRules string
|
||||
ip net.IP
|
||||
}{{
|
||||
name: "new_one",
|
||||
host: "host2",
|
||||
wantRules: "||host2^$dnsrewrite=NOERROR;A;1.2.3.4\n" +
|
||||
"||4.3.2.1.in-addr.arpa^$dnsrewrite=NOERROR;PTR;host2.\n",
|
||||
ip: knownIP,
|
||||
}, {
|
||||
name: "existing_one",
|
||||
host: knownHost,
|
||||
wantRules: "",
|
||||
ip: knownIP,
|
||||
}, {
|
||||
name: "new_ip",
|
||||
host: knownHost,
|
||||
wantRules: "||" + knownHost + "^$dnsrewrite=NOERROR;A;1.2.3.5\n" +
|
||||
"||5.3.2.1.in-addr.arpa^$dnsrewrite=NOERROR;PTR;" + knownHost + ".\n",
|
||||
ip: net.IP{1, 2, 3, 5},
|
||||
}, {
|
||||
name: "bad_ip",
|
||||
host: knownHost,
|
||||
wantRules: "",
|
||||
ip: net.IP{1, 2, 3, 4, 5},
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
hp := hostsParser{
|
||||
rules: &strings.Builder{},
|
||||
table: ipToHost.ShallowClone(),
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
hp.addPair(tc.ip, tc.host)
|
||||
assert.Equal(t, tc.wantRules, hp.rules.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueRules_ParseLine(t *testing.T) {
|
||||
const (
|
||||
hostname = "localhost"
|
||||
alias = "hocallost"
|
||||
)
|
||||
|
||||
knownIP := net.IP{127, 0, 0, 1}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
line string
|
||||
wantIP net.IP
|
||||
wantHosts []string
|
||||
}{{
|
||||
name: "simple",
|
||||
line: strings.Join([]string{knownIP.String(), hostname}, sp),
|
||||
wantIP: knownIP,
|
||||
wantHosts: []string{"localhost"},
|
||||
}, {
|
||||
name: "aliases",
|
||||
line: strings.Join([]string{knownIP.String(), hostname, alias}, sp),
|
||||
wantIP: knownIP,
|
||||
wantHosts: []string{"localhost", "hocallost"},
|
||||
}, {
|
||||
name: "invalid_line",
|
||||
line: knownIP.String(),
|
||||
wantIP: nil,
|
||||
wantHosts: nil,
|
||||
}, {
|
||||
name: "invalid_line_hostname",
|
||||
line: strings.Join([]string{knownIP.String(), "#" + hostname}, sp),
|
||||
wantIP: knownIP,
|
||||
wantHosts: nil,
|
||||
}, {
|
||||
name: "commented_aliases",
|
||||
line: strings.Join([]string{knownIP.String(), hostname, "#" + alias}, sp),
|
||||
wantIP: knownIP,
|
||||
wantHosts: []string{"localhost"},
|
||||
}, {
|
||||
name: "whole_comment",
|
||||
line: strings.Join([]string{"#", knownIP.String(), hostname}, sp),
|
||||
wantIP: nil,
|
||||
wantHosts: nil,
|
||||
}, {
|
||||
name: "partial_comment",
|
||||
line: strings.Join([]string{knownIP.String(), hostname[:4] + "#" + hostname[4:]}, sp),
|
||||
wantIP: knownIP,
|
||||
wantHosts: []string{hostname[:4]},
|
||||
}, {
|
||||
name: "empty",
|
||||
line: ``,
|
||||
wantIP: nil,
|
||||
wantHosts: nil,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
hp := hostsParser{}
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ip, hosts := hp.parseLine(tc.line)
|
||||
assert.True(t, tc.wantIP.Equal(ip))
|
||||
assert.Equal(t, tc.wantHosts, hosts)
|
||||
})
|
||||
}
|
||||
}
|
||||
33
internal/aghnet/hostscontainer_windows.go
Normal file
33
internal/aghnet/hostscontainer_windows.go
Normal file
@@ -0,0 +1,33 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package aghnet
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func defaultHostsPaths() (paths []string) {
|
||||
sysDir, err := windows.GetSystemDirectory()
|
||||
if err != nil {
|
||||
log.Error("getting system directory: %s", err)
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Split all the elements of the path to join them afterwards. This is
|
||||
// needed to make the Windows-specific path string returned by
|
||||
// windows.GetSystemDirectory to be compatible with fs.FS.
|
||||
pathElems := strings.Split(sysDir, string(os.PathSeparator))
|
||||
if len(pathElems) > 0 && pathElems[0] == filepath.VolumeName(sysDir) {
|
||||
pathElems = pathElems[1:]
|
||||
}
|
||||
|
||||
return []string{path.Join(append(pathElems, "drivers/etc/hosts")...)}
|
||||
}
|
||||
@@ -18,9 +18,11 @@ func canBindPrivilegedPorts() (can bool, err error) {
|
||||
}
|
||||
|
||||
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
|
||||
const filename = "/etc/rc.conf"
|
||||
const rcConfFilename = "etc/rc.conf"
|
||||
|
||||
return aghos.FileWalker(interfaceName(ifaceName).rcConfStaticConfig).Walk(filename)
|
||||
walker := aghos.FileWalker(interfaceName(ifaceName).rcConfStaticConfig)
|
||||
|
||||
return walker.Walk(aghos.RootDirFS(), rcConfFilename)
|
||||
}
|
||||
|
||||
// rcConfStaticConfig checks if the interface is configured by /etc/rc.conf to
|
||||
|
||||
@@ -85,17 +85,17 @@ func ifaceHasStaticIP(ifaceName string) (has bool, err error) {
|
||||
|
||||
iface := interfaceName(ifaceName)
|
||||
|
||||
for _, pair := range []struct {
|
||||
for _, pair := range [...]struct {
|
||||
aghos.FileWalker
|
||||
filename string
|
||||
}{{
|
||||
FileWalker: iface.dhcpcdStaticConfig,
|
||||
filename: "/etc/dhcpcd.conf",
|
||||
filename: "etc/dhcpcd.conf",
|
||||
}, {
|
||||
FileWalker: iface.ifacesStaticConfig,
|
||||
filename: "/etc/network/interfaces",
|
||||
filename: "etc/network/interfaces",
|
||||
}} {
|
||||
has, err = pair.Walk(pair.filename)
|
||||
has, err = pair.Walk(aghos.RootDirFS(), pair.filename)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const nl = "\n"
|
||||
|
||||
func TestDHCPCDStaticConfig(t *testing.T) {
|
||||
const iface interfaceName = `wlan0`
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ func canBindPrivilegedPorts() (can bool, err error) {
|
||||
}
|
||||
|
||||
func ifaceHasStaticIP(ifaceName string) (ok bool, err error) {
|
||||
filename := fmt.Sprintf("/etc/hostname.%s", ifaceName)
|
||||
filename := fmt.Sprintf("etc/hostname.%s", ifaceName)
|
||||
|
||||
return aghos.FileWalker(hostnameIfStaticConfig).Walk(filename)
|
||||
return aghos.FileWalker(hostnameIfStaticConfig).Walk(aghos.RootDirFS(), filename)
|
||||
}
|
||||
|
||||
// hostnameIfStaticConfig checks if the interface is configured by
|
||||
|
||||
@@ -4,10 +4,15 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
aghtest.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
func TestGetValidNetInterfacesForWeb(t *testing.T) {
|
||||
ifaces, err := GetValidNetInterfacesForWeb()
|
||||
require.NoErrorf(t, err, "cannot get net interfaces: %s", err)
|
||||
|
||||
@@ -79,8 +79,8 @@ func TestSystemResolvers_DialFunc(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
conn, err := imp.dialFunc(context.Background(), "", tc.address)
|
||||
|
||||
require.Nil(t, conn)
|
||||
|
||||
assert.ErrorIs(t, err, tc.want)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ package aghos
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"io/fs"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
@@ -14,10 +13,10 @@ import (
|
||||
// FileWalker is the signature of a function called for files in the file tree.
|
||||
// As opposed to filepath.Walk it only walk the files (not directories) matching
|
||||
// the provided pattern and those returned by function itself. All patterns
|
||||
// should be valid for filepath.Glob. If cont is false, the walking terminates.
|
||||
// Each opened file is also limited for reading to MaxWalkedFileSize.
|
||||
// should be valid for fs.Glob. If cont is false, the walking terminates. Each
|
||||
// opened file is also limited for reading to MaxWalkedFileSize.
|
||||
//
|
||||
// TODO(e.burkov): Consider moving to the separate package like pathutil.
|
||||
// TODO(e.burkov, a.garipov): Move into another package like aghfs.
|
||||
//
|
||||
// TODO(e.burkov): Think about passing filename or any additional data.
|
||||
type FileWalker func(r io.Reader) (patterns []string, cont bool, err error)
|
||||
@@ -26,15 +25,19 @@ type FileWalker func(r io.Reader) (patterns []string, cont bool, err error)
|
||||
// check.
|
||||
const MaxWalkedFileSize = 1024 * 1024
|
||||
|
||||
// checkFile tries to open and process a single file located on sourcePath.
|
||||
func checkFile(c FileWalker, sourcePath string) (patterns []string, cont bool, err error) {
|
||||
var f *os.File
|
||||
f, err = os.Open(sourcePath)
|
||||
// checkFile tries to open and process a single file located on sourcePath in
|
||||
// the specified fsys. The path is skipped if it's a directory.
|
||||
func checkFile(
|
||||
fsys fs.FS,
|
||||
c FileWalker,
|
||||
sourcePath string,
|
||||
) (patterns []string, cont bool, err error) {
|
||||
var f fs.File
|
||||
f, err = fsys.Open(sourcePath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Ignore non-existing files since this may only happen
|
||||
// when the file was removed after filepath.Glob matched
|
||||
// it.
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// Ignore non-existing files since this may only happen when the
|
||||
// file was removed after filepath.Glob matched it.
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
@@ -42,9 +45,18 @@ func checkFile(c FileWalker, sourcePath string) (patterns []string, cont bool, e
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, f.Close()) }()
|
||||
|
||||
var fi fs.FileInfo
|
||||
if fi, err = f.Stat(); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
// Skip the directories.
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
var r io.Reader
|
||||
// Ignore the error since LimitReader function returns error only if
|
||||
// passed limit value is less than zero, but the constant used.
|
||||
// Ignore the error since LimitReader function returns error only if passed
|
||||
// limit value is less than zero, but the constant used.
|
||||
//
|
||||
// TODO(e.burkov): Make variable.
|
||||
r, _ = aghio.LimitReader(f, MaxWalkedFileSize)
|
||||
@@ -52,13 +64,17 @@ func checkFile(c FileWalker, sourcePath string) (patterns []string, cont bool, e
|
||||
return c(r)
|
||||
}
|
||||
|
||||
// handlePatterns parses the patterns and ignores duplicates using srcSet.
|
||||
// srcSet must be non-nil.
|
||||
func handlePatterns(srcSet *stringutil.Set, patterns ...string) (sub []string, err error) {
|
||||
// handlePatterns parses the patterns in fsys and ignores duplicates using
|
||||
// srcSet. srcSet must be non-nil.
|
||||
func handlePatterns(
|
||||
fsys fs.FS,
|
||||
srcSet *stringutil.Set,
|
||||
patterns ...string,
|
||||
) (sub []string, err error) {
|
||||
sub = make([]string, 0, len(patterns))
|
||||
for _, p := range patterns {
|
||||
var matches []string
|
||||
matches, err = filepath.Glob(p)
|
||||
matches, err = fs.Glob(fsys, p)
|
||||
if err != nil {
|
||||
// Enrich error with the pattern because filepath.Glob
|
||||
// doesn't do it.
|
||||
@@ -78,14 +94,14 @@ func handlePatterns(srcSet *stringutil.Set, patterns ...string) (sub []string, e
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
// Walk starts walking the files defined by initPattern. It only returns true
|
||||
// if c signed to stop walking.
|
||||
func (c FileWalker) Walk(initPattern string) (ok bool, err error) {
|
||||
// The slice of sources keeps the order in which the files are walked
|
||||
// since srcSet.Values() returns strings in undefined order.
|
||||
// Walk starts walking the files in fsys defined by patterns from initial.
|
||||
// It only returns true if fw signed to stop walking.
|
||||
func (fw FileWalker) Walk(fsys fs.FS, initial ...string) (ok bool, err error) {
|
||||
// The slice of sources keeps the order in which the files are walked since
|
||||
// srcSet.Values() returns strings in undefined order.
|
||||
srcSet := stringutil.NewSet()
|
||||
var src []string
|
||||
src, err = handlePatterns(srcSet, initPattern)
|
||||
src, err = handlePatterns(fsys, srcSet, initial...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -97,7 +113,7 @@ func (c FileWalker) Walk(initPattern string) (ok bool, err error) {
|
||||
var patterns []string
|
||||
var cont bool
|
||||
filename = src[i]
|
||||
patterns, cont, err = checkFile(c, src[i])
|
||||
patterns, cont, err = checkFile(fsys, fw, src[i])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -107,7 +123,7 @@ func (c FileWalker) Walk(initPattern string) (ok bool, err error) {
|
||||
}
|
||||
|
||||
var subsrc []string
|
||||
subsrc, err = handlePatterns(srcSet, patterns...)
|
||||
subsrc, err = handlePatterns(fsys, srcSet, patterns...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -4,56 +4,19 @@ import (
|
||||
"bufio"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testFSDir maps entries' names to entries which should either be a testFSDir
|
||||
// or byte slice.
|
||||
type testFSDir map[string]interface{}
|
||||
|
||||
// testFSGen is used to generate a temporary filesystem consisting of
|
||||
// directories and plain text files from itself.
|
||||
type testFSGen testFSDir
|
||||
|
||||
// gen returns the name of top directory of the generated filesystem.
|
||||
func (g testFSGen) gen(t *testing.T) (dirName string) {
|
||||
t.Helper()
|
||||
|
||||
dirName = t.TempDir()
|
||||
g.rangeThrough(t, dirName)
|
||||
|
||||
return dirName
|
||||
}
|
||||
|
||||
func (g testFSGen) rangeThrough(t *testing.T, dirName string) {
|
||||
const perm fs.FileMode = 0o777
|
||||
|
||||
for k, e := range g {
|
||||
switch e := e.(type) {
|
||||
case []byte:
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dirName, k), e, perm))
|
||||
|
||||
case testFSDir:
|
||||
newDir := filepath.Join(dirName, k)
|
||||
require.NoError(t, os.Mkdir(newDir, perm))
|
||||
|
||||
testFSGen(e).rangeThrough(t, newDir)
|
||||
default:
|
||||
t.Fatalf("unexpected entry type %T", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileWalker_Walk(t *testing.T) {
|
||||
const attribute = `000`
|
||||
|
||||
makeFileWalker := func(dirName string) (fw FileWalker) {
|
||||
makeFileWalker := func(_ string) (fw FileWalker) {
|
||||
return func(r io.Reader) (patterns []string, cont bool, err error) {
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
@@ -63,7 +26,7 @@ func TestFileWalker_Walk(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(line) != 0 {
|
||||
patterns = append(patterns, filepath.Join(dirName, line))
|
||||
patterns = append(patterns, path.Join(".", line))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,136 +37,150 @@ func TestFileWalker_Walk(t *testing.T) {
|
||||
const nl = "\n"
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFS testFSGen
|
||||
testFS fstest.MapFS
|
||||
want assert.BoolAssertionFunc
|
||||
initPattern string
|
||||
want bool
|
||||
name string
|
||||
}{{
|
||||
name: "simple",
|
||||
testFS: testFSGen{
|
||||
"simple_0001.txt": []byte(attribute + nl),
|
||||
testFS: fstest.MapFS{
|
||||
"simple_0001.txt": &fstest.MapFile{Data: []byte(attribute + nl)},
|
||||
},
|
||||
initPattern: "simple_0001.txt",
|
||||
want: true,
|
||||
want: assert.True,
|
||||
}, {
|
||||
name: "chain",
|
||||
testFS: testFSGen{
|
||||
"chain_0001.txt": []byte(`chain_0002.txt` + nl),
|
||||
"chain_0002.txt": []byte(`chain_0003.txt` + nl),
|
||||
"chain_0003.txt": []byte(attribute + nl),
|
||||
testFS: fstest.MapFS{
|
||||
"chain_0001.txt": &fstest.MapFile{Data: []byte(`chain_0002.txt` + nl)},
|
||||
"chain_0002.txt": &fstest.MapFile{Data: []byte(`chain_0003.txt` + nl)},
|
||||
"chain_0003.txt": &fstest.MapFile{Data: []byte(attribute + nl)},
|
||||
},
|
||||
initPattern: "chain_0001.txt",
|
||||
want: true,
|
||||
want: assert.True,
|
||||
}, {
|
||||
name: "several",
|
||||
testFS: testFSGen{
|
||||
"several_0001.txt": []byte(`several_*` + nl),
|
||||
"several_0002.txt": []byte(`several_0001.txt` + nl),
|
||||
"several_0003.txt": []byte(attribute + nl),
|
||||
testFS: fstest.MapFS{
|
||||
"several_0001.txt": &fstest.MapFile{Data: []byte(`several_*` + nl)},
|
||||
"several_0002.txt": &fstest.MapFile{Data: []byte(`several_0001.txt` + nl)},
|
||||
"several_0003.txt": &fstest.MapFile{Data: []byte(attribute + nl)},
|
||||
},
|
||||
initPattern: "several_0001.txt",
|
||||
want: true,
|
||||
want: assert.True,
|
||||
}, {
|
||||
name: "no",
|
||||
testFS: testFSGen{
|
||||
"no_0001.txt": []byte(nl),
|
||||
"no_0002.txt": []byte(nl),
|
||||
"no_0003.txt": []byte(nl),
|
||||
testFS: fstest.MapFS{
|
||||
"no_0001.txt": &fstest.MapFile{Data: []byte(nl)},
|
||||
"no_0002.txt": &fstest.MapFile{Data: []byte(nl)},
|
||||
"no_0003.txt": &fstest.MapFile{Data: []byte(nl)},
|
||||
},
|
||||
initPattern: "no_*",
|
||||
want: false,
|
||||
want: assert.False,
|
||||
}, {
|
||||
name: "subdirectory",
|
||||
testFS: testFSGen{
|
||||
"dir": testFSDir{
|
||||
"subdir_0002.txt": []byte(attribute + nl),
|
||||
testFS: fstest.MapFS{
|
||||
path.Join("dir", "subdir_0002.txt"): &fstest.MapFile{
|
||||
Data: []byte(attribute + nl),
|
||||
},
|
||||
"subdir_0001.txt": []byte(`dir/*`),
|
||||
"subdir_0001.txt": &fstest.MapFile{Data: []byte(`dir/*`)},
|
||||
},
|
||||
initPattern: "subdir_0001.txt",
|
||||
want: true,
|
||||
want: assert.True,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
testDir := tc.testFS.gen(t)
|
||||
fw := makeFileWalker(testDir)
|
||||
fw := makeFileWalker("")
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ok, err := fw.Walk(filepath.Join(testDir, tc.initPattern))
|
||||
ok, err := fw.Walk(tc.testFS, tc.initPattern)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.want, ok)
|
||||
tc.want(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("pattern_malformed", func(t *testing.T) {
|
||||
ok, err := makeFileWalker("").Walk("[]")
|
||||
f := fstest.MapFS{}
|
||||
ok, err := makeFileWalker("").Walk(f, "[]")
|
||||
require.Error(t, err)
|
||||
|
||||
assert.False(t, ok)
|
||||
assert.ErrorIs(t, err, filepath.ErrBadPattern)
|
||||
assert.ErrorIs(t, err, path.ErrBadPattern)
|
||||
})
|
||||
|
||||
t.Run("bad_filename", func(t *testing.T) {
|
||||
dir := testFSGen{
|
||||
"bad_filename.txt": []byte("[]"),
|
||||
}.gen(t)
|
||||
fw := FileWalker(func(r io.Reader) (patterns []string, cont bool, err error) {
|
||||
const filename = "bad_filename.txt"
|
||||
|
||||
f := fstest.MapFS{
|
||||
filename: &fstest.MapFile{Data: []byte("[]")},
|
||||
}
|
||||
ok, err := FileWalker(func(r io.Reader) (patterns []string, cont bool, err error) {
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
patterns = append(patterns, s.Text())
|
||||
}
|
||||
|
||||
return patterns, true, s.Err()
|
||||
})
|
||||
|
||||
ok, err := fw.Walk(filepath.Join(dir, "bad_filename.txt"))
|
||||
}).Walk(f, filename)
|
||||
require.Error(t, err)
|
||||
|
||||
assert.False(t, ok)
|
||||
assert.ErrorIs(t, err, filepath.ErrBadPattern)
|
||||
assert.ErrorIs(t, err, path.ErrBadPattern)
|
||||
})
|
||||
|
||||
t.Run("itself_error", func(t *testing.T) {
|
||||
const rerr errors.Error = "returned error"
|
||||
|
||||
dir := testFSGen{
|
||||
"mockfile.txt": []byte(`mockdata`),
|
||||
}.gen(t)
|
||||
f := fstest.MapFS{
|
||||
"mockfile.txt": &fstest.MapFile{Data: []byte(`mockdata`)},
|
||||
}
|
||||
|
||||
ok, err := FileWalker(func(r io.Reader) (patterns []string, ok bool, err error) {
|
||||
return nil, true, rerr
|
||||
}).Walk(filepath.Join(dir, "*"))
|
||||
require.Error(t, err)
|
||||
require.False(t, ok)
|
||||
}).Walk(f, "*")
|
||||
require.ErrorIs(t, err, rerr)
|
||||
|
||||
assert.ErrorIs(t, err, rerr)
|
||||
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(nil, "lol")
|
||||
_, ok, err := checkFile(emptyFS, nil, "lol")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("invalid_argument", func(t *testing.T) {
|
||||
const badPath = "\x00"
|
||||
|
||||
_, ok, err := checkFile(nil, badPath)
|
||||
require.Error(t, err)
|
||||
_, ok, err := checkFile(&errFS{}, nil, "")
|
||||
require.ErrorIs(t, err, errErrFSOpen)
|
||||
|
||||
assert.False(t, ok)
|
||||
// TODO(e.burkov): Use assert.ErrorsIs within the error from
|
||||
// less platform-dependent package instead of syscall.EINVAL.
|
||||
//
|
||||
// See https://github.com/golang/go/issues/46849 and
|
||||
// https://github.com/golang/go/issues/30322.
|
||||
pathErr := &os.PathError{}
|
||||
require.ErrorAs(t, err, &pathErr)
|
||||
assert.Equal(t, "open", pathErr.Op)
|
||||
assert.Equal(t, badPath, pathErr.Path)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
131
internal/aghos/fswatcher.go
Normal file
131
internal/aghos/fswatcher.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package aghos
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
// event is a convenient alias for an empty struct to signal that watching
|
||||
// event happened.
|
||||
type event = struct{}
|
||||
|
||||
// FSWatcher tracks all the fyle system events and notifies about those.
|
||||
//
|
||||
// TODO(e.burkov, a.garipov): Move into another package like aghfs.
|
||||
type FSWatcher interface {
|
||||
io.Closer
|
||||
|
||||
// Events should return a read-only channel which notifies about events.
|
||||
Events() (e <-chan event)
|
||||
|
||||
// Add should check if the file named name is accessible and starts tracking
|
||||
// it.
|
||||
Add(name string) (err error)
|
||||
}
|
||||
|
||||
// osWatcher tracks the file system provided by the OS.
|
||||
type osWatcher struct {
|
||||
// w is the actual notifier that is handled by osWatcher.
|
||||
w *fsnotify.Watcher
|
||||
|
||||
// events is the channel to notify.
|
||||
events chan event
|
||||
}
|
||||
|
||||
const (
|
||||
// osWatcherPref is a prefix for logging and wrapping errors in osWathcer's
|
||||
// methods.
|
||||
osWatcherPref = "os watcher"
|
||||
)
|
||||
|
||||
// NewOSWritesWatcher creates FSWatcher that tracks the real file system of the
|
||||
// OS and notifies only about writing events.
|
||||
func NewOSWritesWatcher() (w FSWatcher, err error) {
|
||||
defer func() { err = errors.Annotate(err, "%s: %w", osWatcherPref) }()
|
||||
|
||||
var watcher *fsnotify.Watcher
|
||||
watcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating watcher: %w", err)
|
||||
}
|
||||
|
||||
fsw := &osWatcher{
|
||||
w: watcher,
|
||||
events: make(chan event, 1),
|
||||
}
|
||||
|
||||
go fsw.handleErrors()
|
||||
go fsw.handleEvents()
|
||||
|
||||
return fsw, nil
|
||||
}
|
||||
|
||||
// handleErrors handles accompanying errors. It used to be called in a separate
|
||||
// goroutine.
|
||||
func (w *osWatcher) handleErrors() {
|
||||
defer log.OnPanic(fmt.Sprintf("%s: handling errors", osWatcherPref))
|
||||
|
||||
for err := range w.w.Errors {
|
||||
log.Error("%s: %s", osWatcherPref, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Events implements the FSWatcher interface for *osWatcher.
|
||||
func (w *osWatcher) Events() (e <-chan event) {
|
||||
return w.events
|
||||
}
|
||||
|
||||
// Add implements the FSWatcher interface for *osWatcher.
|
||||
func (w *osWatcher) Add(name string) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "%s: %w", osWatcherPref) }()
|
||||
|
||||
if _, err = fs.Stat(RootDirFS(), name); err != nil {
|
||||
return fmt.Errorf("checking file %q: %w", name, err)
|
||||
}
|
||||
|
||||
return w.w.Add(filepath.Join("/", name))
|
||||
}
|
||||
|
||||
// Close implements the FSWatcher interface for *osWatcher.
|
||||
func (w *osWatcher) Close() (err error) {
|
||||
return w.w.Close()
|
||||
}
|
||||
|
||||
// handleEvents notifies about the received file system's event if needed. It
|
||||
// used to be called in a separate goroutine.
|
||||
func (w *osWatcher) handleEvents() {
|
||||
defer log.OnPanic(fmt.Sprintf("%s: handling events", osWatcherPref))
|
||||
|
||||
defer close(w.events)
|
||||
|
||||
ch := w.w.Events
|
||||
for e := range ch {
|
||||
if e.Op&fsnotify.Write == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip the following events assuming that sometimes the same event
|
||||
// occurrs several times.
|
||||
for ok := true; ok; {
|
||||
select {
|
||||
case _, ok = <-ch:
|
||||
// Go on.
|
||||
default:
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case w.events <- event{}:
|
||||
// Go on.
|
||||
default:
|
||||
log.Debug("%s: events buffer is full", osWatcherPref)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
@@ -159,3 +161,10 @@ ScanLoop:
|
||||
func IsOpenWrt() (ok bool) {
|
||||
return isOpenWrt()
|
||||
}
|
||||
|
||||
// RootDirFS returns the fs.FS rooted at the operating system's root.
|
||||
func RootDirFS() (fsys fs.FS) {
|
||||
// Use empty string since os.DirFS implicitly prepends a slash to it. This
|
||||
// behavior is undocumented but it currently works.
|
||||
return os.DirFS("")
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ func haveAdminRights() (bool, error) {
|
||||
}
|
||||
|
||||
func isOpenWrt() (ok bool) {
|
||||
const etcReleasePattern = "etc/*release*"
|
||||
|
||||
var err error
|
||||
ok, err = FileWalker(func(r io.Reader) (_ []string, cont bool, err error) {
|
||||
const osNameData = "openwrt"
|
||||
@@ -39,7 +41,7 @@ func isOpenWrt() (ok bool) {
|
||||
}
|
||||
|
||||
return nil, !stringutil.ContainsFold(string(data), osNameData), nil
|
||||
}).Walk("/etc/*release*")
|
||||
}).Walk(RootDirFS(), etcReleasePattern)
|
||||
|
||||
return err == nil && ok
|
||||
}
|
||||
|
||||
23
internal/aghtest/fswatcher.go
Normal file
23
internal/aghtest/fswatcher.go
Normal file
@@ -0,0 +1,23 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
// Package aghtime defines some types for convenient work with time values.
|
||||
package aghtime
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
)
|
||||
|
||||
// Duration is a wrapper for time.Duration providing functionality for encoding.
|
||||
type Duration struct {
|
||||
// time.Duration is embedded here to avoid implementing all the methods.
|
||||
time.Duration
|
||||
}
|
||||
|
||||
// String implements the fmt.Stringer interface for Duration. It wraps
|
||||
// time.Duration.String method and additionally cuts off non-leading zero values
|
||||
// of minutes and seconds. Some values which are differ between the
|
||||
// implementations:
|
||||
//
|
||||
// Duration: "1m", time.Duration: "1m0s"
|
||||
// Duration: "1h", time.Duration: "1h0m0s"
|
||||
// Duration: "1h1m", time.Duration: "1h1m0s"
|
||||
//
|
||||
func (d Duration) String() (str string) {
|
||||
str = d.Duration.String()
|
||||
|
||||
const (
|
||||
tailMin = len(`0s`)
|
||||
tailMinSec = len(`0m0s`)
|
||||
|
||||
secsInHour = time.Hour / time.Second
|
||||
minsInHour = time.Hour / time.Minute
|
||||
)
|
||||
|
||||
switch rounded := d.Duration / time.Second; {
|
||||
case
|
||||
rounded == 0,
|
||||
rounded*time.Second != d.Duration,
|
||||
rounded%60 != 0:
|
||||
// Return the uncut value if it's either equal to zero or has
|
||||
// fractions of a second or even whole seconds in it.
|
||||
return str
|
||||
|
||||
case (rounded%secsInHour)/minsInHour != 0:
|
||||
return str[:len(str)-tailMin]
|
||||
|
||||
default:
|
||||
return str[:len(str)-tailMinSec]
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalText implements the encoding.TextMarshaler interface for Duration.
|
||||
func (d Duration) MarshalText() (text []byte, err error) {
|
||||
return []byte(d.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the encoding.TextUnmarshaler interface for
|
||||
// *Duration.
|
||||
//
|
||||
// TODO(e.burkov): Make it able to parse larger units like days.
|
||||
func (d *Duration) UnmarshalText(b []byte) (err error) {
|
||||
defer func() { err = errors.Annotate(err, "unmarshaling duration: %w") }()
|
||||
|
||||
d.Duration, err = time.ParseDuration(string(b))
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
package aghtime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestDuration_String(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
val time.Duration
|
||||
}{{
|
||||
name: "1s",
|
||||
val: time.Second,
|
||||
}, {
|
||||
name: "1m",
|
||||
val: time.Minute,
|
||||
}, {
|
||||
name: "1h",
|
||||
val: time.Hour,
|
||||
}, {
|
||||
name: "1m1s",
|
||||
val: time.Minute + time.Second,
|
||||
}, {
|
||||
name: "1h1m",
|
||||
val: time.Hour + time.Minute,
|
||||
}, {
|
||||
name: "1h0m1s",
|
||||
val: time.Hour + time.Second,
|
||||
}, {
|
||||
name: "1ms",
|
||||
val: time.Millisecond,
|
||||
}, {
|
||||
name: "1h0m0.001s",
|
||||
val: time.Hour + time.Millisecond,
|
||||
}, {
|
||||
name: "1.001s",
|
||||
val: time.Second + time.Millisecond,
|
||||
}, {
|
||||
name: "1m1.001s",
|
||||
val: time.Minute + time.Second + time.Millisecond,
|
||||
}, {
|
||||
name: "0s",
|
||||
val: 0,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
d := Duration{Duration: tc.val}
|
||||
assert.Equal(t, tc.name, d.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// durationEncodingTester is a helper struct to simplify testing different
|
||||
// Duration marshalling and unmarshaling cases.
|
||||
type durationEncodingTester struct {
|
||||
PtrMap map[string]*Duration `json:"ptr_map" yaml:"ptr_map"`
|
||||
PtrSlice []*Duration `json:"ptr_slice" yaml:"ptr_slice"`
|
||||
PtrValue *Duration `json:"ptr_value" yaml:"ptr_value"`
|
||||
PtrArray [1]*Duration `json:"ptr_array" yaml:"ptr_array"`
|
||||
Map map[string]Duration `json:"map" yaml:"map"`
|
||||
Slice []Duration `json:"slice" yaml:"slice"`
|
||||
Value Duration `json:"value" yaml:"value"`
|
||||
Array [1]Duration `json:"array" yaml:"array"`
|
||||
}
|
||||
|
||||
const nl = "\n"
|
||||
const (
|
||||
jsonStr = `{` +
|
||||
`"ptr_map":{"dur":"1ms"},` +
|
||||
`"ptr_slice":["1ms"],` +
|
||||
`"ptr_value":"1ms",` +
|
||||
`"ptr_array":["1ms"],` +
|
||||
`"map":{"dur":"1ms"},` +
|
||||
`"slice":["1ms"],` +
|
||||
`"value":"1ms",` +
|
||||
`"array":["1ms"]` +
|
||||
`}`
|
||||
yamlStr = `ptr_map:` + nl +
|
||||
` dur: 1ms` + nl +
|
||||
`ptr_slice:` + nl +
|
||||
`- 1ms` + nl +
|
||||
`ptr_value: 1ms` + nl +
|
||||
`ptr_array:` + nl +
|
||||
`- 1ms` + nl +
|
||||
`map:` + nl +
|
||||
` dur: 1ms` + nl +
|
||||
`slice:` + nl +
|
||||
`- 1ms` + nl +
|
||||
`value: 1ms` + nl +
|
||||
`array:` + nl +
|
||||
`- 1ms`
|
||||
)
|
||||
|
||||
// defaultTestDur is the default time.Duration value to be used throughout the tests of
|
||||
// Duration.
|
||||
const defaultTestDur = time.Millisecond
|
||||
|
||||
// checkFields verifies m's fields. It expects the m to be unmarshaled from
|
||||
// one of the constant strings above.
|
||||
func (m *durationEncodingTester) checkFields(t *testing.T, d Duration) {
|
||||
t.Run("pointers_map", func(t *testing.T) {
|
||||
require.NotNil(t, m.PtrMap)
|
||||
|
||||
fromPtrMap, ok := m.PtrMap["dur"]
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, fromPtrMap)
|
||||
|
||||
assert.Equal(t, d, *fromPtrMap)
|
||||
})
|
||||
|
||||
t.Run("pointers_slice", func(t *testing.T) {
|
||||
require.Len(t, m.PtrSlice, 1)
|
||||
|
||||
fromPtrSlice := m.PtrSlice[0]
|
||||
require.NotNil(t, fromPtrSlice)
|
||||
|
||||
assert.Equal(t, d, *fromPtrSlice)
|
||||
})
|
||||
|
||||
t.Run("pointers_array", func(t *testing.T) {
|
||||
fromPtrArray := m.PtrArray[0]
|
||||
require.NotNil(t, fromPtrArray)
|
||||
|
||||
assert.Equal(t, d, *fromPtrArray)
|
||||
})
|
||||
|
||||
t.Run("pointer_value", func(t *testing.T) {
|
||||
require.NotNil(t, m.PtrValue)
|
||||
|
||||
assert.Equal(t, d, *m.PtrValue)
|
||||
})
|
||||
|
||||
t.Run("map", func(t *testing.T) {
|
||||
fromMap, ok := m.Map["dur"]
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, d, fromMap)
|
||||
})
|
||||
|
||||
t.Run("slice", func(t *testing.T) {
|
||||
require.Len(t, m.Slice, 1)
|
||||
|
||||
assert.Equal(t, d, m.Slice[0])
|
||||
})
|
||||
|
||||
t.Run("array", func(t *testing.T) {
|
||||
assert.Equal(t, d, m.Array[0])
|
||||
})
|
||||
|
||||
t.Run("value", func(t *testing.T) {
|
||||
assert.Equal(t, d, m.Value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDuration_MarshalText(t *testing.T) {
|
||||
d := Duration{defaultTestDur}
|
||||
dPtr := &d
|
||||
|
||||
v := durationEncodingTester{
|
||||
PtrMap: map[string]*Duration{"dur": dPtr},
|
||||
PtrSlice: []*Duration{dPtr},
|
||||
PtrValue: dPtr,
|
||||
PtrArray: [1]*Duration{dPtr},
|
||||
Map: map[string]Duration{"dur": d},
|
||||
Slice: []Duration{d},
|
||||
Value: d,
|
||||
Array: [1]Duration{d},
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
t.Run("json", func(t *testing.T) {
|
||||
t.Cleanup(b.Reset)
|
||||
err := json.NewEncoder(b).Encode(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.JSONEq(t, jsonStr, b.String())
|
||||
})
|
||||
|
||||
t.Run("yaml", func(t *testing.T) {
|
||||
t.Cleanup(b.Reset)
|
||||
err := yaml.NewEncoder(b).Encode(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.YAMLEq(t, yamlStr, b.String(), b.String())
|
||||
})
|
||||
|
||||
t.Run("direct", func(t *testing.T) {
|
||||
data, err := d.MarshalText()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, []byte(defaultTestDur.String()), data)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDuration_UnmarshalText(t *testing.T) {
|
||||
d := Duration{defaultTestDur}
|
||||
var v *durationEncodingTester
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
v = &durationEncodingTester{}
|
||||
|
||||
r := strings.NewReader(jsonStr)
|
||||
err := json.NewDecoder(r).Decode(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
v.checkFields(t, d)
|
||||
})
|
||||
|
||||
t.Run("yaml", func(t *testing.T) {
|
||||
v = &durationEncodingTester{}
|
||||
|
||||
r := strings.NewReader(yamlStr)
|
||||
err := yaml.NewDecoder(r).Decode(v)
|
||||
require.NoError(t, err)
|
||||
|
||||
v.checkFields(t, d)
|
||||
})
|
||||
|
||||
t.Run("direct", func(t *testing.T) {
|
||||
dd := &Duration{}
|
||||
|
||||
err := dd.UnmarshalText([]byte(d.String()))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, d, *dd)
|
||||
})
|
||||
|
||||
t.Run("bad_data", func(t *testing.T) {
|
||||
assert.Error(t, (&Duration{}).UnmarshalText([]byte(`abc`)))
|
||||
})
|
||||
}
|
||||
@@ -5,33 +5,17 @@ package dhcpd
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
// broadcast sends resp to the broadcast address specific for network interface.
|
||||
func (s *v4Server) broadcast(peer net.Addr, conn net.PacketConn, resp *dhcpv4.DHCPv4) {
|
||||
// peer is expected to be of type *net.UDPConn as the server4.NewServer
|
||||
// initializes it.
|
||||
udpPeer, ok := peer.(*net.UDPAddr)
|
||||
if !ok {
|
||||
log.Error("dhcpv4: peer is of unexpected type %T", peer)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *dhcpConn) broadcast(respData []byte, peer *net.UDPAddr) (n int, err error) {
|
||||
// Despite the fact that server4.NewIPv4UDPConn explicitly sets socket
|
||||
// options to allow broadcasting, it also binds the connection to a
|
||||
// specific interface. On FreeBSD and OpenBSD conn.WriteTo causes
|
||||
// errors while writing to the addresses that belong to another
|
||||
// interface. So, use the broadcast address specific for the binded
|
||||
// interface.
|
||||
udpPeer.IP = s.conf.broadcastIP
|
||||
// specific interface. On FreeBSD and OpenBSD net.UDPConn.WriteTo
|
||||
// causes errors while writing to the addresses that belong to another
|
||||
// interface. So, use the broadcast address specific for the interface
|
||||
// bound.
|
||||
peer.IP = c.bcastIP
|
||||
|
||||
log.Debug("dhcpv4: sending to %s: %s", peer, resp.Summary())
|
||||
|
||||
if _, err := conn.WriteTo(resp.ToBytes(), peer); err != nil {
|
||||
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
|
||||
}
|
||||
return c.udpConn.WriteTo(respData, peer)
|
||||
}
|
||||
|
||||
@@ -8,17 +8,16 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestV4Server_Send_broadcast(t *testing.T) {
|
||||
func TestDHCPConn_Broadcast(t *testing.T) {
|
||||
b := &bytes.Buffer{}
|
||||
var peer *net.UDPAddr
|
||||
|
||||
conn := &fakePacketConn{
|
||||
udpConn := &fakePacketConn{
|
||||
writeTo: func(p []byte, addr net.Addr) (n int, err error) {
|
||||
udpPeer, ok := addr.(*net.UDPAddr)
|
||||
require.True(t, ok)
|
||||
@@ -31,57 +30,22 @@ func TestV4Server_Send_broadcast(t *testing.T) {
|
||||
return n, nil
|
||||
},
|
||||
}
|
||||
|
||||
conn := &dhcpConn{
|
||||
udpConn: udpConn,
|
||||
bcastIP: net.IP{1, 2, 3, 255},
|
||||
}
|
||||
defaultPeer := &net.UDPAddr{
|
||||
IP: net.IP{1, 2, 3, 4},
|
||||
// Use neither client nor server port.
|
||||
Port: 1234,
|
||||
}
|
||||
s := &v4Server{
|
||||
conf: V4ServerConf{
|
||||
broadcastIP: net.IP{1, 2, 3, 255},
|
||||
},
|
||||
}
|
||||
respData := (&dhcpv4.DHCPv4{}).ToBytes()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
req *dhcpv4.DHCPv4
|
||||
resp *dhcpv4.DHCPv4
|
||||
}{{
|
||||
name: "nak",
|
||||
req: &dhcpv4.DHCPv4{
|
||||
GatewayIPAddr: netutil.IPv4Zero(),
|
||||
},
|
||||
resp: &dhcpv4.DHCPv4{
|
||||
Options: dhcpv4.OptionsFromList(
|
||||
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
|
||||
),
|
||||
},
|
||||
}, {
|
||||
name: "fully_unspecified",
|
||||
req: &dhcpv4.DHCPv4{
|
||||
GatewayIPAddr: netutil.IPv4Zero(),
|
||||
ClientIPAddr: netutil.IPv4Zero(),
|
||||
},
|
||||
resp: &dhcpv4.DHCPv4{
|
||||
Options: dhcpv4.OptionsFromList(
|
||||
dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer),
|
||||
),
|
||||
},
|
||||
}}
|
||||
_, _ = conn.broadcast(respData, cloneUDPAddr(defaultPeer))
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
|
||||
assert.EqualValues(t, tc.resp.ToBytes(), b.Bytes())
|
||||
assert.Equal(t, &net.UDPAddr{
|
||||
IP: s.conf.broadcastIP,
|
||||
Port: defaultPeer.Port,
|
||||
Zone: defaultPeer.Zone,
|
||||
}, peer)
|
||||
})
|
||||
|
||||
b.Reset()
|
||||
peer = nil
|
||||
}
|
||||
assert.EqualValues(t, respData, b.Bytes())
|
||||
assert.Equal(t, &net.UDPAddr{
|
||||
IP: conn.bcastIP,
|
||||
Port: defaultPeer.Port,
|
||||
}, peer)
|
||||
}
|
||||
|
||||
@@ -5,17 +5,10 @@ package dhcpd
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
// broadcast sends resp to the broadcast address specific for network interface.
|
||||
func (s *v4Server) broadcast(peer net.Addr, conn net.PacketConn, resp *dhcpv4.DHCPv4) {
|
||||
respData := resp.ToBytes()
|
||||
|
||||
log.Debug("dhcpv4: sending to %s: %s", peer, resp.Summary())
|
||||
|
||||
func (c *dhcpConn) broadcast(respData []byte, peer *net.UDPAddr) (n int, err error) {
|
||||
// This write to 0xffffffff reverts some behavior changes made in
|
||||
// https://github.com/AdguardTeam/AdGuardHome/issues/3289. The DHCP
|
||||
// server should broadcast the message to 0xffffffff but it's
|
||||
@@ -26,26 +19,13 @@ func (s *v4Server) broadcast(peer net.Addr, conn net.PacketConn, resp *dhcpv4.DH
|
||||
// https://github.com/AdguardTeam/AdGuardHome/issues/3366.
|
||||
//
|
||||
// See also https://github.com/AdguardTeam/AdGuardHome/issues/3539.
|
||||
if _, err := conn.WriteTo(respData, peer); err != nil {
|
||||
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
|
||||
}
|
||||
|
||||
// peer is expected to be of type *net.UDPConn as the server4.NewServer
|
||||
// initializes it.
|
||||
udpPeer, ok := peer.(*net.UDPAddr)
|
||||
if !ok {
|
||||
log.Error("dhcpv4: peer is of unexpected type %T", peer)
|
||||
|
||||
return
|
||||
if n, err = c.udpConn.WriteTo(respData, peer); err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Broadcast the message one more time using the interface-specific
|
||||
// broadcast address.
|
||||
udpPeer.IP = s.conf.broadcastIP
|
||||
peer.IP = c.bcastIP
|
||||
|
||||
log.Debug("dhcpv4: sending to %s: %s", peer, resp.Summary())
|
||||
|
||||
if _, err := conn.WriteTo(respData, peer); err != nil {
|
||||
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
|
||||
}
|
||||
return c.udpConn.WriteTo(respData, peer)
|
||||
}
|
||||
|
||||
@@ -8,17 +8,16 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestV4Server_Send_broadcast(t *testing.T) {
|
||||
func TestDHCPConn_Broadcast(t *testing.T) {
|
||||
b := &bytes.Buffer{}
|
||||
var peers []*net.UDPAddr
|
||||
|
||||
conn := &fakePacketConn{
|
||||
udpConn := &fakePacketConn{
|
||||
writeTo: func(p []byte, addr net.Addr) (n int, err error) {
|
||||
udpPeer, ok := addr.(*net.UDPAddr)
|
||||
require.True(t, ok)
|
||||
@@ -31,66 +30,27 @@ func TestV4Server_Send_broadcast(t *testing.T) {
|
||||
return n, nil
|
||||
},
|
||||
}
|
||||
|
||||
conn := &dhcpConn{
|
||||
udpConn: udpConn,
|
||||
bcastIP: net.IP{1, 2, 3, 255},
|
||||
}
|
||||
defaultPeer := &net.UDPAddr{
|
||||
IP: net.IP{1, 2, 3, 4},
|
||||
// Use neither client nor server port.
|
||||
Port: 1234,
|
||||
}
|
||||
s := &v4Server{
|
||||
conf: V4ServerConf{
|
||||
broadcastIP: net.IP{1, 2, 3, 255},
|
||||
},
|
||||
}
|
||||
respData := (&dhcpv4.DHCPv4{}).ToBytes()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
req *dhcpv4.DHCPv4
|
||||
resp *dhcpv4.DHCPv4
|
||||
}{{
|
||||
name: "nak",
|
||||
req: &dhcpv4.DHCPv4{
|
||||
GatewayIPAddr: netutil.IPv4Zero(),
|
||||
},
|
||||
resp: &dhcpv4.DHCPv4{
|
||||
Options: dhcpv4.OptionsFromList(
|
||||
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
|
||||
),
|
||||
},
|
||||
}, {
|
||||
name: "fully_unspecified",
|
||||
req: &dhcpv4.DHCPv4{
|
||||
GatewayIPAddr: netutil.IPv4Zero(),
|
||||
ClientIPAddr: netutil.IPv4Zero(),
|
||||
},
|
||||
resp: &dhcpv4.DHCPv4{
|
||||
Options: dhcpv4.OptionsFromList(
|
||||
dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer),
|
||||
),
|
||||
},
|
||||
}}
|
||||
_, _ = conn.broadcast(respData, cloneUDPAddr(defaultPeer))
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
|
||||
// The same response is written twice but for different peers.
|
||||
assert.EqualValues(t, append(respData, respData...), b.Bytes())
|
||||
|
||||
// The same response is written twice.
|
||||
respData := tc.resp.ToBytes()
|
||||
assert.EqualValues(t, append(respData, respData...), b.Bytes())
|
||||
require.Len(t, peers, 2)
|
||||
|
||||
require.Len(t, peers, 2)
|
||||
|
||||
assert.Equal(t, &net.UDPAddr{
|
||||
IP: defaultPeer.IP,
|
||||
Port: defaultPeer.Port,
|
||||
}, peers[0])
|
||||
assert.Equal(t, &net.UDPAddr{
|
||||
IP: s.conf.broadcastIP,
|
||||
Port: defaultPeer.Port,
|
||||
}, peers[1])
|
||||
})
|
||||
|
||||
b.Reset()
|
||||
peers = nil
|
||||
}
|
||||
assert.Equal(t, cloneUDPAddr(defaultPeer), peers[0])
|
||||
assert.Equal(t, &net.UDPAddr{
|
||||
IP: conn.bcastIP,
|
||||
Port: defaultPeer.Port,
|
||||
}, peers[1])
|
||||
}
|
||||
|
||||
244
internal/dhcpd/conn_unix.go
Normal file
244
internal/dhcpd/conn_unix.go
Normal file
@@ -0,0 +1,244 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
||||
"github.com/mdlayher/ethernet"
|
||||
"github.com/mdlayher/raw"
|
||||
)
|
||||
|
||||
// dhcpUnicastAddr is the combination of MAC and IP addresses for responding to
|
||||
// the unconfigured host.
|
||||
type dhcpUnicastAddr struct {
|
||||
// raw.Addr is embedded here to make *dhcpUcastAddr a net.Addr without
|
||||
// actually implementing all methods. It also contains the client's
|
||||
// hardware address.
|
||||
raw.Addr
|
||||
|
||||
// yiaddr is an IP address just allocated by server for the host.
|
||||
yiaddr net.IP
|
||||
}
|
||||
|
||||
// dhcpConn is the net.PacketConn capable of handling both net.UDPAddr and
|
||||
// net.HardwareAddr.
|
||||
type dhcpConn struct {
|
||||
// udpConn is the connection for UDP addresses.
|
||||
udpConn net.PacketConn
|
||||
// bcastIP is the broadcast address specific for the configured
|
||||
// interface's subnet.
|
||||
bcastIP net.IP
|
||||
|
||||
// rawConn is the connection for MAC addresses.
|
||||
rawConn net.PacketConn
|
||||
// srcMAC is the hardware address of the configured network interface.
|
||||
srcMAC net.HardwareAddr
|
||||
// srcIP is the IP address of the configured network interface.
|
||||
srcIP net.IP
|
||||
}
|
||||
|
||||
// newDHCPConn creates the special connection for DHCP server.
|
||||
func (s *v4Server) newDHCPConn(ifi *net.Interface) (c net.PacketConn, err error) {
|
||||
// Create the raw connection.
|
||||
var ucast net.PacketConn
|
||||
if ucast, err = raw.ListenPacket(ifi, uint16(ethernet.EtherTypeIPv4), nil); err != nil {
|
||||
return nil, fmt.Errorf("creating raw udp connection: %w", err)
|
||||
}
|
||||
|
||||
// Create the UDP connection.
|
||||
var bcast net.PacketConn
|
||||
bcast, err = server4.NewIPv4UDPConn(ifi.Name, &net.UDPAddr{
|
||||
// TODO(e.burkov): Listening on zeroes makes the server handle
|
||||
// requests from all the interfaces. Inspect the ways to
|
||||
// specify the interface-specific listening addresses.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.
|
||||
IP: net.IP{0, 0, 0, 0},
|
||||
Port: dhcpv4.ServerPort,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating ipv4 udp connection: %w", err)
|
||||
}
|
||||
|
||||
return &dhcpConn{
|
||||
udpConn: bcast,
|
||||
bcastIP: s.conf.broadcastIP,
|
||||
rawConn: ucast,
|
||||
srcMAC: ifi.HardwareAddr,
|
||||
srcIP: s.conf.dnsIPAddrs[0],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// wrapErrs is a helper to wrap the errors from two independent underlying
|
||||
// connections.
|
||||
func (c *dhcpConn) wrapErrs(action string, udpConnErr, rawConnErr error) (err error) {
|
||||
switch {
|
||||
case udpConnErr != nil && rawConnErr != nil:
|
||||
return errors.List(fmt.Sprintf("%s both connections", action), udpConnErr, rawConnErr)
|
||||
case udpConnErr != nil:
|
||||
return fmt.Errorf("%s udp connection: %w", action, udpConnErr)
|
||||
case rawConnErr != nil:
|
||||
return fmt.Errorf("%s raw connection: %w", action, rawConnErr)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WriteTo implements net.PacketConn for *dhcpConn. It selects the underlying
|
||||
// connection to write to based on the type of addr.
|
||||
func (c *dhcpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
switch addr := addr.(type) {
|
||||
case *dhcpUnicastAddr:
|
||||
// Unicast the message to the client's MAC address. Use the raw
|
||||
// connection.
|
||||
//
|
||||
// Note: unicasting is performed on the only network interface
|
||||
// that is configured. For now it may be not what users expect
|
||||
// so additionally broadcast the message via UDP connection.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.
|
||||
var rerr error
|
||||
n, rerr = c.unicast(p, addr)
|
||||
|
||||
_, uerr := c.broadcast(p, &net.UDPAddr{
|
||||
IP: netutil.IPv4bcast(),
|
||||
Port: dhcpv4.ClientPort,
|
||||
})
|
||||
|
||||
return n, c.wrapErrs("writing to", uerr, rerr)
|
||||
case *net.UDPAddr:
|
||||
if addr.IP.Equal(net.IPv4bcast) {
|
||||
// Broadcast the message for the client which supports
|
||||
// it. Use the UDP connection.
|
||||
return c.broadcast(p, addr)
|
||||
}
|
||||
|
||||
// Unicast the message to the client's IP address. Use the UDP
|
||||
// connection.
|
||||
return c.udpConn.WriteTo(p, addr)
|
||||
default:
|
||||
return 0, fmt.Errorf("peer is of unexpected type %T", addr)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadFrom implements net.PacketConn for *dhcpConn.
|
||||
func (c *dhcpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
return c.udpConn.ReadFrom(p)
|
||||
}
|
||||
|
||||
// unicast wraps respData with required frames and writes it to the peer.
|
||||
func (c *dhcpConn) unicast(respData []byte, peer *dhcpUnicastAddr) (n int, err error) {
|
||||
var data []byte
|
||||
data, err = c.buildEtherPkt(respData, peer)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return c.rawConn.WriteTo(data, &peer.Addr)
|
||||
}
|
||||
|
||||
// Close implements net.PacketConn for *dhcpConn.
|
||||
func (c *dhcpConn) Close() (err error) {
|
||||
rerr := c.rawConn.Close()
|
||||
if errors.Is(rerr, os.ErrClosed) {
|
||||
// Ignore the error since the actual file is closed already.
|
||||
rerr = nil
|
||||
}
|
||||
|
||||
return c.wrapErrs("closing", c.udpConn.Close(), rerr)
|
||||
}
|
||||
|
||||
// LocalAddr implements net.PacketConn for *dhcpConn.
|
||||
func (c *dhcpConn) LocalAddr() (a net.Addr) {
|
||||
return c.udpConn.LocalAddr()
|
||||
}
|
||||
|
||||
// SetDeadline implements net.PacketConn for *dhcpConn.
|
||||
func (c *dhcpConn) SetDeadline(t time.Time) (err error) {
|
||||
return c.wrapErrs("setting deadline on", c.udpConn.SetDeadline(t), c.rawConn.SetDeadline(t))
|
||||
}
|
||||
|
||||
// SetReadDeadline implements net.PacketConn for *dhcpConn.
|
||||
func (c *dhcpConn) SetReadDeadline(t time.Time) error {
|
||||
return c.wrapErrs(
|
||||
"setting reading deadline on",
|
||||
c.udpConn.SetReadDeadline(t),
|
||||
c.rawConn.SetReadDeadline(t),
|
||||
)
|
||||
}
|
||||
|
||||
// SetWriteDeadline implements net.PacketConn for *dhcpConn.
|
||||
func (c *dhcpConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.wrapErrs(
|
||||
"setting writing deadline on",
|
||||
c.udpConn.SetWriteDeadline(t),
|
||||
c.rawConn.SetWriteDeadline(t),
|
||||
)
|
||||
}
|
||||
|
||||
// ipv4DefaultTTL is the default Time to Live value as recommended by
|
||||
// RFC-1700 (https://datatracker.ietf.org/doc/html/rfc1700) in seconds.
|
||||
const ipv4DefaultTTL = 64
|
||||
|
||||
// errInvalidPktDHCP is returned when the provided payload is not a valid DHCP
|
||||
// packet.
|
||||
const errInvalidPktDHCP errors.Error = "packet is not a valid dhcp packet"
|
||||
|
||||
// buildEtherPkt wraps the payload with IPv4, UDP and Ethernet frames. The
|
||||
// payload is expected to be an encoded DHCP packet.
|
||||
func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []byte, err error) {
|
||||
dhcpLayer := gopacket.NewPacket(payload, layers.LayerTypeDHCPv4, gopacket.DecodeOptions{
|
||||
NoCopy: true,
|
||||
}).Layer(layers.LayerTypeDHCPv4)
|
||||
|
||||
// Check if the decoding succeeded and the resulting layer doesn't
|
||||
// contain any errors. It should guarantee panic-safe converting of the
|
||||
// layer into gopacket.SerializableLayer.
|
||||
if dhcpLayer == nil || dhcpLayer.LayerType() != layers.LayerTypeDHCPv4 {
|
||||
return nil, errInvalidPktDHCP
|
||||
}
|
||||
|
||||
udpLayer := &layers.UDP{
|
||||
SrcPort: dhcpv4.ServerPort,
|
||||
DstPort: dhcpv4.ClientPort,
|
||||
}
|
||||
ipv4Layer := &layers.IPv4{
|
||||
Version: uint8(layers.IPProtocolIPv4),
|
||||
Flags: layers.IPv4DontFragment,
|
||||
TTL: ipv4DefaultTTL,
|
||||
Protocol: layers.IPProtocolUDP,
|
||||
SrcIP: c.srcIP,
|
||||
DstIP: peer.yiaddr,
|
||||
}
|
||||
|
||||
// Ignore the error since it's only returned for invalid network layer's
|
||||
// type.
|
||||
_ = udpLayer.SetNetworkLayerForChecksum(ipv4Layer)
|
||||
ethLayer := &layers.Ethernet{
|
||||
SrcMAC: c.srcMAC,
|
||||
DstMAC: peer.HardwareAddr,
|
||||
EthernetType: layers.EthernetTypeIPv4,
|
||||
}
|
||||
|
||||
buf := gopacket.NewSerializeBuffer()
|
||||
err = gopacket.SerializeLayers(buf, gopacket.SerializeOptions{
|
||||
FixLengths: true,
|
||||
ComputeChecksums: true,
|
||||
}, ethLayer, ipv4Layer, udpLayer, dhcpLayer.(gopacket.SerializableLayer))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("serializing layers: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
114
internal/dhcpd/conn_unix_test.go
Normal file
114
internal/dhcpd/conn_unix_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/mdlayher/raw"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDHCPConn_WriteTo_common(t *testing.T) {
|
||||
respData := (&dhcpv4.DHCPv4{}).ToBytes()
|
||||
udpAddr := &net.UDPAddr{
|
||||
IP: net.IP{1, 2, 3, 4},
|
||||
Port: dhcpv4.ClientPort,
|
||||
}
|
||||
|
||||
t.Run("unicast_ip", func(t *testing.T) {
|
||||
writeTo := func(_ []byte, addr net.Addr) (_ int, _ error) {
|
||||
assert.Equal(t, udpAddr, addr)
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
conn := &dhcpConn{udpConn: &fakePacketConn{writeTo: writeTo}}
|
||||
|
||||
_, err := conn.WriteTo(respData, udpAddr)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("unexpected_addr_type", func(t *testing.T) {
|
||||
type unexpectedAddrType struct {
|
||||
net.Addr
|
||||
}
|
||||
|
||||
conn := &dhcpConn{}
|
||||
n, err := conn.WriteTo(nil, &unexpectedAddrType{})
|
||||
require.Error(t, err)
|
||||
|
||||
testutil.AssertErrorMsg(t, "peer is of unexpected type *dhcpd.unexpectedAddrType", err)
|
||||
assert.Zero(t, n)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildEtherPkt(t *testing.T) {
|
||||
conn := &dhcpConn{
|
||||
srcMAC: net.HardwareAddr{1, 2, 3, 4, 5, 6},
|
||||
srcIP: net.IP{1, 2, 3, 4},
|
||||
}
|
||||
peer := &dhcpUnicastAddr{
|
||||
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}},
|
||||
yiaddr: net.IP{4, 3, 2, 1},
|
||||
}
|
||||
payload := (&dhcpv4.DHCPv4{}).ToBytes()
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
pkt, err := conn.buildEtherPkt(payload, peer)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, pkt)
|
||||
|
||||
actualPkt := gopacket.NewPacket(pkt, layers.LayerTypeEthernet, gopacket.DecodeOptions{
|
||||
NoCopy: true,
|
||||
})
|
||||
require.NotNil(t, actualPkt)
|
||||
|
||||
wantTypes := []gopacket.LayerType{
|
||||
layers.LayerTypeEthernet,
|
||||
layers.LayerTypeIPv4,
|
||||
layers.LayerTypeUDP,
|
||||
layers.LayerTypeDHCPv4,
|
||||
}
|
||||
actualLayers := actualPkt.Layers()
|
||||
require.Len(t, actualLayers, len(wantTypes))
|
||||
|
||||
for i, wantType := range wantTypes {
|
||||
layer := actualLayers[i]
|
||||
require.NotNil(t, layer)
|
||||
|
||||
assert.Equal(t, wantType, layer.LayerType())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-serializable", func(t *testing.T) {
|
||||
// Create an invalid DHCP packet.
|
||||
invalidPayload := []byte{1, 2, 3, 4}
|
||||
pkt, err := conn.buildEtherPkt(invalidPayload, nil)
|
||||
require.Error(t, err)
|
||||
|
||||
assert.ErrorIs(t, err, errInvalidPktDHCP)
|
||||
assert.Empty(t, pkt)
|
||||
})
|
||||
|
||||
t.Run("serializing_error", func(t *testing.T) {
|
||||
// Create a peer with invalid MAC.
|
||||
badPeer := &dhcpUnicastAddr{
|
||||
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}},
|
||||
yiaddr: net.IP{4, 3, 2, 1},
|
||||
}
|
||||
|
||||
pkt, err := conn.buildEtherPkt(payload, badPeer)
|
||||
require.Error(t, err)
|
||||
|
||||
assert.Empty(t, pkt)
|
||||
})
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -67,9 +68,7 @@ func TestDB(t *testing.T) {
|
||||
err = s.dbStore()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, os.Remove(dbFilename))
|
||||
})
|
||||
testutil.CleanupAndRequireSuccess(t, func() (err error) { return os.Remove(dbFilename) })
|
||||
|
||||
err = s.srv4.ResetLeases(nil)
|
||||
require.NoError(t, err)
|
||||
@@ -138,8 +137,51 @@ func TestNormalizeLeases(t *testing.T) {
|
||||
assert.Equal(t, leases[2].HWAddr, dynLeases[1].HWAddr)
|
||||
}
|
||||
|
||||
func TestV4Server_badRange(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
gatewayIP net.IP
|
||||
subnetMask net.IP
|
||||
wantErrMsg string
|
||||
}{{
|
||||
name: "gateway_in_range",
|
||||
gatewayIP: net.IP{192, 168, 10, 120},
|
||||
subnetMask: net.IP{255, 255, 255, 0},
|
||||
wantErrMsg: "dhcpv4: gateway ip 192.168.10.120 in the ip range: " +
|
||||
"192.168.10.20-192.168.10.200",
|
||||
}, {
|
||||
name: "outside_range_start",
|
||||
gatewayIP: net.IP{192, 168, 10, 1},
|
||||
subnetMask: net.IP{255, 255, 255, 240},
|
||||
wantErrMsg: "dhcpv4: range start 192.168.10.20 is outside network " +
|
||||
"192.168.10.1/28",
|
||||
}, {
|
||||
name: "outside_range_end",
|
||||
gatewayIP: net.IP{192, 168, 10, 1},
|
||||
subnetMask: net.IP{255, 255, 255, 224},
|
||||
wantErrMsg: "dhcpv4: range end 192.168.10.200 is outside network " +
|
||||
"192.168.10.1/27",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
conf := V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.IP{192, 168, 10, 20},
|
||||
RangeEnd: net.IP{192, 168, 10, 200},
|
||||
GatewayIP: tc.gatewayIP,
|
||||
SubnetMask: tc.subnetMask,
|
||||
notify: testNotify,
|
||||
}
|
||||
|
||||
_, err := v4Create(conf)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// cloneUDPAddr returns a deep copy of a.
|
||||
func cloneUDPAddr(a *net.UDPAddr) (copy *net.UDPAddr) {
|
||||
func cloneUDPAddr(a *net.UDPAddr) (clone *net.UDPAddr) {
|
||||
return &net.UDPAddr{
|
||||
IP: netutil.CloneIP(a.IP),
|
||||
Port: a.Port,
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestServer_notImplemented(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r, err := http.NewRequest(http.MethodGet, "/unsupported", nil)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
h(w, r)
|
||||
assert.Equal(t, http.StatusNotImplemented, w.Code)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -64,14 +65,8 @@ func TestNewIPRange(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r, err := newIPRange(tc.start, tc.end)
|
||||
if tc.wantErrMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, r)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tc.wantErrMsg, err.Error())
|
||||
}
|
||||
_, err := newIPRange(tc.start, tc.end)
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -45,13 +46,7 @@ func TestNullBool_UnmarshalJSON(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var got nullBool
|
||||
err := got.UnmarshalJSON(tc.data)
|
||||
if tc.wantErrMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantErrMsg, err.Error())
|
||||
}
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
|
||||
@@ -157,8 +157,8 @@ func prepareOptions(conf V4ServerConf) (opts dhcpv4.Options) {
|
||||
dhcpv4.OptionPerformRouterDiscovery.Code(): []byte{1},
|
||||
// The all-routers address is preferred wherever possible, see
|
||||
// https://datatracker.ietf.org/doc/html/rfc1256#section-5.1.
|
||||
dhcpv4.OptionRouterSolicitationAddress.Code(): net.IPv4allrouter.To4(),
|
||||
dhcpv4.OptionBroadcastAddress.Code(): net.IPv4bcast.To4(),
|
||||
dhcpv4.OptionRouterSolicitationAddress.Code(): netutil.IPv4allrouter(),
|
||||
dhcpv4.OptionBroadcastAddress.Code(): netutil.IPv4bcast(),
|
||||
|
||||
// Link-Layer Per Interface
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ func TestParseOpt(t *testing.T) {
|
||||
opt, err := parseDHCPOption(tc.in)
|
||||
if tc.wantErrMsg != "" {
|
||||
require.Error(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantErrMsg, err.Error())
|
||||
|
||||
return
|
||||
|
||||
@@ -16,9 +16,11 @@ import (
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"github.com/go-ping/ping"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
||||
"github.com/mdlayher/raw"
|
||||
)
|
||||
|
||||
// v4Server is a DHCPv4 server.
|
||||
@@ -291,6 +293,8 @@ func (s *v4Server) addLease(l *Lease) (err error) {
|
||||
offset, inOffset := r.offset(l.IP)
|
||||
|
||||
if l.IsStatic() {
|
||||
// TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is
|
||||
// disabled.
|
||||
if sn := s.conf.subnet; !sn.Contains(l.IP) {
|
||||
return fmt.Errorf("subnet %s does not contain the ip %q", sn, l.IP)
|
||||
}
|
||||
@@ -898,9 +902,10 @@ func (s *v4Server) process(req, resp *dhcpv4.DHCPv4) int {
|
||||
resp.UpdateOption(dhcpv4.OptGeneric(code, configured.Get(code)))
|
||||
}
|
||||
}
|
||||
// Update the value of Domain Name Server option separately from others
|
||||
// since its value is set after server's creating.
|
||||
if requested.Has(dhcpv4.OptionDomainNameServer) {
|
||||
// Update the value of Domain Name Server option separately from others if
|
||||
// not assigned yet since its value is set after server's creating.
|
||||
if requested.Has(dhcpv4.OptionDomainNameServer) &&
|
||||
!resp.Options.Has(dhcpv4.OptionDomainNameServer) {
|
||||
resp.UpdateOption(dhcpv4.OptDNS(s.conf.dnsIPAddrs...))
|
||||
}
|
||||
|
||||
@@ -955,43 +960,44 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
|
||||
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
|
||||
var unicast bool
|
||||
if giaddr := req.GatewayIPAddr; giaddr != nil && !giaddr.IsUnspecified() {
|
||||
switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
|
||||
case giaddr != nil && !giaddr.IsUnspecified():
|
||||
// Send any return messages to the server port on the BOOTP
|
||||
// relay agent whose address appears in giaddr.
|
||||
peer = &net.UDPAddr{
|
||||
IP: giaddr,
|
||||
Port: dhcpv4.ServerPort,
|
||||
}
|
||||
unicast = true
|
||||
} else if mtype := resp.MessageType(); mtype == dhcpv4.MessageTypeNak {
|
||||
if mtype == dhcpv4.MessageTypeNak {
|
||||
// Set the broadcast bit in the DHCPNAK, so that the
|
||||
// relay agent broadcasted it to the client, because the
|
||||
// client may not have a correct network address or
|
||||
// subnet mask, and the client may not be answering ARP
|
||||
// requests.
|
||||
resp.SetBroadcast()
|
||||
}
|
||||
case mtype == dhcpv4.MessageTypeNak:
|
||||
// Broadcast any DHCPNAK messages to 0xffffffff.
|
||||
} else if ciaddr := req.ClientIPAddr; ciaddr != nil && !ciaddr.IsUnspecified() {
|
||||
case ciaddr != nil && !ciaddr.IsUnspecified():
|
||||
// Unicast DHCPOFFER and DHCPACK messages to the address in
|
||||
// ciaddr.
|
||||
peer = &net.UDPAddr{
|
||||
IP: ciaddr,
|
||||
Port: dhcpv4.ClientPort,
|
||||
}
|
||||
unicast = true
|
||||
}
|
||||
|
||||
// TODO(e.burkov): Unicast the message to the actual link-layer address
|
||||
// of the client if broadcast bit is not set. Perhaps, use custom
|
||||
// connection when creating the server.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/3443.
|
||||
|
||||
if !unicast {
|
||||
s.broadcast(peer, conn, resp)
|
||||
|
||||
return
|
||||
case !req.IsBroadcast() && req.ClientHWAddr != nil:
|
||||
// Unicast DHCPOFFER and DHCPACK messages to the client's
|
||||
// hardware address and yiaddr.
|
||||
peer = &dhcpUnicastAddr{
|
||||
Addr: raw.Addr{HardwareAddr: req.ClientHWAddr},
|
||||
yiaddr: resp.YourIPAddr,
|
||||
}
|
||||
default:
|
||||
// Go on since peer is already set to broadcast.
|
||||
}
|
||||
|
||||
log.Debug("dhcpv4: sending to %s: %s", peer, resp.Summary())
|
||||
|
||||
_, err := conn.WriteTo(resp.ToBytes(), peer)
|
||||
if err != nil {
|
||||
if _, err := conn.WriteTo(resp.ToBytes(), peer); err != nil {
|
||||
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
|
||||
}
|
||||
}
|
||||
@@ -1029,11 +1035,18 @@ func (s *v4Server) Start() (err error) {
|
||||
|
||||
s.conf.dnsIPAddrs = dnsIPAddrs
|
||||
|
||||
laddr := &net.UDPAddr{
|
||||
IP: net.IP{0, 0, 0, 0},
|
||||
Port: dhcpv4.ServerPort,
|
||||
var c net.PacketConn
|
||||
if c, err = s.newDHCPConn(iface); err != nil {
|
||||
return err
|
||||
}
|
||||
s.srv, err = server4.NewServer(iface.Name, laddr, s.packetHandler, server4.WithDebugLogger())
|
||||
|
||||
s.srv, err = server4.NewServer(
|
||||
iface.Name,
|
||||
nil,
|
||||
s.packetHandler,
|
||||
server4.WithConn(c),
|
||||
server4.WithDebugLogger(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1114,10 +1127,33 @@ func v4Create(conf V4ServerConf) (srv DHCPServer, err error) {
|
||||
return s, fmt.Errorf("dhcpv4: %w", err)
|
||||
}
|
||||
|
||||
if s.conf.ipRange.contains(routerIP) {
|
||||
return s, fmt.Errorf("dhcpv4: gateway ip %v in the ip range: %v-%v",
|
||||
routerIP,
|
||||
conf.RangeStart,
|
||||
conf.RangeEnd,
|
||||
)
|
||||
}
|
||||
|
||||
if !s.conf.subnet.Contains(conf.RangeStart) {
|
||||
return s, fmt.Errorf("dhcpv4: range start %v is outside network %v",
|
||||
conf.RangeStart,
|
||||
s.conf.subnet,
|
||||
)
|
||||
}
|
||||
|
||||
if !s.conf.subnet.Contains(conf.RangeEnd) {
|
||||
return s, fmt.Errorf("dhcpv4: range end %v is outside network %v",
|
||||
conf.RangeEnd,
|
||||
s.conf.subnet,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(a.garipov, d.seregin): Check that every lease is inside the IPRange.
|
||||
s.leasedOffsets = newBitSet()
|
||||
|
||||
if conf.LeaseDuration == 0 {
|
||||
s.conf.leaseTime = time.Hour * 24
|
||||
s.conf.leaseTime = timeutil.Day
|
||||
s.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds())
|
||||
} else {
|
||||
s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
package dhcpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/mdlayher/raw"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -17,17 +19,34 @@ import (
|
||||
func notify4(flags uint32) {
|
||||
}
|
||||
|
||||
func TestV4_AddRemove_static(t *testing.T) {
|
||||
s, err := v4Create(V4ServerConf{
|
||||
// defaultV4ServerConf returns the default configuration for *v4Server to use in
|
||||
// tests.
|
||||
func defaultV4ServerConf() (conf V4ServerConf) {
|
||||
return V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.IP{192, 168, 10, 100},
|
||||
RangeEnd: net.IP{192, 168, 10, 200},
|
||||
GatewayIP: net.IP{192, 168, 10, 1},
|
||||
SubnetMask: net.IP{255, 255, 255, 0},
|
||||
notify: notify4,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// defaultSrv prepares the default DHCPServer to use in tests. The underlying
|
||||
// type of s is *v4Server.
|
||||
func defaultSrv(t *testing.T) (s DHCPServer) {
|
||||
t.Helper()
|
||||
|
||||
var err error
|
||||
s, err = v4Create(defaultV4ServerConf())
|
||||
require.NoError(t, err)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func TestV4_AddRemove_static(t *testing.T) {
|
||||
s := defaultSrv(t)
|
||||
|
||||
ls := s.GetLeases(LeasesStatic)
|
||||
assert.Empty(t, ls)
|
||||
|
||||
@@ -38,7 +57,7 @@ func TestV4_AddRemove_static(t *testing.T) {
|
||||
IP: net.IP{192, 168, 10, 150},
|
||||
}
|
||||
|
||||
err = s.AddStaticLease(l)
|
||||
err := s.AddStaticLease(l)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.AddStaticLease(l)
|
||||
@@ -66,15 +85,7 @@ func TestV4_AddRemove_static(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestV4_AddReplace(t *testing.T) {
|
||||
sIface, err := v4Create(V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.IP{192, 168, 10, 100},
|
||||
RangeEnd: net.IP{192, 168, 10, 200},
|
||||
GatewayIP: net.IP{192, 168, 10, 1},
|
||||
SubnetMask: net.IP{255, 255, 255, 0},
|
||||
notify: notify4,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
sIface := defaultSrv(t)
|
||||
|
||||
s, ok := sIface.(*v4Server)
|
||||
require.True(t, ok)
|
||||
@@ -90,7 +101,7 @@ func TestV4_AddReplace(t *testing.T) {
|
||||
}}
|
||||
|
||||
for i := range dynLeases {
|
||||
err = s.addLease(&dynLeases[i])
|
||||
err := s.addLease(&dynLeases[i])
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -105,7 +116,7 @@ func TestV4_AddReplace(t *testing.T) {
|
||||
}}
|
||||
|
||||
for _, l := range stLeases {
|
||||
err = s.AddStaticLease(l)
|
||||
err := s.AddStaticLease(l)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -119,17 +130,80 @@ func TestV4_AddReplace(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestV4StaticLease_Get(t *testing.T) {
|
||||
var err error
|
||||
sIface, err := v4Create(V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.IP{192, 168, 10, 100},
|
||||
RangeEnd: net.IP{192, 168, 10, 200},
|
||||
GatewayIP: net.IP{192, 168, 10, 1},
|
||||
SubnetMask: net.IP{255, 255, 255, 0},
|
||||
notify: notify4,
|
||||
func TestV4Server_Process_optionsPriority(t *testing.T) {
|
||||
defaultIP := net.IP{192, 168, 1, 1}
|
||||
knownIP := net.IP{1, 2, 3, 4}
|
||||
|
||||
// prepareSrv creates a *v4Server and sets the opt6IPs in the initial
|
||||
// configuration of the server as the value for DHCP option 6.
|
||||
prepareSrv := func(t *testing.T, opt6IPs []net.IP) (s *v4Server) {
|
||||
t.Helper()
|
||||
|
||||
conf := defaultV4ServerConf()
|
||||
if len(opt6IPs) > 0 {
|
||||
b := &strings.Builder{}
|
||||
stringutil.WriteToBuilder(b, "6 ips ", opt6IPs[0].String())
|
||||
for _, ip := range opt6IPs[1:] {
|
||||
stringutil.WriteToBuilder(b, ",", ip.String())
|
||||
}
|
||||
conf.Options = []string{b.String()}
|
||||
}
|
||||
|
||||
ss, err := v4Create(conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
var ok bool
|
||||
s, ok = ss.(*v4Server)
|
||||
require.True(t, ok)
|
||||
|
||||
s.conf.dnsIPAddrs = []net.IP{defaultIP}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// checkResp creates a discovery message with DHCP option 6 requested amd
|
||||
// asserts the response to contain wantIPs in this option.
|
||||
checkResp := func(t *testing.T, s *v4Server, wantIPs []net.IP) {
|
||||
t.Helper()
|
||||
|
||||
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||
req, err := dhcpv4.NewDiscovery(mac, dhcpv4.WithRequestedOptions(
|
||||
dhcpv4.OptionDomainNameServer,
|
||||
))
|
||||
require.NoError(t, err)
|
||||
|
||||
var resp *dhcpv4.DHCPv4
|
||||
resp, err = dhcpv4.NewReplyFromRequest(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
res := s.process(req, resp)
|
||||
require.Equal(t, 1, res)
|
||||
|
||||
o := resp.GetOneOption(dhcpv4.OptionDomainNameServer)
|
||||
require.NotEmpty(t, o)
|
||||
|
||||
wantData := []byte{}
|
||||
for _, ip := range wantIPs {
|
||||
wantData = append(wantData, ip...)
|
||||
}
|
||||
assert.Equal(t, o, wantData)
|
||||
}
|
||||
|
||||
t.Run("default", func(t *testing.T) {
|
||||
s := prepareSrv(t, nil)
|
||||
|
||||
checkResp(t, s, []net.IP{defaultIP})
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("explicitly_configured", func(t *testing.T) {
|
||||
s := prepareSrv(t, []net.IP{knownIP, knownIP})
|
||||
|
||||
checkResp(t, s, []net.IP{knownIP, knownIP})
|
||||
})
|
||||
}
|
||||
|
||||
func TestV4StaticLease_Get(t *testing.T) {
|
||||
sIface := defaultSrv(t)
|
||||
|
||||
s, ok := sIface.(*v4Server)
|
||||
require.True(t, ok)
|
||||
@@ -141,7 +215,7 @@ func TestV4StaticLease_Get(t *testing.T) {
|
||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||
IP: net.IP{192, 168, 10, 150},
|
||||
}
|
||||
err = s.AddStaticLease(l)
|
||||
err := s.AddStaticLease(l)
|
||||
require.NoError(t, err)
|
||||
|
||||
var req, resp *dhcpv4.DHCPv4
|
||||
@@ -209,19 +283,14 @@ func TestV4StaticLease_Get(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestV4DynamicLease_Get(t *testing.T) {
|
||||
conf := defaultV4ServerConf()
|
||||
conf.Options = []string{
|
||||
"81 hex 303132",
|
||||
"82 ip 1.2.3.4",
|
||||
}
|
||||
|
||||
var err error
|
||||
sIface, err := v4Create(V4ServerConf{
|
||||
Enabled: true,
|
||||
RangeStart: net.IP{192, 168, 10, 100},
|
||||
RangeEnd: net.IP{192, 168, 10, 200},
|
||||
GatewayIP: net.IP{192, 168, 10, 1},
|
||||
SubnetMask: net.IP{255, 255, 255, 0},
|
||||
notify: notify4,
|
||||
Options: []string{
|
||||
"81 hex 303132",
|
||||
"82 ip 1.2.3.4",
|
||||
},
|
||||
})
|
||||
sIface, err := v4Create(conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
s, ok := sIface.(*v4Server)
|
||||
@@ -362,14 +431,7 @@ func TestNormalizeHostname(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := normalizeHostname(tc.hostname)
|
||||
if tc.wantErrMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantErrMsg, err.Error())
|
||||
}
|
||||
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
@@ -390,67 +452,107 @@ func (fc *fakePacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
return fc.writeTo(p, addr)
|
||||
}
|
||||
|
||||
func TestV4Server_Send_unicast(t *testing.T) {
|
||||
b := &bytes.Buffer{}
|
||||
var peer *net.UDPAddr
|
||||
|
||||
conn := &fakePacketConn{
|
||||
writeTo: func(p []byte, addr net.Addr) (n int, err error) {
|
||||
udpPeer, ok := addr.(*net.UDPAddr)
|
||||
require.True(t, ok)
|
||||
|
||||
peer = cloneUDPAddr(udpPeer)
|
||||
|
||||
n, err = b.Write(p)
|
||||
require.NoError(t, err)
|
||||
|
||||
return n, nil
|
||||
},
|
||||
}
|
||||
|
||||
defaultPeer := &net.UDPAddr{
|
||||
IP: net.IP{1, 2, 3, 4},
|
||||
// Use neither client nor server port.
|
||||
Port: 1234,
|
||||
}
|
||||
defaultResp := &dhcpv4.DHCPv4{
|
||||
OpCode: dhcpv4.OpcodeBootReply,
|
||||
}
|
||||
func TestV4Server_Send(t *testing.T) {
|
||||
s := &v4Server{}
|
||||
|
||||
var (
|
||||
defaultIP = net.IP{99, 99, 99, 99}
|
||||
knownIP = net.IP{4, 2, 4, 2}
|
||||
knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1}
|
||||
)
|
||||
|
||||
defaultPeer := &net.UDPAddr{
|
||||
IP: defaultIP,
|
||||
// Use neither client nor server port to check it actually
|
||||
// changed.
|
||||
Port: dhcpv4.ClientPort + dhcpv4.ServerPort,
|
||||
}
|
||||
defaultResp := &dhcpv4.DHCPv4{}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
req *dhcpv4.DHCPv4
|
||||
wantPeer net.Addr
|
||||
want net.Addr
|
||||
req *dhcpv4.DHCPv4
|
||||
resp *dhcpv4.DHCPv4
|
||||
name string
|
||||
}{{
|
||||
name: "relay_agent",
|
||||
req: &dhcpv4.DHCPv4{
|
||||
GatewayIPAddr: defaultPeer.IP,
|
||||
},
|
||||
wantPeer: &net.UDPAddr{
|
||||
IP: defaultPeer.IP,
|
||||
name: "giaddr",
|
||||
req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},
|
||||
resp: defaultResp,
|
||||
want: &net.UDPAddr{
|
||||
IP: knownIP,
|
||||
Port: dhcpv4.ServerPort,
|
||||
},
|
||||
}, {
|
||||
name: "known_client",
|
||||
req: &dhcpv4.DHCPv4{
|
||||
GatewayIPAddr: netutil.IPv4Zero(),
|
||||
ClientIPAddr: net.IP{2, 3, 4, 5},
|
||||
name: "nak",
|
||||
req: &dhcpv4.DHCPv4{},
|
||||
resp: &dhcpv4.DHCPv4{
|
||||
Options: dhcpv4.OptionsFromList(
|
||||
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
|
||||
),
|
||||
},
|
||||
wantPeer: &net.UDPAddr{
|
||||
IP: net.IP{2, 3, 4, 5},
|
||||
want: defaultPeer,
|
||||
}, {
|
||||
name: "ciaddr",
|
||||
req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP},
|
||||
resp: &dhcpv4.DHCPv4{},
|
||||
want: &net.UDPAddr{
|
||||
IP: knownIP,
|
||||
Port: dhcpv4.ClientPort,
|
||||
},
|
||||
}, {
|
||||
name: "chaddr",
|
||||
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
|
||||
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
|
||||
want: &dhcpUnicastAddr{
|
||||
Addr: raw.Addr{HardwareAddr: knownMAC},
|
||||
yiaddr: knownIP,
|
||||
},
|
||||
}, {
|
||||
name: "who_are_you",
|
||||
req: &dhcpv4.DHCPv4{},
|
||||
resp: &dhcpv4.DHCPv4{},
|
||||
want: defaultPeer,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s.send(defaultPeer, conn, tc.req, defaultResp)
|
||||
assert.EqualValues(t, defaultResp.ToBytes(), b.Bytes())
|
||||
assert.Equal(t, tc.wantPeer, peer)
|
||||
})
|
||||
conn := &fakePacketConn{
|
||||
writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) {
|
||||
assert.Equal(t, tc.want, addr)
|
||||
|
||||
b.Reset()
|
||||
peer = nil
|
||||
return 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("giaddr_nak", func(t *testing.T) {
|
||||
req := &dhcpv4.DHCPv4{
|
||||
GatewayIPAddr: knownIP,
|
||||
}
|
||||
// Ensure the request is for unicast.
|
||||
req.SetUnicast()
|
||||
resp := &dhcpv4.DHCPv4{
|
||||
Options: dhcpv4.OptionsFromList(
|
||||
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
|
||||
),
|
||||
}
|
||||
want := &net.UDPAddr{
|
||||
IP: req.GatewayIPAddr,
|
||||
Port: dhcpv4.ServerPort,
|
||||
}
|
||||
|
||||
conn := &fakePacketConn{
|
||||
writeTo: func(_ []byte, addr net.Addr) (n int, err error) {
|
||||
assert.Equal(t, want, addr)
|
||||
|
||||
return 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
s.send(cloneUDPAddr(defaultPeer), conn, req, resp)
|
||||
assert.True(t, resp.IsBroadcast())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6/server6"
|
||||
"github.com/insomniacslk/dhcp/iana"
|
||||
@@ -707,7 +708,7 @@ func v6Create(conf V6ServerConf) (DHCPServer, error) {
|
||||
}
|
||||
|
||||
if conf.LeaseDuration == 0 {
|
||||
s.conf.leaseTime = time.Hour * 24
|
||||
s.conf.leaseTime = timeutil.Day
|
||||
s.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds())
|
||||
} else {
|
||||
s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testTLSConn is a tlsConn for tests.
|
||||
@@ -179,13 +179,7 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||
clientID, err := srv.clientIDFromDNSContext(pctx)
|
||||
assert.Equal(t, tc.wantClientID, clientID)
|
||||
|
||||
if tc.wantErrMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantErrMsg, err.Error())
|
||||
}
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -250,13 +244,7 @@ func TestClientIDFromDNSContextHTTPS(t *testing.T) {
|
||||
clientID, err := clientIDFromDNSContextHTTPS(pctx)
|
||||
assert.Equal(t, tc.wantClientID, clientID)
|
||||
|
||||
if tc.wantErrMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
|
||||
assert.Equal(t, tc.wantErrMsg, err.Error())
|
||||
}
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtime"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
@@ -19,6 +18,7 @@ import (
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"github.com/ameshkov/dnscrypt/v2"
|
||||
)
|
||||
|
||||
@@ -90,7 +90,7 @@ type FilteringConfig struct {
|
||||
FastestAddr bool `yaml:"fastest_addr"` // use Fastest Address algorithm
|
||||
// FastestTimeout replaces the default timeout for dialing IP addresses
|
||||
// when FastestAddr is true.
|
||||
FastestTimeout aghtime.Duration `yaml:"fastest_timeout"`
|
||||
FastestTimeout timeutil.Duration `yaml:"fastest_timeout"`
|
||||
|
||||
// Access settings
|
||||
// --
|
||||
|
||||
@@ -90,7 +90,7 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error {
|
||||
s.processRestrictLocal,
|
||||
s.processInternalIPAddrs,
|
||||
s.processClientID,
|
||||
processFilteringBeforeRequest,
|
||||
s.processFilteringBeforeRequest,
|
||||
s.processLocalPTR,
|
||||
s.processUpstream,
|
||||
processDNSSECAfterResponse,
|
||||
@@ -468,19 +468,18 @@ func (s *Server) processLocalPTR(ctx *dnsContext) (rc resultCode) {
|
||||
}
|
||||
|
||||
// Apply filtering logic
|
||||
func processFilteringBeforeRequest(ctx *dnsContext) (rc resultCode) {
|
||||
s := ctx.srv
|
||||
d := ctx.proxyCtx
|
||||
|
||||
if d.Res != nil {
|
||||
return resultCodeSuccess // response is already set - nothing to do
|
||||
func (s *Server) processFilteringBeforeRequest(ctx *dnsContext) (rc resultCode) {
|
||||
if ctx.proxyCtx.Res != nil {
|
||||
// Go on since the response is already set.
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
s.serverLock.RLock()
|
||||
defer s.serverLock.RUnlock()
|
||||
|
||||
ctx.protectionEnabled = s.conf.ProtectionEnabled && s.dnsFilter != nil
|
||||
if !ctx.protectionEnabled {
|
||||
ctx.protectionEnabled = s.conf.ProtectionEnabled
|
||||
|
||||
if s.dnsFilter == nil {
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
@@ -489,8 +488,7 @@ func processFilteringBeforeRequest(ctx *dnsContext) (rc resultCode) {
|
||||
}
|
||||
|
||||
var err error
|
||||
ctx.result, err = s.filterDNSRequest(ctx)
|
||||
if err != nil {
|
||||
if ctx.result, err = s.filterDNSRequest(ctx); err != nil {
|
||||
ctx.err = err
|
||||
|
||||
return resultCodeError
|
||||
@@ -540,8 +538,16 @@ func (s *Server) processUpstream(ctx *dnsContext) (rc resultCode) {
|
||||
}
|
||||
}
|
||||
|
||||
// request was not filtered so let it be processed further
|
||||
if ctx.err = s.dnsProxy.Resolve(d); ctx.err != nil {
|
||||
// Process the request further since it wasn't filtered.
|
||||
|
||||
prx := s.proxy()
|
||||
if prx == nil {
|
||||
ctx.err = srvClosedErr
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
if ctx.err = prx.Resolve(d); ctx.err != nil {
|
||||
return resultCodeError
|
||||
}
|
||||
|
||||
@@ -600,48 +606,50 @@ func processDNSSECAfterResponse(ctx *dnsContext) (rc resultCode) {
|
||||
func processFilteringAfterResponse(ctx *dnsContext) (rc resultCode) {
|
||||
s := ctx.srv
|
||||
d := ctx.proxyCtx
|
||||
res := ctx.result
|
||||
var err error
|
||||
|
||||
switch res.Reason {
|
||||
case filtering.Rewritten,
|
||||
switch res := ctx.result; res.Reason {
|
||||
case filtering.NotFilteredAllowList:
|
||||
// Go on.
|
||||
case
|
||||
filtering.Rewritten,
|
||||
filtering.RewrittenRule:
|
||||
|
||||
if len(ctx.origQuestion.Name) == 0 {
|
||||
// origQuestion is set in case we get only CNAME without IP from rewrites table
|
||||
// origQuestion is set in case we get only CNAME without IP from
|
||||
// rewrites table.
|
||||
break
|
||||
}
|
||||
|
||||
d.Req.Question[0] = ctx.origQuestion
|
||||
d.Res.Question[0] = ctx.origQuestion
|
||||
|
||||
if len(d.Res.Answer) != 0 {
|
||||
answer := []dns.RR{}
|
||||
answer = append(answer, s.genAnswerCNAME(d.Req, res.CanonName))
|
||||
answer = append(answer, d.Res.Answer...)
|
||||
d.Req.Question[0], d.Res.Question[0] = ctx.origQuestion, ctx.origQuestion
|
||||
if len(d.Res.Answer) > 0 {
|
||||
answer := append([]dns.RR{s.genAnswerCNAME(d.Req, res.CanonName)}, d.Res.Answer...)
|
||||
d.Res.Answer = answer
|
||||
}
|
||||
|
||||
case filtering.NotFilteredAllowList:
|
||||
// nothing
|
||||
|
||||
default:
|
||||
if !ctx.protectionEnabled || // filters are disabled: there's nothing to check for
|
||||
!ctx.responseFromUpstream { // only check response if it's from an upstream server
|
||||
// Check the response only if the it's from an upstream. Don't check
|
||||
// the response if the protection is disabled since dnsrewrite rules
|
||||
// aren't applied to it anyway.
|
||||
if !ctx.protectionEnabled || !ctx.responseFromUpstream || s.dnsFilter == nil {
|
||||
break
|
||||
}
|
||||
origResp2 := d.Res
|
||||
ctx.result, err = s.filterDNSResponse(ctx)
|
||||
|
||||
origResp := d.Res
|
||||
result, err := s.filterDNSResponse(ctx)
|
||||
if err != nil {
|
||||
ctx.err = err
|
||||
|
||||
return resultCodeError
|
||||
}
|
||||
if ctx.result != nil {
|
||||
ctx.origResp = origResp2 // matched by response
|
||||
} else {
|
||||
ctx.result = &filtering.Result{}
|
||||
|
||||
if result != nil {
|
||||
ctx.result = result
|
||||
ctx.origResp = origResp
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.result == nil {
|
||||
ctx.result = &filtering.Result{}
|
||||
}
|
||||
|
||||
return resultCodeSuccess
|
||||
}
|
||||
|
||||
@@ -551,6 +551,21 @@ func (s *Server) IsRunning() bool {
|
||||
return s.isRunning
|
||||
}
|
||||
|
||||
// srvClosedErr is returned when the method can't complete without unacessible
|
||||
// data from the closing server.
|
||||
const srvClosedErr errors.Error = "server is closed"
|
||||
|
||||
// proxy returns a pointer to the current DNS proxy instance. If p is nil, the
|
||||
// server is closing.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/3655.
|
||||
func (s *Server) proxy() (p *proxy.Proxy) {
|
||||
s.serverLock.RLock()
|
||||
defer s.serverLock.RUnlock()
|
||||
|
||||
return s.dnsProxy
|
||||
}
|
||||
|
||||
// Reconfigure applies the new configuration to the DNS server.
|
||||
func (s *Server) Reconfigure(config *ServerConfig) error {
|
||||
s.serverLock.Lock()
|
||||
@@ -581,17 +596,8 @@ func (s *Server) Reconfigure(config *ServerConfig) error {
|
||||
|
||||
// ServeHTTP is a HTTP handler method we use to provide DNS-over-HTTPS.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var p *proxy.Proxy
|
||||
|
||||
func() {
|
||||
s.serverLock.RLock()
|
||||
defer s.serverLock.RUnlock()
|
||||
|
||||
p = s.dnsProxy
|
||||
}()
|
||||
|
||||
if p != nil {
|
||||
p.ServeHTTP(w, r)
|
||||
if prx := s.proxy(); prx != nil {
|
||||
prx.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
@@ -23,6 +24,8 @@ import (
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -43,10 +46,7 @@ func startDeferStop(t *testing.T, s *Server) {
|
||||
err := s.Start()
|
||||
require.NoErrorf(t, err, "failed to start server: %s", err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
serr := s.Stop()
|
||||
require.NoErrorf(t, serr, "dns server failed to stop: %s", serr)
|
||||
})
|
||||
testutil.CleanupAndRequireSuccess(t, s.Stop)
|
||||
}
|
||||
|
||||
func createTestServer(
|
||||
@@ -107,7 +107,7 @@ func createServerTLSConfig(t *testing.T) (*tls.Config, []byte, []byte) {
|
||||
require.NoErrorf(t, err, "failed to generate serial number: %s", err)
|
||||
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(5 * 365 * time.Hour * 24)
|
||||
notAfter := notBefore.Add(5 * 365 * timeutil.Day)
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
@@ -907,6 +907,7 @@ func TestRewrite(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
f := filtering.New(c, nil)
|
||||
f.SetEnabled(true)
|
||||
|
||||
snd, err := aghnet.NewSubnetDetector()
|
||||
require.NoError(t, err)
|
||||
@@ -943,45 +944,56 @@ func TestRewrite(t *testing.T) {
|
||||
|
||||
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
|
||||
|
||||
req := createTestMessageWithType("test.com.", dns.TypeA)
|
||||
reply, err := dns.Exchange(req, addr.String())
|
||||
require.NoError(t, err)
|
||||
subTestFunc := func(t *testing.T) {
|
||||
req := createTestMessageWithType("test.com.", dns.TypeA)
|
||||
reply, eerr := dns.Exchange(req, addr.String())
|
||||
require.NoError(t, eerr)
|
||||
|
||||
require.Len(t, reply.Answer, 1)
|
||||
require.Len(t, reply.Answer, 1)
|
||||
|
||||
a, ok := reply.Answer[0].(*dns.A)
|
||||
require.True(t, ok)
|
||||
a, ok := reply.Answer[0].(*dns.A)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.True(t, net.IP{1, 2, 3, 4}.Equal(a.A))
|
||||
assert.True(t, net.IP{1, 2, 3, 4}.Equal(a.A))
|
||||
|
||||
req = createTestMessageWithType("test.com.", dns.TypeAAAA)
|
||||
reply, err = dns.Exchange(req, addr.String())
|
||||
require.NoError(t, err)
|
||||
req = createTestMessageWithType("test.com.", dns.TypeAAAA)
|
||||
reply, eerr = dns.Exchange(req, addr.String())
|
||||
require.NoError(t, eerr)
|
||||
|
||||
assert.Empty(t, reply.Answer)
|
||||
assert.Empty(t, reply.Answer)
|
||||
|
||||
req = createTestMessageWithType("alias.test.com.", dns.TypeA)
|
||||
reply, err = dns.Exchange(req, addr.String())
|
||||
require.NoError(t, err)
|
||||
req = createTestMessageWithType("alias.test.com.", dns.TypeA)
|
||||
reply, eerr = dns.Exchange(req, addr.String())
|
||||
require.NoError(t, eerr)
|
||||
|
||||
require.Len(t, reply.Answer, 2)
|
||||
require.Len(t, reply.Answer, 2)
|
||||
|
||||
assert.Equal(t, "test.com.", reply.Answer[0].(*dns.CNAME).Target)
|
||||
assert.True(t, net.IP{1, 2, 3, 4}.Equal(reply.Answer[1].(*dns.A).A))
|
||||
assert.Equal(t, "test.com.", reply.Answer[0].(*dns.CNAME).Target)
|
||||
assert.True(t, net.IP{1, 2, 3, 4}.Equal(reply.Answer[1].(*dns.A).A))
|
||||
|
||||
req = createTestMessageWithType("my.alias.example.org.", dns.TypeA)
|
||||
reply, err = dns.Exchange(req, addr.String())
|
||||
require.NoError(t, err)
|
||||
req = createTestMessageWithType("my.alias.example.org.", dns.TypeA)
|
||||
reply, eerr = dns.Exchange(req, addr.String())
|
||||
require.NoError(t, eerr)
|
||||
|
||||
// The original question is restored.
|
||||
require.Len(t, reply.Question, 1)
|
||||
// The original question is restored.
|
||||
require.Len(t, reply.Question, 1)
|
||||
|
||||
assert.Equal(t, "my.alias.example.org.", reply.Question[0].Name)
|
||||
assert.Equal(t, "my.alias.example.org.", reply.Question[0].Name)
|
||||
|
||||
require.Len(t, reply.Answer, 2)
|
||||
require.Len(t, reply.Answer, 2)
|
||||
|
||||
assert.Equal(t, "example.org.", reply.Answer[0].(*dns.CNAME).Target)
|
||||
assert.Equal(t, dns.TypeA, reply.Answer[1].Header().Rrtype)
|
||||
assert.Equal(t, "example.org.", reply.Answer[0].(*dns.CNAME).Target)
|
||||
assert.Equal(t, dns.TypeA, reply.Answer[1].Header().Rrtype)
|
||||
}
|
||||
|
||||
for _, protect := range []bool{true, false} {
|
||||
val := protect
|
||||
conf := s.getDNSConfig()
|
||||
conf.ProtectionEnabled = &val
|
||||
s.setConfig(conf)
|
||||
|
||||
t.Run(fmt.Sprintf("protection_is_%t", val), subTestFunc)
|
||||
}
|
||||
}
|
||||
|
||||
func publicKey(priv interface{}) interface{} {
|
||||
@@ -1035,9 +1047,7 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
|
||||
err = s.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
s.Close()
|
||||
})
|
||||
t.Cleanup(s.Close)
|
||||
|
||||
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
|
||||
req := createTestMessageWithType("34.12.168.192.in-addr.arpa.", dns.TypePTR)
|
||||
@@ -1056,23 +1066,44 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPTRResponseFromHosts(t *testing.T) {
|
||||
c := filtering.Config{
|
||||
EtcHosts: &aghnet.EtcHostsContainer{},
|
||||
// Prepare test hosts file.
|
||||
|
||||
const hostsFilename = "hosts"
|
||||
|
||||
testFS := fstest.MapFS{
|
||||
hostsFilename: &fstest.MapFile{Data: []byte(`
|
||||
127.0.0.1 host # comment
|
||||
::1 localhost#comment
|
||||
`)},
|
||||
}
|
||||
|
||||
// Prepare test hosts file.
|
||||
hf, err := os.CreateTemp("", "")
|
||||
const closeCalled errors.Error = "close method called"
|
||||
|
||||
var eventsCalledCounter uint32
|
||||
hc, err := aghnet.NewHostsContainer(testFS, &aghtest.FSWatcher{
|
||||
OnEvents: func() (e <-chan struct{}) {
|
||||
assert.Equal(t, uint32(1), atomic.AddUint32(&eventsCalledCounter, 1))
|
||||
|
||||
return nil
|
||||
},
|
||||
OnAdd: func(name string) (err error) {
|
||||
assert.Equal(t, hostsFilename, name)
|
||||
|
||||
return nil
|
||||
},
|
||||
OnClose: func() (err error) { return closeCalled },
|
||||
}, hostsFilename)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, hf.Close())
|
||||
assert.NoError(t, os.Remove(hf.Name()))
|
||||
assert.Equal(t, uint32(1), atomic.LoadUint32(&eventsCalledCounter))
|
||||
|
||||
require.ErrorIs(t, hc.Close(), closeCalled)
|
||||
})
|
||||
|
||||
_, _ = hf.WriteString(" 127.0.0.1 host # comment \n")
|
||||
_, _ = hf.WriteString(" ::1 localhost#comment \n")
|
||||
|
||||
c.EtcHosts.Init(hf.Name())
|
||||
t.Cleanup(c.EtcHosts.Close)
|
||||
flt := filtering.New(&filtering.Config{
|
||||
EtcHosts: hc,
|
||||
}, nil)
|
||||
flt.SetEnabled(true)
|
||||
|
||||
var snd *aghnet.SubnetDetector
|
||||
snd, err = aghnet.NewSubnetDetector()
|
||||
@@ -1082,7 +1113,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
|
||||
var s *Server
|
||||
s, err = NewServer(DNSCreateParams{
|
||||
DHCPServer: &testDHCP{},
|
||||
DNSFilter: filtering.New(&c, nil),
|
||||
DNSFilter: flt,
|
||||
SubnetDetector: snd,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -1090,32 +1121,39 @@ func TestPTRResponseFromHosts(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
|
||||
|
||||
err = s.Prepare(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = s.Start()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(s.Close)
|
||||
|
||||
t.Cleanup(func() {
|
||||
s.Close()
|
||||
})
|
||||
subTestFunc := func(t *testing.T) {
|
||||
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
|
||||
req := createTestMessageWithType("1.0.0.127.in-addr.arpa.", dns.TypePTR)
|
||||
|
||||
addr := s.dnsProxy.Addr(proxy.ProtoUDP)
|
||||
req := createTestMessageWithType("1.0.0.127.in-addr.arpa.", dns.TypePTR)
|
||||
resp, eerr := dns.Exchange(req, addr.String())
|
||||
require.NoError(t, eerr)
|
||||
|
||||
resp, err := dns.Exchange(req, addr.String())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Answer, 1)
|
||||
|
||||
require.Len(t, resp.Answer, 1)
|
||||
assert.Equal(t, dns.TypePTR, resp.Answer[0].Header().Rrtype)
|
||||
assert.Equal(t, "1.0.0.127.in-addr.arpa.", resp.Answer[0].Header().Name)
|
||||
|
||||
assert.Equal(t, dns.TypePTR, resp.Answer[0].Header().Rrtype)
|
||||
assert.Equal(t, "1.0.0.127.in-addr.arpa.", resp.Answer[0].Header().Name)
|
||||
ptr, ok := resp.Answer[0].(*dns.PTR)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "host.", ptr.Ptr)
|
||||
}
|
||||
|
||||
ptr, ok := resp.Answer[0].(*dns.PTR)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "host.", ptr.Ptr)
|
||||
for _, protect := range []bool{true, false} {
|
||||
val := protect
|
||||
conf := s.getDNSConfig()
|
||||
conf.ProtectionEnabled = &val
|
||||
s.setConfig(conf)
|
||||
|
||||
t.Run(fmt.Sprintf("protection_is_%t", val), subTestFunc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServer(t *testing.T) {
|
||||
@@ -1153,12 +1191,7 @@ func TestNewServer(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := NewServer(tc.in)
|
||||
if tc.wantErrMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tc.wantErrMsg, err.Error())
|
||||
}
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
|
||||
d := &proxy.DNSContext{}
|
||||
|
||||
err := srv.filterDNSRewrite(req, res, d)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, dns.RcodeNameError, d.Res.Rcode)
|
||||
})
|
||||
|
||||
@@ -72,7 +72,8 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
|
||||
d := &proxy.DNSContext{}
|
||||
|
||||
err := srv.filterDNSRewrite(req, res, d)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)
|
||||
assert.Empty(t, d.Res.Answer)
|
||||
})
|
||||
@@ -83,7 +84,8 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
|
||||
d := &proxy.DNSContext{}
|
||||
|
||||
err := srv.filterDNSRewrite(req, res, d)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)
|
||||
|
||||
require.Len(t, d.Res.Answer, 1)
|
||||
@@ -96,7 +98,8 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
|
||||
d := &proxy.DNSContext{}
|
||||
|
||||
err := srv.filterDNSRewrite(req, res, d)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)
|
||||
|
||||
require.Len(t, d.Res.Answer, 1)
|
||||
@@ -109,7 +112,8 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
|
||||
d := &proxy.DNSContext{}
|
||||
|
||||
err := srv.filterDNSRewrite(req, res, d)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)
|
||||
|
||||
require.Len(t, d.Res.Answer, 1)
|
||||
@@ -122,7 +126,8 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
|
||||
d := &proxy.DNSContext{}
|
||||
|
||||
err := srv.filterDNSRewrite(req, res, d)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)
|
||||
|
||||
require.Len(t, d.Res.Answer, 1)
|
||||
@@ -135,7 +140,8 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
|
||||
d := &proxy.DNSContext{}
|
||||
|
||||
err := srv.filterDNSRewrite(req, res, d)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)
|
||||
|
||||
require.Len(t, d.Res.Answer, 1)
|
||||
@@ -152,7 +158,8 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
|
||||
d := &proxy.DNSContext{}
|
||||
|
||||
err := srv.filterDNSRewrite(req, res, d)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)
|
||||
|
||||
require.Len(t, d.Res.Answer, 1)
|
||||
@@ -171,7 +178,8 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
|
||||
d := &proxy.DNSContext{}
|
||||
|
||||
err := srv.filterDNSRewrite(req, res, d)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)
|
||||
|
||||
require.Len(t, d.Res.Answer, 1)
|
||||
@@ -190,7 +198,8 @@ func TestServer_FilterDNSRewrite(t *testing.T) {
|
||||
d := &proxy.DNSContext{}
|
||||
|
||||
err := srv.filterDNSRewrite(req, res, d)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, dns.RcodeSuccess, d.Res.Rcode)
|
||||
|
||||
require.Len(t, d.Res.Answer, 1)
|
||||
|
||||
@@ -52,6 +52,7 @@ func (s *Server) beforeRequestHandler(
|
||||
// the client's IP address and ID, if any, from ctx.
|
||||
func (s *Server) getClientRequestFilteringSettings(ctx *dnsContext) *filtering.Settings {
|
||||
setts := s.dnsFilter.GetConfig()
|
||||
setts.ProtectionEnabled = ctx.protectionEnabled
|
||||
if s.conf.FilterHandler != nil {
|
||||
ip, _ := netutil.IPAndPortFromAddr(ctx.proxyCtx.Addr)
|
||||
s.conf.FilterHandler(ip, ctx.clientID, &setts)
|
||||
@@ -65,32 +66,31 @@ func (s *Server) getClientRequestFilteringSettings(ctx *dnsContext) *filtering.S
|
||||
func (s *Server) filterDNSRequest(ctx *dnsContext) (*filtering.Result, error) {
|
||||
d := ctx.proxyCtx
|
||||
req := d.Req
|
||||
host := strings.TrimSuffix(req.Question[0].Name, ".")
|
||||
res, err := s.dnsFilter.CheckHost(host, req.Question[0].Qtype, ctx.setts)
|
||||
if err != nil {
|
||||
// Return immediately if there's an error
|
||||
return nil, fmt.Errorf("filtering failed to check host %q: %w", host, err)
|
||||
} else if res.IsFiltered {
|
||||
log.Tracef("Host %s is filtered, reason - %q, matched rule: %q", host, res.Reason, res.Rules[0].Text)
|
||||
q := req.Question[0]
|
||||
host := strings.TrimSuffix(q.Name, ".")
|
||||
res, err := s.dnsFilter.CheckHost(host, q.Qtype, ctx.setts)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("failed to check host %q: %w", host, err)
|
||||
case res.IsFiltered:
|
||||
log.Tracef("host %q is filtered, reason %q, rule: %q", host, res.Reason, res.Rules[0].Text)
|
||||
d.Res = s.genDNSFilterMessage(d, &res)
|
||||
} else if res.Reason.In(filtering.Rewritten, filtering.RewrittenRule) &&
|
||||
case res.Reason.In(filtering.Rewritten, filtering.RewrittenRule) &&
|
||||
res.CanonName != "" &&
|
||||
len(res.IPList) == 0 {
|
||||
// Resolve the new canonical name, not the original host
|
||||
// name. The original question is readded in
|
||||
// processFilteringAfterResponse.
|
||||
ctx.origQuestion = req.Question[0]
|
||||
len(res.IPList) == 0:
|
||||
// Resolve the new canonical name, not the original host name. The
|
||||
// original question is readded in processFilteringAfterResponse.
|
||||
ctx.origQuestion = q
|
||||
req.Question[0].Name = dns.Fqdn(res.CanonName)
|
||||
} else if res.Reason == filtering.RewrittenAutoHosts && len(res.ReverseHosts) != 0 {
|
||||
case res.Reason == filtering.RewrittenAutoHosts && len(res.ReverseHosts) != 0:
|
||||
resp := s.makeResponse(req)
|
||||
hdr := dns.RR_Header{
|
||||
Name: q.Name,
|
||||
Rrtype: dns.TypePTR,
|
||||
Ttl: s.conf.BlockedResponseTTL,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
for _, h := range res.ReverseHosts {
|
||||
hdr := dns.RR_Header{
|
||||
Name: req.Question[0].Name,
|
||||
Rrtype: dns.TypePTR,
|
||||
Ttl: s.conf.BlockedResponseTTL,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
|
||||
ptr := &dns.PTR{
|
||||
Hdr: hdr,
|
||||
Ptr: h,
|
||||
@@ -100,7 +100,7 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*filtering.Result, error) {
|
||||
}
|
||||
|
||||
d.Res = resp
|
||||
} else if res.Reason.In(filtering.Rewritten, filtering.RewrittenAutoHosts) {
|
||||
case res.Reason.In(filtering.Rewritten, filtering.RewrittenAutoHosts):
|
||||
resp := s.makeResponse(req)
|
||||
|
||||
name := host
|
||||
@@ -110,11 +110,12 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*filtering.Result, error) {
|
||||
}
|
||||
|
||||
for _, ip := range res.IPList {
|
||||
if req.Question[0].Qtype == dns.TypeA {
|
||||
switch q.Qtype {
|
||||
case dns.TypeA:
|
||||
a := s.genAnswerA(req, ip.To4())
|
||||
a.Hdr.Name = dns.Fqdn(name)
|
||||
resp.Answer = append(resp.Answer, a)
|
||||
} else if req.Question[0].Qtype == dns.TypeAAAA {
|
||||
case dns.TypeAAAA:
|
||||
a := s.genAnswerAAAA(req, ip)
|
||||
a.Hdr.Name = dns.Fqdn(name)
|
||||
resp.Answer = append(resp.Answer, a)
|
||||
@@ -122,9 +123,8 @@ func (s *Server) filterDNSRequest(ctx *dnsContext) (*filtering.Result, error) {
|
||||
}
|
||||
|
||||
d.Res = resp
|
||||
} else if res.Reason == filtering.RewrittenRule {
|
||||
err = s.filterDNSRewrite(req, res, d)
|
||||
if err != nil {
|
||||
case res.Reason == filtering.RewrittenRule:
|
||||
if err = s.filterDNSRewrite(req, res, d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -179,6 +179,7 @@ func (s *Server) filterDNSResponse(ctx *dnsContext) (*filtering.Result, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
host = strings.TrimSuffix(host, ".")
|
||||
res, err := s.checkHostRules(host, d.Req.Question[0].Qtype, ctx.setts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -38,9 +39,7 @@ func loadTestData(t *testing.T, casesFileName string, cases interface{}) {
|
||||
var f *os.File
|
||||
f, err := os.Open(filepath.Join("testdata", casesFileName))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, f.Close())
|
||||
})
|
||||
testutil.CleanupAndRequireSuccess(t, f.Close)
|
||||
|
||||
err = json.NewDecoder(f).Decode(cases)
|
||||
require.NoError(t, err)
|
||||
@@ -69,10 +68,8 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
|
||||
s := createTestServer(t, filterConf, forwardConf, nil)
|
||||
s.sysResolvers = &fakeSystemResolvers{}
|
||||
|
||||
require.Nil(t, s.Start())
|
||||
t.Cleanup(func() {
|
||||
require.Nil(t, s.Stop())
|
||||
})
|
||||
require.NoError(t, s.Start())
|
||||
testutil.CleanupAndRequireSuccess(t, s.Stop)
|
||||
|
||||
defaultConf := s.conf
|
||||
|
||||
@@ -147,10 +144,8 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
defaultConf := s.conf
|
||||
|
||||
err := s.Start()
|
||||
assert.Nil(t, err)
|
||||
t.Cleanup(func() {
|
||||
assert.Nil(t, s.Stop())
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
testutil.CleanupAndRequireSuccess(t, s.Stop)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
@@ -221,14 +216,12 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
s.conf = defaultConf
|
||||
})
|
||||
t.Cleanup(func() { s.conf = defaultConf })
|
||||
|
||||
rBody := io.NopCloser(bytes.NewReader(caseData.Req))
|
||||
var r *http.Request
|
||||
r, err = http.NewRequest(http.MethodPost, "http://example.com", rBody)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.handleSetConfig(w, r)
|
||||
assert.Equal(t, tc.wantSet, strings.TrimSuffix(w.Body.String(), "\n"))
|
||||
|
||||
@@ -249,9 +249,17 @@ func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, d *proxy.DNSCo
|
||||
Req: &replReq,
|
||||
}
|
||||
|
||||
err := s.dnsProxy.Resolve(newContext)
|
||||
prx := s.proxy()
|
||||
if prx == nil {
|
||||
log.Debug("dns: %s", srvClosedErr)
|
||||
|
||||
return s.genServerFailure(request)
|
||||
}
|
||||
|
||||
err := prx.Resolve(newContext)
|
||||
if err != nil {
|
||||
log.Printf("Couldn't look up replacement host %q: %s", newAddr, err)
|
||||
log.Printf("couldn't look up replacement host %q: %s", newAddr, err)
|
||||
|
||||
return s.genServerFailure(request)
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
}}
|
||||
|
||||
ups, err := upstream.AddressToUpstream("1.1.1.1", nil)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
@@ -15,9 +15,9 @@ type DNSRewriteResult struct {
|
||||
// the server returns.
|
||||
type DNSRewriteResultResponse map[rules.RRType][]rules.RRValue
|
||||
|
||||
// processDNSRewrites processes DNS rewrite rules in dnsr. It returns
|
||||
// an empty result if dnsr is empty. Otherwise, the result will have
|
||||
// either CanonName or DNSRewriteResult set.
|
||||
// processDNSRewrites processes DNS rewrite rules in dnsr. It returns an empty
|
||||
// result if dnsr is empty. Otherwise, the result will have either CanonName or
|
||||
// DNSRewriteResult set.
|
||||
func (d *DNSFilter) processDNSRewrites(dnsr []*rules.NetworkRule) (res Result) {
|
||||
if len(dnsr) == 0 {
|
||||
return Result{}
|
||||
|
||||
@@ -38,6 +38,7 @@ type Settings struct {
|
||||
|
||||
ServicesRules []ServiceEntry
|
||||
|
||||
ProtectionEnabled bool
|
||||
FilteringEnabled bool
|
||||
SafeSearchEnabled bool
|
||||
SafeBrowsingEnabled bool
|
||||
@@ -73,7 +74,7 @@ type Config struct {
|
||||
|
||||
// EtcHosts is a container of IP-hostname pairs taken from the operating
|
||||
// system configuration files (e.g. /etc/hosts).
|
||||
EtcHosts *aghnet.EtcHostsContainer `yaml:"-"`
|
||||
EtcHosts *aghnet.HostsContainer `yaml:"-"`
|
||||
|
||||
// Called when the configuration is changed by HTTP request
|
||||
ConfigModified func() `yaml:"-"`
|
||||
@@ -176,8 +177,8 @@ const (
|
||||
// FilteredBlockedService - the host is blocked by "blocked services" settings
|
||||
FilteredBlockedService
|
||||
|
||||
// Rewritten is returned when there was a rewrite by a legacy DNS
|
||||
// rewrite rule.
|
||||
// Rewritten is returned when there was a rewrite by a legacy DNS rewrite
|
||||
// rule.
|
||||
Rewritten
|
||||
|
||||
// RewrittenAutoHosts is returned when there was a rewrite by autohosts
|
||||
@@ -186,8 +187,8 @@ const (
|
||||
|
||||
// RewrittenRule is returned when a $dnsrewrite filter rule was applied.
|
||||
//
|
||||
// TODO(a.garipov): Remove Rewritten and RewrittenAutoHosts by merging
|
||||
// their functionality into RewrittenRule.
|
||||
// TODO(a.garipov): Remove Rewritten and RewrittenAutoHosts by merging their
|
||||
// functionality into RewrittenRule.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2499.
|
||||
RewrittenRule
|
||||
@@ -221,12 +222,13 @@ func (r Reason) String() string {
|
||||
}
|
||||
|
||||
// In returns true if reasons include r.
|
||||
func (r Reason) In(reasons ...Reason) bool {
|
||||
func (r Reason) In(reasons ...Reason) (ok bool) {
|
||||
for _, reason := range reasons {
|
||||
if r == reason {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -245,7 +247,7 @@ func (d *DNSFilter) GetConfig() (s Settings) {
|
||||
defer d.confLock.RUnlock()
|
||||
|
||||
return Settings{
|
||||
FilteringEnabled: atomic.LoadUint32(&d.Config.enabled) == 1,
|
||||
FilteringEnabled: atomic.LoadUint32(&d.Config.enabled) != 0,
|
||||
SafeSearchEnabled: d.Config.SafeSearchEnabled,
|
||||
SafeBrowsingEnabled: d.Config.SafeBrowsingEnabled,
|
||||
ParentalEnabled: d.Config.ParentalEnabled,
|
||||
@@ -371,24 +373,23 @@ type Result struct {
|
||||
// Reason is the reason for blocking or unblocking the request.
|
||||
Reason Reason `json:",omitempty"`
|
||||
|
||||
// Rules are applied rules. If Rules are not empty, each rule
|
||||
// is not nil.
|
||||
// Rules are applied rules. If Rules are not empty, each rule is not nil.
|
||||
Rules []*ResultRule `json:",omitempty"`
|
||||
|
||||
// ReverseHosts is the reverse lookup rewrite result. It is
|
||||
// empty unless Reason is set to RewrittenAutoHosts.
|
||||
// ReverseHosts is the reverse lookup rewrite result. It is empty unless
|
||||
// Reason is set to RewrittenAutoHosts.
|
||||
ReverseHosts []string `json:",omitempty"`
|
||||
|
||||
// IPList is the lookup rewrite result. It is empty unless
|
||||
// Reason is set to RewrittenAutoHosts or Rewritten.
|
||||
// IPList is the lookup rewrite result. It is empty unless Reason is set to
|
||||
// RewrittenAutoHosts or Rewritten.
|
||||
IPList []net.IP `json:",omitempty"`
|
||||
|
||||
// CanonName is the CNAME value from the lookup rewrite result.
|
||||
// It is empty unless Reason is set to Rewritten or RewrittenRule.
|
||||
// CanonName is the CNAME value from the lookup rewrite result. It is empty
|
||||
// unless Reason is set to Rewritten or RewrittenRule.
|
||||
CanonName string `json:",omitempty"`
|
||||
|
||||
// ServiceName is the name of the blocked service. It is empty
|
||||
// unless Reason is set to FilteredBlockedService.
|
||||
// ServiceName is the name of the blocked service. It is empty unless
|
||||
// Reason is set to FilteredBlockedService.
|
||||
ServiceName string `json:",omitempty"`
|
||||
|
||||
// DNSRewriteResult is the $dnsrewrite filter rule result.
|
||||
@@ -422,14 +423,16 @@ func (d *DNSFilter) CheckHost(
|
||||
// Sometimes clients try to resolve ".", which is a request to get root
|
||||
// servers.
|
||||
if host == "" {
|
||||
return Result{Reason: NotFilteredNotFound}, nil
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
host = strings.ToLower(host)
|
||||
|
||||
res = d.processRewrites(host, qtype)
|
||||
if res.Reason == Rewritten {
|
||||
return res, nil
|
||||
if setts.FilteringEnabled {
|
||||
res = d.processRewrites(host, qtype)
|
||||
if res.Reason == Rewritten {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, hc := range d.hostCheckers {
|
||||
@@ -446,43 +449,47 @@ func (d *DNSFilter) CheckHost(
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
// checkEtcHosts compares the host against our /etc/hosts table. The err is
|
||||
// always nil, it is only there to make this a valid hostChecker function.
|
||||
func (d *DNSFilter) checkEtcHosts(
|
||||
host string,
|
||||
qtype uint16,
|
||||
_ *Settings,
|
||||
) (res Result, err error) {
|
||||
if d.Config.EtcHosts == nil {
|
||||
// matchSysHosts tries to match the host against the operating system's hosts
|
||||
// database.
|
||||
func (d *DNSFilter) matchSysHosts(host string, qtype uint16, setts *Settings) (res Result, err error) {
|
||||
if !setts.FilteringEnabled || d.EtcHosts == nil {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
ips := d.Config.EtcHosts.Process(host, qtype)
|
||||
if ips != nil {
|
||||
res = Result{
|
||||
Reason: RewrittenAutoHosts,
|
||||
IPList: ips,
|
||||
}
|
||||
dnsres, _ := d.EtcHosts.MatchRequest(urlfilter.DNSRequest{
|
||||
Hostname: host,
|
||||
SortedClientTags: setts.ClientTags,
|
||||
// TODO(e.burkov): Wait for urlfilter update to pass net.IP.
|
||||
ClientIP: setts.ClientIP.String(),
|
||||
ClientName: setts.ClientName,
|
||||
DNSType: qtype,
|
||||
})
|
||||
|
||||
return res, nil
|
||||
dnsr := dnsres.DNSRewrites()
|
||||
if len(dnsr) == 0 {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
revHosts := d.Config.EtcHosts.ProcessReverse(host, qtype)
|
||||
if len(revHosts) != 0 {
|
||||
res = Result{
|
||||
Reason: RewrittenAutoHosts,
|
||||
var ips []net.IP
|
||||
var revHosts []string
|
||||
for _, nr := range dnsr {
|
||||
if nr.DNSRewrite == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Optimize this with a buffer.
|
||||
res.ReverseHosts = make([]string, len(revHosts))
|
||||
for i := range revHosts {
|
||||
res.ReverseHosts[i] = revHosts[i] + "."
|
||||
switch val := nr.DNSRewrite.Value.(type) {
|
||||
case net.IP:
|
||||
ips = append(ips, val)
|
||||
case string:
|
||||
revHosts = append(revHosts, val)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return Result{}, nil
|
||||
return Result{
|
||||
Reason: RewrittenAutoHosts,
|
||||
IPList: ips,
|
||||
ReverseHosts: revHosts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Process rewrites table
|
||||
@@ -548,6 +555,10 @@ func matchBlockedServicesRules(
|
||||
_ uint16,
|
||||
setts *Settings,
|
||||
) (res Result, err error) {
|
||||
if !setts.ProtectionEnabled {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
svcs := setts.ServicesRules
|
||||
if len(svcs) == 0 {
|
||||
return Result{}, nil
|
||||
@@ -647,15 +658,18 @@ func (d *DNSFilter) initFiltering(allowFilters, blockFilters []Filter) error {
|
||||
return err
|
||||
}
|
||||
|
||||
d.engineLock.Lock()
|
||||
d.reset()
|
||||
d.rulesStorage = rulesStorage
|
||||
d.filteringEngine = filteringEngine
|
||||
d.rulesStorageAllow = rulesStorageAllow
|
||||
d.filteringEngineAllow = filteringEngineAllow
|
||||
d.engineLock.Unlock()
|
||||
func() {
|
||||
d.engineLock.Lock()
|
||||
defer d.engineLock.Unlock()
|
||||
|
||||
// Make sure that the OS reclaims memory as soon as possible
|
||||
d.reset()
|
||||
d.rulesStorage = rulesStorage
|
||||
d.filteringEngine = filteringEngine
|
||||
d.rulesStorageAllow = rulesStorageAllow
|
||||
d.filteringEngineAllow = filteringEngineAllow
|
||||
}()
|
||||
|
||||
// Make sure that the OS reclaims memory as soon as possible.
|
||||
debug.FreeOSMemory()
|
||||
log.Debug("initialized filtering engine")
|
||||
|
||||
@@ -734,8 +748,8 @@ func (d *DNSFilter) matchHostProcessDNSResult(
|
||||
}
|
||||
|
||||
if dnsres.HostRulesV4 != nil || dnsres.HostRulesV6 != nil {
|
||||
// Question type doesn't match the host rules. Return the first
|
||||
// matched host rule, but without an IP address.
|
||||
// Question type doesn't match the host rules. Return the first matched
|
||||
// host rule, but without an IP address.
|
||||
var matchedRules []rules.Rule
|
||||
if dnsres.HostRulesV4 != nil {
|
||||
matchedRules = []rules.Rule{dnsres.HostRulesV4[0]}
|
||||
@@ -760,11 +774,6 @@ func (d *DNSFilter) matchHost(
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
d.engineLock.RLock()
|
||||
// Keep in mind that this lock must be held no just when calling Match()
|
||||
// but also while using the rules returned by it.
|
||||
defer d.engineLock.RUnlock()
|
||||
|
||||
ureq := urlfilter.DNSRequest{
|
||||
Hostname: host,
|
||||
SortedClientTags: setts.ClientTags,
|
||||
@@ -774,7 +783,14 @@ func (d *DNSFilter) matchHost(
|
||||
DNSType: qtype,
|
||||
}
|
||||
|
||||
if d.filteringEngineAllow != nil {
|
||||
d.engineLock.RLock()
|
||||
// Keep in mind that this lock must be held no just when calling Match() but
|
||||
// also while using the rules returned by it.
|
||||
//
|
||||
// TODO(e.burkov): Inspect if the above is true.
|
||||
defer d.engineLock.RUnlock()
|
||||
|
||||
if setts.ProtectionEnabled && d.filteringEngineAllow != nil {
|
||||
dnsres, ok := d.filteringEngineAllow.MatchRequest(ureq)
|
||||
if ok {
|
||||
return d.matchHostProcessAllowList(host, dnsres)
|
||||
@@ -791,8 +807,8 @@ func (d *DNSFilter) matchHost(
|
||||
if dnsr := dnsres.DNSRewrites(); len(dnsr) > 0 {
|
||||
res = d.processDNSRewrites(dnsr)
|
||||
if res.Reason == RewrittenRule && res.CanonName == host {
|
||||
// A rewrite of a host to itself. Go on and try
|
||||
// matching other things.
|
||||
// A rewrite of a host to itself. Go on and try matching other
|
||||
// things.
|
||||
} else {
|
||||
return res, nil
|
||||
}
|
||||
@@ -800,6 +816,11 @@ func (d *DNSFilter) matchHost(
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
if !setts.ProtectionEnabled {
|
||||
// Don't check non-dnsrewrite filtering results.
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
res = d.matchHostProcessDNSResult(qtype, dnsres)
|
||||
for _, r := range res.Rules {
|
||||
log.Debug(
|
||||
@@ -868,8 +889,8 @@ func New(c *Config, blockFilters []Filter) *DNSFilter {
|
||||
}
|
||||
|
||||
d.hostCheckers = []hostChecker{{
|
||||
check: d.checkEtcHosts,
|
||||
name: "etchosts",
|
||||
check: d.matchSysHosts,
|
||||
name: "hosts container",
|
||||
}, {
|
||||
check: d.matchHost,
|
||||
name: "filtering",
|
||||
|
||||
@@ -21,7 +21,9 @@ func TestMain(m *testing.M) {
|
||||
aghtest.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
var setts Settings
|
||||
var setts = Settings{
|
||||
ProtectionEnabled: true,
|
||||
}
|
||||
|
||||
// Helpers.
|
||||
|
||||
@@ -39,9 +41,9 @@ func purgeCaches() {
|
||||
|
||||
func newForTest(c *Config, filters []Filter) *DNSFilter {
|
||||
setts = Settings{
|
||||
FilteringEnabled: true,
|
||||
ProtectionEnabled: true,
|
||||
FilteringEnabled: true,
|
||||
}
|
||||
setts.FilteringEnabled = true
|
||||
if c != nil {
|
||||
c.SafeBrowsingCacheSize = 10000
|
||||
c.ParentalCacheSize = 10000
|
||||
@@ -797,7 +799,11 @@ func TestClientSettings(t *testing.T) {
|
||||
|
||||
makeTester := func(tc testCase, before bool) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
r, _ := d.CheckHost(tc.host, dns.TypeA, &setts)
|
||||
t.Helper()
|
||||
|
||||
r, err := d.CheckHost(tc.host, dns.TypeA, &setts)
|
||||
require.NoError(t, err)
|
||||
|
||||
if before {
|
||||
assert.True(t, r.IsFiltered)
|
||||
assert.Equal(t, tc.wantReason, r.Reason)
|
||||
@@ -808,7 +814,7 @@ func TestClientSettings(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check behaviour without any per-client settings, then apply per-client
|
||||
// settings and check behaviour once again.
|
||||
// settings and check behavior once again.
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, makeTester(tc, tc.before))
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ func (d *DNSFilter) checkSafeBrowsing(
|
||||
_ uint16,
|
||||
setts *Settings,
|
||||
) (res Result, err error) {
|
||||
if !setts.SafeBrowsingEnabled {
|
||||
if !setts.ProtectionEnabled || !setts.SafeBrowsingEnabled {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ func (d *DNSFilter) checkParental(
|
||||
_ uint16,
|
||||
setts *Settings,
|
||||
) (res Result, err error) {
|
||||
if !setts.ParentalEnabled {
|
||||
if !setts.ProtectionEnabled || !setts.ParentalEnabled {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ func TestSBPC_checkErrorUpstream(t *testing.T) {
|
||||
d.SetParentalUpstream(ups)
|
||||
|
||||
setts := &Settings{
|
||||
ProtectionEnabled: true,
|
||||
SafeBrowsingEnabled: true,
|
||||
ParentalEnabled: true,
|
||||
}
|
||||
@@ -135,35 +136,36 @@ func TestSBPC(t *testing.T) {
|
||||
const hostname = "example.org"
|
||||
|
||||
setts := &Settings{
|
||||
ProtectionEnabled: true,
|
||||
SafeBrowsingEnabled: true,
|
||||
ParentalEnabled: true,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
testCache cache.Cache
|
||||
testFunc func(host string, _ uint16, _ *Settings) (res Result, err error)
|
||||
name string
|
||||
block bool
|
||||
testFunc func(host string, _ uint16, _ *Settings) (res Result, err error)
|
||||
testCache cache.Cache
|
||||
}{{
|
||||
testCache: gctx.safebrowsingCache,
|
||||
testFunc: d.checkSafeBrowsing,
|
||||
name: "sb_no_block",
|
||||
block: false,
|
||||
testFunc: d.checkSafeBrowsing,
|
||||
testCache: gctx.safebrowsingCache,
|
||||
}, {
|
||||
testCache: gctx.safebrowsingCache,
|
||||
testFunc: d.checkSafeBrowsing,
|
||||
name: "sb_block",
|
||||
block: true,
|
||||
testFunc: d.checkSafeBrowsing,
|
||||
testCache: gctx.safebrowsingCache,
|
||||
}, {
|
||||
testCache: gctx.parentalCache,
|
||||
testFunc: d.checkParental,
|
||||
name: "pc_no_block",
|
||||
block: false,
|
||||
testFunc: d.checkParental,
|
||||
testCache: gctx.parentalCache,
|
||||
}, {
|
||||
testCache: gctx.parentalCache,
|
||||
testFunc: d.checkParental,
|
||||
name: "pc_block",
|
||||
block: true,
|
||||
testFunc: d.checkParental,
|
||||
testCache: gctx.parentalCache,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -74,7 +74,7 @@ func (d *DNSFilter) checkSafeSearch(
|
||||
_ uint16,
|
||||
setts *Settings,
|
||||
) (res Result, err error) {
|
||||
if !setts.SafeSearchEnabled {
|
||||
if !setts.ProtectionEnabled || !setts.SafeSearchEnabled {
|
||||
return Result{}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,13 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// cookieTTL is the time-to-live of the session cookie.
|
||||
const cookieTTL = 365 * 24 * time.Hour
|
||||
const cookieTTL = 365 * timeutil.Day
|
||||
|
||||
// sessionCookieName is the name of the session cookie.
|
||||
const sessionCookieName = "agh_session"
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -24,19 +25,17 @@ func TestMain(m *testing.M) {
|
||||
func TestNewSessionToken(t *testing.T) {
|
||||
// Successful case.
|
||||
token, err := newSessionToken()
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, token, sessionTokenSize)
|
||||
|
||||
// Break the rand.Reader.
|
||||
prevReader := rand.Reader
|
||||
t.Cleanup(func() {
|
||||
rand.Reader = prevReader
|
||||
})
|
||||
t.Cleanup(func() { rand.Reader = prevReader })
|
||||
rand.Reader = &bytes.Buffer{}
|
||||
|
||||
// Unsuccessful case.
|
||||
token, err = newSessionToken()
|
||||
require.NotNil(t, err)
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, token)
|
||||
}
|
||||
|
||||
@@ -58,7 +57,7 @@ func TestAuth(t *testing.T) {
|
||||
a.RemoveSession("notfound")
|
||||
|
||||
sess, err := newSessionToken()
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
sessStr := hex.EncodeToString(sess)
|
||||
|
||||
now := time.Now().UTC().Unix()
|
||||
@@ -152,7 +151,7 @@ func TestAuthHTTP(t *testing.T) {
|
||||
|
||||
// perform login
|
||||
cookie, err := Context.auth.httpCookie(loginJSON{Name: "name", Password: "password"}, "")
|
||||
assert.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, cookie)
|
||||
|
||||
// get /
|
||||
@@ -251,12 +250,7 @@ func TestRealIP(t *testing.T) {
|
||||
ip, err := realIP(r)
|
||||
assert.Equal(t, tc.wantIP, ip)
|
||||
|
||||
if tc.wantErrMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tc.wantErrMsg, err.Error())
|
||||
}
|
||||
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,21 +15,19 @@ func TestAuthGL(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
GLMode = true
|
||||
t.Cleanup(func() {
|
||||
GLMode = false
|
||||
})
|
||||
t.Cleanup(func() { GLMode = false })
|
||||
glFilePrefix = dir + "/gl_token_"
|
||||
|
||||
data := make([]byte, 4)
|
||||
aghos.NativeEndian.PutUint32(data, 1)
|
||||
|
||||
require.Nil(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
|
||||
require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
|
||||
assert.False(t, glCheckToken("test"))
|
||||
|
||||
data = make([]byte, 4)
|
||||
aghos.NativeEndian.PutUint32(data, uint32(time.Now().UTC().Unix()+60))
|
||||
|
||||
require.Nil(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
|
||||
require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
|
||||
r, _ := http.NewRequest(http.MethodGet, "http://localhost/", nil)
|
||||
r.AddCookie(&http.Cookie{Name: glCookieName, Value: "test"})
|
||||
assert.True(t, glProcessCookie(r))
|
||||
|
||||
@@ -95,7 +95,9 @@ type clientsContainer struct {
|
||||
// dnsServer is used for checking clients IP status access list status
|
||||
dnsServer *dnsforward.Server
|
||||
|
||||
etcHosts *aghnet.EtcHostsContainer // get entries from system hosts-files
|
||||
// etcHosts contains list of rewrite rules taken from the operating system's
|
||||
// hosts databse.
|
||||
etcHosts *aghnet.HostsContainer
|
||||
|
||||
testing bool // if TRUE, this object is used for internal tests
|
||||
}
|
||||
@@ -106,7 +108,7 @@ type clientsContainer struct {
|
||||
func (clients *clientsContainer) Init(
|
||||
objects []clientObject,
|
||||
dhcpServer *dhcpd.Server,
|
||||
etcHosts *aghnet.EtcHostsContainer,
|
||||
etcHosts *aghnet.HostsContainer,
|
||||
) {
|
||||
if clients.list != nil {
|
||||
log.Fatal("clients.list != nil")
|
||||
@@ -121,13 +123,22 @@ func (clients *clientsContainer) Init(
|
||||
clients.etcHosts = etcHosts
|
||||
clients.addFromConfig(objects)
|
||||
|
||||
if !clients.testing {
|
||||
clients.updateFromDHCP(true)
|
||||
if clients.dhcpServer != nil {
|
||||
clients.dhcpServer.SetOnLeaseChanged(clients.onDHCPLeaseChanged)
|
||||
}
|
||||
if clients.etcHosts != nil {
|
||||
clients.etcHosts.SetOnChanged(clients.onHostsChanged)
|
||||
if clients.testing {
|
||||
return
|
||||
}
|
||||
|
||||
clients.updateFromDHCP(true)
|
||||
if clients.dhcpServer != nil {
|
||||
clients.dhcpServer.SetOnLeaseChanged(clients.onDHCPLeaseChanged)
|
||||
}
|
||||
|
||||
go clients.handleHostsUpdates()
|
||||
}
|
||||
|
||||
func (clients *clientsContainer) handleHostsUpdates() {
|
||||
if clients.etcHosts != nil {
|
||||
for upd := range clients.etcHosts.Upd() {
|
||||
clients.addFromHostsFile(upd)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,10 +261,6 @@ func (clients *clientsContainer) onDHCPLeaseChanged(flags int) {
|
||||
}
|
||||
}
|
||||
|
||||
func (clients *clientsContainer) onHostsChanged() {
|
||||
clients.addFromHostsFile()
|
||||
}
|
||||
|
||||
// Exists checks if client with this IP address already exists.
|
||||
func (clients *clientsContainer) Exists(ip net.IP, source clientSource) (ok bool) {
|
||||
clients.lock.Lock()
|
||||
@@ -697,7 +704,7 @@ func (clients *clientsContainer) SetWHOISInfo(ip net.IP, wi *RuntimeClientWHOISI
|
||||
log.Debug("clients: set whois info for runtime client with ip %s: %+v", ip, wi)
|
||||
}
|
||||
|
||||
// AddHost adds a new IP-hostname pairing. The priorities of the sources is
|
||||
// AddHost adds a new IP-hostname pairing. The priorities of the sources are
|
||||
// taken into account. ok is true if the pairing was added.
|
||||
func (clients *clientsContainer) AddHost(ip net.IP, host string, src clientSource) (ok bool, err error) {
|
||||
clients.lock.Lock()
|
||||
@@ -757,13 +764,7 @@ func (clients *clientsContainer) rmHostsBySrc(src clientSource) {
|
||||
|
||||
// addFromHostsFile fills the client-hostname pairing index from the system's
|
||||
// hosts files.
|
||||
func (clients *clientsContainer) addFromHostsFile() {
|
||||
if clients.etcHosts == nil {
|
||||
return
|
||||
}
|
||||
|
||||
hosts := clients.etcHosts.List()
|
||||
|
||||
func (clients *clientsContainer) addFromHostsFile(hosts *netutil.IPMap) {
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
|
||||
@@ -290,7 +290,9 @@ func TestClientsAddExisting(t *testing.T) {
|
||||
|
||||
clients.dhcpServer, err = dhcpd.Create(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// TODO(e.burkov): leases.db isn't created on Windows so removing it
|
||||
// causes an error. Split the test to make it run properly on different
|
||||
// operating systems.
|
||||
t.Cleanup(func() { _ = os.Remove("leases.db") })
|
||||
|
||||
err = clients.dhcpServer.AddStaticLease(&dhcpd.Lease{
|
||||
@@ -309,8 +311,7 @@ func TestClientsAddExisting(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
|
||||
// Add a new client with the IP from the first client's IP
|
||||
// range.
|
||||
// Add a new client with the IP from the first client's IP range.
|
||||
ok, err = clients.Add(&Client{
|
||||
IDs: []string{"2.2.2.2"},
|
||||
Name: "client3",
|
||||
|
||||
@@ -6,9 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtime"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
@@ -18,6 +16,7 @@ import (
|
||||
"github.com/AdguardTeam/dnsproxy/fastip"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
"github.com/google/renameio/maybe"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
@@ -108,9 +107,9 @@ type dnsConfig struct {
|
||||
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
|
||||
QueryLogFileEnabled bool `yaml:"querylog_file_enabled"` // if true, query log will be written to a file
|
||||
// QueryLogInterval is the interval for query log's files rotation.
|
||||
QueryLogInterval aghtime.Duration `yaml:"querylog_interval"`
|
||||
QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk
|
||||
AnonymizeClientIP bool `yaml:"anonymize_client_ip"` // anonymize clients' IP addresses in logs and stats
|
||||
QueryLogInterval timeutil.Duration `yaml:"querylog_interval"`
|
||||
QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk
|
||||
AnonymizeClientIP bool `yaml:"anonymize_client_ip"` // anonymize clients' IP addresses in logs and stats
|
||||
|
||||
dnsforward.FilteringConfig `yaml:",inline"`
|
||||
|
||||
@@ -119,7 +118,7 @@ type dnsConfig struct {
|
||||
DnsfilterConf filtering.Config `yaml:",inline"`
|
||||
|
||||
// UpstreamTimeout is the timeout for querying upstream servers.
|
||||
UpstreamTimeout aghtime.Duration `yaml:"upstream_timeout"`
|
||||
UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"`
|
||||
|
||||
// LocalDomainName is the domain name used for known internal hosts.
|
||||
// For example, a machine called "myhost" can be addressed as
|
||||
@@ -182,7 +181,7 @@ var config = &configuration{
|
||||
Ratelimit: 20,
|
||||
RefuseAny: true,
|
||||
AllServers: false,
|
||||
FastestTimeout: aghtime.Duration{
|
||||
FastestTimeout: timeutil.Duration{
|
||||
Duration: fastip.DefaultPingWaitTimeout,
|
||||
},
|
||||
|
||||
@@ -196,7 +195,7 @@ var config = &configuration{
|
||||
},
|
||||
FilteringEnabled: true, // whether or not use filter lists
|
||||
FiltersUpdateIntervalHours: 24,
|
||||
UpstreamTimeout: aghtime.Duration{Duration: dnsforward.DefaultTimeout},
|
||||
UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
|
||||
LocalDomainName: "lan",
|
||||
ResolveClients: true,
|
||||
UsePrivateRDNS: true,
|
||||
@@ -223,7 +222,7 @@ func initConfig() {
|
||||
|
||||
config.DNS.QueryLogEnabled = true
|
||||
config.DNS.QueryLogFileEnabled = true
|
||||
config.DNS.QueryLogInterval = aghtime.Duration{Duration: 90 * 24 * time.Hour}
|
||||
config.DNS.QueryLogInterval = timeutil.Duration{Duration: 90 * timeutil.Day}
|
||||
config.DNS.QueryLogMemSize = 1000
|
||||
|
||||
config.DNS.CacheSize = 4 * 1024 * 1024
|
||||
@@ -292,7 +291,7 @@ func parseConfig() error {
|
||||
}
|
||||
|
||||
if config.DNS.UpstreamTimeout.Duration == 0 {
|
||||
config.DNS.UpstreamTimeout = aghtime.Duration{Duration: dnsforward.DefaultTimeout}
|
||||
config.DNS.UpstreamTimeout = timeutil.Duration{Duration: dnsforward.DefaultTimeout}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -339,7 +338,7 @@ func (c *configuration) write() error {
|
||||
Context.queryLog.WriteDiskConfig(&dc)
|
||||
config.DNS.QueryLogEnabled = dc.Enabled
|
||||
config.DNS.QueryLogFileEnabled = dc.FileEnabled
|
||||
config.DNS.QueryLogInterval = aghtime.Duration{Duration: dc.RotationIvl}
|
||||
config.DNS.QueryLogInterval = timeutil.Duration{Duration: dc.RotationIvl}
|
||||
config.DNS.QueryLogMemSize = dc.MemSize
|
||||
config.DNS.AnonymizeClientIP = dc.AnonymizeClientIP
|
||||
}
|
||||
|
||||
@@ -404,6 +404,7 @@ func (f *Filtering) handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
setts := Context.dnsFilter.GetConfig()
|
||||
setts.FilteringEnabled = true
|
||||
setts.ProtectionEnabled = true
|
||||
Context.dnsFilter.ApplyBlockedServices(&setts, nil, true)
|
||||
result, err := Context.dnsFilter.CheckHost(host, dns.TypeA, &setts)
|
||||
if err != nil {
|
||||
|
||||
@@ -710,7 +710,6 @@ func enableFilters(async bool) {
|
||||
}
|
||||
|
||||
func enableFiltersLocked(async bool) {
|
||||
var whiteFilters []filtering.Filter
|
||||
filters := []filtering.Filter{{
|
||||
Data: []byte(strings.Join(config.UserRules, "\n")),
|
||||
}}
|
||||
@@ -725,18 +724,20 @@ func enableFiltersLocked(async bool) {
|
||||
FilePath: filter.Path(),
|
||||
})
|
||||
}
|
||||
|
||||
var allowFilters []filtering.Filter
|
||||
for _, filter := range config.WhitelistFilters {
|
||||
if !filter.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
whiteFilters = append(whiteFilters, filtering.Filter{
|
||||
allowFilters = append(allowFilters, filtering.Filter{
|
||||
ID: filter.ID,
|
||||
FilePath: filter.Path(),
|
||||
})
|
||||
}
|
||||
|
||||
if err := Context.dnsFilter.SetFilters(filters, whiteFilters, async); err != nil {
|
||||
if err := Context.dnsFilter.SetFilters(filters, allowFilters, async); err != nil {
|
||||
log.Debug("enabling filters: %s", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/netutil"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -21,7 +22,7 @@ const testFltsFileName = "1.txt"
|
||||
func testStartFilterListener(t *testing.T, fltContent *[]byte) (l net.Listener) {
|
||||
t.Helper()
|
||||
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
n, werr := w.Write(*fltContent)
|
||||
require.NoError(t, werr)
|
||||
require.Equal(t, len(*fltContent), n)
|
||||
@@ -34,9 +35,7 @@ func testStartFilterListener(t *testing.T, fltContent *[]byte) (l net.Listener)
|
||||
go func() {
|
||||
_ = http.Serve(l, h)
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, l.Close())
|
||||
})
|
||||
testutil.CleanupAndRequireSuccess(t, l.Close)
|
||||
|
||||
return l
|
||||
}
|
||||
@@ -100,9 +99,7 @@ func TestFilters(t *testing.T) {
|
||||
|
||||
t.Run("refresh_actually", func(t *testing.T) {
|
||||
fltContent = []byte(`||example.com^`)
|
||||
t.Cleanup(func() {
|
||||
fltContent = []byte(content)
|
||||
})
|
||||
t.Cleanup(func() { fltContent = []byte(content) })
|
||||
|
||||
updateAndAssert(t, require.True, 1)
|
||||
})
|
||||
|
||||
@@ -44,20 +44,22 @@ type homeContext struct {
|
||||
// Modules
|
||||
// --
|
||||
|
||||
clients clientsContainer // per-client-settings module
|
||||
stats stats.Stats // statistics module
|
||||
queryLog querylog.QueryLog // query log module
|
||||
dnsServer *dnsforward.Server // DNS module
|
||||
rdns *RDNS // rDNS module
|
||||
whois *WHOIS // WHOIS module
|
||||
dnsFilter *filtering.DNSFilter // DNS filtering module
|
||||
dhcpServer *dhcpd.Server // DHCP module
|
||||
auth *Auth // HTTP authentication module
|
||||
filters Filtering // DNS filtering module
|
||||
web *Web // Web (HTTP, HTTPS) module
|
||||
tls *TLSMod // TLS module
|
||||
etcHosts *aghnet.EtcHostsContainer // IP-hostname pairs taken from system configuration (e.g. /etc/hosts) files
|
||||
updater *updater.Updater
|
||||
clients clientsContainer // per-client-settings module
|
||||
stats stats.Stats // statistics module
|
||||
queryLog querylog.QueryLog // query log module
|
||||
dnsServer *dnsforward.Server // DNS module
|
||||
rdns *RDNS // rDNS module
|
||||
whois *WHOIS // WHOIS module
|
||||
dnsFilter *filtering.DNSFilter // DNS filtering module
|
||||
dhcpServer *dhcpd.Server // DHCP module
|
||||
auth *Auth // HTTP authentication module
|
||||
filters Filtering // DNS filtering module
|
||||
web *Web // Web (HTTP, HTTPS) module
|
||||
tls *TLSMod // TLS module
|
||||
// etcHosts is an IP-hostname pairs set taken from system configuration
|
||||
// (e.g. /etc/hosts) files.
|
||||
etcHosts *aghnet.HostsContainer
|
||||
updater *updater.Updater
|
||||
|
||||
subnetDetector *aghnet.SubnetDetector
|
||||
|
||||
@@ -257,8 +259,20 @@ func setupConfig(args options) (err error) {
|
||||
})
|
||||
|
||||
if !args.noEtcHosts {
|
||||
Context.etcHosts = &aghnet.EtcHostsContainer{}
|
||||
Context.etcHosts.Init("")
|
||||
var osWritesWatcher aghos.FSWatcher
|
||||
osWritesWatcher, err = aghos.NewOSWritesWatcher()
|
||||
if err != nil {
|
||||
return fmt.Errorf("initing os watcher: %w", err)
|
||||
}
|
||||
|
||||
Context.etcHosts, err = aghnet.NewHostsContainer(
|
||||
aghos.RootDirFS(),
|
||||
osWritesWatcher,
|
||||
aghnet.DefaultHostsPaths()...,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initing hosts container: %w", err)
|
||||
}
|
||||
}
|
||||
Context.clients.Init(config.Clients, Context.dhcpServer, Context.etcHosts)
|
||||
config.Clients = nil
|
||||
@@ -424,7 +438,6 @@ func run(args options, clientBuildFS fs.FS) {
|
||||
fatalOnError(err)
|
||||
|
||||
Context.tls.Start()
|
||||
Context.etcHosts.Start()
|
||||
|
||||
go func() {
|
||||
serr := startDNSServer()
|
||||
@@ -647,7 +660,11 @@ func cleanup(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
Context.etcHosts.Close()
|
||||
if Context.etcHosts != nil {
|
||||
if err = Context.etcHosts.Close(); err != nil {
|
||||
log.Error("stopping hosts container: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if Context.tls != nil {
|
||||
Context.tls.Close()
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestLimitRequestBody(t *testing.T) {
|
||||
var b []byte
|
||||
b, *err = io.ReadAll(r.Body)
|
||||
_, werr := w.Write(b)
|
||||
require.Nil(t, werr)
|
||||
require.NoError(t, werr)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ type dnsSettings struct {
|
||||
ServerName string `plist:",omitempty"`
|
||||
|
||||
// ServerAddresses is a list IP addresses of the server.
|
||||
//
|
||||
// TODO(a.garipov): Allow users to set this.
|
||||
//
|
||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/3607.
|
||||
ServerAddresses []net.IP `plist:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -157,19 +161,9 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
|
||||
}
|
||||
}
|
||||
|
||||
dnsIPs, err := collectDNSIPs()
|
||||
if err != nil {
|
||||
// Don't add a lot of formatting, since the error is already
|
||||
// wrapped by collectDNSIPs.
|
||||
respondJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
d := &dnsSettings{
|
||||
DNSProtocol: dnsp,
|
||||
ServerName: host,
|
||||
ServerAddresses: dnsIPs,
|
||||
DNSProtocol: dnsp,
|
||||
ServerName: host,
|
||||
}
|
||||
|
||||
mobileconfig, err := encodeMobileConfig(d, clientID)
|
||||
@@ -203,25 +197,3 @@ func handleMobileConfigDoH(w http.ResponseWriter, r *http.Request) {
|
||||
func handleMobileConfigDoT(w http.ResponseWriter, r *http.Request) {
|
||||
handleMobileConfig(w, r, dnsProtoTLS)
|
||||
}
|
||||
|
||||
// collectDNSIPs returns a slice of IP addresses the server is listening
|
||||
// on, including the addresses on all interfaces in cases of unspecified IPs but
|
||||
// excluding loopback addresses.
|
||||
func collectDNSIPs() (ips []net.IP, err error) {
|
||||
// TODO(a.garipov): This really shouldn't be a function that parses
|
||||
// a list of strings. Instead, we need a function that returns this
|
||||
// data as []net.IP or []*netutil.IPPort. Maybe someday.
|
||||
addrs, err := collectDNSAddresses()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
ip := net.ParseIP(addr)
|
||||
if ip != nil && !ip.IsLoopback() {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ func TestHandleMobileConfigDoH(t *testing.T) {
|
||||
s := mc.PayloadContent[0].DNSSettings
|
||||
require.NotNil(t, s)
|
||||
|
||||
assert.NotEmpty(t, s.ServerAddresses)
|
||||
assert.Empty(t, s.ServerName)
|
||||
assert.Equal(t, "https://example.org/dns-query", s.ServerURL)
|
||||
})
|
||||
@@ -104,7 +103,6 @@ func TestHandleMobileConfigDoH(t *testing.T) {
|
||||
s := mc.PayloadContent[0].DNSSettings
|
||||
require.NotNil(t, s)
|
||||
|
||||
assert.NotEmpty(t, s.ServerAddresses)
|
||||
assert.Empty(t, s.ServerName)
|
||||
assert.Equal(t, "https://example.org/dns-query/cli42", s.ServerURL)
|
||||
})
|
||||
@@ -132,7 +130,6 @@ func TestHandleMobileConfigDoT(t *testing.T) {
|
||||
s := mc.PayloadContent[0].DNSSettings
|
||||
require.NotNil(t, s)
|
||||
|
||||
assert.NotEmpty(t, s.ServerAddresses)
|
||||
assert.Equal(t, "example.org", s.ServerName)
|
||||
assert.Empty(t, s.ServerURL)
|
||||
})
|
||||
@@ -156,7 +153,6 @@ func TestHandleMobileConfigDoT(t *testing.T) {
|
||||
|
||||
handleMobileConfigDoT(w, r)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
|
||||
assert.JSONEq(t, w.Body.String(), b.String())
|
||||
})
|
||||
|
||||
@@ -179,7 +175,6 @@ func TestHandleMobileConfigDoT(t *testing.T) {
|
||||
s := mc.PayloadContent[0].DNSSettings
|
||||
require.NotNil(t, s)
|
||||
|
||||
assert.NotEmpty(t, s.ServerAddresses)
|
||||
assert.Equal(t, "cli42.example.org", s.ServerName)
|
||||
assert.Empty(t, s.ServerURL)
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@ func testParseOK(t *testing.T, ss ...string) options {
|
||||
t.Helper()
|
||||
|
||||
o, _, err := parse("", ss)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
return o
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func testParseErr(t *testing.T, descr string, ss ...string) {
|
||||
t.Helper()
|
||||
|
||||
_, _, err := parse("", ss)
|
||||
require.NotNilf(t, err, "expected an error because %s but no error returned", descr)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func testParseParamMissing(t *testing.T, param string) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
@@ -449,26 +450,36 @@ func validatePkey(data *tlsConfigStatus, pkey string) error {
|
||||
if decoded == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if decoded.Type == "PRIVATE KEY" || strings.HasSuffix(decoded.Type, " PRIVATE KEY") {
|
||||
key = decoded
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if key == nil {
|
||||
data.WarningValidation = "No valid keys were found"
|
||||
|
||||
return errors.Error(data.WarningValidation)
|
||||
}
|
||||
|
||||
// parse the decoded key
|
||||
_, keytype, err := parsePrivateKey(key.Bytes)
|
||||
_, keyType, err := parsePrivateKey(key.Bytes)
|
||||
if err != nil {
|
||||
data.WarningValidation = fmt.Sprintf("Failed to parse private key: %s", err)
|
||||
|
||||
return errors.Error(data.WarningValidation)
|
||||
} else if keyType == keyTypeED25519 {
|
||||
data.WarningValidation = "ED25519 keys are not supported by browsers; " +
|
||||
"did you mean to use X25519 for key exchange?"
|
||||
|
||||
return errors.Error(data.WarningValidation)
|
||||
}
|
||||
|
||||
data.ValidKey = true
|
||||
data.KeyType = keytype
|
||||
data.KeyType = keyType
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -506,27 +517,42 @@ func validateCertificates(certChain, pkey, serverName string) tlsConfigStatus {
|
||||
return data
|
||||
}
|
||||
|
||||
// Key types.
|
||||
const (
|
||||
keyTypeECDSA = "ECDSA"
|
||||
keyTypeED25519 = "ED25519"
|
||||
keyTypeRSA = "RSA"
|
||||
)
|
||||
|
||||
// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
|
||||
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
|
||||
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
|
||||
func parsePrivateKey(der []byte) (crypto.PrivateKey, string, error) {
|
||||
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||
return key, "RSA", nil
|
||||
//
|
||||
// TODO(a.garipov): Find out if this version of parsePrivateKey from the stdlib
|
||||
// is actually necessary.
|
||||
func parsePrivateKey(der []byte) (key crypto.PrivateKey, typ string, err error) {
|
||||
if key, err = x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||
return key, keyTypeRSA, nil
|
||||
}
|
||||
|
||||
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
||||
if key, err = x509.ParsePKCS8PrivateKey(der); err == nil {
|
||||
switch key := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return key, "RSA", nil
|
||||
return key, keyTypeRSA, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return key, "ECDSA", nil
|
||||
return key, keyTypeECDSA, nil
|
||||
case ed25519.PrivateKey:
|
||||
return key, keyTypeED25519, nil
|
||||
default:
|
||||
return nil, "", errors.Error("tls: found unknown private key type in PKCS#8 wrapping")
|
||||
return nil, "", fmt.Errorf(
|
||||
"tls: found unknown private key type %T in PKCS#8 wrapping",
|
||||
key,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if key, err := x509.ParseECPrivateKey(der); err == nil {
|
||||
return key, "ECDSA", nil
|
||||
if key, err = x509.ParseECPrivateKey(der); err == nil {
|
||||
return key, keyTypeECDSA, nil
|
||||
}
|
||||
|
||||
return nil, "", errors.Error("tls: failed to parse private key")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user