all: sync with master; upd chlog
This commit is contained in:
parent
0dad53b5f7
commit
d9c57cdd9a
|
@ -399,6 +399,7 @@ Response:
|
||||||
"protection_enabled":true,
|
"protection_enabled":true,
|
||||||
"running":true,
|
"running":true,
|
||||||
"dhcp_available":true,
|
"dhcp_available":true,
|
||||||
|
"protection_disabled_duration":0
|
||||||
"version":"undefined"
|
"version":"undefined"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
158
CHANGELOG.md
158
CHANGELOG.md
|
@ -14,15 +14,154 @@ and this project adheres to
|
||||||
<!--
|
<!--
|
||||||
## [v0.108.0] - TBA
|
## [v0.108.0] - TBA
|
||||||
|
|
||||||
## [v0.107.27] - 2023-03-29 (APPROX.)
|
## [v0.107.29] - 2023-04-26 (APPROX.)
|
||||||
|
|
||||||
See also the [v0.107.27 GitHub milestone][ms-v0.107.27].
|
See also the [v0.107.29 GitHub milestone][ms-v0.107.29].
|
||||||
|
|
||||||
[ms-v0.107.27]: https://github.com/AdguardTeam/AdGuardHome/milestone/63?closed=1
|
[ms-v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/milestone/65?closed=1
|
||||||
|
|
||||||
NOTE: Add new changes BELOW THIS COMMENT.
|
NOTE: Add new changes BELOW THIS COMMENT.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.107.28] - 2023-04-12
|
||||||
|
|
||||||
|
See also the [v0.107.28 GitHub milestone][ms-v0.107.28].
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- The ability to exclude client activity from the query log or statistics by
|
||||||
|
using the new properties `ignore_querylog` and `ignore_statistics` of the
|
||||||
|
items of the `clients.persistent` array ([#1717], [#4299]). The UI changes
|
||||||
|
are coming in the upcoming releases.
|
||||||
|
- Better profiling information when `debug_pprof` is set to `true`.
|
||||||
|
- IPv6 support in Safe Search for some services.
|
||||||
|
- The ability to make bootstrap DNS lookups prefer IPv6 addresses to IPv4 ones
|
||||||
|
using the new `dns.bootstrap_prefer_ipv6` configuration file property
|
||||||
|
([#4262]).
|
||||||
|
- Docker container's healthcheck ([#3290]).
|
||||||
|
- The new HTTP API `POST /control/protection`, that updates protection state
|
||||||
|
and adds an optional pause duration ([#1333]). The format of request body
|
||||||
|
is described in `openapi/openapi.yaml`. The duration of this pause could
|
||||||
|
also be set with the config field `protection_disabled_until` in `dns`
|
||||||
|
section of the YAML configuration file.
|
||||||
|
- The ability to create a static DHCP lease from a dynamic one more easily
|
||||||
|
([#3459]).
|
||||||
|
- Two new HTTP APIs, `PUT /control/stats/config/update` and `GET
|
||||||
|
control/stats/config`, which can be used to set and receive the query log
|
||||||
|
configuration. See openapi/openapi.yaml for the full description.
|
||||||
|
- Two new HTTP APIs, `PUT /control/querylog/config/update` and `GET
|
||||||
|
control/querylog/config`, which can be used to set and receive the statistics
|
||||||
|
configuration. See openapi/openapi.yaml for the full description.
|
||||||
|
- The ability to set custom IP for EDNS Client Subnet by using the DNS-server
|
||||||
|
configuration section on the DNS settings page in the UI ([#1472]).
|
||||||
|
- The ability to manage safesearch for each service by using the new
|
||||||
|
`safe_search` field ([#1163]).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- ARPA domain names containing a subnet within private networks now also
|
||||||
|
considered private, behaving closer to [RFC 6761][rfc6761] ([#5567]).
|
||||||
|
|
||||||
|
#### Configuration Changes
|
||||||
|
|
||||||
|
In this release, the schema version has changed from 17 to 20.
|
||||||
|
|
||||||
|
- Property `statistics.interval`, which in schema versions 19 and earlier used
|
||||||
|
to be an integer number of days, is now a string with a human-readable
|
||||||
|
duration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# BEFORE:
|
||||||
|
'statistics':
|
||||||
|
# …
|
||||||
|
'interval': 1
|
||||||
|
|
||||||
|
# AFTER:
|
||||||
|
'statistics':
|
||||||
|
# …
|
||||||
|
'interval': '24h'
|
||||||
|
```
|
||||||
|
|
||||||
|
To rollback this change, convert the property back into days and change the
|
||||||
|
`schema_version` back to `19`.
|
||||||
|
- The `dns.safesearch_enabled` field has been replaced with `safe_search`
|
||||||
|
object containing per-service settings.
|
||||||
|
- The `clients.persistent.safesearch_enabled` field has been replaced with
|
||||||
|
`safe_search` object containing per-service settings.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# BEFORE:
|
||||||
|
'safesearch_enabled': true
|
||||||
|
|
||||||
|
# AFTER:
|
||||||
|
'safe_search':
|
||||||
|
'enabled': true
|
||||||
|
'bing': true
|
||||||
|
'duckduckgo': true
|
||||||
|
'google': true
|
||||||
|
'pixabay': true
|
||||||
|
'yandex': true
|
||||||
|
'youtube': true
|
||||||
|
```
|
||||||
|
|
||||||
|
To rollback this change, move the value of `dns.safe_search.enabled` into the
|
||||||
|
`dns.safesearch_enabled`, then remove `dns.safe_search` field. Do the same
|
||||||
|
client's specific `clients.persistent.safesearch` and then change the
|
||||||
|
`schema_version` back to `17`.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- The `POST /control/safesearch/enable` HTTP API is deprecated. Use the new
|
||||||
|
`PUT /control/safesearch/settings` API.
|
||||||
|
- The `POST /control/safesearch/disable` HTTP API is deprecated. Use the new
|
||||||
|
`PUT /control/safesearch/settings` API
|
||||||
|
- The `safesearch_enabled` field is deprecated in the following HTTP APIs:
|
||||||
|
- `GET /control/clients`;
|
||||||
|
- `POST /control/clients/add`;
|
||||||
|
- `POST /control/clients/update`;
|
||||||
|
- `GET /control/clients/find?ip0=...&ip1=...&ip2=...`.
|
||||||
|
|
||||||
|
Check `openapi/openapi.yaml` for more details.
|
||||||
|
- The `GET /control/stats_info` HTTP API; use the new `GET
|
||||||
|
/control/stats/config` API instead.
|
||||||
|
|
||||||
|
**NOTE:** If interval is custom then it will be equal to `90` days for
|
||||||
|
compatibility reasons. See openapi/openapi.yaml and `openapi/CHANGELOG.md`.
|
||||||
|
- The `POST /control/stats_config` HTTP API; use the new `PUT
|
||||||
|
/control/stats/config/update` API instead.
|
||||||
|
- The `GET /control/querylog_info` HTTP API; use the new `GET
|
||||||
|
/control/querylog/config` API instead.
|
||||||
|
|
||||||
|
**NOTE:** If interval is custom then it will be equal to `90` days for
|
||||||
|
compatibility reasons. See openapi/openapi.yaml and `openapi/CHANGELOG.md`.
|
||||||
|
- The `POST /control/querylog_config` HTTP API; use the new `PUT
|
||||||
|
/control/querylog/config/update` API instead.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Logging of the client's IP address after failed login attempts ([#5701]).
|
||||||
|
|
||||||
|
[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163
|
||||||
|
[#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333
|
||||||
|
[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1717
|
||||||
|
[#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472
|
||||||
|
[#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290
|
||||||
|
[#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459
|
||||||
|
[#4262]: https://github.com/AdguardTeam/AdGuardHome/issues/4262
|
||||||
|
[#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/4299
|
||||||
|
[#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567
|
||||||
|
[#5701]: https://github.com/AdguardTeam/AdGuardHome/issues/5701
|
||||||
|
|
||||||
|
[ms-v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/milestone/64?closed=1
|
||||||
|
[rfc6761]: https://www.rfc-editor.org/rfc/rfc6761
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.107.27] - 2023-04-05
|
## [v0.107.27] - 2023-04-05
|
||||||
|
@ -73,8 +212,6 @@ See also the [v0.107.26 GitHub milestone][ms-v0.107.26].
|
||||||
|
|
||||||
#### Configuration Changes
|
#### Configuration Changes
|
||||||
|
|
||||||
In this release, the schema version has changed from 16 to 17.
|
|
||||||
|
|
||||||
- Property `edns_client_subnet`, which in schema versions 16 and earlier used
|
- Property `edns_client_subnet`, which in schema versions 16 and earlier used
|
||||||
to be a part of the `dns` object, is now part of the `dns.edns_client_subnet`
|
to be a part of the `dns` object, is now part of the `dns.edns_client_subnet`
|
||||||
object:
|
object:
|
||||||
|
@ -126,10 +263,6 @@ In this release, the schema version has changed from 16 to 17.
|
||||||
[ms-v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/milestone/62?closed=1
|
[ms-v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/milestone/62?closed=1
|
||||||
[rfc3696]: https://datatracker.ietf.org/doc/html/rfc3696
|
[rfc3696]: https://datatracker.ietf.org/doc/html/rfc3696
|
||||||
|
|
||||||
<!--
|
|
||||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.107.25] - 2023-02-21
|
## [v0.107.25] - 2023-02-21
|
||||||
|
@ -1787,11 +1920,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.29...HEAD
|
||||||
[v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.27...v0.107.28
|
[v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...v0.107.29
|
||||||
-->
|
-->
|
||||||
|
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.27...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...HEAD
|
||||||
|
[v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.27...v0.107.28
|
||||||
[v0.107.27]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.26...v0.107.27
|
[v0.107.27]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.26...v0.107.27
|
||||||
[v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.25...v0.107.26
|
[v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.25...v0.107.26
|
||||||
[v0.107.25]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.24...v0.107.25
|
[v0.107.25]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.24...v0.107.25
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Zapnutá Rodičovská kontrola",
|
"enabled_parental_toast": "Zapnutá Rodičovská kontrola",
|
||||||
"disabled_safe_search_toast": "Vypnuté bezpečné vyhledávání",
|
"disabled_safe_search_toast": "Vypnuté bezpečné vyhledávání",
|
||||||
"enabled_save_search_toast": "Zapnuté bezpečné vyhledávání",
|
"enabled_save_search_toast": "Zapnuté bezpečné vyhledávání",
|
||||||
|
"updated_save_search_toast": "Nastavení Bezpečného vyhledávání aktualizováno",
|
||||||
"enabled_table_header": "Zapnuto",
|
"enabled_table_header": "Zapnuto",
|
||||||
"name_table_header": "Název",
|
"name_table_header": "Název",
|
||||||
"list_url_table_header": "Seznam URL",
|
"list_url_table_header": "Seznam URL",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Rychlostní limit",
|
"rate_limit": "Rychlostní limit",
|
||||||
"edns_enable": "Povolit klientskou podsíť EDNS",
|
"edns_enable": "Povolit klientskou podsíť EDNS",
|
||||||
"edns_cs_desc": "Přidá možnost podsítě klienta EDNS (ECS) do odchozích požadavků a zaznamá hodnoty odeslané klienty do protokolu dotazů.",
|
"edns_cs_desc": "Přidá možnost podsítě klienta EDNS (ECS) do odchozích požadavků a zaznamá hodnoty odeslané klienty do protokolu dotazů.",
|
||||||
|
"edns_use_custom_ip": "Použít vlastní IP pro EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Povolit použití vlastní IP pro EDNS",
|
||||||
"rate_limit_desc": "Počet požadavků za sekundu, které smí jeden klient provádět (0: neomezeno)",
|
"rate_limit_desc": "Počet požadavků za sekundu, které smí jeden klient provádět (0: neomezeno)",
|
||||||
"blocking_ipv4_desc": "IP adresa, která se má vrátit v případě blokovaného požadavku typu A",
|
"blocking_ipv4_desc": "IP adresa, která se má vrátit v případě blokovaného požadavku typu A",
|
||||||
"blocking_ipv6_desc": "IP adresa, která se má vrátit v případě blokovaného požadavku typu AAAA",
|
"blocking_ipv6_desc": "IP adresa, která se má vrátit v případě blokovaného požadavku typu AAAA",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Opravdu chcete změnit uchovávání statistik? Pokud snížíte hodnotu intervalu, některá data budou ztracena",
|
"statistics_retention_confirm": "Opravdu chcete změnit uchovávání statistik? Pokud snížíte hodnotu intervalu, některá data budou ztracena",
|
||||||
"statistics_cleared": "Statistiky úspěšně vyčištěny",
|
"statistics_cleared": "Statistiky úspěšně vyčištěny",
|
||||||
"statistics_enable": "Povolit statistiky",
|
"statistics_enable": "Povolit statistiky",
|
||||||
|
"ignore_domains": "Ignorované domény (oddělené novým řádkem)",
|
||||||
|
"ignore_domains_title": "Ignorované domény",
|
||||||
|
"ignore_domains_desc_stats": "Dotazy pro tyto domény se do statistik nezapisují",
|
||||||
|
"ignore_domains_desc_query": "Dotazy pro tyto domény se do záznamu dotazů nezapisují",
|
||||||
"interval_hours": "Hodiny: {{count}}",
|
"interval_hours": "Hodiny: {{count}}",
|
||||||
"interval_hours_plural": "Hodiny: {{count}}",
|
"interval_hours_plural": "Hodiny: {{count}}",
|
||||||
"filters_configuration": "Konfigurace filtrů",
|
"filters_configuration": "Konfigurace filtrů",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Poznámka:</0> Anonymizace IP je zapnuta. Můžete ji vypnout v <1>Obecných nastaveních</1>.",
|
"anonymizer_notification": "<0>Poznámka:</0> Anonymizace IP je zapnuta. Můžete ji vypnout v <1>Obecných nastaveních</1>.",
|
||||||
"confirm_dns_cache_clear": "Opravdu chcete vymazat mezipaměť DNS?",
|
"confirm_dns_cache_clear": "Opravdu chcete vymazat mezipaměť DNS?",
|
||||||
"cache_cleared": "Mezipaměť DNS úspěšně vymazána",
|
"cache_cleared": "Mezipaměť DNS úspěšně vymazána",
|
||||||
"clear_cache": "Vymazat mezipaměť"
|
"clear_cache": "Vymazat mezipaměť",
|
||||||
|
"make_static": "Nastavit jako statickou",
|
||||||
|
"theme_auto_desc": "Automatický (podle barevného motivu vašeho zařízení)",
|
||||||
|
"theme_dark_desc": "Tmavý motiv",
|
||||||
|
"theme_light_desc": "Světlý motiv",
|
||||||
|
"disable_for_seconds": "Na {{count}} sek.",
|
||||||
|
"disable_for_seconds_plural": "Na {{count}} sek.",
|
||||||
|
"disable_for_minutes": "Na {{count}} min.",
|
||||||
|
"disable_for_minutes_plural": "Na {{count}} min.",
|
||||||
|
"disable_for_hours": "Na {{count}} hod.",
|
||||||
|
"disable_for_hours_plural": "Na {{count}} hod.",
|
||||||
|
"disable_until_tomorrow": "Do zítřka",
|
||||||
|
"disable_notify_for_seconds": "Vypnout ochranu na {{count}} sek.",
|
||||||
|
"disable_notify_for_seconds_plural": "Vypnout ochranu na {{count}} sek.",
|
||||||
|
"disable_notify_for_minutes": "Vypnout ochranu na {{count}} min.",
|
||||||
|
"disable_notify_for_minutes_plural": "Vypnout ochranu na {{count}} min.",
|
||||||
|
"disable_notify_for_hours": "Vypnout ochranu na {{count}} hod.",
|
||||||
|
"disable_notify_for_hours_plural": "Vypnout ochranu na {{count}} hod.",
|
||||||
|
"disable_notify_until_tomorrow": "Vypnout ochranu do zítřka",
|
||||||
|
"enable_protection_timer": "Ochrana bude zapnuta za {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Forældrekontrol aktiveret",
|
"enabled_parental_toast": "Forældrekontrol aktiveret",
|
||||||
"disabled_safe_search_toast": "Sikker søgning deaktiveret",
|
"disabled_safe_search_toast": "Sikker søgning deaktiveret",
|
||||||
"enabled_save_search_toast": "Sikker søgning aktiveret",
|
"enabled_save_search_toast": "Sikker søgning aktiveret",
|
||||||
|
"updated_save_search_toast": "Sikker søgning opdateret",
|
||||||
"enabled_table_header": "Aktiveret",
|
"enabled_table_header": "Aktiveret",
|
||||||
"name_table_header": "Navn",
|
"name_table_header": "Navn",
|
||||||
"list_url_table_header": "Liste-URL",
|
"list_url_table_header": "Liste-URL",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Hyppighedsgrænse",
|
"rate_limit": "Hyppighedsgrænse",
|
||||||
"edns_enable": "Aktivér EDNS-klientundernet",
|
"edns_enable": "Aktivér EDNS-klientundernet",
|
||||||
"edns_cs_desc": "Tilføj indstillingen EDNS Client Subnet (ECS) til upstream-forespørgsler og log de af klienterne sendte værdier i forespørgselsloggen.",
|
"edns_cs_desc": "Tilføj indstillingen EDNS Client Subnet (ECS) til upstream-forespørgsler og log de af klienterne sendte værdier i forespørgselsloggen.",
|
||||||
|
"edns_use_custom_ip": "Brug tilpasset IP til EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Tillad brug af tilpasset IP til EDNS",
|
||||||
"rate_limit_desc": "Antallet af forespørgsler pr. sekund tilladt pr. klient (værdien 0 = ubegrænset)",
|
"rate_limit_desc": "Antallet af forespørgsler pr. sekund tilladt pr. klient (værdien 0 = ubegrænset)",
|
||||||
"blocking_ipv4_desc": "Returneret IP-adresse for en blokeret A-forespørgsel",
|
"blocking_ipv4_desc": "Returneret IP-adresse for en blokeret A-forespørgsel",
|
||||||
"blocking_ipv6_desc": "Returneret IP-adresse for en blokeret AAAA-forespørgsel",
|
"blocking_ipv6_desc": "Returneret IP-adresse for en blokeret AAAA-forespørgsel",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Sikker på, at du vil ændre på statistikbevaring? Mindskes intervalværdien, vil nogle data gå tabt",
|
"statistics_retention_confirm": "Sikker på, at du vil ændre på statistikbevaring? Mindskes intervalværdien, vil nogle data gå tabt",
|
||||||
"statistics_cleared": "Statistikkerne er ryddet",
|
"statistics_cleared": "Statistikkerne er ryddet",
|
||||||
"statistics_enable": "Aktivér statistikker",
|
"statistics_enable": "Aktivér statistikker",
|
||||||
|
"ignore_domains": "Ignorerede domæner (adskilt af ny linje)",
|
||||||
|
"ignore_domains_title": "Ignorerede domæner",
|
||||||
|
"ignore_domains_desc_stats": "Forespørgsler til disse domæner opføres ikke i statistikken",
|
||||||
|
"ignore_domains_desc_query": "Forespørgsler til disse domæner opføres ikke i forespørgselsloggen",
|
||||||
"interval_hours": "{{count}} time",
|
"interval_hours": "{{count}} time",
|
||||||
"interval_hours_plural": "{{count}} timer",
|
"interval_hours_plural": "{{count}} timer",
|
||||||
"filters_configuration": "Filteropsætninger",
|
"filters_configuration": "Filteropsætninger",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Bemærk:</0> IP-anonymisering er aktiveret. Det kan deaktiveres via <1>Generelle indstillinger</1>.",
|
"anonymizer_notification": "<0>Bemærk:</0> IP-anonymisering er aktiveret. Det kan deaktiveres via <1>Generelle indstillinger</1>.",
|
||||||
"confirm_dns_cache_clear": "Sikker på, at DNS-cache skal ryddes?",
|
"confirm_dns_cache_clear": "Sikker på, at DNS-cache skal ryddes?",
|
||||||
"cache_cleared": "DNS-cache hermed ryddet",
|
"cache_cleared": "DNS-cache hermed ryddet",
|
||||||
"clear_cache": "Ryd cache"
|
"clear_cache": "Ryd cache",
|
||||||
|
"make_static": "Gør statisk",
|
||||||
|
"theme_auto_desc": "Auto (baseret på enhedens farveskema)",
|
||||||
|
"theme_dark_desc": "Mørkt tema",
|
||||||
|
"theme_light_desc": "Lyst tema",
|
||||||
|
"disable_for_seconds": "I {{count}} sekund",
|
||||||
|
"disable_for_seconds_plural": "I {{count}} sekunder",
|
||||||
|
"disable_for_minutes": "I {{count}} minut",
|
||||||
|
"disable_for_minutes_plural": "I {{count}} minutter",
|
||||||
|
"disable_for_hours": "I {{count}} time",
|
||||||
|
"disable_for_hours_plural": "I {{count}} timer",
|
||||||
|
"disable_until_tomorrow": "Indtil i morgen",
|
||||||
|
"disable_notify_for_seconds": "Deaktivere beskyttelse i {{count}} sekund",
|
||||||
|
"disable_notify_for_seconds_plural": "Deaktivere beskyttelse i {{count}} sekunder",
|
||||||
|
"disable_notify_for_minutes": "Deaktivere beskyttelse i {{count}} minut",
|
||||||
|
"disable_notify_for_minutes_plural": "Deaktivere beskyttelse i {{count}} minutter",
|
||||||
|
"disable_notify_for_hours": "Deaktivere beskyttelse i {{count}} time",
|
||||||
|
"disable_notify_for_hours_plural": "Deaktivere beskyttelse i {{count}} timer",
|
||||||
|
"disable_notify_until_tomorrow": "Deaktiver beskyttelse indtil i morgen",
|
||||||
|
"enable_protection_timer": "Beskyttelse deaktiveres om {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Kindersicherung aktiviert",
|
"enabled_parental_toast": "Kindersicherung aktiviert",
|
||||||
"disabled_safe_search_toast": "Sichere Suche deaktiviert",
|
"disabled_safe_search_toast": "Sichere Suche deaktiviert",
|
||||||
"enabled_save_search_toast": "Sichere Suche aktiviert",
|
"enabled_save_search_toast": "Sichere Suche aktiviert",
|
||||||
|
"updated_save_search_toast": "Einstellungen für die sichere Suche aktualisiert",
|
||||||
"enabled_table_header": "Aktiviert",
|
"enabled_table_header": "Aktiviert",
|
||||||
"name_table_header": "Name",
|
"name_table_header": "Name",
|
||||||
"list_url_table_header": "Adressliste",
|
"list_url_table_header": "Adressliste",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Begrenzungswert",
|
"rate_limit": "Begrenzungswert",
|
||||||
"edns_enable": "EDNS Client Subnetz aktivieren",
|
"edns_enable": "EDNS Client Subnetz aktivieren",
|
||||||
"edns_cs_desc": "Die Option EDNS Client Subnetz (ECS) zu Upstream-Anfragen hinzufügen und die von Clients gesendeten Werte protokollieren.",
|
"edns_cs_desc": "Die Option EDNS Client Subnetz (ECS) zu Upstream-Anfragen hinzufügen und die von Clients gesendeten Werte protokollieren.",
|
||||||
|
"edns_use_custom_ip": "Benutzerdefinierte IP für EDNS verwenden",
|
||||||
|
"edns_use_custom_ip_desc": "Benutzerdefinierte IP für EDNS zulassen",
|
||||||
"rate_limit_desc": "Die Anzahl der Anfragen pro Sekunde, die ein einzelner Client stellen darf. Das Setzen auf 0 bedeutet keine Begrenzung.",
|
"rate_limit_desc": "Die Anzahl der Anfragen pro Sekunde, die ein einzelner Client stellen darf. Das Setzen auf 0 bedeutet keine Begrenzung.",
|
||||||
"blocking_ipv4_desc": "IP-Adresse, die für eine gesperrte A-Anfrage zurückgegeben werden soll",
|
"blocking_ipv4_desc": "IP-Adresse, die für eine gesperrte A-Anfrage zurückgegeben werden soll",
|
||||||
"blocking_ipv6_desc": "IP-Adresse, die für eine gesperrte AAAA-Anfrage zurückgegeben werden soll",
|
"blocking_ipv6_desc": "IP-Adresse, die für eine gesperrte AAAA-Anfrage zurückgegeben werden soll",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Möchten Sie wirklich die Aufbewahrung der Statistiken ändern? Wenn Sie den Zeitabstand verringern, gehen einige Daten verloren.",
|
"statistics_retention_confirm": "Möchten Sie wirklich die Aufbewahrung der Statistiken ändern? Wenn Sie den Zeitabstand verringern, gehen einige Daten verloren.",
|
||||||
"statistics_cleared": "Statistiken wurden erfolgreich gelöscht",
|
"statistics_cleared": "Statistiken wurden erfolgreich gelöscht",
|
||||||
"statistics_enable": "Statistiken aktivieren",
|
"statistics_enable": "Statistiken aktivieren",
|
||||||
|
"ignore_domains": "Ignorierte Domains (durch Zeilenumbruch getrennt)",
|
||||||
|
"ignore_domains_title": "Ignorierte Domains",
|
||||||
|
"ignore_domains_desc_stats": "Abfragen für diese Domains werden nicht in die Statistik aufgenommen",
|
||||||
|
"ignore_domains_desc_query": "Abfragen für diese Domains werden nicht in das Abfrageprotokoll aufgenommen",
|
||||||
"interval_hours": "{{count}} Stunde",
|
"interval_hours": "{{count}} Stunde",
|
||||||
"interval_hours_plural": "{{count}} Stunden",
|
"interval_hours_plural": "{{count}} Stunden",
|
||||||
"filters_configuration": "Filterkonfiguration",
|
"filters_configuration": "Filterkonfiguration",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Hinweis:</0> Die IP-Anonymisierung ist aktiviert. Sie können sie in den <1>Allgemeinen Einstellungen</1> deaktivieren.",
|
"anonymizer_notification": "<0>Hinweis:</0> Die IP-Anonymisierung ist aktiviert. Sie können sie in den <1>Allgemeinen Einstellungen</1> deaktivieren.",
|
||||||
"confirm_dns_cache_clear": "Möchten Sie den DNS-Cache wirklich leeren?",
|
"confirm_dns_cache_clear": "Möchten Sie den DNS-Cache wirklich leeren?",
|
||||||
"cache_cleared": "DNS-Cache erfolgreich geleert",
|
"cache_cleared": "DNS-Cache erfolgreich geleert",
|
||||||
"clear_cache": "Cache leeren"
|
"clear_cache": "Cache leeren",
|
||||||
|
"make_static": "Statisch machen",
|
||||||
|
"theme_auto_desc": "Automatisch (basierend auf dem Farbschema Ihres Geräts)",
|
||||||
|
"theme_dark_desc": "Dunkles Farbschema",
|
||||||
|
"theme_light_desc": "Helles Farbschema",
|
||||||
|
"disable_for_seconds": "Für {{count}} Sekunde",
|
||||||
|
"disable_for_seconds_plural": "Für {{count}} Sekunden",
|
||||||
|
"disable_for_minutes": "Für {{count}} Minute",
|
||||||
|
"disable_for_minutes_plural": "Für {{count}} Minuten",
|
||||||
|
"disable_for_hours": "Für {{count}} Stunde",
|
||||||
|
"disable_for_hours_plural": "Für {{count}} Stunden",
|
||||||
|
"disable_until_tomorrow": "Bis morgen",
|
||||||
|
"disable_notify_for_seconds": "Schutz für {{count}} Sekunde deaktivieren",
|
||||||
|
"disable_notify_for_seconds_plural": "Schutz für {{count}} Sekunden deaktivieren",
|
||||||
|
"disable_notify_for_minutes": "Schutz für {{count}} Minute deaktivieren",
|
||||||
|
"disable_notify_for_minutes_plural": "Schutz für {{count}} Minuten deaktivieren",
|
||||||
|
"disable_notify_for_hours": "Schutz für {{count}} Stunde deaktivieren",
|
||||||
|
"disable_notify_for_hours_plural": "Schutz für {{count}} Stunden deaktivieren",
|
||||||
|
"disable_notify_until_tomorrow": "Schutz bis morgen deaktivieren",
|
||||||
|
"enable_protection_timer": "Der Schutz wird in {{time}} wieder aktiviert"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Enabled Parental Control",
|
"enabled_parental_toast": "Enabled Parental Control",
|
||||||
"disabled_safe_search_toast": "Disabled Safe Search",
|
"disabled_safe_search_toast": "Disabled Safe Search",
|
||||||
"enabled_save_search_toast": "Enabled Safe Search",
|
"enabled_save_search_toast": "Enabled Safe Search",
|
||||||
|
"updated_save_search_toast": "Safe Search settings updated",
|
||||||
"enabled_table_header": "Enabled",
|
"enabled_table_header": "Enabled",
|
||||||
"name_table_header": "Name",
|
"name_table_header": "Name",
|
||||||
"list_url_table_header": "List URL",
|
"list_url_table_header": "List URL",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Rate limit",
|
"rate_limit": "Rate limit",
|
||||||
"edns_enable": "Enable EDNS client subnet",
|
"edns_enable": "Enable EDNS client subnet",
|
||||||
"edns_cs_desc": "Add the EDNS Client Subnet option (ECS) to upstream requests and log the values sent by the clients in the query log.",
|
"edns_cs_desc": "Add the EDNS Client Subnet option (ECS) to upstream requests and log the values sent by the clients in the query log.",
|
||||||
|
"edns_use_custom_ip": "Use custom IP for EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Allow to use custom IP for EDNS",
|
||||||
"rate_limit_desc": "The number of requests per second allowed per client. Setting it to 0 means no limit.",
|
"rate_limit_desc": "The number of requests per second allowed per client. Setting it to 0 means no limit.",
|
||||||
"blocking_ipv4_desc": "IP address to be returned for a blocked A request",
|
"blocking_ipv4_desc": "IP address to be returned for a blocked A request",
|
||||||
"blocking_ipv6_desc": "IP address to be returned for a blocked AAAA request",
|
"blocking_ipv6_desc": "IP address to be returned for a blocked AAAA request",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Are you sure you want to change statistics retention? If you decrease the interval value, some data will be lost",
|
"statistics_retention_confirm": "Are you sure you want to change statistics retention? If you decrease the interval value, some data will be lost",
|
||||||
"statistics_cleared": "Statistics successfully cleared",
|
"statistics_cleared": "Statistics successfully cleared",
|
||||||
"statistics_enable": "Enable statistics",
|
"statistics_enable": "Enable statistics",
|
||||||
|
"ignore_domains": "Ignored domains (separated by newline)",
|
||||||
|
"ignore_domains_title": "Ignored domains",
|
||||||
|
"ignore_domains_desc_stats": "Queries for these domains are not written to the statistics",
|
||||||
|
"ignore_domains_desc_query": "Queries for these domains are not written to the query log",
|
||||||
"interval_hours": "{{count}} hour",
|
"interval_hours": "{{count}} hour",
|
||||||
"interval_hours_plural": "{{count}} hours",
|
"interval_hours_plural": "{{count}} hours",
|
||||||
"filters_configuration": "Filters configuration",
|
"filters_configuration": "Filters configuration",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Note:</0> IP anonymization is enabled. You can disable it in <1>General settings</1>.",
|
"anonymizer_notification": "<0>Note:</0> IP anonymization is enabled. You can disable it in <1>General settings</1>.",
|
||||||
"confirm_dns_cache_clear": "Are you sure you want to clear DNS cache?",
|
"confirm_dns_cache_clear": "Are you sure you want to clear DNS cache?",
|
||||||
"cache_cleared": "DNS cache successfully cleared",
|
"cache_cleared": "DNS cache successfully cleared",
|
||||||
"clear_cache": "Clear cache"
|
"clear_cache": "Clear cache",
|
||||||
|
"make_static": "Make static",
|
||||||
|
"theme_auto_desc": "Auto (based on the color scheme of your device)",
|
||||||
|
"theme_dark_desc": "Dark theme",
|
||||||
|
"theme_light_desc": "Light theme",
|
||||||
|
"disable_for_seconds": "For {{count}} second",
|
||||||
|
"disable_for_seconds_plural": "For {{count}} seconds",
|
||||||
|
"disable_for_minutes": "For {{count}} minute",
|
||||||
|
"disable_for_minutes_plural": "For {{count}} minutes",
|
||||||
|
"disable_for_hours": "For {{count}} hour",
|
||||||
|
"disable_for_hours_plural": "For {{count}} hours",
|
||||||
|
"disable_until_tomorrow": "Until tomorrow",
|
||||||
|
"disable_notify_for_seconds": "Disable protection for {{count}} second",
|
||||||
|
"disable_notify_for_seconds_plural": "Disable protection for {{count}} seconds",
|
||||||
|
"disable_notify_for_minutes": "Disable protection for {{count}} minute",
|
||||||
|
"disable_notify_for_minutes_plural": "Disable protection for {{count}} minutes",
|
||||||
|
"disable_notify_for_hours": "Disable protection for {{count}} hour",
|
||||||
|
"disable_notify_for_hours_plural": "Disable protection for {{count}} hours",
|
||||||
|
"disable_notify_until_tomorrow": "Disable protection until tomorrow",
|
||||||
|
"enable_protection_timer": "Protection will be enabled in {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Control parental habilitado",
|
"enabled_parental_toast": "Control parental habilitado",
|
||||||
"disabled_safe_search_toast": "Búsqueda segura deshabilitada",
|
"disabled_safe_search_toast": "Búsqueda segura deshabilitada",
|
||||||
"enabled_save_search_toast": "Búsqueda segura habilitada",
|
"enabled_save_search_toast": "Búsqueda segura habilitada",
|
||||||
|
"updated_save_search_toast": "Configuración de búsqueda segura actualizada",
|
||||||
"enabled_table_header": "Habilitado",
|
"enabled_table_header": "Habilitado",
|
||||||
"name_table_header": "Nombre",
|
"name_table_header": "Nombre",
|
||||||
"list_url_table_header": "URL de la lista",
|
"list_url_table_header": "URL de la lista",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Límite de cantidad",
|
"rate_limit": "Límite de cantidad",
|
||||||
"edns_enable": "Habilitar subred de cliente EDNS",
|
"edns_enable": "Habilitar subred de cliente EDNS",
|
||||||
"edns_cs_desc": "Añade la opción subred de cliente EDNS (ECS) a las peticiones del DNS de subida y registra los valores enviados por los clientes en el registro de consultas.",
|
"edns_cs_desc": "Añade la opción subred de cliente EDNS (ECS) a las peticiones del DNS de subida y registra los valores enviados por los clientes en el registro de consultas.",
|
||||||
|
"edns_use_custom_ip": "Usar IP personalizada para EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Permitir el uso de IP personalizadas para EDNS",
|
||||||
"rate_limit_desc": "Número de peticiones por segundo permitidas por cliente. Establecerlo en 0 significa que no hay límite.",
|
"rate_limit_desc": "Número de peticiones por segundo permitidas por cliente. Establecerlo en 0 significa que no hay límite.",
|
||||||
"blocking_ipv4_desc": "Dirección IP devolverá una petición A bloqueada",
|
"blocking_ipv4_desc": "Dirección IP devolverá una petición A bloqueada",
|
||||||
"blocking_ipv6_desc": "Dirección IP devolverá una petición AAAA bloqueada",
|
"blocking_ipv6_desc": "Dirección IP devolverá una petición AAAA bloqueada",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "¿Estás seguro de que deseas cambiar la retención de estadísticas? Si disminuye el valor del intervalo, se perderán algunos datos",
|
"statistics_retention_confirm": "¿Estás seguro de que deseas cambiar la retención de estadísticas? Si disminuye el valor del intervalo, se perderán algunos datos",
|
||||||
"statistics_cleared": "Estadísticas borradas correctamente",
|
"statistics_cleared": "Estadísticas borradas correctamente",
|
||||||
"statistics_enable": "Habilitar estadísticas",
|
"statistics_enable": "Habilitar estadísticas",
|
||||||
|
"ignore_domains": "Dominios ignorados (separados por una nueva línea)",
|
||||||
|
"ignore_domains_title": "Dominios ignorados",
|
||||||
|
"ignore_domains_desc_stats": "Las consultas para estos dominios no aparecen en las estadísticas",
|
||||||
|
"ignore_domains_desc_query": "Las consultas para estos dominios no aparecen en el registro de consultas",
|
||||||
"interval_hours": "{{count}} hora",
|
"interval_hours": "{{count}} hora",
|
||||||
"interval_hours_plural": "{{count}} horas",
|
"interval_hours_plural": "{{count}} horas",
|
||||||
"filters_configuration": "Configuración de filtros",
|
"filters_configuration": "Configuración de filtros",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Nota:</0> La anonimización de IP está habilitada. Puedes deshabilitarla en <1>Configuración general</1>.",
|
"anonymizer_notification": "<0>Nota:</0> La anonimización de IP está habilitada. Puedes deshabilitarla en <1>Configuración general</1>.",
|
||||||
"confirm_dns_cache_clear": "¿Estás seguro de que deseas borrar la caché DNS?",
|
"confirm_dns_cache_clear": "¿Estás seguro de que deseas borrar la caché DNS?",
|
||||||
"cache_cleared": "Caché DNS borrado correctamente",
|
"cache_cleared": "Caché DNS borrado correctamente",
|
||||||
"clear_cache": "Borrar caché"
|
"clear_cache": "Borrar caché",
|
||||||
|
"make_static": "Hacer estático",
|
||||||
|
"theme_auto_desc": "Automático (basado en el esquema de colores de tu dispositivo)",
|
||||||
|
"theme_dark_desc": "Tema oscuro",
|
||||||
|
"theme_light_desc": "Tema claro",
|
||||||
|
"disable_for_seconds": "Por {{count}} segundo",
|
||||||
|
"disable_for_seconds_plural": "Por {{count}} segundos",
|
||||||
|
"disable_for_minutes": "Por {{count}} minuto",
|
||||||
|
"disable_for_minutes_plural": "Por {{count}} minutos",
|
||||||
|
"disable_for_hours": "Por {{count}} hora",
|
||||||
|
"disable_for_hours_plural": "Por {{count}} horas",
|
||||||
|
"disable_until_tomorrow": "Hasta mañana",
|
||||||
|
"disable_notify_for_seconds": "Desactivar la protección por {{count}} segundo",
|
||||||
|
"disable_notify_for_seconds_plural": "Desactivar la protección por {{count}} segundos",
|
||||||
|
"disable_notify_for_minutes": "Desactivar la protección por {{count}} minuto",
|
||||||
|
"disable_notify_for_minutes_plural": "Desactivar la protección por {{count}} minutos",
|
||||||
|
"disable_notify_for_hours": "Desactivar la protección por {{count}} hora",
|
||||||
|
"disable_notify_for_hours_plural": "Desactivar la protección por {{count}} horas",
|
||||||
|
"disable_notify_until_tomorrow": "Desactivar la protección hasta mañana",
|
||||||
|
"enable_protection_timer": "La protección se activará en {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,12 @@
|
||||||
"bootstrap_dns": "Bootstrap DNS-palvelimet",
|
"bootstrap_dns": "Bootstrap DNS-palvelimet",
|
||||||
"bootstrap_dns_desc": "Bootstrap DNS-palvelimia käytetään ylävirroiksi määritettyjen DoH/DoT-resolvereiden IP-osoitteiden selvitykseen.",
|
"bootstrap_dns_desc": "Bootstrap DNS-palvelimia käytetään ylävirroiksi määritettyjen DoH/DoT-resolvereiden IP-osoitteiden selvitykseen.",
|
||||||
"local_ptr_title": "Yksityiset käänteiset DNS-palvelimet",
|
"local_ptr_title": "Yksityiset käänteiset DNS-palvelimet",
|
||||||
"local_ptr_desc": "DNS-palvelimet, joita AdGuard Home käyttää paikallisille PTR-kyselyille. Näitä palvelimia käytetään yksityistä IP-osoitetta käyttävien PTR-kyselyiden osoitteiden, kuten \"192.168.12.34\", selvitykseen käänteisen DNS:n avulla. Jos ei käytössä, AdGuard Home käyttää käyttöjärjestelmän oletusarvoisia DNS-resolvereita, poislukien AdGuard Homen omat osoitteet.",
|
"local_ptr_desc": "DNS-palvelimet, joita AdGuard Home käyttää paikallisille PTR-pyynnöille. Näitä palvelimia käytetään yksityistä IP-osoitetta käyttävien PTR-pyyntöjen osoitteiden, kuten \"192.168.12.34\", selvitykseen käänteisen DNS:n avulla. Jos ei käytössä, AdGuard Home käyttää käyttöjärjestelmän oletusarvoisia DNS-resolvereita, poislukien AdGuard Homen omat osoitteet.",
|
||||||
"local_ptr_default_resolver": "Oletusarvoisesti AdGuard Home käyttää seuraavia käänteisiä DNS-resolvereita: {{ip}}.",
|
"local_ptr_default_resolver": "Oletusarvoisesti AdGuard Home käyttää seuraavia käänteisiä DNS-resolvereita: {{ip}}.",
|
||||||
"local_ptr_no_default_resolver": "AdGuard Home ei voinut määrittää tälle järjestelmälle sopivaa yksityistä käänteistä DNS-resolveria.",
|
"local_ptr_no_default_resolver": "AdGuard Home ei voinut määrittää tälle järjestelmälle sopivaa yksityistä käänteistä DNS-resolveria.",
|
||||||
"local_ptr_placeholder": "Syötä yksi palvelimen osoite per rivi",
|
"local_ptr_placeholder": "Syötä yksi palvelimen osoite per rivi",
|
||||||
"resolve_clients_title": "Käytä päätelaitteiden IP-osoitteille käänteistä selvitystä",
|
"resolve_clients_title": "Käytä päätelaitteiden IP-osoitteille käänteistä selvitystä",
|
||||||
"resolve_clients_desc": "Selvitä päätelaitteiden IP-osoitteiden isäntänimet käänteisesti lähettämällä PTR-kyselyt sopiville resolvereille (yksityiset DNS-palvelimet paikallisille päätelaitteille, lähtevät palvelimet päätelaitteille, joilla on julkiset IP-osoitteet).",
|
"resolve_clients_desc": "Selvitä päätelaitteiden IP-osoitteiden isäntänimet käänteisesti lähettämällä PTR-pyynnöt sopiville resolvereille (yksityiset DNS-palvelimet paikallisille päätelaitteille, lähtevät palvelimet päätelaitteille, joilla on julkiset IP-osoitteet).",
|
||||||
"use_private_ptr_resolvers_title": "Käytä yksityisiä käänteisiä DNS-resolvereita",
|
"use_private_ptr_resolvers_title": "Käytä yksityisiä käänteisiä DNS-resolvereita",
|
||||||
"use_private_ptr_resolvers_desc": "Suorita käänteiset DNS-selvitykset paikallisesti tarjotuille osoitteille käyttäen näitä ylävirran palvelimia. Jos ei käytössä, vastaa AdGuard Home kaikkiin sen tyyppisiin PTR-pyyntöihin NXDOMAIN-arvolla, pois lukien DHCP, /etc/hosts, yms. -tiedoista tunnistettut päätelaitteet.",
|
"use_private_ptr_resolvers_desc": "Suorita käänteiset DNS-selvitykset paikallisesti tarjotuille osoitteille käyttäen näitä ylävirran palvelimia. Jos ei käytössä, vastaa AdGuard Home kaikkiin sen tyyppisiin PTR-pyyntöihin NXDOMAIN-arvolla, pois lukien DHCP, /etc/hosts, yms. -tiedoista tunnistettut päätelaitteet.",
|
||||||
"check_dhcp_servers": "Etsi DHCP-palvelimia",
|
"check_dhcp_servers": "Etsi DHCP-palvelimia",
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
"dhcp_add_static_lease": "Lisää kiinteä laina",
|
"dhcp_add_static_lease": "Lisää kiinteä laina",
|
||||||
"dhcp_reset_leases": "Tyhjennä kaikki lainat",
|
"dhcp_reset_leases": "Tyhjennä kaikki lainat",
|
||||||
"dhcp_reset_leases_confirm": "Haluatko varmasti tyhjentää kaikki lainat?",
|
"dhcp_reset_leases_confirm": "Haluatko varmasti tyhjentää kaikki lainat?",
|
||||||
"dhcp_reset_leases_success": "DHCP-lainat tyhjennettiin",
|
"dhcp_reset_leases_success": "DHCP-lainojen tyhjennys onnistui",
|
||||||
"dhcp_reset": "Haluatko varmasti palauttaa DHCP-asetukset?",
|
"dhcp_reset": "Haluatko varmasti palauttaa DHCP-asetukset?",
|
||||||
"country": "Maa",
|
"country": "Maa",
|
||||||
"city": "Kaupunki",
|
"city": "Kaupunki",
|
||||||
|
@ -167,10 +167,11 @@
|
||||||
"enabled_parental_toast": "Lapsilukko otettiin käyttöön",
|
"enabled_parental_toast": "Lapsilukko otettiin käyttöön",
|
||||||
"disabled_safe_search_toast": "Turvallinen haku poistettiin käytöstä",
|
"disabled_safe_search_toast": "Turvallinen haku poistettiin käytöstä",
|
||||||
"enabled_save_search_toast": "Turvallinen haku otettiin käyttöön",
|
"enabled_save_search_toast": "Turvallinen haku otettiin käyttöön",
|
||||||
|
"updated_save_search_toast": "Turvallisen haun asetukset päivitettiin",
|
||||||
"enabled_table_header": "Käytössä",
|
"enabled_table_header": "Käytössä",
|
||||||
"name_table_header": "Nimi",
|
"name_table_header": "Nimi",
|
||||||
"list_url_table_header": "Listan URL",
|
"list_url_table_header": "Listan URL",
|
||||||
"rules_count_table_header": "Sääntöjä",
|
"rules_count_table_header": "Sääntöjen määrä",
|
||||||
"last_time_updated_table_header": "Viimeisin päivitys",
|
"last_time_updated_table_header": "Viimeisin päivitys",
|
||||||
"actions_table_header": "Toiminnot",
|
"actions_table_header": "Toiminnot",
|
||||||
"request_table_header": "Pyyntö",
|
"request_table_header": "Pyyntö",
|
||||||
|
@ -253,8 +254,8 @@
|
||||||
"query_log_response_status": "Tila: {{value}}",
|
"query_log_response_status": "Tila: {{value}}",
|
||||||
"query_log_filtered": "Suodattanut {{filter}}",
|
"query_log_filtered": "Suodattanut {{filter}}",
|
||||||
"query_log_confirm_clear": "Haluatko varmasti tyhjentää pyyntöhistorian?",
|
"query_log_confirm_clear": "Haluatko varmasti tyhjentää pyyntöhistorian?",
|
||||||
"query_log_cleared": "Pyyntöhistoria tyhjennettiin",
|
"query_log_cleared": "Pyyntöhistorian tyhjennys onnistui",
|
||||||
"query_log_updated": "Pyyntöhistoria päivitettiin",
|
"query_log_updated": "Pyyntöhistorian päivitys onnistui",
|
||||||
"query_log_clear": "Tyhjennä pyyntöhistoria",
|
"query_log_clear": "Tyhjennä pyyntöhistoria",
|
||||||
"query_log_retention": "Pyyntöhistorian säilytys",
|
"query_log_retention": "Pyyntöhistorian säilytys",
|
||||||
"query_log_enable": "Käytä historiaa",
|
"query_log_enable": "Käytä historiaa",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Pyyntöjen ajoitus",
|
"rate_limit": "Pyyntöjen ajoitus",
|
||||||
"edns_enable": "Käytä EDNS-päätelaitealivekkoa",
|
"edns_enable": "Käytä EDNS-päätelaitealivekkoa",
|
||||||
"edns_cs_desc": "Lähetä päätelaitteiden aliverkot DNS-palvelimille.",
|
"edns_cs_desc": "Lähetä päätelaitteiden aliverkot DNS-palvelimille.",
|
||||||
|
"edns_use_custom_ip": "Käytä omaa IP-osoitetta EDNS:lle",
|
||||||
|
"edns_use_custom_ip_desc": "Salli oman IP-osoitteen käyttö EDNS-mekanismille.",
|
||||||
"rate_limit_desc": "Päätelaitteelle sallittu pyyntöjen enimmäismäärä sekunnissa. Arvo 0 tarkoittaa rajatonta.",
|
"rate_limit_desc": "Päätelaitteelle sallittu pyyntöjen enimmäismäärä sekunnissa. Arvo 0 tarkoittaa rajatonta.",
|
||||||
"blocking_ipv4_desc": "Estettyyn A-pyyntöön palautettava IP-osoite",
|
"blocking_ipv4_desc": "Estettyyn A-pyyntöön palautettava IP-osoite",
|
||||||
"blocking_ipv6_desc": "Estettyyn AAAA-pyyntöön palautettava IP-osoite",
|
"blocking_ipv6_desc": "Estettyyn AAAA-pyyntöön palautettava IP-osoite",
|
||||||
|
@ -367,7 +370,7 @@
|
||||||
"encryption_config_saved": "Salausasetukset tallennettiin",
|
"encryption_config_saved": "Salausasetukset tallennettiin",
|
||||||
"encryption_server": "Palvelimen nimi",
|
"encryption_server": "Palvelimen nimi",
|
||||||
"encryption_server_enter": "Syötä verkkotunnuksesi",
|
"encryption_server_enter": "Syötä verkkotunnuksesi",
|
||||||
"encryption_server_desc": "Jos määritetty, AdGuard Home tunnistaa ClientID-tunnisteet, vastaa DDR-kyselyihin ja suorittaa yhteyden lisätarkistuksia. Jos ei määritetty, nämä ominaisuudet eivät ole käytössä. On vastattava yhtä varmenteen DNS-nimistä.",
|
"encryption_server_desc": "Jos määritetty, AdGuard Home tunnistaa ClientID-tunnisteet, vastaa DDR-pyyntöihin ja suorittaa yhteyden lisätarkistuksia. Jos ei määritetty, nämä ominaisuudet eivät ole käytössä. On vastattava yhtä varmenteen DNS-nimistä.",
|
||||||
"encryption_redirect": "Automaattinen HTTPS-ohjaus",
|
"encryption_redirect": "Automaattinen HTTPS-ohjaus",
|
||||||
"encryption_redirect_desc": "Jos käytössä, AdGuard Home ohjaa HTTP-osoitteet automaattisesti HTTPS-osoitteisiin.",
|
"encryption_redirect_desc": "Jos käytössä, AdGuard Home ohjaa HTTP-osoitteet automaattisesti HTTPS-osoitteisiin.",
|
||||||
"encryption_https": "HTTPS-portti",
|
"encryption_https": "HTTPS-portti",
|
||||||
|
@ -416,7 +419,7 @@
|
||||||
"clients_title": "Pysyvät päätelaitteet",
|
"clients_title": "Pysyvät päätelaitteet",
|
||||||
"clients_desc": "Määritä pysyvät AdGuard Homeen yhdistetyt päätelaitetiedot.",
|
"clients_desc": "Määritä pysyvät AdGuard Homeen yhdistetyt päätelaitetiedot.",
|
||||||
"settings_global": "Yleinen",
|
"settings_global": "Yleinen",
|
||||||
"settings_custom": "Muut aiheet",
|
"settings_custom": "Mukautettu",
|
||||||
"table_client": "Asiakas",
|
"table_client": "Asiakas",
|
||||||
"table_name": "Nimi",
|
"table_name": "Nimi",
|
||||||
"save_btn": "Tallenna",
|
"save_btn": "Tallenna",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Haluatko varmasti muuttaa tilastojen säilytysaikaa? Jos aikaa lyhennetään, joitakin tietoja menetetään.",
|
"statistics_retention_confirm": "Haluatko varmasti muuttaa tilastojen säilytysaikaa? Jos aikaa lyhennetään, joitakin tietoja menetetään.",
|
||||||
"statistics_cleared": "Tilastot tyhjennettiin",
|
"statistics_cleared": "Tilastot tyhjennettiin",
|
||||||
"statistics_enable": "Ota tilastointi käyttöön",
|
"statistics_enable": "Ota tilastointi käyttöön",
|
||||||
|
"ignore_domains": "Ohitettavat verkkotunnukset (erotettu rivinvaihdolla)",
|
||||||
|
"ignore_domains_title": "Ohitettavat verkkotunnukset",
|
||||||
|
"ignore_domains_desc_stats": "Näihin verkkotunnuksiin lähetettyjä pyyntöjä ei tallenneta tilastoihin.",
|
||||||
|
"ignore_domains_desc_query": "Näihin verkkotunnuksiin lähetettyjä pyyntöjä ei tallenneta pyyntöhistoriaan.",
|
||||||
"interval_hours": "{{count}} tunti",
|
"interval_hours": "{{count}} tunti",
|
||||||
"interval_hours_plural": "{{count}} tuntia",
|
"interval_hours_plural": "{{count}} tuntia",
|
||||||
"filters_configuration": "Suodatinten määritys",
|
"filters_configuration": "Suodatinten määritys",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Huomioi:</0> IP-osoitteen anonymisointi on käytössä. Voit poistaa sen käytöstä <1>Yleisistä asetuksista</1>.",
|
"anonymizer_notification": "<0>Huomioi:</0> IP-osoitteen anonymisointi on käytössä. Voit poistaa sen käytöstä <1>Yleisistä asetuksista</1>.",
|
||||||
"confirm_dns_cache_clear": "Haluatko varmasti tyhjentää DNS-välimuistin?",
|
"confirm_dns_cache_clear": "Haluatko varmasti tyhjentää DNS-välimuistin?",
|
||||||
"cache_cleared": "DNS-välimuistin tyhjennys onnistui",
|
"cache_cleared": "DNS-välimuistin tyhjennys onnistui",
|
||||||
"clear_cache": "Tyhjennä välimuisti"
|
"clear_cache": "Tyhjennä välimuisti",
|
||||||
|
"make_static": "Tallenna kiinteäksi",
|
||||||
|
"theme_auto_desc": "Automaattinen (seuraa laitteen väriteemaa)",
|
||||||
|
"theme_dark_desc": "Tumma teema",
|
||||||
|
"theme_light_desc": "Vaalea teema",
|
||||||
|
"disable_for_seconds": "{{count}} sekunniksi",
|
||||||
|
"disable_for_seconds_plural": "{{count}} sekunniksi",
|
||||||
|
"disable_for_minutes": "{{count}} minuutiksi",
|
||||||
|
"disable_for_minutes_plural": "{{count}} minuutiksi",
|
||||||
|
"disable_for_hours": "{{count}} tunniksi",
|
||||||
|
"disable_for_hours_plural": "{{count}} tunniksi",
|
||||||
|
"disable_until_tomorrow": "Huomiseen asti",
|
||||||
|
"disable_notify_for_seconds": "Poista suojaus käytöstä {{count}} sekunniksi",
|
||||||
|
"disable_notify_for_seconds_plural": "Poista suojaus käytöstä {{count}} sekunniksi",
|
||||||
|
"disable_notify_for_minutes": "Poista suojaus käytöstä {{count}} minuutiksi",
|
||||||
|
"disable_notify_for_minutes_plural": "Poista suojaus käytöstä {{count}} minuutiksi",
|
||||||
|
"disable_notify_for_hours": "Poista suojaus käytöstä {{count}} tunniksi",
|
||||||
|
"disable_notify_for_hours_plural": "Poista suojaus käytöstä {{count}} tunniksi",
|
||||||
|
"disable_notify_until_tomorrow": "Poista suojaus käytöstä huomiseen asti",
|
||||||
|
"enable_protection_timer": "Suojaus otetaan käyttöön {{time}} kuluttua"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Contrôle Parental activé",
|
"enabled_parental_toast": "Contrôle Parental activé",
|
||||||
"disabled_safe_search_toast": "Recherche Sécurisée désactivée",
|
"disabled_safe_search_toast": "Recherche Sécurisée désactivée",
|
||||||
"enabled_save_search_toast": "Recherche Sécurisée activée",
|
"enabled_save_search_toast": "Recherche Sécurisée activée",
|
||||||
|
"updated_save_search_toast": "Les paramètres de Recherche sécurisée sont mis à jour",
|
||||||
"enabled_table_header": "Activé",
|
"enabled_table_header": "Activé",
|
||||||
"name_table_header": "Nom",
|
"name_table_header": "Nom",
|
||||||
"list_url_table_header": "URL de la liste",
|
"list_url_table_header": "URL de la liste",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Limite de taux",
|
"rate_limit": "Limite de taux",
|
||||||
"edns_enable": "Activer le sous-réseau du client EDNS",
|
"edns_enable": "Activer le sous-réseau du client EDNS",
|
||||||
"edns_cs_desc": "Ajouter l'option du sous-réseau Client EDNS (ECS) au requêtes en amont et enregistrer les valeurs envoyées par les clients dans le journal des requêtes.",
|
"edns_cs_desc": "Ajouter l'option du sous-réseau Client EDNS (ECS) au requêtes en amont et enregistrer les valeurs envoyées par les clients dans le journal des requêtes.",
|
||||||
|
"edns_use_custom_ip": "Utiliser une IP personnalisée pour EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Autoriser l'utilisation d'une adresse IP personnalisée pour EDNS",
|
||||||
"rate_limit_desc": "Le nombre de requêtes par seconde qu’un seul client est autorisé à faire. Le réglage 0 fait illimité.",
|
"rate_limit_desc": "Le nombre de requêtes par seconde qu’un seul client est autorisé à faire. Le réglage 0 fait illimité.",
|
||||||
"blocking_ipv4_desc": "Adresse IP à renvoyer pour une demande A bloquée",
|
"blocking_ipv4_desc": "Adresse IP à renvoyer pour une demande A bloquée",
|
||||||
"blocking_ipv6_desc": "Adresse IP à renvoyer pour une demande AAAA bloquée",
|
"blocking_ipv6_desc": "Adresse IP à renvoyer pour une demande AAAA bloquée",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Êtes-vous sûr de vouloir modifier le maintien des statistiques ? Si vous diminuez la valeur de l'intervalle, certaines données seront perdues",
|
"statistics_retention_confirm": "Êtes-vous sûr de vouloir modifier le maintien des statistiques ? Si vous diminuez la valeur de l'intervalle, certaines données seront perdues",
|
||||||
"statistics_cleared": "Statistiques effacées",
|
"statistics_cleared": "Statistiques effacées",
|
||||||
"statistics_enable": "Activer les statistiques",
|
"statistics_enable": "Activer les statistiques",
|
||||||
|
"ignore_domains": "Domaines ignorés (séparés par une nouvelle ligne)",
|
||||||
|
"ignore_domains_title": "Domaines ignorés",
|
||||||
|
"ignore_domains_desc_stats": "Les requêtes pour ces domaines ne sont pas écrites dans les statistiques",
|
||||||
|
"ignore_domains_desc_query": "Les requêtes pour ces domaines ne sont pas écrites dans le journal des requêtes",
|
||||||
"interval_hours": "{{count}} heure",
|
"interval_hours": "{{count}} heure",
|
||||||
"interval_hours_plural": "{{count}} heures",
|
"interval_hours_plural": "{{count}} heures",
|
||||||
"filters_configuration": "Configuration des filtres",
|
"filters_configuration": "Configuration des filtres",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Note :</0> L'anonymisation IP est activée. Vous pouvez la désactiver dans les <1>paramètres généraux</1>.",
|
"anonymizer_notification": "<0>Note :</0> L'anonymisation IP est activée. Vous pouvez la désactiver dans les <1>paramètres généraux</1>.",
|
||||||
"confirm_dns_cache_clear": "Voulez-vous vraiment vider le cache DNS ?",
|
"confirm_dns_cache_clear": "Voulez-vous vraiment vider le cache DNS ?",
|
||||||
"cache_cleared": "Le cache DNS a été vidé",
|
"cache_cleared": "Le cache DNS a été vidé",
|
||||||
"clear_cache": "Vider le cache"
|
"clear_cache": "Vider le cache",
|
||||||
|
"make_static": "Rendre statique",
|
||||||
|
"theme_auto_desc": "Auto (en fonction de la palette de couleurs de votre appareil)",
|
||||||
|
"theme_dark_desc": "Thème sombre",
|
||||||
|
"theme_light_desc": "Thème clair",
|
||||||
|
"disable_for_seconds": "Pendant {{count}} seconde",
|
||||||
|
"disable_for_seconds_plural": "Pendant {{count}} secondes",
|
||||||
|
"disable_for_minutes": "Pendant {{count}} minute",
|
||||||
|
"disable_for_minutes_plural": "Pendant {{count}} minutes",
|
||||||
|
"disable_for_hours": "Pendant {{count}} heure",
|
||||||
|
"disable_for_hours_plural": "Pendant {{count}} heures",
|
||||||
|
"disable_until_tomorrow": "Jusqu'à demain",
|
||||||
|
"disable_notify_for_seconds": "Désactiver la protection pendant {{count}} seconde",
|
||||||
|
"disable_notify_for_seconds_plural": "Désactiver la protection pendant {{count}} secondes",
|
||||||
|
"disable_notify_for_minutes": "Désactiver la protection pendant {{count}} minute",
|
||||||
|
"disable_notify_for_minutes_plural": "Désactiver la protection pendant {{count}} minutes",
|
||||||
|
"disable_notify_for_hours": "Désactiver la protection pendant {{count}} heure",
|
||||||
|
"disable_notify_for_hours_plural": "Désactiver la protection pendant {{count}} heures",
|
||||||
|
"disable_notify_until_tomorrow": "Désactiver la protection jusqu'à demain",
|
||||||
|
"enable_protection_timer": "La protection sera activée dans {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Omogućen roditeljski nadzor",
|
"enabled_parental_toast": "Omogućen roditeljski nadzor",
|
||||||
"disabled_safe_search_toast": "Onemogućeno sigurno pretraživanje",
|
"disabled_safe_search_toast": "Onemogućeno sigurno pretraživanje",
|
||||||
"enabled_save_search_toast": "Omogućeno sigurno pretraživanje",
|
"enabled_save_search_toast": "Omogućeno sigurno pretraživanje",
|
||||||
|
"updated_save_search_toast": "Ažurirane postavke sigurnog pretraživanja",
|
||||||
"enabled_table_header": "Omogućeno",
|
"enabled_table_header": "Omogućeno",
|
||||||
"name_table_header": "Naziv",
|
"name_table_header": "Naziv",
|
||||||
"list_url_table_header": "URL popisa",
|
"list_url_table_header": "URL popisa",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Ograničenje",
|
"rate_limit": "Ograničenje",
|
||||||
"edns_enable": "Omogući podmrežu klijenta EDNS-a",
|
"edns_enable": "Omogući podmrežu klijenta EDNS-a",
|
||||||
"edns_cs_desc": "Dodajte opciju EDNS klijentske podmreže (ECS) uzvodnim zahtjevima i zabilježite vrijednosti koje su klijenti poslali u dnevnik upita.",
|
"edns_cs_desc": "Dodajte opciju EDNS klijentske podmreže (ECS) uzvodnim zahtjevima i zabilježite vrijednosti koje su klijenti poslali u dnevnik upita.",
|
||||||
|
"edns_use_custom_ip": "Koristi prilagođeni IP za EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Dopusti korištenje prilagođenog IP-a za EDNS",
|
||||||
"rate_limit_desc": "Broj zahtjeva u sekundi koji su dopušteni po jednom klijentu. Postavljanje na 0 znači neograničeno.",
|
"rate_limit_desc": "Broj zahtjeva u sekundi koji su dopušteni po jednom klijentu. Postavljanje na 0 znači neograničeno.",
|
||||||
"blocking_ipv4_desc": "Povratna IP adresa za blokirane A zahtjeve",
|
"blocking_ipv4_desc": "Povratna IP adresa za blokirane A zahtjeve",
|
||||||
"blocking_ipv6_desc": "Povratna IP adresa za blokirane AAAA zahtjeve",
|
"blocking_ipv6_desc": "Povratna IP adresa za blokirane AAAA zahtjeve",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Jeste li sigurni da želite promijeniti zadržavanje statistike? Ako smanjite vrijednost intervala, neki će podaci biti izgubljeni",
|
"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_cleared": "Statistika je uspješno uklonjenja",
|
||||||
"statistics_enable": "Omogući statistiku",
|
"statistics_enable": "Omogući statistiku",
|
||||||
|
"ignore_domains": "Zanemarene domene (odvojene novim retkom)",
|
||||||
|
"ignore_domains_title": "Zanemarene domene",
|
||||||
|
"ignore_domains_desc_stats": "Upiti za ove domene ne upisuju se u statistiku",
|
||||||
|
"ignore_domains_desc_query": "Upiti za te domene nisu zapisani u zapisnik upita",
|
||||||
"interval_hours": "{{count}} sata/i",
|
"interval_hours": "{{count}} sata/i",
|
||||||
"interval_hours_plural": "{{count}} sata/i",
|
"interval_hours_plural": "{{count}} sata/i",
|
||||||
"filters_configuration": "Postavke filtara",
|
"filters_configuration": "Postavke filtara",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Napomena:</0>IP anonimizacija je omogućena. Možete ju onemogućiti u <1>općim postavkama</1>.",
|
"anonymizer_notification": "<0>Napomena:</0>IP anonimizacija je omogućena. Možete ju onemogućiti u <1>općim postavkama</1>.",
|
||||||
"confirm_dns_cache_clear": "Jeste li sigurni da želite očistiti DNS predmemoriju?",
|
"confirm_dns_cache_clear": "Jeste li sigurni da želite očistiti DNS predmemoriju?",
|
||||||
"cache_cleared": "DNS predmemorija je uspješno izbrisana",
|
"cache_cleared": "DNS predmemorija je uspješno izbrisana",
|
||||||
"clear_cache": "Očisti predmemoriju"
|
"clear_cache": "Očisti predmemoriju",
|
||||||
|
"make_static": "Učini statičnim",
|
||||||
|
"theme_auto_desc": "Automatski (na temelju sheme boja vašeg uređaja)",
|
||||||
|
"theme_dark_desc": "Tamna tema",
|
||||||
|
"theme_light_desc": "Svijetla tema",
|
||||||
|
"disable_for_seconds": "Za {{count}} sekundi",
|
||||||
|
"disable_for_seconds_plural": "Za {{count}} sekundi",
|
||||||
|
"disable_for_minutes": "Za {{count}} minuta",
|
||||||
|
"disable_for_minutes_plural": "Za {{count}} minuta",
|
||||||
|
"disable_for_hours": "Za {{count}} sati",
|
||||||
|
"disable_for_hours_plural": "Za {{count}} sati",
|
||||||
|
"disable_until_tomorrow": "Do sutra",
|
||||||
|
"disable_notify_for_seconds": "Isključi zaštitu na {{count}} sekundi",
|
||||||
|
"disable_notify_for_seconds_plural": "Onemogući zaštitu na {{count}} sekundi",
|
||||||
|
"disable_notify_for_minutes": "Isključi zaštitu na {{count}} minuta",
|
||||||
|
"disable_notify_for_minutes_plural": "Isključi zaštitu na {{count}} minuta",
|
||||||
|
"disable_notify_for_hours": "Isključi zaštitu na {{count}} sati",
|
||||||
|
"disable_notify_for_hours_plural": "Isključi zaštitu na {{count}} sati",
|
||||||
|
"disable_notify_until_tomorrow": "Isključi zaštitu do sutra",
|
||||||
|
"enable_protection_timer": "Zaštita će biti omogućena u {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Il Controllo Parentale è attivo",
|
"enabled_parental_toast": "Il Controllo Parentale è attivo",
|
||||||
"disabled_safe_search_toast": "La Ricerca Sicura è disattivata",
|
"disabled_safe_search_toast": "La Ricerca Sicura è disattivata",
|
||||||
"enabled_save_search_toast": "La Ricerca Sicura è attiva",
|
"enabled_save_search_toast": "La Ricerca Sicura è attiva",
|
||||||
|
"updated_save_search_toast": "Impostazioni di Safe Search aggiornate",
|
||||||
"enabled_table_header": "Attivo",
|
"enabled_table_header": "Attivo",
|
||||||
"name_table_header": "Nome",
|
"name_table_header": "Nome",
|
||||||
"list_url_table_header": "Elenco URL",
|
"list_url_table_header": "Elenco URL",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Limite delle richieste",
|
"rate_limit": "Limite delle richieste",
|
||||||
"edns_enable": "Attiva client di sottorete EDNS",
|
"edns_enable": "Attiva client di sottorete EDNS",
|
||||||
"edns_cs_desc": "Aggiunge l'opzione EDNS Client Subnet (ECS) alle richieste upstream e registra i valori inviati dai client nel registro delle richieste.",
|
"edns_cs_desc": "Aggiunge l'opzione EDNS Client Subnet (ECS) alle richieste upstream e registra i valori inviati dai client nel registro delle richieste.",
|
||||||
|
"edns_use_custom_ip": "Usa IP personalizzato per EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Consentire l'uso di un IP personalizzato per EDNS",
|
||||||
"rate_limit_desc": "Il numero di richieste al secondo consentite da un singolo client. Impostare questo valore a 0 rimuove le limitazioni.",
|
"rate_limit_desc": "Il numero di richieste al secondo consentite da un singolo client. Impostare questo valore a 0 rimuove le limitazioni.",
|
||||||
"blocking_ipv4_desc": "Indirizzo IP per una richiesta DNS IPv4 bloccata",
|
"blocking_ipv4_desc": "Indirizzo IP per una richiesta DNS IPv4 bloccata",
|
||||||
"blocking_ipv6_desc": "Indirizzo IP restituito per una richiesta DNS IPv6 bloccata",
|
"blocking_ipv6_desc": "Indirizzo IP restituito per una richiesta DNS IPv6 bloccata",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Sei sicuro di voler modificare la conservazione delle statistiche? Se il valore di intervallo dovesse diminuire, alcuni dati andranno persi",
|
"statistics_retention_confirm": "Sei sicuro di voler modificare la conservazione delle statistiche? Se il valore di intervallo dovesse diminuire, alcuni dati andranno persi",
|
||||||
"statistics_cleared": "Statistiche azzerate correttamente",
|
"statistics_cleared": "Statistiche azzerate correttamente",
|
||||||
"statistics_enable": "Attiva statistiche",
|
"statistics_enable": "Attiva statistiche",
|
||||||
|
"ignore_domains": "Domini ignorati (separati da nuova riga)",
|
||||||
|
"ignore_domains_title": "Domini ignorati",
|
||||||
|
"ignore_domains_desc_stats": "Le richieste per questi domini non vengono scritte nelle statistiche",
|
||||||
|
"ignore_domains_desc_query": "Le richieste per questi domini non vengono scritte nel registro delle richieste",
|
||||||
"interval_hours": "{{count}} ora",
|
"interval_hours": "{{count}} ora",
|
||||||
"interval_hours_plural": "{{count}} ore",
|
"interval_hours_plural": "{{count}} ore",
|
||||||
"filters_configuration": "Configurazione filtri",
|
"filters_configuration": "Configurazione filtri",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Attenzione:</0> L'anonimizzazione dell'IP è abilitata. Puoi disabilitarla in <1>Impostazioni generali</1>.",
|
"anonymizer_notification": "<0>Attenzione:</0> L'anonimizzazione dell'IP è abilitata. Puoi disabilitarla in <1>Impostazioni generali</1>.",
|
||||||
"confirm_dns_cache_clear": "Sei sicuro di voler cancellare la cache DNS?",
|
"confirm_dns_cache_clear": "Sei sicuro di voler cancellare la cache DNS?",
|
||||||
"cache_cleared": "Cache DNS è stata cancellata correttamente",
|
"cache_cleared": "Cache DNS è stata cancellata correttamente",
|
||||||
"clear_cache": "Cancella cache"
|
"clear_cache": "Cancella cache",
|
||||||
|
"make_static": "Rendere statico",
|
||||||
|
"theme_auto_desc": "Auto (in base alla combinazione di colori del tuo dispositivo)",
|
||||||
|
"theme_dark_desc": "Tema scuro",
|
||||||
|
"theme_light_desc": "Tema chiaro",
|
||||||
|
"disable_for_seconds": "Per {{count}} secondo",
|
||||||
|
"disable_for_seconds_plural": "Per {{count}} secondi",
|
||||||
|
"disable_for_minutes": "Per {{count}} minuto",
|
||||||
|
"disable_for_minutes_plural": "Per {{count}} minuti",
|
||||||
|
"disable_for_hours": "Per {{count}} ora",
|
||||||
|
"disable_for_hours_plural": "Per {{count}} ore",
|
||||||
|
"disable_until_tomorrow": "Fino a domani",
|
||||||
|
"disable_notify_for_seconds": "Disattiva la protezione per {{count}} secondo",
|
||||||
|
"disable_notify_for_seconds_plural": "Disattiva la protezione per {{count}} secondi",
|
||||||
|
"disable_notify_for_minutes": "Disattiva protezione per {{count}} minuto",
|
||||||
|
"disable_notify_for_minutes_plural": "Disattiva la protezione per {{count}} minuti",
|
||||||
|
"disable_notify_for_hours": "Disattiva la protezione per {{count}} ora",
|
||||||
|
"disable_notify_for_hours_plural": "Disattiva la protezione per {{count}} ore",
|
||||||
|
"disable_notify_until_tomorrow": "Disattiva la protezione fino a domani",
|
||||||
|
"enable_protection_timer": "La protezione verrà attivata in {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "ペアレンタルコントロールが有効になりました",
|
"enabled_parental_toast": "ペアレンタルコントロールが有効になりました",
|
||||||
"disabled_safe_search_toast": "セーフサーチが無効になりました",
|
"disabled_safe_search_toast": "セーフサーチが無効になりました",
|
||||||
"enabled_save_search_toast": "セーフサーチが有効になりました",
|
"enabled_save_search_toast": "セーフサーチが有効になりました",
|
||||||
|
"updated_save_search_toast": "セーフ サーチの設定が更新されました。",
|
||||||
"enabled_table_header": "有効",
|
"enabled_table_header": "有効",
|
||||||
"name_table_header": "名称",
|
"name_table_header": "名称",
|
||||||
"list_url_table_header": "URLリスト",
|
"list_url_table_header": "URLリスト",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "頻度制限",
|
"rate_limit": "頻度制限",
|
||||||
"edns_enable": "EDNSクライアントサブネットを有効にする",
|
"edns_enable": "EDNSクライアントサブネットを有効にする",
|
||||||
"edns_cs_desc": "アップストリームリクエストにEDNSクライアントサブネットオプション(ECS)を追加し、クライアントから送信された値をクエリログに記録します。",
|
"edns_cs_desc": "アップストリームリクエストにEDNSクライアントサブネットオプション(ECS)を追加し、クライアントから送信された値をクエリログに記録します。",
|
||||||
|
"edns_use_custom_ip": "EDNSにカスタムIPを使用する",
|
||||||
|
"edns_use_custom_ip_desc": "EDNS に対してカスタム IP の使用を許可します。",
|
||||||
"rate_limit_desc": "一つのクライアントに対して許可される1秒あたりのリクエスト数(「0」に設定すると、制限なしになります)",
|
"rate_limit_desc": "一つのクライアントに対して許可される1秒あたりのリクエスト数(「0」に設定すると、制限なしになります)",
|
||||||
"blocking_ipv4_desc": "ブロックされたAリクエストに対して応答されるIPアドレス",
|
"blocking_ipv4_desc": "ブロックされたAリクエストに対して応答されるIPアドレス",
|
||||||
"blocking_ipv6_desc": "ブロックされたAAAAリクエストに対して応答されるIPアドレス",
|
"blocking_ipv6_desc": "ブロックされたAAAAリクエストに対して応答されるIPアドレス",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "統計の保持を変更してもよろしいですか? 期間を短くすると、一部のデータが失われます",
|
"statistics_retention_confirm": "統計の保持を変更してもよろしいですか? 期間を短くすると、一部のデータが失われます",
|
||||||
"statistics_cleared": "統計の消去に成功しました",
|
"statistics_cleared": "統計の消去に成功しました",
|
||||||
"statistics_enable": "統計を有効にする",
|
"statistics_enable": "統計を有効にする",
|
||||||
|
"ignore_domains": "無視するドメイン(それぞれ改行で区切ってください)",
|
||||||
|
"ignore_domains_title": "無視するドメイン",
|
||||||
|
"ignore_domains_desc_stats": "これらのドメインへのクエリは統計に書き込まれません。",
|
||||||
|
"ignore_domains_desc_query": "これらのドメインへのクエリはクエリログに書き込まれません。",
|
||||||
"interval_hours": "{{count}}時間",
|
"interval_hours": "{{count}}時間",
|
||||||
"interval_hours_plural": "{{count}}時間",
|
"interval_hours_plural": "{{count}}時間",
|
||||||
"filters_configuration": "フィルタ設定",
|
"filters_configuration": "フィルタ設定",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "【<0>注意</0>】IPの匿名化が有効になっています。 <1>一般設定</1>で無効にできます。",
|
"anonymizer_notification": "【<0>注意</0>】IPの匿名化が有効になっています。 <1>一般設定</1>で無効にできます。",
|
||||||
"confirm_dns_cache_clear": "DNS キャッシュをクリアしてもよろしいですか?",
|
"confirm_dns_cache_clear": "DNS キャッシュをクリアしてもよろしいですか?",
|
||||||
"cache_cleared": "DNSキャッシュのクリア完了です。",
|
"cache_cleared": "DNSキャッシュのクリア完了です。",
|
||||||
"clear_cache": "キャッシュをクリアする"
|
"clear_cache": "キャッシュをクリアする",
|
||||||
|
"make_static": "静的(static)にする",
|
||||||
|
"theme_auto_desc": "自動(デバイスの配色に合わせる)",
|
||||||
|
"theme_dark_desc": "ダークテーマ",
|
||||||
|
"theme_light_desc": "ライトテーマ",
|
||||||
|
"disable_for_seconds": "{{count}}秒間",
|
||||||
|
"disable_for_seconds_plural": "{{count}}秒間",
|
||||||
|
"disable_for_minutes": "{{count}}分間",
|
||||||
|
"disable_for_minutes_plural": "{{count}}分間",
|
||||||
|
"disable_for_hours": "{{count}}時間",
|
||||||
|
"disable_for_hours_plural": "{{count}}時間",
|
||||||
|
"disable_until_tomorrow": "明日まで",
|
||||||
|
"disable_notify_for_seconds": "保護を {{count}} 秒間無効にする",
|
||||||
|
"disable_notify_for_seconds_plural": "保護を {{count}} 秒間無効にする",
|
||||||
|
"disable_notify_for_minutes": "保護を {{count}} 分間無効にする",
|
||||||
|
"disable_notify_for_minutes_plural": "保護を {{count}} 分間無効にする",
|
||||||
|
"disable_notify_for_hours": "保護を {{count}} 時間無効にする",
|
||||||
|
"disable_notify_for_hours_plural": "保護を {{count}} 時間無効にする",
|
||||||
|
"disable_notify_until_tomorrow": "明日まで保護を無効にする",
|
||||||
|
"enable_protection_timer": "保護は後 {{time}} で有効になります"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "자녀 보호 활성화됨",
|
"enabled_parental_toast": "자녀 보호 활성화됨",
|
||||||
"disabled_safe_search_toast": "세이프서치 비활성화됨",
|
"disabled_safe_search_toast": "세이프서치 비활성화됨",
|
||||||
"enabled_save_search_toast": "세이프서치 활성화됨",
|
"enabled_save_search_toast": "세이프서치 활성화됨",
|
||||||
|
"updated_save_search_toast": "세이프서치 설정 업데이트됨",
|
||||||
"enabled_table_header": "활성화됨",
|
"enabled_table_header": "활성화됨",
|
||||||
"name_table_header": "이름",
|
"name_table_header": "이름",
|
||||||
"list_url_table_header": "리스트 URL",
|
"list_url_table_header": "리스트 URL",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "한도 제한",
|
"rate_limit": "한도 제한",
|
||||||
"edns_enable": "EDNS 클라이언트 서브넷 활성화",
|
"edns_enable": "EDNS 클라이언트 서브넷 활성화",
|
||||||
"edns_cs_desc": "업스트림 요청에 EDNS 클라이언트 서브넷 옵션(ECS)을 추가하고 쿼리 로그에 클라이언트가 보낸 값을 기록합니다.",
|
"edns_cs_desc": "업스트림 요청에 EDNS 클라이언트 서브넷 옵션(ECS)을 추가하고 쿼리 로그에 클라이언트가 보낸 값을 기록합니다.",
|
||||||
|
"edns_use_custom_ip": "EDNS에 사용자 지정 IP 사용",
|
||||||
|
"edns_use_custom_ip_desc": "EDNS에 사용자 지정 IP 사용하도록 허용합니다.",
|
||||||
"rate_limit_desc": "단일 클라이언트에서 허용 가능한 초 당 요청 생성 숫자 (0: 무제한)",
|
"rate_limit_desc": "단일 클라이언트에서 허용 가능한 초 당 요청 생성 숫자 (0: 무제한)",
|
||||||
"blocking_ipv4_desc": "차단된 A 요청에 대해서 반환할 IP 주소",
|
"blocking_ipv4_desc": "차단된 A 요청에 대해서 반환할 IP 주소",
|
||||||
"blocking_ipv6_desc": "차단된 AAAA 요청에 대해서 반환할 IP 주소",
|
"blocking_ipv6_desc": "차단된 AAAA 요청에 대해서 반환할 IP 주소",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "정말로 통계 저장 기간을 변경하시겠습니까? 저장 주기를 낮출 경우, 일부 데이터가 손실됩니다",
|
"statistics_retention_confirm": "정말로 통계 저장 기간을 변경하시겠습니까? 저장 주기를 낮출 경우, 일부 데이터가 손실됩니다",
|
||||||
"statistics_cleared": "통계를 성공적으로 초기화했습니다.",
|
"statistics_cleared": "통계를 성공적으로 초기화했습니다.",
|
||||||
"statistics_enable": "통계 활성화",
|
"statistics_enable": "통계 활성화",
|
||||||
|
"ignore_domains": "무시된 도메인(줄 바꿈으로 구분)",
|
||||||
|
"ignore_domains_title": "무시된 도메인",
|
||||||
|
"ignore_domains_desc_stats": "이러한 도메인에 대한 쿼리는 통계에 기록되지 않습니다.",
|
||||||
|
"ignore_domains_desc_query": "이러한 도메인에 대한 쿼리는 쿼리 로그에 기록되지 않습니다.",
|
||||||
"interval_hours": "{{count}} 시간",
|
"interval_hours": "{{count}} 시간",
|
||||||
"interval_hours_plural": "{{count}} 시간",
|
"interval_hours_plural": "{{count}} 시간",
|
||||||
"filters_configuration": "필터 구성",
|
"filters_configuration": "필터 구성",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>참고:</0> IP 익명화가 활성화되었습니다. <1>일반 설정</1>에서 비활성화할 수 있습니다.",
|
"anonymizer_notification": "<0>참고:</0> IP 익명화가 활성화되었습니다. <1>일반 설정</1>에서 비활성화할 수 있습니다.",
|
||||||
"confirm_dns_cache_clear": "정말로 DNS 캐시를 지우시겠습니까?",
|
"confirm_dns_cache_clear": "정말로 DNS 캐시를 지우시겠습니까?",
|
||||||
"cache_cleared": "DNS 캐시를 성공적으로 지웠습니다",
|
"cache_cleared": "DNS 캐시를 성공적으로 지웠습니다",
|
||||||
"clear_cache": "캐시 지우기"
|
"clear_cache": "캐시 지우기",
|
||||||
|
"make_static": "정적으로 만들기",
|
||||||
|
"theme_auto_desc": "자동(기기의 색 구성표에 따라 설정)",
|
||||||
|
"theme_dark_desc": "다크 테마",
|
||||||
|
"theme_light_desc": "라이트 테마",
|
||||||
|
"disable_for_seconds": "{{count}}초",
|
||||||
|
"disable_for_seconds_plural": "{{count}}초",
|
||||||
|
"disable_for_minutes": "{{count}}분",
|
||||||
|
"disable_for_minutes_plural": "{{count}}분간",
|
||||||
|
"disable_for_hours": "{{count}}시간",
|
||||||
|
"disable_for_hours_plural": "{{count}}시간",
|
||||||
|
"disable_until_tomorrow": "내일까지",
|
||||||
|
"disable_notify_for_seconds": "{{count}}초 동안 보호 기능 비활성화",
|
||||||
|
"disable_notify_for_seconds_plural": "{{count}}초 동안 보호 기능 비활성화",
|
||||||
|
"disable_notify_for_minutes": "{{count}}분 동안 보호 기능 비활성화",
|
||||||
|
"disable_notify_for_minutes_plural": "{{count}}분 동안 보호 기능 비활성화",
|
||||||
|
"disable_notify_for_hours": "{{count}}시간 동안 보호 기능 비활성화",
|
||||||
|
"disable_notify_for_hours_plural": "{{count}}시간 동안 보호 기능 비활성화",
|
||||||
|
"disable_notify_until_tomorrow": "내일까지 보호 기능 비활성화",
|
||||||
|
"enable_protection_timer": "{{time}}에 보호 기능이 활성화됩니다."
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Ingeschakeld Ouderlijk toezicht",
|
"enabled_parental_toast": "Ingeschakeld Ouderlijk toezicht",
|
||||||
"disabled_safe_search_toast": "Uitgeschakeld Veilig zoeken",
|
"disabled_safe_search_toast": "Uitgeschakeld Veilig zoeken",
|
||||||
"enabled_save_search_toast": "Ingeschakeld Veilig zoeken",
|
"enabled_save_search_toast": "Ingeschakeld Veilig zoeken",
|
||||||
|
"updated_save_search_toast": "Safe Search-instellingen bijgewerkt",
|
||||||
"enabled_table_header": "Ingeschakeld",
|
"enabled_table_header": "Ingeschakeld",
|
||||||
"name_table_header": "Naam",
|
"name_table_header": "Naam",
|
||||||
"list_url_table_header": "URL lijst",
|
"list_url_table_header": "URL lijst",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Ratio limiet",
|
"rate_limit": "Ratio limiet",
|
||||||
"edns_enable": "EDNS client subnet inschakelen",
|
"edns_enable": "EDNS client subnet inschakelen",
|
||||||
"edns_cs_desc": "De EDNS Client Subnet-optie (ECS) toevoegen aan upstream-verzoeken en de waarden die door de clients zijn verzonden registreren in het querylogboek.",
|
"edns_cs_desc": "De EDNS Client Subnet-optie (ECS) toevoegen aan upstream-verzoeken en de waarden die door de clients zijn verzonden registreren in het querylogboek.",
|
||||||
|
"edns_use_custom_ip": "Aangepast IP-adres gebruiken voor EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Toestaan om aangepast IP-adres voor EDNS te gebruiken",
|
||||||
"rate_limit_desc": "Het aantal verzoeken per seconde toegelaten per toestel. 0 betekent onbeperkt.",
|
"rate_limit_desc": "Het aantal verzoeken per seconde toegelaten per toestel. 0 betekent onbeperkt.",
|
||||||
"blocking_ipv4_desc": "IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek",
|
"blocking_ipv4_desc": "IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek",
|
||||||
"blocking_ipv6_desc": "IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek",
|
"blocking_ipv6_desc": "IP-adres dat moet worden teruggegeven voor een geblokkeerd A-verzoek",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Weet u zeker dat u de bewaartermijn van de statistieken wilt wijzigen? Als u de intervalwaarde verlaagt, gaan sommige gegevens verloren",
|
"statistics_retention_confirm": "Weet u zeker dat u de bewaartermijn van de statistieken wilt wijzigen? Als u de intervalwaarde verlaagt, gaan sommige gegevens verloren",
|
||||||
"statistics_cleared": "Statistieken succesvol gewist",
|
"statistics_cleared": "Statistieken succesvol gewist",
|
||||||
"statistics_enable": "Statistieken inschakelen",
|
"statistics_enable": "Statistieken inschakelen",
|
||||||
|
"ignore_domains": "Genegeerde domeinen (gescheiden door nieuwe regel)",
|
||||||
|
"ignore_domains_title": "Genegeerde domeinen",
|
||||||
|
"ignore_domains_desc_stats": "Zoekopdrachten voor deze domeinen worden niet naar de statistieken geschreven",
|
||||||
|
"ignore_domains_desc_query": "Zoekopdrachten voor deze domeinen worden niet naar het zoeklogboek geschreven",
|
||||||
"interval_hours": "{{count}} uur",
|
"interval_hours": "{{count}} uur",
|
||||||
"interval_hours_plural": "{{count}} uren",
|
"interval_hours_plural": "{{count}} uren",
|
||||||
"filters_configuration": "Filters instellingen",
|
"filters_configuration": "Filters instellingen",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Opmerking:</0> IP-anonimisering is ingeschakeld. Je kunt het uitschakelen in <1>Algemene instellingen</1>.",
|
"anonymizer_notification": "<0>Opmerking:</0> IP-anonimisering is ingeschakeld. Je kunt het uitschakelen in <1>Algemene instellingen</1>.",
|
||||||
"confirm_dns_cache_clear": "Weet je zeker dat je de DNS-cache wilt wissen?",
|
"confirm_dns_cache_clear": "Weet je zeker dat je de DNS-cache wilt wissen?",
|
||||||
"cache_cleared": "DNS-cache succesvol gewist",
|
"cache_cleared": "DNS-cache succesvol gewist",
|
||||||
"clear_cache": "Cache wissen"
|
"clear_cache": "Cache wissen",
|
||||||
|
"make_static": "Statisch maken",
|
||||||
|
"theme_auto_desc": "Automatisch (op basis van het kleurenschema van jouw apparaat)",
|
||||||
|
"theme_dark_desc": "Donker thema",
|
||||||
|
"theme_light_desc": "Licht thema",
|
||||||
|
"disable_for_seconds": "Voor {{count}} seconde",
|
||||||
|
"disable_for_seconds_plural": "Voor {{count}} seconden",
|
||||||
|
"disable_for_minutes": "Voor {{count}} minuut",
|
||||||
|
"disable_for_minutes_plural": "Voor {{count}} minuten",
|
||||||
|
"disable_for_hours": "Voor {{count}} uur",
|
||||||
|
"disable_for_hours_plural": "Voor {{count}} uren",
|
||||||
|
"disable_until_tomorrow": "Tot morgen",
|
||||||
|
"disable_notify_for_seconds": "Beveiliging uitschakelen voor {{count}} seconde",
|
||||||
|
"disable_notify_for_seconds_plural": "Beveiliging uitschakelen voor {{count}} seconden",
|
||||||
|
"disable_notify_for_minutes": "Beveiliging uitschakelen voor {{count}} minuut",
|
||||||
|
"disable_notify_for_minutes_plural": "Beveiliging uitschakelen voor {{count}} minuten",
|
||||||
|
"disable_notify_for_hours": "Beveiliging uitschakelen voor {{count}} uur",
|
||||||
|
"disable_notify_for_hours_plural": "Beveiliging uitschakelen voor {{count}} uren",
|
||||||
|
"disable_notify_until_tomorrow": "Beveiliging uitschakelen tot morgen",
|
||||||
|
"enable_protection_timer": "Bescherming wordt ingeschakeld over {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Controle parental ativado",
|
"enabled_parental_toast": "Controle parental ativado",
|
||||||
"disabled_safe_search_toast": "Pesquisa segura desativada",
|
"disabled_safe_search_toast": "Pesquisa segura desativada",
|
||||||
"enabled_save_search_toast": "Pesquisa segura ativada",
|
"enabled_save_search_toast": "Pesquisa segura ativada",
|
||||||
|
"updated_save_search_toast": "Configurações de Pesquisa Segura atualizadas",
|
||||||
"enabled_table_header": "Ativado",
|
"enabled_table_header": "Ativado",
|
||||||
"name_table_header": "Nome",
|
"name_table_header": "Nome",
|
||||||
"list_url_table_header": "URL da lista",
|
"list_url_table_header": "URL da lista",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Taxa limite",
|
"rate_limit": "Taxa limite",
|
||||||
"edns_enable": "Ativar a sub-rede do cliente EDNS",
|
"edns_enable": "Ativar a sub-rede do cliente EDNS",
|
||||||
"edns_cs_desc": "Adicione a opção de sub-rede de cliente EDNS (ECS) às solicitações de servidor DNS primário e registre os valores enviados pelos clientes no registro de consulta.",
|
"edns_cs_desc": "Adicione a opção de sub-rede de cliente EDNS (ECS) às solicitações de servidor DNS primário e registre os valores enviados pelos clientes no registro de consulta.",
|
||||||
|
"edns_use_custom_ip": "Usar IP personalizado para EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Permitir o uso de IP personalizado para EDNS",
|
||||||
"rate_limit_desc": "O número de solicitações por segundo permitidas por cliente. Definir como 0 significa que não há limite.",
|
"rate_limit_desc": "O número de solicitações por segundo permitidas por cliente. Definir como 0 significa que não há limite.",
|
||||||
"blocking_ipv4_desc": "Endereço de IP a ser retornado para uma solicitação bloqueada",
|
"blocking_ipv4_desc": "Endereço de IP a ser retornado para uma solicitação bloqueada",
|
||||||
"blocking_ipv6_desc": "Endereço de IP a ser retornado para uma solicitação AAAA bloqueada",
|
"blocking_ipv6_desc": "Endereço de IP a ser retornado para uma solicitação AAAA bloqueada",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Você tem certeza que quer alterar o arquivamento das estatísticas? Se diminuir o valor do intervalo, alguns dados serão perdidos",
|
"statistics_retention_confirm": "Você tem certeza que quer alterar o arquivamento das estatísticas? Se diminuir o valor do intervalo, alguns dados serão perdidos",
|
||||||
"statistics_cleared": "As estatísticas foram limpas com sucesso",
|
"statistics_cleared": "As estatísticas foram limpas com sucesso",
|
||||||
"statistics_enable": "Ativar estatísticas",
|
"statistics_enable": "Ativar estatísticas",
|
||||||
|
"ignore_domains": "Domínios ignorados (separados por nova linha)",
|
||||||
|
"ignore_domains_title": "Domínios ignorados",
|
||||||
|
"ignore_domains_desc_stats": "As consultas para esses domínios não são gravadas nas estatísticas",
|
||||||
|
"ignore_domains_desc_query": "As consultas para esses domínios não são gravadas no log de consulta",
|
||||||
"interval_hours": "{{count}} hora",
|
"interval_hours": "{{count}} hora",
|
||||||
"interval_hours_plural": "{{count}} horas",
|
"interval_hours_plural": "{{count}} horas",
|
||||||
"filters_configuration": "Configuração de filtros",
|
"filters_configuration": "Configuração de filtros",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Observação:</0> A anonimização de IP está ativada. Você pode desativá-lo em <1>Configurações gerais</1>.",
|
"anonymizer_notification": "<0>Observação:</0> A anonimização de IP está ativada. Você pode desativá-lo em <1>Configurações gerais</1>.",
|
||||||
"confirm_dns_cache_clear": "Tem certeza de que deseja limpar o cache DNS?",
|
"confirm_dns_cache_clear": "Tem certeza de que deseja limpar o cache DNS?",
|
||||||
"cache_cleared": "Cache DNS limpo com sucesso",
|
"cache_cleared": "Cache DNS limpo com sucesso",
|
||||||
"clear_cache": "Limpar cache"
|
"clear_cache": "Limpar cache",
|
||||||
|
"make_static": "Tornar estático",
|
||||||
|
"theme_auto_desc": "Automático (com base no esquema de cores do seu dispositivo)",
|
||||||
|
"theme_dark_desc": "Tema escuro",
|
||||||
|
"theme_light_desc": "Tema claro",
|
||||||
|
"disable_for_seconds": "Por {{count}} segundo",
|
||||||
|
"disable_for_seconds_plural": "Por {{count}} segundos",
|
||||||
|
"disable_for_minutes": "Por {{count}} minuto",
|
||||||
|
"disable_for_minutes_plural": "Por {{count}} minutos",
|
||||||
|
"disable_for_hours": "Por {{count}} hora",
|
||||||
|
"disable_for_hours_plural": "Por {{count}} horas",
|
||||||
|
"disable_until_tomorrow": "Até amanhã",
|
||||||
|
"disable_notify_for_seconds": "Desativar proteção por {{count}} segundo",
|
||||||
|
"disable_notify_for_seconds_plural": "Desativar proteção por {{count}} segundos",
|
||||||
|
"disable_notify_for_minutes": "Desativar proteção por {{count}} minuto",
|
||||||
|
"disable_notify_for_minutes_plural": "Desativar proteção por {{count}} minutos",
|
||||||
|
"disable_notify_for_hours": "Desativar proteção por {{count}} hora",
|
||||||
|
"disable_notify_for_hours_plural": "Desativar proteção por {{count}} horas",
|
||||||
|
"disable_notify_until_tomorrow": "Desativar a proteção até amanhã",
|
||||||
|
"enable_protection_timer": "A proteção será ativada em {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Controlo parental ativado",
|
"enabled_parental_toast": "Controlo parental ativado",
|
||||||
"disabled_safe_search_toast": "Pesquisa segura desativada",
|
"disabled_safe_search_toast": "Pesquisa segura desativada",
|
||||||
"enabled_save_search_toast": "Pesquisa segura ativada",
|
"enabled_save_search_toast": "Pesquisa segura ativada",
|
||||||
|
"updated_save_search_toast": "Configurações de pesquisa segura actualizadas",
|
||||||
"enabled_table_header": "Ativado",
|
"enabled_table_header": "Ativado",
|
||||||
"name_table_header": "Nome",
|
"name_table_header": "Nome",
|
||||||
"list_url_table_header": "URL da lista",
|
"list_url_table_header": "URL da lista",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Limite de taxa",
|
"rate_limit": "Limite de taxa",
|
||||||
"edns_enable": "Ativar a sub-rede do cliente EDNS",
|
"edns_enable": "Ativar a sub-rede do cliente EDNS",
|
||||||
"edns_cs_desc": "Adicione a opção de sub-rede de cliente EDNS (ECS) às solicitações de servidor DNS primário e registre os valores enviados pelos clientes no registo de consulta.",
|
"edns_cs_desc": "Adicione a opção de sub-rede de cliente EDNS (ECS) às solicitações de servidor DNS primário e registre os valores enviados pelos clientes no registo de consulta.",
|
||||||
|
"edns_use_custom_ip": "Usar IP personalizado para EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Permitir a utilização de IP personalizado para EDNS",
|
||||||
"rate_limit_desc": "O número de solicitações por segundo permitido por cliente. Configurando para 0 significa sem limite.",
|
"rate_limit_desc": "O número de solicitações por segundo permitido por cliente. Configurando para 0 significa sem limite.",
|
||||||
"blocking_ipv4_desc": "Endereço IP a ser devolvido para uma solicitação A bloqueada",
|
"blocking_ipv4_desc": "Endereço IP a ser devolvido para uma solicitação A bloqueada",
|
||||||
"blocking_ipv6_desc": "Endereço IP a ser devolvido para uma solicitação AAAA bloqueada",
|
"blocking_ipv6_desc": "Endereço IP a ser devolvido para uma solicitação AAAA bloqueada",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Tem a certeza que quer alterar a retenção de estatísticas? Se diminuir o valor do intervalo, alguns dados serão perdidos",
|
"statistics_retention_confirm": "Tem a certeza que quer alterar a retenção de estatísticas? Se diminuir o valor do intervalo, alguns dados serão perdidos",
|
||||||
"statistics_cleared": "As estatísticas foram apagadas com sucesso",
|
"statistics_cleared": "As estatísticas foram apagadas com sucesso",
|
||||||
"statistics_enable": "Ativar estatísticas",
|
"statistics_enable": "Ativar estatísticas",
|
||||||
|
"ignore_domains": "Domínios ignorados (separados por nova linha)",
|
||||||
|
"ignore_domains_title": "Domínios ignorados",
|
||||||
|
"ignore_domains_desc_stats": "As consultas para estes domínios não aparecem nas estatísticas",
|
||||||
|
"ignore_domains_desc_query": "As consultas para estes domínios nãoaparecem no registo de consultas",
|
||||||
"interval_hours": "{{count}} hora",
|
"interval_hours": "{{count}} hora",
|
||||||
"interval_hours_plural": "{{count}} horas",
|
"interval_hours_plural": "{{count}} horas",
|
||||||
"filters_configuration": "Definição dos filtros",
|
"filters_configuration": "Definição dos filtros",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Observação:</0> A anonimização de IP está ativada. Você pode desativá-la em <1>Definições gerais</1>.",
|
"anonymizer_notification": "<0>Observação:</0> A anonimização de IP está ativada. Você pode desativá-la em <1>Definições gerais</1>.",
|
||||||
"confirm_dns_cache_clear": "Tem certeza de que quer limpar a cache DNS?",
|
"confirm_dns_cache_clear": "Tem certeza de que quer limpar a cache DNS?",
|
||||||
"cache_cleared": "O cache DNS foi apagado com sucesso",
|
"cache_cleared": "O cache DNS foi apagado com sucesso",
|
||||||
"clear_cache": "Limpar cache"
|
"clear_cache": "Limpar cache",
|
||||||
|
"make_static": "Tornar estático",
|
||||||
|
"theme_auto_desc": "Automático (com base no esquema de cores do seu dispositivo)",
|
||||||
|
"theme_dark_desc": "Tema escuro",
|
||||||
|
"theme_light_desc": "Tema claro",
|
||||||
|
"disable_for_seconds": "Por {{count}} segundo",
|
||||||
|
"disable_for_seconds_plural": "Por {{count}} segundos",
|
||||||
|
"disable_for_minutes": "Por {{count}} minuto",
|
||||||
|
"disable_for_minutes_plural": "Por {{count}} minutos",
|
||||||
|
"disable_for_hours": "Por {{count}} hora",
|
||||||
|
"disable_for_hours_plural": "Por {{count}} horas",
|
||||||
|
"disable_until_tomorrow": "Até amanhã",
|
||||||
|
"disable_notify_for_seconds": "Desativar proteção por {{count}} segundo",
|
||||||
|
"disable_notify_for_seconds_plural": "Desativar proteção por {{count}} segundos",
|
||||||
|
"disable_notify_for_minutes": "Desativar proteção por {{count}} minuto",
|
||||||
|
"disable_notify_for_minutes_plural": "Desativar proteção por {{count}} minutos",
|
||||||
|
"disable_notify_for_hours": "Desativar proteção por {{count}} hora",
|
||||||
|
"disable_notify_for_hours_plural": "Desativar proteção por {{count}} horas",
|
||||||
|
"disable_notify_until_tomorrow": "Desativar a proteção até amanhã",
|
||||||
|
"enable_protection_timer": "A proteção será habilitada em {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Родительский контроль вкл.",
|
"enabled_parental_toast": "Родительский контроль вкл.",
|
||||||
"disabled_safe_search_toast": "Безопасный поиск выкл.",
|
"disabled_safe_search_toast": "Безопасный поиск выкл.",
|
||||||
"enabled_save_search_toast": "Безопасный поиск вкл.",
|
"enabled_save_search_toast": "Безопасный поиск вкл.",
|
||||||
|
"updated_save_search_toast": "Настройки безопасного поиска обновлены",
|
||||||
"enabled_table_header": "Вкл.",
|
"enabled_table_header": "Вкл.",
|
||||||
"name_table_header": "Имя",
|
"name_table_header": "Имя",
|
||||||
"list_url_table_header": "URL-адрес списка",
|
"list_url_table_header": "URL-адрес списка",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Rate limit",
|
"rate_limit": "Rate limit",
|
||||||
"edns_enable": "Включить отправку EDNS Client Subnet",
|
"edns_enable": "Включить отправку EDNS Client Subnet",
|
||||||
"edns_cs_desc": "Добавлять опцию EDNS Client Subnet (ECS) к запросам к upstream-серверам, а также записывать присланные клиентами значения в журнал.",
|
"edns_cs_desc": "Добавлять опцию EDNS Client Subnet (ECS) к запросам к upstream-серверам, а также записывать присланные клиентами значения в журнал.",
|
||||||
|
"edns_use_custom_ip": "Использовать указанный IP для EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Разрешить использовать собственный IP для EDNS",
|
||||||
"rate_limit_desc": "Ограничение на количество запросов в секунду для каждого клиента (0 — неограниченно).",
|
"rate_limit_desc": "Ограничение на количество запросов в секунду для каждого клиента (0 — неограниченно).",
|
||||||
"blocking_ipv4_desc": "IP-адрес, возвращаемый при блокировке A-запроса",
|
"blocking_ipv4_desc": "IP-адрес, возвращаемый при блокировке A-запроса",
|
||||||
"blocking_ipv6_desc": "IP-адрес, возвращаемый при блокировке AAAA-запроса",
|
"blocking_ipv6_desc": "IP-адрес, возвращаемый при блокировке AAAA-запроса",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Вы уверены, что хотите изменить срок хранения статистики? При сокращении интервала данные могут быть утеряны",
|
"statistics_retention_confirm": "Вы уверены, что хотите изменить срок хранения статистики? При сокращении интервала данные могут быть утеряны",
|
||||||
"statistics_cleared": "Статистика успешно очищена",
|
"statistics_cleared": "Статистика успешно очищена",
|
||||||
"statistics_enable": "Включить статистику",
|
"statistics_enable": "Включить статистику",
|
||||||
|
"ignore_domains": "Игнорируемые домены (построчно)",
|
||||||
|
"ignore_domains_title": "Игнорируемые домены",
|
||||||
|
"ignore_domains_desc_stats": "Запросы для этих доменов не записываются в статистику",
|
||||||
|
"ignore_domains_desc_query": "Запросы для этих доменов не записываются в журнал запросов",
|
||||||
"interval_hours": "{{count}} час",
|
"interval_hours": "{{count}} час",
|
||||||
"interval_hours_plural": "{{count}} часов",
|
"interval_hours_plural": "{{count}} часов",
|
||||||
"filters_configuration": "Настройка фильтров",
|
"filters_configuration": "Настройка фильтров",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Внимание:</0> включена анонимизация IP-адресов. Вы можете отключить её в разделе <1>Основные настройки</1>.",
|
"anonymizer_notification": "<0>Внимание:</0> включена анонимизация IP-адресов. Вы можете отключить её в разделе <1>Основные настройки</1>.",
|
||||||
"confirm_dns_cache_clear": "Вы уверены, что хотите очистить кеш DNS?",
|
"confirm_dns_cache_clear": "Вы уверены, что хотите очистить кеш DNS?",
|
||||||
"cache_cleared": "Кеш DNS успешно очищен",
|
"cache_cleared": "Кеш DNS успешно очищен",
|
||||||
"clear_cache": "Очистить кеш"
|
"clear_cache": "Очистить кеш",
|
||||||
|
"make_static": "Сделать статической",
|
||||||
|
"theme_auto_desc": "Авто (на основе цветовой схемы вашего устройства)",
|
||||||
|
"theme_dark_desc": "Тёмная тема",
|
||||||
|
"theme_light_desc": "Светлая тема",
|
||||||
|
"disable_for_seconds": "На {{count}} секунд",
|
||||||
|
"disable_for_seconds_plural": "На {{count}} секунд",
|
||||||
|
"disable_for_minutes": "На {{count}} минуту",
|
||||||
|
"disable_for_minutes_plural": "На {{count}} минут",
|
||||||
|
"disable_for_hours": "На {{count}} час",
|
||||||
|
"disable_for_hours_plural": "На {{count}} часов",
|
||||||
|
"disable_until_tomorrow": "До завтра",
|
||||||
|
"disable_notify_for_seconds": "Отключить защиту на {{count}} секунд",
|
||||||
|
"disable_notify_for_seconds_plural": "Отключить защиту на {{count}} секунд",
|
||||||
|
"disable_notify_for_minutes": "Отключить защиту на {{count}} минуту",
|
||||||
|
"disable_notify_for_minutes_plural": "Отключить защиту на {{count}} минут",
|
||||||
|
"disable_notify_for_hours": "Отключить защиту на {{count}} час",
|
||||||
|
"disable_notify_for_hours_plural": "Отключить защиту на {{count}} часов",
|
||||||
|
"disable_notify_until_tomorrow": "Отключить защиту до завтра",
|
||||||
|
"enable_protection_timer": "Защита будет включена в {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Zapnutá Rodičovská kontrola",
|
"enabled_parental_toast": "Zapnutá Rodičovská kontrola",
|
||||||
"disabled_safe_search_toast": "Vypnuté Bezpečné vyhľadávanie",
|
"disabled_safe_search_toast": "Vypnuté Bezpečné vyhľadávanie",
|
||||||
"enabled_save_search_toast": "Zapnuté Bezpečné vyhľadávanie",
|
"enabled_save_search_toast": "Zapnuté Bezpečné vyhľadávanie",
|
||||||
|
"updated_save_search_toast": "Nastavenia Bezpečného vyhľadávania boli aktualizované",
|
||||||
"enabled_table_header": "Zapnuté",
|
"enabled_table_header": "Zapnuté",
|
||||||
"name_table_header": "Meno",
|
"name_table_header": "Meno",
|
||||||
"list_url_table_header": "Zoznam URL adries",
|
"list_url_table_header": "Zoznam URL adries",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Rýchlostný limit",
|
"rate_limit": "Rýchlostný limit",
|
||||||
"edns_enable": "Povoliť klientsku podsiete EDNS",
|
"edns_enable": "Povoliť klientsku podsiete EDNS",
|
||||||
"edns_cs_desc": "Pridáva možnosť EDNS Client Subnet (ECS) do upstream požiadaviek a zapíše hodnoty odoslané klientmi do denníka dopytov.",
|
"edns_cs_desc": "Pridáva možnosť EDNS Client Subnet (ECS) do upstream požiadaviek a zapíše hodnoty odoslané klientmi do denníka dopytov.",
|
||||||
|
"edns_use_custom_ip": "Použiť vlastnú IP adresu pre EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Povoliť používanie vlastnej IP adresy pre EDNS",
|
||||||
"rate_limit_desc": "Počet požiadaviek za sekundu, ktoré môže jeden klient vykonať. Nastavenie na hodnotu 0 znamená neobmedzene.",
|
"rate_limit_desc": "Počet požiadaviek za sekundu, ktoré môže jeden klient vykonať. Nastavenie na hodnotu 0 znamená neobmedzene.",
|
||||||
"blocking_ipv4_desc": "IP adresa, ktorá sa má vrátiť v prípade blokovanej žiadosti A",
|
"blocking_ipv4_desc": "IP adresa, ktorá sa má vrátiť v prípade blokovanej žiadosti A",
|
||||||
"blocking_ipv6_desc": "IP adresa, ktorá sa má vrátiť v prípade blokovanej žiadosti AAAA",
|
"blocking_ipv6_desc": "IP adresa, ktorá sa má vrátiť v prípade blokovanej žiadosti AAAA",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Naozaj chcete zmeniť uchovávanie štatistík? Ak znížite hodnotu intervalu, niektoré údaje sa stratia",
|
"statistics_retention_confirm": "Naozaj chcete zmeniť uchovávanie štatistík? Ak znížite hodnotu intervalu, niektoré údaje sa stratia",
|
||||||
"statistics_cleared": "Štatistika bola úspešne vynulovaná",
|
"statistics_cleared": "Štatistika bola úspešne vynulovaná",
|
||||||
"statistics_enable": "Zapnúť štatistiku",
|
"statistics_enable": "Zapnúť štatistiku",
|
||||||
|
"ignore_domains": "Ignorované domény (oddelené novým riadkom)",
|
||||||
|
"ignore_domains_title": "Ignorované domény",
|
||||||
|
"ignore_domains_desc_stats": "Dopyty pre tieto domény sa nezapisujú do štatistík",
|
||||||
|
"ignore_domains_desc_query": "Dopyty pre tieto domény sa nezapisujú do denníka dopytov",
|
||||||
"interval_hours": "{{count}} hodina",
|
"interval_hours": "{{count}} hodina",
|
||||||
"interval_hours_plural": "{{count}} hodín",
|
"interval_hours_plural": "{{count}} hodín",
|
||||||
"filters_configuration": "Konfigurácia filtrov",
|
"filters_configuration": "Konfigurácia filtrov",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Poznámka:</0> Anonymizácia IP je zapnutá. Môžete ju vypnúť vo <1>Všeobecných nastaveniach</1>.",
|
"anonymizer_notification": "<0>Poznámka:</0> Anonymizácia IP je zapnutá. Môžete ju vypnúť vo <1>Všeobecných nastaveniach</1>.",
|
||||||
"confirm_dns_cache_clear": "Naozaj chcete vymazať vyrovnávaciu pamäť DNS?",
|
"confirm_dns_cache_clear": "Naozaj chcete vymazať vyrovnávaciu pamäť DNS?",
|
||||||
"cache_cleared": "Vyrovnávacia pamäť DNS bola úspešne vymazaná",
|
"cache_cleared": "Vyrovnávacia pamäť DNS bola úspešne vymazaná",
|
||||||
"clear_cache": "Vymazať vyrovnávaciu pamäť"
|
"clear_cache": "Vymazať vyrovnávaciu pamäť",
|
||||||
|
"make_static": "Vytvárať štatistiku",
|
||||||
|
"theme_auto_desc": "Automaticky (na základe farebnej schémy Vášho zariadenia)",
|
||||||
|
"theme_dark_desc": "Tmavá téma",
|
||||||
|
"theme_light_desc": "Svetlá téma",
|
||||||
|
"disable_for_seconds": "Na {{count}} sekundu",
|
||||||
|
"disable_for_seconds_plural": "Na {{count}} sekúnd",
|
||||||
|
"disable_for_minutes": "Na {{count}} minútu",
|
||||||
|
"disable_for_minutes_plural": "Na {{count}} minút",
|
||||||
|
"disable_for_hours": "Na {{count}} hodinu",
|
||||||
|
"disable_for_hours_plural": "Na {{count}} hodín",
|
||||||
|
"disable_until_tomorrow": "Do zajtra",
|
||||||
|
"disable_notify_for_seconds": "Vypnite ochranu na {{count}} sekundu",
|
||||||
|
"disable_notify_for_seconds_plural": "Vypnite ochranu na {{count}} sekúnd",
|
||||||
|
"disable_notify_for_minutes": "Vypnite ochranu na {{count}} minútu",
|
||||||
|
"disable_notify_for_minutes_plural": "Vypnite ochranu na {{count}} minút",
|
||||||
|
"disable_notify_for_hours": "Vypnite ochranu na {{count}} hodinu",
|
||||||
|
"disable_notify_for_hours_plural": "Vypnite ochranu na {{count}} hodín",
|
||||||
|
"disable_notify_until_tomorrow": "Vypnúť ochranu do zajtra",
|
||||||
|
"enable_protection_timer": "Ochrana bude zapnutá o {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Omogočen starševski nadzor",
|
"enabled_parental_toast": "Omogočen starševski nadzor",
|
||||||
"disabled_safe_search_toast": "Onemogočeno Varno iskanje",
|
"disabled_safe_search_toast": "Onemogočeno Varno iskanje",
|
||||||
"enabled_save_search_toast": "Omogočeno Varno iskanje",
|
"enabled_save_search_toast": "Omogočeno Varno iskanje",
|
||||||
|
"updated_save_search_toast": "Nastavitve varnega iskanja so posodobljene",
|
||||||
"enabled_table_header": "Omogočeno",
|
"enabled_table_header": "Omogočeno",
|
||||||
"name_table_header": "Ime",
|
"name_table_header": "Ime",
|
||||||
"list_url_table_header": "Seznam URL naslovov",
|
"list_url_table_header": "Seznam URL naslovov",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Omejitev hitrosti",
|
"rate_limit": "Omejitev hitrosti",
|
||||||
"edns_enable": "Omogoči odjemalsko podomrežje EDNS",
|
"edns_enable": "Omogoči odjemalsko podomrežje EDNS",
|
||||||
"edns_cs_desc": "Dodaj možnost podomrežja odjemalca EDNS (ECS) zahtevam v gorvodnem toku in zabeleži vrednosti, ki jih pošljejo odjemalci, v dnevnik poizvedb.",
|
"edns_cs_desc": "Dodaj možnost podomrežja odjemalca EDNS (ECS) zahtevam v gorvodnem toku in zabeleži vrednosti, ki jih pošljejo odjemalci, v dnevnik poizvedb.",
|
||||||
|
"edns_use_custom_ip": "Uporabi IP po meri za EDNS",
|
||||||
|
"edns_use_custom_ip_desc": "Dovoli uporabo naslova IP po meri za EDNS",
|
||||||
"rate_limit_desc": "Dovoljeno število zahtev na sekundo na odjemalca. Nastavitev na 0 pomeni brez omejitve.",
|
"rate_limit_desc": "Dovoljeno število zahtev na sekundo na odjemalca. Nastavitev na 0 pomeni brez omejitve.",
|
||||||
"blocking_ipv4_desc": "IP naslov, ki mora biti vrnjen za onemogočeno zahtevo A",
|
"blocking_ipv4_desc": "IP naslov, ki mora biti vrnjen za onemogočeno zahtevo A",
|
||||||
"blocking_ipv6_desc": "IP naslov, ki mora biti vrnjen za onemogočeno zahtevo AAAA",
|
"blocking_ipv6_desc": "IP naslov, ki mora biti vrnjen za onemogočeno zahtevo AAAA",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "Ali ste prepričani, da želite spremeniti zadrževanje statistike? Če zmanjšate vrednost intervala, bodo nekateri podatki izgubljeni",
|
"statistics_retention_confirm": "Ali ste prepričani, da želite spremeniti zadrževanje statistike? Če zmanjšate vrednost intervala, bodo nekateri podatki izgubljeni",
|
||||||
"statistics_cleared": "Statistika je bila uspešno počiščena",
|
"statistics_cleared": "Statistika je bila uspešno počiščena",
|
||||||
"statistics_enable": "Omogoči statistiko",
|
"statistics_enable": "Omogoči statistiko",
|
||||||
|
"ignore_domains": "Prezrte domene (ločene z novo vrstico)",
|
||||||
|
"ignore_domains_title": "Prezrte domene",
|
||||||
|
"ignore_domains_desc_stats": "Poizvedbe za te domene niso zapisane v statistiko",
|
||||||
|
"ignore_domains_desc_query": "Poizvedbe za te domene niso zapisane v dnevnik poizvedb",
|
||||||
"interval_hours": "{{count}} ur",
|
"interval_hours": "{{count}} ur",
|
||||||
"interval_hours_plural": "{{count}} ur",
|
"interval_hours_plural": "{{count}} ur",
|
||||||
"filters_configuration": "Nastavitve filtrov",
|
"filters_configuration": "Nastavitve filtrov",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Opomba:</0> Anonimizacija IP je omogočena. Onemogočite ga lahko v <1>Splošnih nastavitvah</1>.",
|
"anonymizer_notification": "<0>Opomba:</0> Anonimizacija IP je omogočena. Onemogočite ga lahko v <1>Splošnih nastavitvah</1>.",
|
||||||
"confirm_dns_cache_clear": "Ali ste prepričani, da želite počistiti predpomnilnik DNS?",
|
"confirm_dns_cache_clear": "Ali ste prepričani, da želite počistiti predpomnilnik DNS?",
|
||||||
"cache_cleared": "Predpomnilnik DNS je bil uspešno počiščen",
|
"cache_cleared": "Predpomnilnik DNS je bil uspešno počiščen",
|
||||||
"clear_cache": "Počisti predpomnilnik"
|
"clear_cache": "Počisti predpomnilnik",
|
||||||
|
"make_static": "Naredi statično",
|
||||||
|
"theme_auto_desc": "Samodejno (glede na barvno shemo vaše naprave)",
|
||||||
|
"theme_dark_desc": "Temna tema",
|
||||||
|
"theme_light_desc": "Svetla tema",
|
||||||
|
"disable_for_seconds": "Za {{count}} sekundo",
|
||||||
|
"disable_for_seconds_plural": "Za {{count}} sekund",
|
||||||
|
"disable_for_minutes": "Za {{count}} minuto",
|
||||||
|
"disable_for_minutes_plural": "Za {{count}} minut",
|
||||||
|
"disable_for_hours": "Za {{count}} uro",
|
||||||
|
"disable_for_hours_plural": "Za {{count}} ur",
|
||||||
|
"disable_until_tomorrow": "Do jutri",
|
||||||
|
"disable_notify_for_seconds": "Onemogoči zaščito za {{count}} sekundo",
|
||||||
|
"disable_notify_for_seconds_plural": "Onemogoči zaščito za {{count}} sekund",
|
||||||
|
"disable_notify_for_minutes": "Onemogoči zaščito za {{count}} minuto",
|
||||||
|
"disable_notify_for_minutes_plural": "Onemogoči zaščito za {{count}} minut",
|
||||||
|
"disable_notify_for_hours": "Onemogoči zaščito za {{count}} uro",
|
||||||
|
"disable_notify_for_hours_plural": "Onemogoči zaščito za {{count}} ur",
|
||||||
|
"disable_notify_until_tomorrow": "Onemogoči zaščito do jutri",
|
||||||
|
"enable_protection_timer": "Zaščita bo omogočena ob {{time}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -523,6 +523,7 @@
|
||||||
"statistics_retention_confirm": "Är du säker på att du vill ändra retentionstiden för statistik? Om du minskar intervallet kommer viss data att gå förlorad",
|
"statistics_retention_confirm": "Är du säker på att du vill ändra retentionstiden för statistik? Om du minskar intervallet kommer viss data att gå förlorad",
|
||||||
"statistics_cleared": "Statistiken har rensats",
|
"statistics_cleared": "Statistiken har rensats",
|
||||||
"statistics_enable": "Aktivera statistik",
|
"statistics_enable": "Aktivera statistik",
|
||||||
|
"ignore_domains_title": "Ignorerade domäner",
|
||||||
"interval_hours": "{{count}} timme",
|
"interval_hours": "{{count}} timme",
|
||||||
"interval_hours_plural": "{{count}} timmar",
|
"interval_hours_plural": "{{count}} timmar",
|
||||||
"filters_configuration": "Filterinställningar",
|
"filters_configuration": "Filterinställningar",
|
||||||
|
@ -642,5 +643,14 @@
|
||||||
"anonymizer_notification": "<0>Observera:</0> IP-anonymisering är aktiverad. Du kan inaktivera den i <1>Allmänna inställningar</1>.",
|
"anonymizer_notification": "<0>Observera:</0> IP-anonymisering är aktiverad. Du kan inaktivera den i <1>Allmänna inställningar</1>.",
|
||||||
"confirm_dns_cache_clear": "Är du säker på att du vill rensa DNS-cache?",
|
"confirm_dns_cache_clear": "Är du säker på att du vill rensa DNS-cache?",
|
||||||
"cache_cleared": "DNS-cacheminnet har rensats",
|
"cache_cleared": "DNS-cacheminnet har rensats",
|
||||||
"clear_cache": "Rensa cache"
|
"clear_cache": "Rensa cache",
|
||||||
|
"theme_dark_desc": "Mörkt tema",
|
||||||
|
"theme_light_desc": "Ljust tema",
|
||||||
|
"disable_for_seconds": "I {{count}} sekund",
|
||||||
|
"disable_for_seconds_plural": "I {{count}} sekunder",
|
||||||
|
"disable_for_minutes": "I {{count}} minut",
|
||||||
|
"disable_for_minutes_plural": "I {{count}} minuter",
|
||||||
|
"disable_for_hours": "I {{count}} timme",
|
||||||
|
"disable_for_hours_plural": "I {{count}} timmar",
|
||||||
|
"disable_until_tomorrow": "Tills imorgon"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "Ebeveyn Denetimi etkinleştirildi",
|
"enabled_parental_toast": "Ebeveyn Denetimi etkinleştirildi",
|
||||||
"disabled_safe_search_toast": "Güvenli Arama devre dışı bırakıldı",
|
"disabled_safe_search_toast": "Güvenli Arama devre dışı bırakıldı",
|
||||||
"enabled_save_search_toast": "Güvenli Arama etkinleştirildi",
|
"enabled_save_search_toast": "Güvenli Arama etkinleştirildi",
|
||||||
|
"updated_save_search_toast": "Güvenli Arama ayarları güncellendi",
|
||||||
"enabled_table_header": "Etkin",
|
"enabled_table_header": "Etkin",
|
||||||
"name_table_header": "Ad",
|
"name_table_header": "Ad",
|
||||||
"list_url_table_header": "Liste URL'si",
|
"list_url_table_header": "Liste URL'si",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "Sıklık limiti",
|
"rate_limit": "Sıklık limiti",
|
||||||
"edns_enable": "EDNS istemci alt ağını etkinleştir",
|
"edns_enable": "EDNS istemci alt ağını etkinleştir",
|
||||||
"edns_cs_desc": "Kaynak yönü isteklerine EDNS İstemci Alt Ağı seçeneğini (ECS) ekleyin ve istemciler tarafından gönderilen değerleri sorgu günlüğüne kaydedin.",
|
"edns_cs_desc": "Kaynak yönü isteklerine EDNS İstemci Alt Ağı seçeneğini (ECS) ekleyin ve istemciler tarafından gönderilen değerleri sorgu günlüğüne kaydedin.",
|
||||||
|
"edns_use_custom_ip": "EDNS için özel IP kullan",
|
||||||
|
"edns_use_custom_ip_desc": "EDNS için özel IP kullanımına izin ver",
|
||||||
"rate_limit_desc": "İstemci başına izin verilen saniyedeki istek sayısı. 0 olarak ayarlamak, sınır olmadığı anlamına gelir.",
|
"rate_limit_desc": "İstemci başına izin verilen saniyedeki istek sayısı. 0 olarak ayarlamak, sınır olmadığı anlamına gelir.",
|
||||||
"blocking_ipv4_desc": "Engellenen bir A isteği için geri döndürülecek IP adresi",
|
"blocking_ipv4_desc": "Engellenen bir A isteği için geri döndürülecek IP adresi",
|
||||||
"blocking_ipv6_desc": "Engellenen bir AAAA isteği için geri döndürülecek IP adresi",
|
"blocking_ipv6_desc": "Engellenen bir AAAA isteği için geri döndürülecek IP adresi",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "İstatistik saklama süresini değiştirmek istediğinizden emin misiniz? Aralık değerini azaltırsanız, bazı veriler kaybolacaktır",
|
"statistics_retention_confirm": "İstatistik saklama süresini değiştirmek istediğinizden emin misiniz? Aralık değerini azaltırsanız, bazı veriler kaybolacaktır",
|
||||||
"statistics_cleared": "İstatistikler başarıyla temizlendi",
|
"statistics_cleared": "İstatistikler başarıyla temizlendi",
|
||||||
"statistics_enable": "İstatistikleri etkinleştir",
|
"statistics_enable": "İstatistikleri etkinleştir",
|
||||||
|
"ignore_domains": "Yok sayılan alan adları (yeni satırla ayrılmış)",
|
||||||
|
"ignore_domains_title": "Yok sayılan alan adları",
|
||||||
|
"ignore_domains_desc_stats": "Bu alan adları için yapılan sorgular istatistiklere yazılmaz",
|
||||||
|
"ignore_domains_desc_query": "Bu alan adları için yapılan sorgular sorgu günlüğüne yazılmaz",
|
||||||
"interval_hours": "{{count}} saat",
|
"interval_hours": "{{count}} saat",
|
||||||
"interval_hours_plural": "{{count}} saat",
|
"interval_hours_plural": "{{count}} saat",
|
||||||
"filters_configuration": "Filtre yapılandırması",
|
"filters_configuration": "Filtre yapılandırması",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>Not:</0> IP anonimleştirme etkinleştirildi. Bunu <1>Genel ayarlardan</1> devre dışı bırakabilirsiniz.",
|
"anonymizer_notification": "<0>Not:</0> IP anonimleştirme etkinleştirildi. Bunu <1>Genel ayarlardan</1> devre dışı bırakabilirsiniz.",
|
||||||
"confirm_dns_cache_clear": "DNS önbelleğini temizlemek istediğinizden emin misiniz?",
|
"confirm_dns_cache_clear": "DNS önbelleğini temizlemek istediğinizden emin misiniz?",
|
||||||
"cache_cleared": "DNS önbelleği başarıyla temizlendi",
|
"cache_cleared": "DNS önbelleği başarıyla temizlendi",
|
||||||
"clear_cache": "Önbelleği temizle"
|
"clear_cache": "Önbelleği temizle",
|
||||||
|
"make_static": "Statik yap",
|
||||||
|
"theme_auto_desc": "Otomatik (cihazınızın renk düzenine göre)",
|
||||||
|
"theme_dark_desc": "Koyu tema",
|
||||||
|
"theme_light_desc": "Açık tema",
|
||||||
|
"disable_for_seconds": "{{count}} saniye için",
|
||||||
|
"disable_for_seconds_plural": "{{count}} saniye için",
|
||||||
|
"disable_for_minutes": "{{count}} dakika için",
|
||||||
|
"disable_for_minutes_plural": "{{count}} dakika için",
|
||||||
|
"disable_for_hours": "{{count}} saat için",
|
||||||
|
"disable_for_hours_plural": "{{count}} saat için",
|
||||||
|
"disable_until_tomorrow": "Yarına kadar",
|
||||||
|
"disable_notify_for_seconds": "Korumayı {{count}} saniyeliğine devre dışı bırak",
|
||||||
|
"disable_notify_for_seconds_plural": "Korumayı {{count}} saniyeliğine devre dışı bırak",
|
||||||
|
"disable_notify_for_minutes": "Korumayı {{count}} dakiklığına devre dışı bırak",
|
||||||
|
"disable_notify_for_minutes_plural": "Korumayı {{count}} dakiklığına devre dışı bırak",
|
||||||
|
"disable_notify_for_hours": "Korumayı {{count}} saatliğine devre dışı bırak",
|
||||||
|
"disable_notify_for_hours_plural": "Korumayı {{count}} saatliğine devre dışı bırak",
|
||||||
|
"disable_notify_until_tomorrow": "Korumayı yarına kadar devre dışı bırak",
|
||||||
|
"enable_protection_timer": "Koruma {{time}} içinde etkinleştirilecektir"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "家长控制已启用",
|
"enabled_parental_toast": "家长控制已启用",
|
||||||
"disabled_safe_search_toast": "安全搜索已禁用",
|
"disabled_safe_search_toast": "安全搜索已禁用",
|
||||||
"enabled_save_search_toast": "安全搜索已启用",
|
"enabled_save_search_toast": "安全搜索已启用",
|
||||||
|
"updated_save_search_toast": "安全搜索设置更新成功",
|
||||||
"enabled_table_header": "已启用",
|
"enabled_table_header": "已启用",
|
||||||
"name_table_header": "名称",
|
"name_table_header": "名称",
|
||||||
"list_url_table_header": "清单网址",
|
"list_url_table_header": "清单网址",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "速度限制",
|
"rate_limit": "速度限制",
|
||||||
"edns_enable": "启用 EDNS 客户端子网",
|
"edns_enable": "启用 EDNS 客户端子网",
|
||||||
"edns_cs_desc": "在上游请求中加入 EDNS 客户端子网(“EDNS Client Subnet”,即 ECS)选项,并在查询日志中记录客户端发送的数值。",
|
"edns_cs_desc": "在上游请求中加入 EDNS 客户端子网(“EDNS Client Subnet”,即 ECS)选项,并在查询日志中记录客户端发送的数值。",
|
||||||
|
"edns_use_custom_ip": "为 EDNS 使用自定义 IP",
|
||||||
|
"edns_use_custom_ip_desc": "允许为 EDNS 使用自定义 IP",
|
||||||
"rate_limit_desc": "每个客户端每秒钟查询次数的限制。设置为 0 意味着不限制。",
|
"rate_limit_desc": "每个客户端每秒钟查询次数的限制。设置为 0 意味着不限制。",
|
||||||
"blocking_ipv4_desc": "拦截 A 记录请求返回的 IP 地址",
|
"blocking_ipv4_desc": "拦截 A 记录请求返回的 IP 地址",
|
||||||
"blocking_ipv6_desc": "拦截 AAAA 记录请求返回的 IP 地址",
|
"blocking_ipv6_desc": "拦截 AAAA 记录请求返回的 IP 地址",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "您确定要更改统计记录保留时间吗? 如果您减少间隔时间的值, 某些数据可能会丢失。",
|
"statistics_retention_confirm": "您确定要更改统计记录保留时间吗? 如果您减少间隔时间的值, 某些数据可能会丢失。",
|
||||||
"statistics_cleared": "统计数据已成功清除",
|
"statistics_cleared": "统计数据已成功清除",
|
||||||
"statistics_enable": "启用统计数据",
|
"statistics_enable": "启用统计数据",
|
||||||
|
"ignore_domains": "忽略的网域(以换行符分隔)",
|
||||||
|
"ignore_domains_title": "被忽略的网域",
|
||||||
|
"ignore_domains_desc_stats": "这些网域的查询不在统计信息",
|
||||||
|
"ignore_domains_desc_query": "这些网域的查询不在查询日志记录",
|
||||||
"interval_hours": "{{count}} 小时",
|
"interval_hours": "{{count}} 小时",
|
||||||
"interval_hours_plural": "{{count}} 小时",
|
"interval_hours_plural": "{{count}} 小时",
|
||||||
"filters_configuration": "过滤器配置",
|
"filters_configuration": "过滤器配置",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>注意:</0> IP 匿名化已启用。您可以在<1>常规设置</1>中禁用它。",
|
"anonymizer_notification": "<0>注意:</0> IP 匿名化已启用。您可以在<1>常规设置</1>中禁用它。",
|
||||||
"confirm_dns_cache_clear": "您确定要清除 DNS 缓存吗?",
|
"confirm_dns_cache_clear": "您确定要清除 DNS 缓存吗?",
|
||||||
"cache_cleared": "已成功清除 DNS 缓存",
|
"cache_cleared": "已成功清除 DNS 缓存",
|
||||||
"clear_cache": "清除缓存"
|
"clear_cache": "清除缓存",
|
||||||
|
"make_static": "静态化",
|
||||||
|
"theme_auto_desc": "自动(基于设备的配色方案)",
|
||||||
|
"theme_dark_desc": "暗黑主题",
|
||||||
|
"theme_light_desc": "浅色主题",
|
||||||
|
"disable_for_seconds": "{{count}} 秒",
|
||||||
|
"disable_for_seconds_plural": "{{count}} 秒",
|
||||||
|
"disable_for_minutes": "{{count}} 分钟",
|
||||||
|
"disable_for_minutes_plural": "{{count}} 分钟",
|
||||||
|
"disable_for_hours": "{{count}} 小时",
|
||||||
|
"disable_for_hours_plural": "{{count}} 小时",
|
||||||
|
"disable_until_tomorrow": "直到明天",
|
||||||
|
"disable_notify_for_seconds": "禁用保护 {{count}} 秒",
|
||||||
|
"disable_notify_for_seconds_plural": "禁用保护 {{count}} 秒",
|
||||||
|
"disable_notify_for_minutes": "禁用保护 {{count}} 分钟",
|
||||||
|
"disable_notify_for_minutes_plural": "禁用保护 {{count}} 分钟",
|
||||||
|
"disable_notify_for_hours": "禁用保护 {{count}} 小时",
|
||||||
|
"disable_notify_for_hours_plural": "禁用保护 {{count}} 小时",
|
||||||
|
"disable_notify_until_tomorrow": "禁用保护直到明天",
|
||||||
|
"enable_protection_timer": "保护将于 {{time}} 启用"
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
"enabled_parental_toast": "已啟用家長控制",
|
"enabled_parental_toast": "已啟用家長控制",
|
||||||
"disabled_safe_search_toast": "已禁用安全搜尋",
|
"disabled_safe_search_toast": "已禁用安全搜尋",
|
||||||
"enabled_save_search_toast": "已啟用安全搜尋",
|
"enabled_save_search_toast": "已啟用安全搜尋",
|
||||||
|
"updated_save_search_toast": "安全搜尋設定更新成功",
|
||||||
"enabled_table_header": "已啟用",
|
"enabled_table_header": "已啟用",
|
||||||
"name_table_header": "名稱",
|
"name_table_header": "名稱",
|
||||||
"list_url_table_header": "清單網址",
|
"list_url_table_header": "清單網址",
|
||||||
|
@ -290,6 +291,8 @@
|
||||||
"rate_limit": "速率限制",
|
"rate_limit": "速率限制",
|
||||||
"edns_enable": "啟用對於 DNS 的擴充機制(EDNS)用戶端子網路",
|
"edns_enable": "啟用對於 DNS 的擴充機制(EDNS)用戶端子網路",
|
||||||
"edns_cs_desc": "新增對於 DNS 的擴充機制(EDNS)用戶端子網路選項到上游的請求,並在查詢記錄中記錄由用戶端傳送的數值。",
|
"edns_cs_desc": "新增對於 DNS 的擴充機制(EDNS)用戶端子網路選項到上游的請求,並在查詢記錄中記錄由用戶端傳送的數值。",
|
||||||
|
"edns_use_custom_ip": "為 EDNS 使用自訂的 IP",
|
||||||
|
"edns_use_custom_ip_desc": "允許為 EDNS 使用自訂的 IP",
|
||||||
"rate_limit_desc": "每個用戶端被允許的每秒請求之數量。設定它為 0 表示無限制。",
|
"rate_limit_desc": "每個用戶端被允許的每秒請求之數量。設定它為 0 表示無限制。",
|
||||||
"blocking_ipv4_desc": "要被返回給已封鎖的 A 請求之 IP 位址",
|
"blocking_ipv4_desc": "要被返回給已封鎖的 A 請求之 IP 位址",
|
||||||
"blocking_ipv6_desc": "要被返回給已封鎖的 AAAA 請求之 IP 位址",
|
"blocking_ipv6_desc": "要被返回給已封鎖的 AAAA 請求之 IP 位址",
|
||||||
|
@ -523,6 +526,10 @@
|
||||||
"statistics_retention_confirm": "您確定您想要更改統計資料保留嗎?如果您減少該間隔值,某些資料將被丟失",
|
"statistics_retention_confirm": "您確定您想要更改統計資料保留嗎?如果您減少該間隔值,某些資料將被丟失",
|
||||||
"statistics_cleared": "統計資料被成功地清除",
|
"statistics_cleared": "統計資料被成功地清除",
|
||||||
"statistics_enable": "啟用統計資料",
|
"statistics_enable": "啟用統計資料",
|
||||||
|
"ignore_domains": "忽略的網域(以換行符分隔)",
|
||||||
|
"ignore_domains_title": "被忽略的網域",
|
||||||
|
"ignore_domains_desc_stats": "這些網域的查詢不在統計資料",
|
||||||
|
"ignore_domains_desc_query": "這些網域的查詢不在查詢記錄",
|
||||||
"interval_hours": "{{count}} 小時",
|
"interval_hours": "{{count}} 小時",
|
||||||
"interval_hours_plural": "{{count}} 小時",
|
"interval_hours_plural": "{{count}} 小時",
|
||||||
"filters_configuration": "過濾器配置",
|
"filters_configuration": "過濾器配置",
|
||||||
|
@ -642,5 +649,24 @@
|
||||||
"anonymizer_notification": "<0>注意:</0>IP 匿名化被啟用。您可在<1>一般設定</1>中禁用它。",
|
"anonymizer_notification": "<0>注意:</0>IP 匿名化被啟用。您可在<1>一般設定</1>中禁用它。",
|
||||||
"confirm_dns_cache_clear": "您確定您想要清除 DNS 快取嗎?",
|
"confirm_dns_cache_clear": "您確定您想要清除 DNS 快取嗎?",
|
||||||
"cache_cleared": "DNS 快取被成功地清除",
|
"cache_cleared": "DNS 快取被成功地清除",
|
||||||
"clear_cache": "清除快取"
|
"clear_cache": "清除快取",
|
||||||
|
"make_static": "靜態化",
|
||||||
|
"theme_auto_desc": "自動(基於裝置的配色方案)",
|
||||||
|
"theme_dark_desc": "深色主題",
|
||||||
|
"theme_light_desc": "淺色主題",
|
||||||
|
"disable_for_seconds": "{{count}} 秒",
|
||||||
|
"disable_for_seconds_plural": "{{count}} 秒",
|
||||||
|
"disable_for_minutes": "{{count}} 分鐘",
|
||||||
|
"disable_for_minutes_plural": "{{count}} 分鐘",
|
||||||
|
"disable_for_hours": "{{count}} 小時",
|
||||||
|
"disable_for_hours_plural": "{{count}} 小時",
|
||||||
|
"disable_until_tomorrow": "直到明天",
|
||||||
|
"disable_notify_for_seconds": "計 {{count}} 秒禁用防護",
|
||||||
|
"disable_notify_for_seconds_plural": "計 {{count}} 秒禁用防護",
|
||||||
|
"disable_notify_for_minutes": "計 {{count}} 分鐘禁用防護",
|
||||||
|
"disable_notify_for_minutes_plural": "計 {{count}} 分鐘禁用防護",
|
||||||
|
"disable_notify_for_hours": "計 {{count}} 小時禁用防護",
|
||||||
|
"disable_notify_for_hours_plural": "計 {{count}} 小時禁用防護",
|
||||||
|
"disable_notify_until_tomorrow": "禁用防護直到明天",
|
||||||
|
"enable_protection_timer": "防護將於 {{time}} 被啟用"
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,9 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
|
||||||
|
|
||||||
const dnsStatus = await apiClient.getGlobalStatus();
|
const dnsStatus = await apiClient.getGlobalStatus();
|
||||||
if (dnsStatus) {
|
if (dnsStatus) {
|
||||||
|
if (dnsStatus.protection_disabled_duration === 0) {
|
||||||
|
dnsStatus.protection_disabled_duration = null;
|
||||||
|
}
|
||||||
dispatch(dnsStatusSuccess(dnsStatus));
|
dispatch(dnsStatusSuccess(dnsStatus));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,14 @@ import endsWith from 'lodash/endsWith';
|
||||||
import escapeRegExp from 'lodash/escapeRegExp';
|
import escapeRegExp from 'lodash/escapeRegExp';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { compose } from 'redux';
|
import { compose } from 'redux';
|
||||||
import { splitByNewLine, sortClients, filterOutComments } from '../helpers/helpers';
|
import {
|
||||||
|
splitByNewLine,
|
||||||
|
sortClients,
|
||||||
|
filterOutComments,
|
||||||
|
msToSeconds,
|
||||||
|
msToMinutes,
|
||||||
|
msToHours,
|
||||||
|
} from '../helpers/helpers';
|
||||||
import {
|
import {
|
||||||
BLOCK_ACTIONS,
|
BLOCK_ACTIONS,
|
||||||
CHECK_TIMEOUT,
|
CHECK_TIMEOUT,
|
||||||
|
@ -14,6 +21,7 @@ import {
|
||||||
SETTINGS_NAMES,
|
SETTINGS_NAMES,
|
||||||
FORM_NAME,
|
FORM_NAME,
|
||||||
MANUAL_UPDATE_LINK,
|
MANUAL_UPDATE_LINK,
|
||||||
|
DISABLE_PROTECTION_TIMINGS,
|
||||||
} from '../helpers/constants';
|
} from '../helpers/constants';
|
||||||
import { areEqualVersions } from '../helpers/version';
|
import { areEqualVersions } from '../helpers/version';
|
||||||
import { getTlsStatus } from './encryption';
|
import { getTlsStatus } from './encryption';
|
||||||
|
@ -24,6 +32,12 @@ import { getFilteringStatus, setRules } from './filtering';
|
||||||
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
|
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
|
||||||
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
|
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} settingKey = SETTINGS_NAMES
|
||||||
|
* @param {*} status: boolean | SafeSearchConfig
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
||||||
let successMessage = '';
|
let successMessage = '';
|
||||||
try {
|
try {
|
||||||
|
@ -49,14 +63,9 @@ export const toggleSetting = (settingKey, status) => async (dispatch) => {
|
||||||
dispatch(toggleSettingStatus({ settingKey }));
|
dispatch(toggleSettingStatus({ settingKey }));
|
||||||
break;
|
break;
|
||||||
case SETTINGS_NAMES.safesearch:
|
case SETTINGS_NAMES.safesearch:
|
||||||
if (status) {
|
successMessage = 'updated_save_search_toast';
|
||||||
successMessage = 'disabled_safe_search_toast';
|
await apiClient.updateSafesearch(status);
|
||||||
await apiClient.disableSafesearch();
|
dispatch(toggleSettingStatus({ settingKey, value: status }));
|
||||||
} else {
|
|
||||||
successMessage = 'enabled_save_search_toast';
|
|
||||||
await apiClient.enableSafesearch();
|
|
||||||
}
|
|
||||||
dispatch(toggleSettingStatus({ settingKey }));
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -71,7 +80,9 @@ export const initSettingsRequest = createAction('SETTINGS_INIT_REQUEST');
|
||||||
export const initSettingsFailure = createAction('SETTINGS_INIT_FAILURE');
|
export const initSettingsFailure = createAction('SETTINGS_INIT_FAILURE');
|
||||||
export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS');
|
export const initSettingsSuccess = createAction('SETTINGS_INIT_SUCCESS');
|
||||||
|
|
||||||
export const initSettings = (settingsList) => async (dispatch) => {
|
export const initSettings = (settingsList = {
|
||||||
|
safebrowsing: {}, parental: {},
|
||||||
|
}) => async (dispatch) => {
|
||||||
dispatch(initSettingsRequest());
|
dispatch(initSettingsRequest());
|
||||||
try {
|
try {
|
||||||
const safebrowsingStatus = await apiClient.getSafebrowsingStatus();
|
const safebrowsingStatus = await apiClient.getSafebrowsingStatus();
|
||||||
|
@ -80,7 +91,6 @@ export const initSettings = (settingsList) => async (dispatch) => {
|
||||||
const {
|
const {
|
||||||
safebrowsing,
|
safebrowsing,
|
||||||
parental,
|
parental,
|
||||||
safesearch,
|
|
||||||
} = settingsList;
|
} = settingsList;
|
||||||
const newSettingsList = {
|
const newSettingsList = {
|
||||||
safebrowsing: {
|
safebrowsing: {
|
||||||
|
@ -92,8 +102,7 @@ export const initSettings = (settingsList) => async (dispatch) => {
|
||||||
enabled: parentalStatus.enabled,
|
enabled: parentalStatus.enabled,
|
||||||
},
|
},
|
||||||
safesearch: {
|
safesearch: {
|
||||||
...safesearch,
|
...safesearchStatus,
|
||||||
enabled: safesearchStatus.enabled,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
|
dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
|
||||||
|
@ -107,19 +116,54 @@ export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST')
|
||||||
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
|
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
|
||||||
export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS');
|
export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS');
|
||||||
|
|
||||||
export const toggleProtection = (status) => async (dispatch) => {
|
const getDisabledMessage = (time) => {
|
||||||
|
switch (time) {
|
||||||
|
case DISABLE_PROTECTION_TIMINGS.HALF_MINUTE:
|
||||||
|
return i18next.t(
|
||||||
|
'disable_notify_for_seconds',
|
||||||
|
{ count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) },
|
||||||
|
);
|
||||||
|
case DISABLE_PROTECTION_TIMINGS.MINUTE:
|
||||||
|
return i18next.t(
|
||||||
|
'disable_notify_for_minutes',
|
||||||
|
{ count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) },
|
||||||
|
);
|
||||||
|
case DISABLE_PROTECTION_TIMINGS.TEN_MINUTES:
|
||||||
|
return i18next.t(
|
||||||
|
'disable_notify_for_minutes',
|
||||||
|
{ count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) },
|
||||||
|
);
|
||||||
|
case DISABLE_PROTECTION_TIMINGS.HOUR:
|
||||||
|
return i18next.t(
|
||||||
|
'disable_notify_for_hours',
|
||||||
|
{ count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) },
|
||||||
|
);
|
||||||
|
case DISABLE_PROTECTION_TIMINGS.TOMORROW:
|
||||||
|
return i18next.t('disable_notify_until_tomorrow');
|
||||||
|
default:
|
||||||
|
return 'disabled_protection';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleProtection = (status, time = null) => async (dispatch) => {
|
||||||
dispatch(toggleProtectionRequest());
|
dispatch(toggleProtectionRequest());
|
||||||
try {
|
try {
|
||||||
const successMessage = status ? 'disabled_protection' : 'enabled_protection';
|
const successMessage = status ? getDisabledMessage(time) : 'enabled_protection';
|
||||||
await apiClient.setDnsConfig({ protection_enabled: !status });
|
await apiClient.setProtection({ enabled: !status, duration: time });
|
||||||
dispatch(addSuccessToast(successMessage));
|
dispatch(addSuccessToast(successMessage));
|
||||||
dispatch(toggleProtectionSuccess());
|
dispatch(toggleProtectionSuccess({ disabledDuration: time }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(toggleProtectionFailure());
|
dispatch(toggleProtectionFailure());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setDisableDurationTime = createAction('SET_DISABLED_DURATION_TIME');
|
||||||
|
|
||||||
|
export const setProtectionTimerTime = (updatedTime) => async (dispatch) => {
|
||||||
|
dispatch(setDisableDurationTime({ timeToEnableProtection: updatedTime }));
|
||||||
|
};
|
||||||
|
|
||||||
export const getVersionRequest = createAction('GET_VERSION_REQUEST');
|
export const getVersionRequest = createAction('GET_VERSION_REQUEST');
|
||||||
export const getVersionFailure = createAction('GET_VERSION_FAILURE');
|
export const getVersionFailure = createAction('GET_VERSION_FAILURE');
|
||||||
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
|
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
|
||||||
|
@ -272,6 +316,9 @@ export const getDnsStatus = () => async (dispatch) => {
|
||||||
|
|
||||||
const handleRequestSuccess = (response) => {
|
const handleRequestSuccess = (response) => {
|
||||||
const dnsStatus = response.data;
|
const dnsStatus = response.data;
|
||||||
|
if (dnsStatus.protection_disabled_duration === 0) {
|
||||||
|
dnsStatus.protection_disabled_duration = null;
|
||||||
|
}
|
||||||
const { running } = dnsStatus;
|
const { running } = dnsStatus;
|
||||||
const runningStatus = dnsStatus && running;
|
const runningStatus = dnsStatus && running;
|
||||||
if (runningStatus === true) {
|
if (runningStatus === true) {
|
||||||
|
|
|
@ -177,7 +177,7 @@ export const getLogsConfigSuccess = createAction('GET_LOGS_CONFIG_SUCCESS');
|
||||||
export const getLogsConfig = () => async (dispatch) => {
|
export const getLogsConfig = () => async (dispatch) => {
|
||||||
dispatch(getLogsConfigRequest());
|
dispatch(getLogsConfigRequest());
|
||||||
try {
|
try {
|
||||||
const data = await apiClient.getQueryLogInfo();
|
const data = await apiClient.getQueryLogConfig();
|
||||||
dispatch(getLogsConfigSuccess(data));
|
dispatch(getLogsConfigSuccess(data));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const getStatsConfigSuccess = createAction('GET_STATS_CONFIG_SUCCESS');
|
||||||
export const getStatsConfig = () => async (dispatch) => {
|
export const getStatsConfig = () => async (dispatch) => {
|
||||||
dispatch(getStatsConfigRequest());
|
dispatch(getStatsConfigRequest());
|
||||||
try {
|
try {
|
||||||
const data = await apiClient.getStatsInfo();
|
const data = await apiClient.getStatsConfig();
|
||||||
dispatch(getStatsConfigSuccess(data));
|
dispatch(getStatsConfigSuccess(data));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
|
|
|
@ -208,24 +208,40 @@ class Api {
|
||||||
// Safesearch
|
// Safesearch
|
||||||
SAFESEARCH_STATUS = { path: 'safesearch/status', method: 'GET' };
|
SAFESEARCH_STATUS = { path: 'safesearch/status', method: 'GET' };
|
||||||
|
|
||||||
SAFESEARCH_ENABLE = { path: 'safesearch/enable', method: 'POST' };
|
SAFESEARCH_UPDATE = { path: 'safesearch/settings', method: 'PUT' };
|
||||||
|
|
||||||
SAFESEARCH_DISABLE = { path: 'safesearch/disable', method: 'POST' };
|
|
||||||
|
|
||||||
getSafesearchStatus() {
|
getSafesearchStatus() {
|
||||||
const { path, method } = this.SAFESEARCH_STATUS;
|
const { path, method } = this.SAFESEARCH_STATUS;
|
||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
enableSafesearch() {
|
/**
|
||||||
const { path, method } = this.SAFESEARCH_ENABLE;
|
* interface SafeSearchConfig {
|
||||||
return this.makeRequest(path, method);
|
"enabled": boolean,
|
||||||
|
"bing": boolean,
|
||||||
|
"duckduckgo": boolean,
|
||||||
|
"google": boolean,
|
||||||
|
"pixabay": boolean,
|
||||||
|
"yandex": boolean,
|
||||||
|
"youtube": boolean
|
||||||
|
* }
|
||||||
|
* @param {*} data - SafeSearchConfig
|
||||||
|
* @returns 200 ok
|
||||||
|
*/
|
||||||
|
updateSafesearch(data) {
|
||||||
|
const { path, method } = this.SAFESEARCH_UPDATE;
|
||||||
|
return this.makeRequest(path, method, { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
disableSafesearch() {
|
// enableSafesearch() {
|
||||||
const { path, method } = this.SAFESEARCH_DISABLE;
|
// const { path, method } = this.SAFESEARCH_ENABLE;
|
||||||
return this.makeRequest(path, method);
|
// return this.makeRequest(path, method);
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// disableSafesearch() {
|
||||||
|
// const { path, method } = this.SAFESEARCH_DISABLE;
|
||||||
|
// return this.makeRequest(path, method);
|
||||||
|
// }
|
||||||
|
|
||||||
// Language
|
// Language
|
||||||
|
|
||||||
|
@ -497,9 +513,9 @@ class Api {
|
||||||
// Settings for statistics
|
// Settings for statistics
|
||||||
GET_STATS = { path: 'stats', method: 'GET' };
|
GET_STATS = { path: 'stats', method: 'GET' };
|
||||||
|
|
||||||
STATS_INFO = { path: 'stats_info', method: 'GET' };
|
GET_STATS_CONFIG = { path: 'stats/config', method: 'GET' };
|
||||||
|
|
||||||
STATS_CONFIG = { path: 'stats_config', method: 'POST' };
|
UPDATE_STATS_CONFIG = { path: 'stats/config/update', method: 'PUT' };
|
||||||
|
|
||||||
STATS_RESET = { path: 'stats_reset', method: 'POST' };
|
STATS_RESET = { path: 'stats_reset', method: 'POST' };
|
||||||
|
|
||||||
|
@ -508,13 +524,13 @@ class Api {
|
||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatsInfo() {
|
getStatsConfig() {
|
||||||
const { path, method } = this.STATS_INFO;
|
const { path, method } = this.GET_STATS_CONFIG;
|
||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatsConfig(data) {
|
setStatsConfig(data) {
|
||||||
const { path, method } = this.STATS_CONFIG;
|
const { path, method } = this.UPDATE_STATS_CONFIG;
|
||||||
const config = {
|
const config = {
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
|
@ -529,9 +545,9 @@ class Api {
|
||||||
// Query log
|
// Query log
|
||||||
GET_QUERY_LOG = { path: 'querylog', method: 'GET' };
|
GET_QUERY_LOG = { path: 'querylog', method: 'GET' };
|
||||||
|
|
||||||
QUERY_LOG_CONFIG = { path: 'querylog_config', method: 'POST' };
|
UPDATE_QUERY_LOG_CONFIG = { path: 'querylog/config/update', method: 'PUT' };
|
||||||
|
|
||||||
QUERY_LOG_INFO = { path: 'querylog_info', method: 'GET' };
|
GET_QUERY_LOG_CONFIG = { path: 'querylog/config', method: 'GET' };
|
||||||
|
|
||||||
QUERY_LOG_CLEAR = { path: 'querylog_clear', method: 'POST' };
|
QUERY_LOG_CLEAR = { path: 'querylog_clear', method: 'POST' };
|
||||||
|
|
||||||
|
@ -543,13 +559,13 @@ class Api {
|
||||||
return this.makeRequest(url, method);
|
return this.makeRequest(url, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
getQueryLogInfo() {
|
getQueryLogConfig() {
|
||||||
const { path, method } = this.QUERY_LOG_INFO;
|
const { path, method } = this.GET_QUERY_LOG_CONFIG;
|
||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
setQueryLogConfig(data) {
|
setQueryLogConfig(data) {
|
||||||
const { path, method } = this.QUERY_LOG_CONFIG;
|
const { path, method } = this.UPDATE_QUERY_LOG_CONFIG;
|
||||||
const config = {
|
const config = {
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
|
@ -611,6 +627,15 @@ class Api {
|
||||||
return this.makeRequest(path, method, config);
|
return this.makeRequest(path, method, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SET_PROTECTION = { path: 'protection', method: 'POST' };
|
||||||
|
|
||||||
|
setProtection(data) {
|
||||||
|
const { enabled, duration } = data;
|
||||||
|
const { path, method } = this.SET_PROTECTION;
|
||||||
|
|
||||||
|
return this.makeRequest(path, method, { data: { enabled, duration } });
|
||||||
|
}
|
||||||
|
|
||||||
// Cache
|
// Cache
|
||||||
CLEAR_CACHE = { path: 'cache_clear', method: 'POST' };
|
CLEAR_CACHE = { path: 'cache_clear', method: 'POST' };
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ import DnsRewrites from '../../containers/DnsRewrites';
|
||||||
import CustomRules from '../../containers/CustomRules';
|
import CustomRules from '../../containers/CustomRules';
|
||||||
import Services from '../Filters/Services';
|
import Services from '../Filters/Services';
|
||||||
import Logs from '../Logs';
|
import Logs from '../Logs';
|
||||||
|
import ProtectionTimer from '../ProtectionTimer';
|
||||||
|
|
||||||
const ROUTES = [
|
const ROUTES = [
|
||||||
{
|
{
|
||||||
|
@ -164,8 +165,7 @@ const App = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
const colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
const prefersDark = colorSchemeMedia.matches;
|
setUITheme(theme);
|
||||||
setUITheme(prefersDark ? THEMES.dark : THEMES.light);
|
|
||||||
|
|
||||||
if (colorSchemeMedia.addEventListener !== undefined) {
|
if (colorSchemeMedia.addEventListener !== undefined) {
|
||||||
colorSchemeMedia.addEventListener('change', (e) => {
|
colorSchemeMedia.addEventListener('change', (e) => {
|
||||||
|
@ -191,6 +191,7 @@ const App = () => {
|
||||||
{!processingEncryption && <EncryptionTopline />}
|
{!processingEncryption && <EncryptionTopline />}
|
||||||
<LoadingBar className="loading-bar" updateTime={1000} />
|
<LoadingBar className="loading-bar" updateTime={1000} />
|
||||||
<Header />
|
<Header />
|
||||||
|
<ProtectionTimer />
|
||||||
<div className="container container--wrap pb-5">
|
<div className="container container--wrap pb-5">
|
||||||
{processing && <Loading />}
|
{processing && <Loading />}
|
||||||
{!isCoreRunning && <div className="row row-cards">
|
{!isCoreRunning && <div className="row row-cards">
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
.dashboard-protection-button.btn-gray {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-right-color: #a4a4a4;
|
||||||
|
}
|
||||||
|
|
||||||
.stats__table .popover__body {
|
.stats__table .popover__body {
|
||||||
left: -10px;
|
left: -10px;
|
||||||
min-width: 270px;
|
min-width: 270px;
|
||||||
|
@ -34,20 +40,11 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-title__button {
|
|
||||||
margin: 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.page-title--dashboard {
|
.page-title--dashboard {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-title__button {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.counters__row {
|
.counters__row {
|
||||||
|
|
|
@ -9,18 +9,25 @@ import Counters from './Counters';
|
||||||
import Clients from './Clients';
|
import Clients from './Clients';
|
||||||
import QueriedDomains from './QueriedDomains';
|
import QueriedDomains from './QueriedDomains';
|
||||||
import BlockedDomains from './BlockedDomains';
|
import BlockedDomains from './BlockedDomains';
|
||||||
import { SETTINGS_URLS } from '../../helpers/constants';
|
import { DISABLE_PROTECTION_TIMINGS, ONE_SECOND_IN_MS, SETTINGS_URLS } from '../../helpers/constants';
|
||||||
|
import {
|
||||||
|
msToSeconds,
|
||||||
|
msToMinutes,
|
||||||
|
msToHours,
|
||||||
|
msToDays,
|
||||||
|
} from '../../helpers/helpers';
|
||||||
|
|
||||||
import PageTitle from '../ui/PageTitle';
|
import PageTitle from '../ui/PageTitle';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import './Dashboard.css';
|
import './Dashboard.css';
|
||||||
|
import Dropdown from '../ui/Dropdown';
|
||||||
|
|
||||||
const Dashboard = ({
|
const Dashboard = ({
|
||||||
getAccessList,
|
getAccessList,
|
||||||
getStats,
|
getStats,
|
||||||
getStatsConfig,
|
getStatsConfig,
|
||||||
dashboard,
|
dashboard,
|
||||||
dashboard: { protectionEnabled, processingProtection },
|
dashboard: { protectionEnabled, processingProtection, protectionDisabledDuration },
|
||||||
toggleProtection,
|
toggleProtection,
|
||||||
stats,
|
stats,
|
||||||
access,
|
access,
|
||||||
|
@ -36,20 +43,20 @@ const Dashboard = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getAllStats();
|
getAllStats();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getSubtitle = () => {
|
const getSubtitle = () => {
|
||||||
if (stats.interval === 0) {
|
const ONE_DAY = 1;
|
||||||
|
const intervalInDays = msToDays(stats.interval);
|
||||||
|
|
||||||
|
if (intervalInDays < ONE_DAY) {
|
||||||
return t('stats_disabled_short');
|
return t('stats_disabled_short');
|
||||||
}
|
}
|
||||||
|
|
||||||
return stats.interval === 1
|
return intervalInDays === ONE_DAY
|
||||||
? t('for_last_24_hours')
|
? t('for_last_24_hours')
|
||||||
: t('for_last_days', { count: stats.interval });
|
: t('for_last_days', { count: msToDays(stats.interval) });
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonText = protectionEnabled ? 'disable_protection' : 'enable_protection';
|
const buttonClass = classNames('btn btn-sm dashboard-protection-button', {
|
||||||
|
|
||||||
const buttonClass = classNames('btn btn-sm dashboard-title__button', {
|
|
||||||
'btn-gray': protectionEnabled,
|
'btn-gray': protectionEnabled,
|
||||||
'btn-success': !protectionEnabled,
|
'btn-success': !protectionEnabled,
|
||||||
});
|
});
|
||||||
|
@ -71,16 +78,87 @@ const Dashboard = ({
|
||||||
|
|
||||||
const subtitle = getSubtitle();
|
const subtitle = getSubtitle();
|
||||||
|
|
||||||
|
const DISABLE_PROTECTION_ITEMS = [
|
||||||
|
{
|
||||||
|
text: t('disable_for_seconds', { count: msToSeconds(DISABLE_PROTECTION_TIMINGS.HALF_MINUTE) }),
|
||||||
|
disableTime: DISABLE_PROTECTION_TIMINGS.HALF_MINUTE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.MINUTE) }),
|
||||||
|
disableTime: DISABLE_PROTECTION_TIMINGS.MINUTE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t('disable_for_minutes', { count: msToMinutes(DISABLE_PROTECTION_TIMINGS.TEN_MINUTES) }),
|
||||||
|
disableTime: DISABLE_PROTECTION_TIMINGS.TEN_MINUTES,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t('disable_for_hours', { count: msToHours(DISABLE_PROTECTION_TIMINGS.HOUR) }),
|
||||||
|
disableTime: DISABLE_PROTECTION_TIMINGS.HOUR,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: t('disable_until_tomorrow'),
|
||||||
|
disableTime: DISABLE_PROTECTION_TIMINGS.TOMORROW,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getDisableProtectionItems = () => (
|
||||||
|
Object.values(DISABLE_PROTECTION_ITEMS)
|
||||||
|
.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={`disable_timings_${index}`}
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={() => {
|
||||||
|
toggleProtection(protectionEnabled, item.disableTime - ONE_SECOND_IN_MS);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.text}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRemaningTimeText = (milliseconds) => {
|
||||||
|
if (!milliseconds) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(milliseconds);
|
||||||
|
const hh = date.getUTCHours();
|
||||||
|
const mm = `0${date.getUTCMinutes()}`.slice(-2);
|
||||||
|
const ss = `0${date.getUTCSeconds()}`.slice(-2);
|
||||||
|
const formattedHH = `0${hh}`.slice(-2);
|
||||||
|
|
||||||
|
return hh ? `${formattedHH}:${mm}:${ss}` : `${mm}:${ss}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProtectionBtnText = (status) => (status ? t('disable_protection') : t('enable_protection'));
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<PageTitle title={t('dashboard')} containerClass="page-title--dashboard">
|
<PageTitle title={t('dashboard')} containerClass="page-title--dashboard">
|
||||||
<button
|
<div className="page-title__protection">
|
||||||
type="button"
|
<button
|
||||||
className={buttonClass}
|
type="button"
|
||||||
onClick={() => toggleProtection(protectionEnabled)}
|
className={buttonClass}
|
||||||
disabled={processingProtection}
|
onClick={() => {
|
||||||
>
|
toggleProtection(protectionEnabled);
|
||||||
<Trans>{buttonText}</Trans>
|
}}
|
||||||
</button>
|
disabled={processingProtection}
|
||||||
|
>
|
||||||
|
{protectionDisabledDuration
|
||||||
|
? `${t('enable_protection_timer')} ${getRemaningTimeText(protectionDisabledDuration)}`
|
||||||
|
: getProtectionBtnText(protectionEnabled)
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{protectionEnabled && <Dropdown
|
||||||
|
label=""
|
||||||
|
baseClassName="dropdown-protection"
|
||||||
|
icon="arrow-down"
|
||||||
|
controlClassName="dropdown-protection__toggle"
|
||||||
|
menuClassName="dropdown-menu dropdown-menu-arrow dropdown-menu--protection"
|
||||||
|
>
|
||||||
|
{getDisableProtectionItems()}
|
||||||
|
</Dropdown>}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-outline-primary btn-sm"
|
className="btn btn-outline-primary btn-sm"
|
||||||
|
@ -107,7 +185,7 @@ const Dashboard = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Statistics
|
<Statistics
|
||||||
interval={stats.interval}
|
interval={msToDays(stats.interval)}
|
||||||
dnsQueries={stats.dnsQueries}
|
dnsQueries={stats.dnsQueries}
|
||||||
blockedFiltering={stats.blockedFiltering}
|
blockedFiltering={stats.blockedFiltering}
|
||||||
replacedSafebrowsing={stats.replacedSafebrowsing}
|
replacedSafebrowsing={stats.replacedSafebrowsing}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { MODAL_TYPE } from '../../helpers/constants';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getCurrentFilter,
|
getCurrentFilter,
|
||||||
getObjDiff,
|
|
||||||
} from '../../helpers/helpers';
|
} from '../../helpers/helpers';
|
||||||
|
|
||||||
import filtersCatalog from '../../helpers/filters/filters';
|
import filtersCatalog from '../../helpers/filters/filters';
|
||||||
|
@ -22,7 +21,7 @@ class DnsBlocklist extends Component {
|
||||||
this.props.getFilteringStatus();
|
this.props.getFilteringStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit = (values, _, { initialValues }) => {
|
handleSubmit = (values) => {
|
||||||
const { modalFilterUrl, modalType } = this.props.filtering;
|
const { modalFilterUrl, modalType } = this.props.filtering;
|
||||||
|
|
||||||
switch (modalType) {
|
switch (modalType) {
|
||||||
|
@ -35,7 +34,12 @@ class DnsBlocklist extends Component {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MODAL_TYPE.CHOOSE_FILTERING_LIST: {
|
case MODAL_TYPE.CHOOSE_FILTERING_LIST: {
|
||||||
const changedValues = getObjDiff(initialValues, values);
|
const changedValues = Object.entries(values)?.reduce((acc, [key, value]) => {
|
||||||
|
if (value && key in filtersCatalog.filters) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
Object.keys(changedValues)
|
Object.keys(changedValues)
|
||||||
.forEach((fieldName) => {
|
.forEach((fieldName) => {
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { ONE_SECOND_IN_MS } from '../../helpers/constants';
|
||||||
|
import { setProtectionTimerTime, toggleProtectionSuccess } from '../../actions';
|
||||||
|
|
||||||
|
let interval = null;
|
||||||
|
|
||||||
|
const ProtectionTimer = ({
|
||||||
|
protectionDisabledDuration,
|
||||||
|
toggleProtectionSuccess,
|
||||||
|
setProtectionTimerTime,
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (protectionDisabledDuration !== null && protectionDisabledDuration < ONE_SECOND_IN_MS) {
|
||||||
|
toggleProtectionSuccess({ disabledDuration: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protectionDisabledDuration) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setProtectionTimerTime(protectionDisabledDuration - ONE_SECOND_IN_MS);
|
||||||
|
}, ONE_SECOND_IN_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [protectionDisabledDuration]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
ProtectionTimer.propTypes = {
|
||||||
|
setProtectionTimerTime: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
const { dashboard } = state;
|
||||||
|
const { protectionEnabled, protectionDisabledDuration } = dashboard;
|
||||||
|
return { protectionEnabled, protectionDisabledDuration };
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleProtectionSuccess,
|
||||||
|
setProtectionTimerTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(ProtectionTimer);
|
|
@ -7,6 +7,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
|
|
||||||
import { getAllBlockedServices } from '../../../../actions/services';
|
import { getAllBlockedServices } from '../../../../actions/services';
|
||||||
|
import { initSettings } from '../../../../actions';
|
||||||
import {
|
import {
|
||||||
splitByNewLine,
|
splitByNewLine,
|
||||||
countClientsStatistics,
|
countClientsStatistics,
|
||||||
|
@ -38,9 +39,13 @@ const ClientsTable = ({
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const services = useSelector((store) => store?.services);
|
const services = useSelector((store) => store?.services);
|
||||||
|
const globalSettings = useSelector((store) => store?.settings.settingsList) || {};
|
||||||
|
|
||||||
|
const { safesearch } = globalSettings;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(getAllBlockedServices());
|
dispatch(getAllBlockedServices());
|
||||||
|
dispatch(initSettings());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleFormAdd = (values) => {
|
const handleFormAdd = (values) => {
|
||||||
|
@ -107,6 +112,7 @@ const ClientsTable = ({
|
||||||
tags: [],
|
tags: [],
|
||||||
use_global_settings: true,
|
use_global_settings: true,
|
||||||
use_global_blocked_services: true,
|
use_global_blocked_services: true,
|
||||||
|
safe_search: { ...(safesearch || {}) },
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Select from 'react-select';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import Tabs from '../../ui/Tabs';
|
import Tabs from '../../ui/Tabs';
|
||||||
import Examples from '../Dns/Upstream/Examples';
|
import Examples from '../Dns/Upstream/Examples';
|
||||||
import { toggleAllServices, trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
import { toggleAllServices, trimLinesAndRemoveEmpty, captitalizeWords } from '../../../helpers/helpers';
|
||||||
import {
|
import {
|
||||||
renderInputField,
|
renderInputField,
|
||||||
renderGroupField,
|
renderGroupField,
|
||||||
|
@ -40,10 +40,6 @@ const settingsCheckboxes = [
|
||||||
name: 'parental_enabled',
|
name: 'parental_enabled',
|
||||||
placeholder: 'use_adguard_parental',
|
placeholder: 'use_adguard_parental',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'safesearch_enabled',
|
|
||||||
placeholder: 'enforce_safe_search',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
const validate = (values) => {
|
const validate = (values) => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
|
@ -139,8 +135,12 @@ let Form = (props) => {
|
||||||
processingUpdating,
|
processingUpdating,
|
||||||
invalid,
|
invalid,
|
||||||
tagsOptions,
|
tagsOptions,
|
||||||
|
initialValues,
|
||||||
} = props;
|
} = props;
|
||||||
const services = useSelector((store) => store?.services);
|
const services = useSelector((store) => store?.services);
|
||||||
|
const { safe_search } = initialValues;
|
||||||
|
const safeSearchServices = { ...safe_search };
|
||||||
|
delete safeSearchServices.enabled;
|
||||||
|
|
||||||
const [activeTabLabel, setActiveTabLabel] = useState('settings');
|
const [activeTabLabel, setActiveTabLabel] = useState('settings');
|
||||||
|
|
||||||
|
@ -163,6 +163,28 @@ let Form = (props) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
name="safe_search.enabled"
|
||||||
|
type="checkbox"
|
||||||
|
component={CheckboxField}
|
||||||
|
placeholder={t('enforce_safe_search')}
|
||||||
|
disabled={useGlobalSettings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='form__group--inner'>
|
||||||
|
{Object.keys(safeSearchServices).map((searchKey) => (
|
||||||
|
<div key={searchKey}>
|
||||||
|
<Field
|
||||||
|
name={`safe_search.${searchKey}`}
|
||||||
|
type="checkbox"
|
||||||
|
component={CheckboxField}
|
||||||
|
placeholder={captitalizeWords(searchKey)}
|
||||||
|
disabled={useGlobalSettings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
},
|
},
|
||||||
block_services: {
|
block_services: {
|
||||||
|
@ -358,6 +380,7 @@ Form.propTypes = {
|
||||||
processingUpdating: PropTypes.bool.isRequired,
|
processingUpdating: PropTypes.bool.isRequired,
|
||||||
invalid: PropTypes.bool.isRequired,
|
invalid: PropTypes.bool.isRequired,
|
||||||
tagsOptions: PropTypes.array.isRequired,
|
tagsOptions: PropTypes.array.isRequired,
|
||||||
|
initialValues: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
const selector = formValueSelector(FORM_NAME.CLIENT);
|
const selector = formValueSelector(FORM_NAME.CLIENT);
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans, withTranslation } from 'react-i18next';
|
||||||
import { LEASES_TABLE_DEFAULT_PAGE_SIZE } from '../../../helpers/constants';
|
import { LEASES_TABLE_DEFAULT_PAGE_SIZE } from '../../../helpers/constants';
|
||||||
import { sortIp } from '../../../helpers/helpers';
|
import { sortIp } from '../../../helpers/helpers';
|
||||||
|
import { toggleLeaseModal } from '../../../actions';
|
||||||
|
|
||||||
class Leases extends Component {
|
class Leases extends Component {
|
||||||
cellWrap = ({ value }) => (
|
cellWrap = ({ value }) => (
|
||||||
|
@ -14,6 +16,30 @@ class Leases extends Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
convertToStatic = (data) => () => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(toggleLeaseModal(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
makeStatic = ({ row }) => {
|
||||||
|
const { t, disabledLeasesButton } = this.props;
|
||||||
|
return (
|
||||||
|
<div className="logs__row logs__row--center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-icon--green btn-outline-secondary btn-sm"
|
||||||
|
title={t('make_static')}
|
||||||
|
onClick={this.convertToStatic(row)}
|
||||||
|
disabled={disabledLeasesButton}
|
||||||
|
>
|
||||||
|
<svg className="icons icon12">
|
||||||
|
<use xlinkHref="#plus" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { leases, t } = this.props;
|
const { leases, t } = this.props;
|
||||||
return (
|
return (
|
||||||
|
@ -39,8 +65,11 @@ class Leases extends Component {
|
||||||
}, {
|
}, {
|
||||||
Header: <Trans>dhcp_table_expires</Trans>,
|
Header: <Trans>dhcp_table_expires</Trans>,
|
||||||
accessor: 'expires',
|
accessor: 'expires',
|
||||||
minWidth: 130,
|
minWidth: 220,
|
||||||
Cell: this.cellWrap,
|
Cell: this.cellWrap,
|
||||||
|
}, {
|
||||||
|
Header: <Trans>actions_table_header</Trans>,
|
||||||
|
Cell: this.makeStatic,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
pageSize={LEASES_TABLE_DEFAULT_PAGE_SIZE}
|
pageSize={LEASES_TABLE_DEFAULT_PAGE_SIZE}
|
||||||
|
@ -57,6 +86,8 @@ class Leases extends Component {
|
||||||
Leases.propTypes = {
|
Leases.propTypes = {
|
||||||
leases: PropTypes.array,
|
leases: PropTypes.array,
|
||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
disabledLeasesButton: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withTranslation()(Leases);
|
export default withTranslation()(connect(() => ({}), (dispatch) => ({ dispatch }))(Leases));
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import { Field, reduxForm } from 'redux-form';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
|
||||||
|
|
||||||
import { renderInputField, normalizeMac } from '../../../../helpers/form';
|
import { renderInputField, normalizeMac } from '../../../../helpers/form';
|
||||||
import {
|
import {
|
||||||
|
@ -25,6 +25,7 @@ const Form = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const dynamicLease = useSelector((store) => store.dhcp.leaseModalConfig, shallowEqual);
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
reset();
|
reset();
|
||||||
|
@ -87,7 +88,7 @@ const Form = ({
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-success btn-standard"
|
className="btn btn-success btn-standard"
|
||||||
disabled={submitting || pristine || processingAdding}
|
disabled={submitting || processingAdding || (pristine && !dynamicLease)}
|
||||||
>
|
>
|
||||||
<Trans>save_btn</Trans>
|
<Trans>save_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans, withTranslation } from 'react-i18next';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import { useDispatch } from 'react-redux';
|
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
import { toggleLeaseModal } from '../../../../actions';
|
import { toggleLeaseModal } from '../../../../actions';
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ const Modal = ({
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const toggleModal = () => dispatch(toggleLeaseModal());
|
const toggleModal = () => dispatch(toggleLeaseModal());
|
||||||
|
const leaseInitialData = useSelector(
|
||||||
|
(state) => state.dhcp.leaseModalConfig, shallowEqual,
|
||||||
|
) || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<ReactModal
|
||||||
|
@ -37,9 +40,9 @@ const Modal = ({
|
||||||
</div>
|
</div>
|
||||||
<Form
|
<Form
|
||||||
initialValues={{
|
initialValues={{
|
||||||
mac: '',
|
mac: leaseInitialData.mac ?? '',
|
||||||
ip: '',
|
ip: leaseInitialData.ip ?? '',
|
||||||
hostname: '',
|
hostname: leaseInitialData.hostname ?? '',
|
||||||
cidr,
|
cidr,
|
||||||
rangeStart,
|
rangeStart,
|
||||||
rangeEnd,
|
rangeEnd,
|
||||||
|
|
|
@ -188,8 +188,8 @@ const Dhcp = () => {
|
||||||
|
|
||||||
const inputtedIPv4values = dhcp?.values?.v4?.gateway_ip && dhcp?.values?.v4?.subnet_mask;
|
const inputtedIPv4values = dhcp?.values?.v4?.gateway_ip && dhcp?.values?.v4?.subnet_mask;
|
||||||
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
|
const isEmptyConfig = !Object.values(dhcp?.values?.v4 ?? {}).some(Boolean);
|
||||||
const disabledLeasesButton = dhcp?.syncErrors || interfaces?.syncErrors
|
const disabledLeasesButton = Boolean(dhcp?.syncErrors || interfaces?.syncErrors
|
||||||
|| !isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values;
|
|| !isInterfaceIncludesIpv4 || isEmptyConfig || processingConfig || !inputtedIPv4values);
|
||||||
const cidr = inputtedIPv4values ? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}` : '';
|
const cidr = inputtedIPv4values ? `${dhcp?.values?.v4?.gateway_ip}/${subnetMaskToBitMask(dhcp?.values?.v4?.subnet_mask)}` : '';
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
@ -260,7 +260,7 @@ const Dhcp = () => {
|
||||||
>
|
>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<Leases leases={leases} />
|
<Leases leases={leases} disabledLeasesButton={disabledLeasesButton}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>}
|
</Card>}
|
||||||
|
|
|
@ -13,15 +13,11 @@ import {
|
||||||
validateIpv4,
|
validateIpv4,
|
||||||
validateIpv6,
|
validateIpv6,
|
||||||
validateRequiredValue,
|
validateRequiredValue,
|
||||||
|
validateIp,
|
||||||
} from '../../../../helpers/validators';
|
} from '../../../../helpers/validators';
|
||||||
import { BLOCKING_MODES, FORM_NAME, UINT32_RANGE } from '../../../../helpers/constants';
|
import { BLOCKING_MODES, FORM_NAME, UINT32_RANGE } from '../../../../helpers/constants';
|
||||||
|
|
||||||
const checkboxes = [
|
const checkboxes = [
|
||||||
{
|
|
||||||
name: 'edns_cs_enabled',
|
|
||||||
placeholder: 'edns_enable',
|
|
||||||
subtitle: 'edns_cs_desc',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'dnssec_enabled',
|
name: 'dnssec_enabled',
|
||||||
placeholder: 'dnssec_enable',
|
placeholder: 'dnssec_enable',
|
||||||
|
@ -66,6 +62,8 @@ const Form = ({
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
blocking_mode,
|
blocking_mode,
|
||||||
|
edns_cs_enabled,
|
||||||
|
edns_cs_use_custom,
|
||||||
} = useSelector((state) => state.form[FORM_NAME.BLOCKING_MODE].values ?? {}, shallowEqual);
|
} = useSelector((state) => state.form[FORM_NAME.BLOCKING_MODE].values ?? {}, shallowEqual);
|
||||||
|
|
||||||
return <form onSubmit={handleSubmit}>
|
return <form onSubmit={handleSubmit}>
|
||||||
|
@ -92,6 +90,39 @@ const Form = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="form__group form__group--settings">
|
||||||
|
<Field
|
||||||
|
name="edns_cs_enabled"
|
||||||
|
type="checkbox"
|
||||||
|
component={CheckboxField}
|
||||||
|
placeholder={t('edns_enable')}
|
||||||
|
disabled={processing}
|
||||||
|
subtitle={t('edns_cs_desc')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 form__group form__group--inner">
|
||||||
|
<div className="form__group ">
|
||||||
|
<Field
|
||||||
|
name="edns_cs_use_custom"
|
||||||
|
type="checkbox"
|
||||||
|
component={CheckboxField}
|
||||||
|
placeholder={t('edns_use_custom_ip')}
|
||||||
|
disabled={processing || !edns_cs_enabled}
|
||||||
|
subtitle={t('edns_use_custom_ip_desc')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{edns_cs_use_custom && (<Field
|
||||||
|
name="edns_cs_custom_ip"
|
||||||
|
component={renderInputField}
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('form_enter_ip')}
|
||||||
|
validate={[validateIp, validateRequiredValue]}
|
||||||
|
/>)}
|
||||||
|
|
||||||
|
</div>
|
||||||
{checkboxes.map(({ name, placeholder, subtitle }) => <div className="col-12" key={name}>
|
{checkboxes.map(({ name, placeholder, subtitle }) => <div className="col-12" key={name}>
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<Field
|
<Field
|
||||||
|
|
|
@ -14,6 +14,8 @@ const Config = () => {
|
||||||
blocking_ipv4,
|
blocking_ipv4,
|
||||||
blocking_ipv6,
|
blocking_ipv6,
|
||||||
edns_cs_enabled,
|
edns_cs_enabled,
|
||||||
|
edns_cs_use_custom,
|
||||||
|
edns_cs_custom_ip,
|
||||||
dnssec_enabled,
|
dnssec_enabled,
|
||||||
disable_ipv6,
|
disable_ipv6,
|
||||||
processingSetConfig,
|
processingSetConfig,
|
||||||
|
@ -39,6 +41,8 @@ const Config = () => {
|
||||||
edns_cs_enabled,
|
edns_cs_enabled,
|
||||||
disable_ipv6,
|
disable_ipv6,
|
||||||
dnssec_enabled,
|
dnssec_enabled,
|
||||||
|
edns_cs_use_custom,
|
||||||
|
edns_cs_custom_ip,
|
||||||
}}
|
}}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
processing={processingSetConfig}
|
processing={processingSetConfig}
|
||||||
|
|
|
@ -4,18 +4,28 @@ import { Field, reduxForm } from 'redux-form';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans, withTranslation } from 'react-i18next';
|
||||||
import flow from 'lodash/flow';
|
import flow from 'lodash/flow';
|
||||||
|
|
||||||
import { CheckboxField, renderRadioField, toFloatNumber } from '../../../helpers/form';
|
import {
|
||||||
import { FORM_NAME, QUERY_LOG_INTERVALS_DAYS } from '../../../helpers/constants';
|
CheckboxField,
|
||||||
|
renderRadioField,
|
||||||
|
toFloatNumber,
|
||||||
|
renderTextareaField,
|
||||||
|
} from '../../../helpers/form';
|
||||||
|
import {
|
||||||
|
FORM_NAME,
|
||||||
|
QUERY_LOG_INTERVALS_DAYS,
|
||||||
|
HOUR,
|
||||||
|
DAY,
|
||||||
|
} from '../../../helpers/constants';
|
||||||
import '../FormButton.css';
|
import '../FormButton.css';
|
||||||
|
|
||||||
const getIntervalTitle = (interval, t) => {
|
const getIntervalTitle = (interval, t) => {
|
||||||
switch (interval) {
|
switch (interval) {
|
||||||
case 0.25:
|
case 6 * HOUR:
|
||||||
return t('interval_6_hour');
|
return t('interval_6_hour');
|
||||||
case 1:
|
case DAY:
|
||||||
return t('interval_24_hour');
|
return t('interval_24_hour');
|
||||||
default:
|
default:
|
||||||
return t('interval_days', { count: interval });
|
return t('interval_days', { count: interval / DAY });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -66,6 +76,22 @@ const Form = (props) => {
|
||||||
{getIntervalFields(processing, t, toFloatNumber)}
|
{getIntervalFields(processing, t, toFloatNumber)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<label className="form__label form__label--with-desc">
|
||||||
|
<Trans>ignore_domains_title</Trans>
|
||||||
|
</label>
|
||||||
|
<div className="form__desc form__desc--top">
|
||||||
|
<Trans>ignore_domains_desc_query</Trans>
|
||||||
|
</div>
|
||||||
|
<div className="form__group form__group--settings">
|
||||||
|
<Field
|
||||||
|
name="ignored"
|
||||||
|
type="textarea"
|
||||||
|
className="form-control form-control--textarea font-monospace text-input"
|
||||||
|
component={renderTextareaField}
|
||||||
|
placeholder={t('ignore_domains')}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
@ -10,13 +10,15 @@ class LogsConfig extends Component {
|
||||||
const { t, interval: prevInterval } = this.props;
|
const { t, interval: prevInterval } = this.props;
|
||||||
const { interval } = values;
|
const { interval } = values;
|
||||||
|
|
||||||
|
const data = { ...values, ignored: values.ignored ? values.ignored.split('\n') : [] };
|
||||||
|
|
||||||
if (interval !== prevInterval) {
|
if (interval !== prevInterval) {
|
||||||
// eslint-disable-next-line no-alert
|
// eslint-disable-next-line no-alert
|
||||||
if (window.confirm(t('query_log_retention_confirm'))) {
|
if (window.confirm(t('query_log_retention_confirm'))) {
|
||||||
this.props.setLogsConfig(values);
|
this.props.setLogsConfig(data);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.props.setLogsConfig(values);
|
this.props.setLogsConfig(data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,7 +32,7 @@ class LogsConfig extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
t, enabled, interval, processing, processingClear, anonymize_client_ip,
|
t, enabled, interval, processing, processingClear, anonymize_client_ip, ignored,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -45,6 +47,7 @@ class LogsConfig extends Component {
|
||||||
enabled,
|
enabled,
|
||||||
interval,
|
interval,
|
||||||
anonymize_client_ip,
|
anonymize_client_ip,
|
||||||
|
ignored: ignored.join('\n'),
|
||||||
}}
|
}}
|
||||||
onSubmit={this.handleFormSubmit}
|
onSubmit={this.handleFormSubmit}
|
||||||
processing={processing}
|
processing={processing}
|
||||||
|
@ -62,6 +65,7 @@ LogsConfig.propTypes = {
|
||||||
enabled: PropTypes.bool.isRequired,
|
enabled: PropTypes.bool.isRequired,
|
||||||
anonymize_client_ip: PropTypes.bool.isRequired,
|
anonymize_client_ip: PropTypes.bool.isRequired,
|
||||||
processing: PropTypes.bool.isRequired,
|
processing: PropTypes.bool.isRequired,
|
||||||
|
ignored: PropTypes.array.isRequired,
|
||||||
processingClear: PropTypes.bool.isRequired,
|
processingClear: PropTypes.bool.isRequired,
|
||||||
setLogsConfig: PropTypes.func.isRequired,
|
setLogsConfig: PropTypes.func.isRequired,
|
||||||
clearLogs: PropTypes.func.isRequired,
|
clearLogs: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -22,6 +22,14 @@
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form__group--inner .form__group--checkbox {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form__group--inner .form__group--checkbox:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.form__inline {
|
.form__inline {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
|
@ -4,23 +4,31 @@ import { Field, reduxForm } from 'redux-form';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans, withTranslation } from 'react-i18next';
|
||||||
import flow from 'lodash/flow';
|
import flow from 'lodash/flow';
|
||||||
|
|
||||||
import { renderRadioField, toNumber, CheckboxField } from '../../../helpers/form';
|
import {
|
||||||
import { FORM_NAME, STATS_INTERVALS_DAYS, DISABLED_STATS_INTERVAL } from '../../../helpers/constants';
|
renderRadioField,
|
||||||
|
toNumber,
|
||||||
|
CheckboxField,
|
||||||
|
renderTextareaField,
|
||||||
|
} from '../../../helpers/form';
|
||||||
|
import {
|
||||||
|
FORM_NAME,
|
||||||
|
STATS_INTERVALS_DAYS,
|
||||||
|
DAY,
|
||||||
|
} from '../../../helpers/constants';
|
||||||
import '../FormButton.css';
|
import '../FormButton.css';
|
||||||
|
|
||||||
const getIntervalTitle = (interval, t) => {
|
const getIntervalTitle = (intervalMs, t) => {
|
||||||
switch (interval) {
|
switch (intervalMs / DAY) {
|
||||||
case 1:
|
case 1:
|
||||||
return t('interval_24_hour');
|
return t('interval_24_hour');
|
||||||
default:
|
default:
|
||||||
return t('interval_days', { count: interval });
|
return t('interval_days', { count: intervalMs / DAY });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Form = (props) => {
|
const Form = (props) => {
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
change,
|
|
||||||
processing,
|
processing,
|
||||||
submitting,
|
submitting,
|
||||||
invalid,
|
invalid,
|
||||||
|
@ -38,13 +46,6 @@ const Form = (props) => {
|
||||||
component={CheckboxField}
|
component={CheckboxField}
|
||||||
placeholder={t('statistics_enable')}
|
placeholder={t('statistics_enable')}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
onChange={(event) => {
|
|
||||||
if (event.target.checked) {
|
|
||||||
change('interval', STATS_INTERVALS_DAYS[0]);
|
|
||||||
} else {
|
|
||||||
change('interval', DISABLED_STATS_INTERVAL);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="form__label form__label--with-desc">
|
<label className="form__label form__label--with-desc">
|
||||||
|
@ -65,15 +66,26 @@ const Form = (props) => {
|
||||||
placeholder={getIntervalTitle(interval, t)}
|
placeholder={getIntervalTitle(interval, t)}
|
||||||
normalize={toNumber}
|
normalize={toNumber}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
onChange={(event) => {
|
|
||||||
if (event.target.checked) {
|
|
||||||
change('enabled', true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<label className="form__label form__label--with-desc">
|
||||||
|
<Trans>ignore_domains_title</Trans>
|
||||||
|
</label>
|
||||||
|
<div className="form__desc form__desc--top">
|
||||||
|
<Trans>ignore_domains_desc_stats</Trans>
|
||||||
|
</div>
|
||||||
|
<div className="form__group form__group--settings">
|
||||||
|
<Field
|
||||||
|
name="ignored"
|
||||||
|
type="textarea"
|
||||||
|
className="form-control form-control--textarea font-monospace text-input"
|
||||||
|
component={renderTextareaField}
|
||||||
|
placeholder={t('ignore_domains')}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
@ -6,9 +6,13 @@ import Card from '../../ui/Card';
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
|
|
||||||
class StatsConfig extends Component {
|
class StatsConfig extends Component {
|
||||||
handleFormSubmit = (values) => {
|
handleFormSubmit = ({ enabled, interval, ignored }) => {
|
||||||
const { t, interval: prevInterval } = this.props;
|
const { t, interval: prevInterval } = this.props;
|
||||||
const config = { interval: values.interval };
|
const config = {
|
||||||
|
enabled,
|
||||||
|
interval,
|
||||||
|
ignored: ignored ? ignored.split('\n') : [],
|
||||||
|
};
|
||||||
|
|
||||||
if (config.interval < prevInterval) {
|
if (config.interval < prevInterval) {
|
||||||
if (window.confirm(t('statistics_retention_confirm'))) {
|
if (window.confirm(t('statistics_retention_confirm'))) {
|
||||||
|
@ -29,7 +33,7 @@ class StatsConfig extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
t, interval, processing, processingReset,
|
t, interval, processing, processingReset, ignored, enabled,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -42,7 +46,8 @@ class StatsConfig extends Component {
|
||||||
<Form
|
<Form
|
||||||
initialValues={{
|
initialValues={{
|
||||||
interval,
|
interval,
|
||||||
enabled: !!interval,
|
enabled,
|
||||||
|
ignored: ignored.join('\n'),
|
||||||
}}
|
}}
|
||||||
onSubmit={this.handleFormSubmit}
|
onSubmit={this.handleFormSubmit}
|
||||||
processing={processing}
|
processing={processing}
|
||||||
|
@ -57,6 +62,8 @@ class StatsConfig extends Component {
|
||||||
|
|
||||||
StatsConfig.propTypes = {
|
StatsConfig.propTypes = {
|
||||||
interval: PropTypes.number.isRequired,
|
interval: PropTypes.number.isRequired,
|
||||||
|
ignored: PropTypes.array.isRequired,
|
||||||
|
enabled: PropTypes.bool.isRequired,
|
||||||
processing: PropTypes.bool.isRequired,
|
processing: PropTypes.bool.isRequired,
|
||||||
processingReset: PropTypes.bool.isRequired,
|
processingReset: PropTypes.bool.isRequired,
|
||||||
setStatsConfig: PropTypes.func.isRequired,
|
setStatsConfig: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import Checkbox from '../ui/Checkbox';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import PageTitle from '../ui/PageTitle';
|
import PageTitle from '../ui/PageTitle';
|
||||||
import Card from '../ui/Card';
|
import Card from '../ui/Card';
|
||||||
import { getObjectKeysSorted } from '../../helpers/helpers';
|
import { getObjectKeysSorted, captitalizeWords } from '../../helpers/helpers';
|
||||||
import './Settings.css';
|
import './Settings.css';
|
||||||
|
|
||||||
const ORDER_KEY = 'order';
|
const ORDER_KEY = 'order';
|
||||||
|
@ -28,12 +28,6 @@ const SETTINGS = {
|
||||||
subtitle: 'use_adguard_parental_hint',
|
subtitle: 'use_adguard_parental_hint',
|
||||||
[ORDER_KEY]: 1,
|
[ORDER_KEY]: 1,
|
||||||
},
|
},
|
||||||
safesearch: {
|
|
||||||
enabled: false,
|
|
||||||
title: 'enforce_safe_search',
|
|
||||||
subtitle: 'enforce_save_search_hint',
|
|
||||||
[ORDER_KEY]: 2,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Settings extends Component {
|
class Settings extends Component {
|
||||||
|
@ -44,7 +38,7 @@ class Settings extends Component {
|
||||||
this.props.getFilteringStatus();
|
this.props.getFilteringStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSettings = (settings) => getObjectKeysSorted(settings, ORDER_KEY)
|
renderSettings = (settings) => getObjectKeysSorted(SETTINGS, ORDER_KEY)
|
||||||
.map((key) => {
|
.map((key) => {
|
||||||
const setting = settings[key];
|
const setting = settings[key];
|
||||||
const { enabled } = setting;
|
const { enabled } = setting;
|
||||||
|
@ -55,6 +49,35 @@ class Settings extends Component {
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
renderSafeSearch = () => {
|
||||||
|
const { settings: { settingsList: { safesearch } } } = this.props;
|
||||||
|
const { enabled } = safesearch || {};
|
||||||
|
const searches = { ...(safesearch || {}) };
|
||||||
|
delete searches.enabled;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Checkbox
|
||||||
|
enabled={enabled}
|
||||||
|
title='enforce_safe_search'
|
||||||
|
subtitle='enforce_save_search_hint'
|
||||||
|
handleChange={({ target: { checked: enabled } }) => this.props.toggleSetting('safesearch', { ...safesearch, enabled })}
|
||||||
|
/>
|
||||||
|
<div className='form__group--inner'>
|
||||||
|
{Object.keys(searches).map((searchKey) => (
|
||||||
|
<Checkbox
|
||||||
|
key={searchKey}
|
||||||
|
enabled={searches[searchKey]}
|
||||||
|
title={captitalizeWords(searchKey)}
|
||||||
|
subtitle=''
|
||||||
|
disabled={!safesearch.enabled}
|
||||||
|
handleChange={({ target: { checked } }) => this.props.toggleSetting('safesearch', { ...safesearch, [searchKey]: checked })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
settings,
|
settings,
|
||||||
|
@ -92,12 +115,14 @@ class Settings extends Component {
|
||||||
setFiltersConfig={setFiltersConfig}
|
setFiltersConfig={setFiltersConfig}
|
||||||
/>
|
/>
|
||||||
{this.renderSettings(settings.settingsList)}
|
{this.renderSettings(settings.settingsList)}
|
||||||
|
{this.renderSafeSearch()}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
<LogsConfig
|
<LogsConfig
|
||||||
enabled={queryLogs.enabled}
|
enabled={queryLogs.enabled}
|
||||||
|
ignored={queryLogs.ignored}
|
||||||
interval={queryLogs.interval}
|
interval={queryLogs.interval}
|
||||||
anonymize_client_ip={queryLogs.anonymize_client_ip}
|
anonymize_client_ip={queryLogs.anonymize_client_ip}
|
||||||
processing={queryLogs.processingSetConfig}
|
processing={queryLogs.processingSetConfig}
|
||||||
|
@ -109,6 +134,8 @@ class Settings extends Component {
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
<StatsConfig
|
<StatsConfig
|
||||||
interval={stats.interval}
|
interval={stats.interval}
|
||||||
|
ignored={stats.ignored}
|
||||||
|
enabled={stats.enabled}
|
||||||
processing={stats.processingSetConfig}
|
processing={stats.processingSetConfig}
|
||||||
processingReset={stats.processingReset}
|
processingReset={stats.processingReset}
|
||||||
setStatsConfig={setStatsConfig}
|
setStatsConfig={setStatsConfig}
|
||||||
|
@ -139,6 +166,8 @@ Settings.propTypes = {
|
||||||
stats: PropTypes.shape({
|
stats: PropTypes.shape({
|
||||||
processingGetConfig: PropTypes.bool,
|
processingGetConfig: PropTypes.bool,
|
||||||
interval: PropTypes.number,
|
interval: PropTypes.number,
|
||||||
|
enabled: PropTypes.bool,
|
||||||
|
ignored: PropTypes.array,
|
||||||
processingSetConfig: PropTypes.bool,
|
processingSetConfig: PropTypes.bool,
|
||||||
processingReset: PropTypes.bool,
|
processingReset: PropTypes.bool,
|
||||||
}),
|
}),
|
||||||
|
@ -149,6 +178,7 @@ Settings.propTypes = {
|
||||||
processingSetConfig: PropTypes.bool,
|
processingSetConfig: PropTypes.bool,
|
||||||
processingClear: PropTypes.bool,
|
processingClear: PropTypes.bool,
|
||||||
processingGetConfig: PropTypes.bool,
|
processingGetConfig: PropTypes.bool,
|
||||||
|
ignored: PropTypes.array,
|
||||||
}),
|
}),
|
||||||
filtering: PropTypes.shape({
|
filtering: PropTypes.shape({
|
||||||
interval: PropTypes.number,
|
interval: PropTypes.number,
|
||||||
|
|
|
@ -54,6 +54,11 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toast__dismiss:hover,
|
||||||
|
.toast__dismiss:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.toast-enter {
|
.toast-enter {
|
||||||
opacity: 0.01;
|
opacity: 0.01;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,13 +11,14 @@ class Checkbox extends Component {
|
||||||
subtitle,
|
subtitle,
|
||||||
enabled,
|
enabled,
|
||||||
handleChange,
|
handleChange,
|
||||||
|
disabled,
|
||||||
t,
|
t,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="form__group form__group--checkbox">
|
<div className="form__group form__group--checkbox">
|
||||||
<label className="checkbox checkbox--settings">
|
<label className="checkbox checkbox--settings">
|
||||||
<span className="checkbox__marker"/>
|
<span className="checkbox__marker"/>
|
||||||
<input type="checkbox" className="checkbox__input" onChange={handleChange} checked={enabled}/>
|
<input type="checkbox" className="checkbox__input" onChange={handleChange} checked={enabled} disabled={disabled}/>
|
||||||
<span className="checkbox__label">
|
<span className="checkbox__label">
|
||||||
<span className="checkbox__label-text">
|
<span className="checkbox__label-text">
|
||||||
<span className="checkbox__label-title">{ t(title) }</span>
|
<span className="checkbox__label-title">{ t(title) }</span>
|
||||||
|
@ -35,6 +36,7 @@ Checkbox.propTypes = {
|
||||||
subtitle: PropTypes.string.isRequired,
|
subtitle: PropTypes.string.isRequired,
|
||||||
enabled: PropTypes.bool.isRequired,
|
enabled: PropTypes.bool.isRequired,
|
||||||
handleChange: PropTypes.func.isRequired,
|
handleChange: PropTypes.func.isRequired,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
.dropdown-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-item.active,
|
.dropdown-item.active,
|
||||||
.dropdown-item:active {
|
.dropdown-item:active {
|
||||||
background-color: var(--btn-success-bgcolor);
|
background-color: var(--btn-success-bgcolor);
|
||||||
|
@ -6,3 +10,55 @@
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-menu.dropdown-menu--protection {
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu.dropdown-menu-arrow.dropdown-menu--protection::before,
|
||||||
|
.dropdown-menu.dropdown-menu-arrow.dropdown-menu--protection::after {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-protection {
|
||||||
|
align-self: stretch;
|
||||||
|
width: 26px;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid #868e96;
|
||||||
|
border-top-right-radius: 3px;
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
border-left: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-protection__toggle {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
background-color: #868e96;
|
||||||
|
transition: background-color 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-protection__toggle:hover {
|
||||||
|
background-color: #727b84;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-protection__toggle .nav-icon {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
color: var(--white);
|
||||||
|
transition: 0.15s ease-in-out transform;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-protection.show .nav-icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
|
@ -71,3 +71,38 @@
|
||||||
margin: 0 20px 0 0;
|
margin: 0 20px 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-secondary.footer__theme-button,
|
||||||
|
[data-theme="dark"] .btn-secondary.footer__theme-button {
|
||||||
|
height: 38px;
|
||||||
|
border-color: var(--ctrl-select-bgcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__theme-icon {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
color: var(--gray-ac);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .footer__theme-icon {
|
||||||
|
color: var(--mcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__theme-icon--active,
|
||||||
|
[data-theme="dark"] .footer__theme-icon--active {
|
||||||
|
color: var(--btn-success-bgcolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__themes {
|
||||||
|
margin: 0 auto 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.footer__themes {
|
||||||
|
margin: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import classNames from 'classnames';
|
import cn from 'classnames';
|
||||||
|
|
||||||
import { REPOSITORY, PRIVACY_POLICY_LINK, THEMES } from '../../helpers/constants';
|
import { REPOSITORY, PRIVACY_POLICY_LINK, THEMES } from '../../helpers/constants';
|
||||||
import { LANGUAGES } from '../../helpers/twosky';
|
import { LANGUAGES } from '../../helpers/twosky';
|
||||||
|
@ -33,14 +33,18 @@ const Footer = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const currentTheme = useSelector((state) => (state.dashboard ? state.dashboard.theme : 'auto'));
|
const currentTheme = useSelector((state) => (
|
||||||
const profileName = useSelector((state) => (state.dashboard ? state.dashboard.name : ''));
|
state.dashboard ? state.dashboard.theme : THEMES.auto
|
||||||
|
));
|
||||||
|
const profileName = useSelector((state) => (
|
||||||
|
state.dashboard ? state.dashboard.name : ''
|
||||||
|
));
|
||||||
const isLoggedIn = profileName !== '';
|
const isLoggedIn = profileName !== '';
|
||||||
const [currentThemeLocal, setCurrentThemeLocal] = useState('auto');
|
const [currentThemeLocal, setCurrentThemeLocal] = useState(THEMES.auto);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
setUITheme(window.matchMedia('(prefers-color-scheme: dark)').matches ? THEMES.dark : THEMES.light);
|
setUITheme(currentThemeLocal);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -55,15 +59,13 @@ const Footer = () => {
|
||||||
setHtmlLangAttr(value);
|
setHtmlLangAttr(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onThemeChanged = (event) => {
|
const onThemeChange = (value) => {
|
||||||
const { value } = event.target;
|
if (isLoggedIn) {
|
||||||
dispatch(changeTheme(value));
|
dispatch(changeTheme(value));
|
||||||
};
|
} else {
|
||||||
|
setUITheme(value);
|
||||||
const onThemeChangedLocal = (event) => {
|
setCurrentThemeLocal(value);
|
||||||
const { value } = event.target;
|
}
|
||||||
setUITheme(value);
|
|
||||||
setCurrentThemeLocal(value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCopyright = () => <div className="footer__column">
|
const renderCopyright = () => <div className="footer__column">
|
||||||
|
@ -76,41 +78,53 @@ const Footer = () => {
|
||||||
const renderLinks = (linksData) => linksData.map(({ name, href, className = '' }) => <a
|
const renderLinks = (linksData) => linksData.map(({ name, href, className = '' }) => <a
|
||||||
key={name}
|
key={name}
|
||||||
href={href}
|
href={href}
|
||||||
className={classNames('footer__link', className)}
|
className={cn('footer__link', className)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{t(name)}
|
{t(name)}
|
||||||
</a>);
|
</a>);
|
||||||
|
|
||||||
const themeSelectOptions = () => (
|
const renderThemeButtons = () => {
|
||||||
Object.values(THEMES)
|
const currentValue = isLoggedIn ? currentTheme : currentThemeLocal;
|
||||||
.map((theme) => (
|
|
||||||
<option key={theme} value={theme}>
|
|
||||||
{t(`theme_${theme}`)}
|
|
||||||
</option>
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderThemeSelect = () => (
|
const content = {
|
||||||
<select
|
auto: {
|
||||||
className="form-control select select--theme"
|
desc: t('theme_auto_desc'),
|
||||||
value={currentTheme}
|
icon: '#auto',
|
||||||
onChange={onThemeChanged}
|
},
|
||||||
>
|
dark: {
|
||||||
{themeSelectOptions()}
|
desc: t('theme_dark_desc'),
|
||||||
</select>
|
icon: '#dark',
|
||||||
);
|
},
|
||||||
|
light: {
|
||||||
|
desc: t('theme_light_desc'),
|
||||||
|
icon: '#light',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const renderThemeSelectLocal = () => (
|
return (
|
||||||
<select
|
Object.values(THEMES)
|
||||||
className="form-control select select--theme"
|
.map((theme) => (
|
||||||
value={currentThemeLocal}
|
<button
|
||||||
onChange={onThemeChangedLocal}
|
key={theme}
|
||||||
>
|
type="button"
|
||||||
{themeSelectOptions()}
|
className="btn btn-sm btn-secondary footer__theme-button"
|
||||||
</select>
|
onClick={() => onThemeChange(theme)}
|
||||||
);
|
title={content[theme].desc}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={cn(
|
||||||
|
'footer__theme-icon',
|
||||||
|
{ 'footer__theme-icon--active': currentValue === theme },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<use xlinkHref={content[theme].icon} />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -121,7 +135,11 @@ const Footer = () => {
|
||||||
{renderLinks(linksData)}
|
{renderLinks(linksData)}
|
||||||
</div>
|
</div>
|
||||||
<div className="footer__column footer__column--theme">
|
<div className="footer__column footer__column--theme">
|
||||||
{isLoggedIn ? renderThemeSelect() : renderThemeSelectLocal()}
|
<div className="footer__themes">
|
||||||
|
<div className="btn-group">
|
||||||
|
{renderThemeButtons()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer__column footer__column--language">
|
<div className="footer__column footer__column--language">
|
||||||
<select
|
<select
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { useSelector } from 'react-redux';
|
||||||
import { MOBILE_CONFIG_LINKS } from '../../../helpers/constants';
|
import { MOBILE_CONFIG_LINKS } from '../../../helpers/constants';
|
||||||
|
|
||||||
import Tabs from '../Tabs';
|
import Tabs from '../Tabs';
|
||||||
import Icons from '../Icons';
|
|
||||||
import MobileConfigForm from './MobileConfigForm';
|
import MobileConfigForm from './MobileConfigForm';
|
||||||
|
|
||||||
const renderLi = ({ label, components }) => <li key={label}>
|
const renderLi = ({ label, components }) => <li key={label}>
|
||||||
|
@ -341,7 +340,6 @@ const Guide = ({ dnsAddresses }) => {
|
||||||
>
|
>
|
||||||
{activeTab}
|
{activeTab}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Icons />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -181,6 +181,12 @@ const Icons = () => (
|
||||||
</svg>
|
</svg>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="arrow-down" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fillRule="evenodd" d="M6.2 8.2a.64.64 0 0 1 .94 0L12 13.32l4.86-5.1a.64.64 0 0 1 .94 0c.27.27.27.71 0 .98l-5.33 5.6a.64.64 0 0 1-.94 0L6.2 9.2a.72.72 0 0 1 0-.98Z" clipRule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
<symbol id="arrow-right" viewBox="0 0 24 24" stroke="currentColor"
|
<symbol id="arrow-right" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5">
|
strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5">
|
||||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
@ -198,11 +204,24 @@ const Icons = () => (
|
||||||
</svg>
|
</svg>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="auto" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z" stroke="currentColor" strokeWidth="1.5" />
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M12 3V21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3Z" fill="currentColor" stroke="currentColor" strokeWidth="1.5" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="dark" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M3.80737 15.731L3.9895 15.0034C3.71002 14.9335 3.41517 15.0298 3.23088 15.2512C3.0466 15.4727 3.00545 15.7801 3.12501 16.0422L3.80737 15.731ZM14.1926 3.26892L14.3747 2.54137C14.0953 2.47141 13.8004 2.56772 13.6161 2.78917C13.4318 3.01062 13.3907 3.31806 13.5102 3.58018L14.1926 3.26892ZM12 20.2499C8.66479 20.2499 5.79026 18.2708 4.48974 15.4197L3.12501 16.0422C4.66034 19.4081 8.05588 21.7499 12 21.7499V20.2499ZM20.25 11.9999C20.25 16.5563 16.5563 20.2499 12 20.2499V21.7499C17.3848 21.7499 21.75 17.3847 21.75 11.9999H20.25ZM14.0105 3.99647C17.5955 4.89391 20.25 8.13787 20.25 11.9999H21.75C21.75 7.43347 18.6114 3.60193 14.3747 2.54137L14.0105 3.99647ZM13.5102 3.58018C13.9851 4.6211 14.25 5.77857 14.25 6.99995H15.75C15.75 5.5595 15.4371 4.1901 14.875 2.95766L13.5102 3.58018ZM14.25 6.99995C14.25 11.5563 10.5563 15.2499 5.99999 15.2499V16.7499C11.3848 16.7499 15.75 12.3847 15.75 6.99995H14.25ZM5.99999 15.2499C5.30559 15.2499 4.63225 15.1643 3.9895 15.0034L3.62525 16.4585C4.38616 16.649 5.18181 16.7499 5.99999 16.7499V15.2499Z" fill="currentColor" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="light" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M12 3.75C16.5563 3.75 20.25 7.44365 20.25 12H21.75C21.75 6.61522 17.3848 2.25 12 2.25V3.75ZM20.25 12C20.25 16.5563 16.5563 20.25 12 20.25V21.75C17.3848 21.75 21.75 17.3848 21.75 12H20.25ZM12 20.25C7.44365 20.25 3.75 16.5563 3.75 12H2.25C2.25 17.3848 6.61522 21.75 12 21.75V20.25ZM3.75 12C3.75 7.44365 7.44365 3.75 12 3.75V2.25C6.61522 2.25 2.25 6.61522 2.25 12H3.75Z" fill="currentColor" />
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M12 10C10.8954 10 10 10.8954 10 12C10 13.1046 10.8954 14 12 14C13.1046 14 14 13.1046 14 12C13.9987 10.896 13.104 10.0013 12 10Z" fill="currentColor" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
<symbol id="chevron-down" width="24" height="24" viewBox="0 0 24 24">
|
<symbol id="chevron-down" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<g fill="none" fillRule="evenodd">
|
<g fill="none" fillRule="evenodd">
|
||||||
<path d="M0 0h24v24H0z" fill="#878787" fillOpacity=".01" />
|
<path d="M0 0h24v24H0z" fill="#878787" fillOpacity=".01" />
|
||||||
<path stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"
|
<path stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" d="M8.036 10.93l3.93 4.07 4.068-3.93" />
|
||||||
d="M8.036 10.93l3.93 4.07 4.068-3.93" />
|
|
||||||
</g>
|
</g>
|
||||||
</symbol>
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -9,11 +9,12 @@ import round from 'lodash/round';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './Line.css';
|
import './Line.css';
|
||||||
|
import { msToDays } from '../../helpers/helpers';
|
||||||
|
|
||||||
const Line = ({
|
const Line = ({
|
||||||
data, color = 'black',
|
data, color = 'black',
|
||||||
}) => {
|
}) => {
|
||||||
const interval = useSelector((state) => state.stats.interval);
|
const interval = msToDays(useSelector((state) => state.stats.interval));
|
||||||
|
|
||||||
return <ResponsiveLine
|
return <ResponsiveLine
|
||||||
enableArea
|
enableArea
|
||||||
|
|
|
@ -10216,6 +10216,18 @@ body.fixed-header .page {
|
||||||
line-height: 2.5rem;
|
line-height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-title__protection {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.page-title__protection {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.page-title-icon {
|
.page-title-icon {
|
||||||
color: #9aa0ac;
|
color: #9aa0ac;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
|
|
|
@ -211,9 +211,14 @@ export const FILTERED = 'Filtered';
|
||||||
export const NOT_FILTERED = 'NotFiltered';
|
export const NOT_FILTERED = 'NotFiltered';
|
||||||
|
|
||||||
export const DISABLED_STATS_INTERVAL = 0;
|
export const DISABLED_STATS_INTERVAL = 0;
|
||||||
export const STATS_INTERVALS_DAYS = [1, 7, 30, 90];
|
|
||||||
|
|
||||||
export const QUERY_LOG_INTERVALS_DAYS = [0.25, 1, 7, 30, 90];
|
export const HOUR = 60 * 60 * 1000;
|
||||||
|
|
||||||
|
export const DAY = HOUR * 24;
|
||||||
|
|
||||||
|
export const STATS_INTERVALS_DAYS = [DAY, DAY * 7, DAY * 30, DAY * 90];
|
||||||
|
|
||||||
|
export const QUERY_LOG_INTERVALS_DAYS = [HOUR * 6, DAY, DAY * 7, DAY * 30, DAY * 90];
|
||||||
|
|
||||||
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];
|
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];
|
||||||
|
|
||||||
|
@ -493,6 +498,8 @@ export const TOAST_TYPES = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SUCCESS_TOAST_TIMEOUT = 5000;
|
export const SUCCESS_TOAST_TIMEOUT = 5000;
|
||||||
|
|
||||||
|
export const ONE_SECOND_IN_MS = 1000;
|
||||||
export const FAILURE_TOAST_TIMEOUT = 30000;
|
export const FAILURE_TOAST_TIMEOUT = 30000;
|
||||||
|
|
||||||
export const TOAST_TIMEOUTS = {
|
export const TOAST_TIMEOUTS = {
|
||||||
|
@ -521,3 +528,12 @@ export const MOBILE_CONFIG_LINKS = {
|
||||||
DOT: 'apple/dot.mobileconfig',
|
DOT: 'apple/dot.mobileconfig',
|
||||||
DOH: 'apple/doh.mobileconfig',
|
DOH: 'apple/doh.mobileconfig',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Timings for disable protection in milliseconds
|
||||||
|
export const DISABLE_PROTECTION_TIMINGS = {
|
||||||
|
HALF_MINUTE: 30 * 1000,
|
||||||
|
MINUTE: 60 * 1000,
|
||||||
|
TEN_MINUTES: 10 * 60 * 1000,
|
||||||
|
HOUR: 60 * 60 * 1000,
|
||||||
|
TOMORROW: 24 * 60 * 60 * 1000,
|
||||||
|
};
|
||||||
|
|
|
@ -185,13 +185,13 @@ export default {
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_32.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_32.txt"
|
||||||
},
|
},
|
||||||
"oisd_basic": {
|
"oisd_basic": {
|
||||||
"name": "OISD Blocklist Basic",
|
"name": "OISD Blocklist Small",
|
||||||
"categoryId": "general",
|
"categoryId": "general",
|
||||||
"homepage": "https://oisd.nl/",
|
"homepage": "https://oisd.nl/",
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_5.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_5.txt"
|
||||||
},
|
},
|
||||||
"oisd_full": {
|
"oisd_full": {
|
||||||
"name": "OISD Blocklist Full",
|
"name": "OISD Blocklist Big",
|
||||||
"categoryId": "general",
|
"categoryId": "general",
|
||||||
"homepage": "https://oisd.nl/",
|
"homepage": "https://oisd.nl/",
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_27.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_27.txt"
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
STANDARD_HTTPS_PORT,
|
STANDARD_HTTPS_PORT,
|
||||||
STANDARD_WEB_PORT,
|
STANDARD_WEB_PORT,
|
||||||
SPECIAL_FILTER_ID,
|
SPECIAL_FILTER_ID,
|
||||||
|
THEMES,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -388,6 +389,12 @@ export const toggleAllServices = (services, change, isSelected) => {
|
||||||
services.forEach((service) => change(`blocked_services.${service.id}`, isSelected));
|
services.forEach((service) => change(`blocked_services.${service.id}`, isSelected));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const msToSeconds = (milliseconds) => Math.floor(milliseconds / 1000);
|
||||||
|
|
||||||
|
export const msToMinutes = (milliseconds) => Math.floor(milliseconds / 1000 / 60);
|
||||||
|
|
||||||
|
export const msToHours = (milliseconds) => Math.floor(milliseconds / 1000 / 60 / 60);
|
||||||
|
|
||||||
export const secondsToMilliseconds = (seconds) => {
|
export const secondsToMilliseconds = (seconds) => {
|
||||||
if (seconds) {
|
if (seconds) {
|
||||||
return seconds * 1000;
|
return seconds * 1000;
|
||||||
|
@ -396,6 +403,8 @@ export const secondsToMilliseconds = (seconds) => {
|
||||||
return seconds;
|
return seconds;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const msToDays = (milliseconds) => Math.floor(milliseconds / 1000 / 60 / 60 / 24);
|
||||||
|
|
||||||
export const normalizeRulesTextarea = (text) => text?.replace(/^\n/g, '')
|
export const normalizeRulesTextarea = (text) => text?.replace(/^\n/g, '')
|
||||||
.replace(/\n\s*\n/g, '\n');
|
.replace(/\n\s*\n/g, '\n');
|
||||||
|
|
||||||
|
@ -676,7 +685,14 @@ export const setHtmlLangAttr = (language) => {
|
||||||
* @param theme
|
* @param theme
|
||||||
*/
|
*/
|
||||||
export const setUITheme = (theme) => {
|
export const setUITheme = (theme) => {
|
||||||
document.body.dataset.theme = theme;
|
let currentTheme = theme;
|
||||||
|
|
||||||
|
if (currentTheme === THEMES.auto) {
|
||||||
|
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
currentTheme = prefersDark ? THEMES.dark : THEMES.light;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.dataset.theme = currentTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"timeUpdated": "2023-03-08T00:09:48.692Z",
|
"timeUpdated": "2023-04-06T10:46:09.881Z",
|
||||||
"categories": {
|
"categories": {
|
||||||
"0": "audio_video_player",
|
"0": "audio_video_player",
|
||||||
"1": "comments",
|
"1": "comments",
|
||||||
|
@ -19225,6 +19225,13 @@
|
||||||
"url": "http://www.zypmedia.com/",
|
"url": "http://www.zypmedia.com/",
|
||||||
"companyId": "zypmedia"
|
"companyId": "zypmedia"
|
||||||
},
|
},
|
||||||
|
"abc": {
|
||||||
|
"name": "Australian Broadcasting Corporation",
|
||||||
|
"categoryId": 8,
|
||||||
|
"url": "https://www.abc.net.au/",
|
||||||
|
"companyId": "abc",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
"adguard_dns": {
|
"adguard_dns": {
|
||||||
"name": "AdGuard DNS",
|
"name": "AdGuard DNS",
|
||||||
"categoryId": 8,
|
"categoryId": 8,
|
||||||
|
@ -19316,6 +19323,13 @@
|
||||||
"companyId": null,
|
"companyId": null,
|
||||||
"source": "AdGuard"
|
"source": "AdGuard"
|
||||||
},
|
},
|
||||||
|
"domainglass": {
|
||||||
|
"name": "Domain Glass",
|
||||||
|
"categoryId": 8,
|
||||||
|
"url": "https://domain.glass/",
|
||||||
|
"companyId": "domainglass",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
"edgio": {
|
"edgio": {
|
||||||
"name": "Edgio",
|
"name": "Edgio",
|
||||||
"categoryId": 9,
|
"categoryId": 9,
|
||||||
|
@ -19344,6 +19358,13 @@
|
||||||
"companyId": "google",
|
"companyId": "google",
|
||||||
"source": "AdGuard"
|
"source": "AdGuard"
|
||||||
},
|
},
|
||||||
|
"freeview": {
|
||||||
|
"name": "Freeview",
|
||||||
|
"categoryId": 0,
|
||||||
|
"url": "https://freeview.com.au/",
|
||||||
|
"companyId": "freeview",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
"gmail": {
|
"gmail": {
|
||||||
"name": "Gmail",
|
"name": "Gmail",
|
||||||
"categoryId": 13,
|
"categoryId": 13,
|
||||||
|
@ -19358,6 +19379,13 @@
|
||||||
"companyId": "google",
|
"companyId": "google",
|
||||||
"source": "AdGuard"
|
"source": "AdGuard"
|
||||||
},
|
},
|
||||||
|
"iqiyi": {
|
||||||
|
"name": "iQiyi",
|
||||||
|
"categoryId": 0,
|
||||||
|
"url": "https://www.iqiyi.com/",
|
||||||
|
"companyId": "iqiyi",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
"hockeyapp": {
|
"hockeyapp": {
|
||||||
"name": "HockeyApp",
|
"name": "HockeyApp",
|
||||||
"categoryId": 101,
|
"categoryId": 101,
|
||||||
|
@ -19365,6 +19393,13 @@
|
||||||
"companyId": null,
|
"companyId": null,
|
||||||
"source": "AdGuard"
|
"source": "AdGuard"
|
||||||
},
|
},
|
||||||
|
"karambasecurity": {
|
||||||
|
"name": "Karamba Security",
|
||||||
|
"categoryId": 8,
|
||||||
|
"url": "https://karambasecurity.com/",
|
||||||
|
"companyId": "karambasecurity",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
"kik": {
|
"kik": {
|
||||||
"name": "Kik",
|
"name": "Kik",
|
||||||
"categoryId": 7,
|
"categoryId": 7,
|
||||||
|
@ -19379,6 +19414,13 @@
|
||||||
"companyId": "lets_encrypt",
|
"companyId": "lets_encrypt",
|
||||||
"source": "AdGuard"
|
"source": "AdGuard"
|
||||||
},
|
},
|
||||||
|
"lgads": {
|
||||||
|
"name": "LG Ad Solutions",
|
||||||
|
"categoryId": 4,
|
||||||
|
"url": "https://lgads.tv/",
|
||||||
|
"companyId": "lgcorp",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
"lgtv": {
|
"lgtv": {
|
||||||
"name": "LG TV",
|
"name": "LG TV",
|
||||||
"categoryId": 8,
|
"categoryId": 8,
|
||||||
|
@ -19428,6 +19470,13 @@
|
||||||
"companyId": "nab",
|
"companyId": "nab",
|
||||||
"source": "AdGuard"
|
"source": "AdGuard"
|
||||||
},
|
},
|
||||||
|
"netify": {
|
||||||
|
"name": "Netify",
|
||||||
|
"categoryId": 8,
|
||||||
|
"url": "https://www.netify.ai/",
|
||||||
|
"companyId": "netify",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
"notion": {
|
"notion": {
|
||||||
"name": "Notion",
|
"name": "Notion",
|
||||||
"categoryId": 8,
|
"categoryId": 8,
|
||||||
|
@ -19519,6 +19568,20 @@
|
||||||
"companyId": "solaredge",
|
"companyId": "solaredge",
|
||||||
"source": "AdGuard"
|
"source": "AdGuard"
|
||||||
},
|
},
|
||||||
|
"sonos": {
|
||||||
|
"name": "Sonos",
|
||||||
|
"categoryId": 8,
|
||||||
|
"url": "https://www.sonos.com/",
|
||||||
|
"companyId": "sonos",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
|
"switchtv": {
|
||||||
|
"name": "Switch Media",
|
||||||
|
"categoryId": 8,
|
||||||
|
"url": "https://www.switch.tv/",
|
||||||
|
"companyId": "switchtv",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
"telstra": {
|
"telstra": {
|
||||||
"name": "Telstra",
|
"name": "Telstra",
|
||||||
"categoryId": 8,
|
"categoryId": 8,
|
||||||
|
@ -19581,6 +19644,13 @@
|
||||||
"url": "https://appmetrica.yandex.com/",
|
"url": "https://appmetrica.yandex.com/",
|
||||||
"companyId": "yandex",
|
"companyId": "yandex",
|
||||||
"source": "AdGuard"
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
|
"3gpp": {
|
||||||
|
"name": "3GPP Network",
|
||||||
|
"categoryId": 5,
|
||||||
|
"url": "https://www.3gpp.org/",
|
||||||
|
"companyId": "3gpp",
|
||||||
|
"source": "AdGuard"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"trackerDomains": {
|
"trackerDomains": {
|
||||||
|
@ -23911,6 +23981,16 @@
|
||||||
"zwaar.net": "zwaar",
|
"zwaar.net": "zwaar",
|
||||||
"zwaar.org": "zwaar",
|
"zwaar.org": "zwaar",
|
||||||
"extend.tv": "zypmedia",
|
"extend.tv": "zypmedia",
|
||||||
|
"abc.net.au": "abc",
|
||||||
|
"ab.co": "abc",
|
||||||
|
"abc-cdn.net.au": "abc",
|
||||||
|
"abc-host.net": "abc",
|
||||||
|
"abc-host.net.au": "abc",
|
||||||
|
"abc-prod.net.au": "abc",
|
||||||
|
"abc-stage.net.au": "abc",
|
||||||
|
"abc-test.net.au": "abc",
|
||||||
|
"abcaustralia.net.au": "abc",
|
||||||
|
"abcradio.net.au": "abc",
|
||||||
"adtidy.org": "adguard",
|
"adtidy.org": "adguard",
|
||||||
"agrd.io": "adguard",
|
"agrd.io": "adguard",
|
||||||
"adguard.app": "adguard",
|
"adguard.app": "adguard",
|
||||||
|
@ -23985,6 +24065,7 @@
|
||||||
"cloudflare-dns.com": "cloudflare",
|
"cloudflare-dns.com": "cloudflare",
|
||||||
"crashlytics.com": "crashlytics",
|
"crashlytics.com": "crashlytics",
|
||||||
"phicdn.net": "digicert_trust_seal",
|
"phicdn.net": "digicert_trust_seal",
|
||||||
|
"domain.glass": "domainglass",
|
||||||
"alphacdn.net": "edgio",
|
"alphacdn.net": "edgio",
|
||||||
"edg.io": "edgio",
|
"edg.io": "edgio",
|
||||||
"edgecast.com": "edgio",
|
"edgecast.com": "edgio",
|
||||||
|
@ -23996,6 +24077,9 @@
|
||||||
"app-measurement.com": "firebase",
|
"app-measurement.com": "firebase",
|
||||||
"flipboard.com": "flipboard",
|
"flipboard.com": "flipboard",
|
||||||
"flurry.com": "flurry",
|
"flurry.com": "flurry",
|
||||||
|
"freeviewaustralia.tv": "freeview",
|
||||||
|
"freeview.com.au": "freeview",
|
||||||
|
"freeview.com": "freeview",
|
||||||
"ghcr.io": "github",
|
"ghcr.io": "github",
|
||||||
"github.dev": "github",
|
"github.dev": "github",
|
||||||
"gmail.com": "gmail",
|
"gmail.com": "gmail",
|
||||||
|
@ -24005,12 +24089,18 @@
|
||||||
"gvt3.com": "google_servers",
|
"gvt3.com": "google_servers",
|
||||||
"pki.goog": "google_trust_services",
|
"pki.goog": "google_trust_services",
|
||||||
"hockeyapp.net": "hockeyapp",
|
"hockeyapp.net": "hockeyapp",
|
||||||
|
"qy.net": "iqiyi",
|
||||||
|
"iqiyi.com": "iqiyi",
|
||||||
|
"iq.com": "iqiyi",
|
||||||
|
"karambasecurity.com": "karambasecurity",
|
||||||
"kik.com": "kik",
|
"kik.com": "kik",
|
||||||
"apikik.com": "kik",
|
"apikik.com": "kik",
|
||||||
"kik-live.com": "kik",
|
"kik-live.com": "kik",
|
||||||
"slatic.net": "lazada",
|
"slatic.net": "lazada",
|
||||||
"lencr.org": "lets_encrypt",
|
"lencr.org": "lets_encrypt",
|
||||||
"letsencrypt.org": "lets_encrypt",
|
"letsencrypt.org": "lets_encrypt",
|
||||||
|
"alphonso.tv": "lgads",
|
||||||
|
"lgads.tv": "lgads",
|
||||||
"lgsmartad.com": "lgtv",
|
"lgsmartad.com": "lgtv",
|
||||||
"lgtvcommon.com": "lgtv",
|
"lgtvcommon.com": "lgtv",
|
||||||
"lgtvsdp.com": "lgtv",
|
"lgtvsdp.com": "lgtv",
|
||||||
|
@ -24032,6 +24122,7 @@
|
||||||
"mozilla.net": "mozilla",
|
"mozilla.net": "mozilla",
|
||||||
"mozilla.org": "mozilla",
|
"mozilla.org": "mozilla",
|
||||||
"nflximg.com": "netflix",
|
"nflximg.com": "netflix",
|
||||||
|
"netify.ai": "netify",
|
||||||
"nab.com": "nab",
|
"nab.com": "nab",
|
||||||
"nab.com.au": "nab",
|
"nab.com.au": "nab",
|
||||||
"nab.net": "nab",
|
"nab.net": "nab",
|
||||||
|
@ -24074,7 +24165,9 @@
|
||||||
"snap-dev.net": "snap",
|
"snap-dev.net": "snap",
|
||||||
"snapads.com": "snap",
|
"snapads.com": "snap",
|
||||||
"snapkit.com": "snap",
|
"snapkit.com": "snap",
|
||||||
|
"switch.tv": "switchtv",
|
||||||
"solaredge.com": "solaredge",
|
"solaredge.com": "solaredge",
|
||||||
|
"sonos.com": "sonos",
|
||||||
"telstra.com.au": "telstra",
|
"telstra.com.au": "telstra",
|
||||||
"telstra.com": "telstra",
|
"telstra.com": "telstra",
|
||||||
"usertrust.com": "trustlogo",
|
"usertrust.com": "trustlogo",
|
||||||
|
@ -24095,6 +24188,8 @@
|
||||||
"yandex.com.tr": "yandex",
|
"yandex.com.tr": "yandex",
|
||||||
"yandex.fr": "yandex",
|
"yandex.fr": "yandex",
|
||||||
"yandex.kz": "yandex",
|
"yandex.kz": "yandex",
|
||||||
"appmetrica.yandex.com": "yandex_appmetrica"
|
"appmetrica.yandex.com": "yandex_appmetrica",
|
||||||
|
"3gppnetwork.org": "3gpp",
|
||||||
|
"3gpp.org": "3gpp"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -22,6 +22,7 @@ import Progress from './Progress';
|
||||||
|
|
||||||
import Toasts from '../../components/Toasts';
|
import Toasts from '../../components/Toasts';
|
||||||
import Footer from '../../components/ui/Footer';
|
import Footer from '../../components/ui/Footer';
|
||||||
|
import Icons from '../../components/ui/Icons';
|
||||||
import logo from '../../components/ui/svg/logo.svg';
|
import logo from '../../components/ui/svg/logo.svg';
|
||||||
|
|
||||||
import './Setup.css';
|
import './Setup.css';
|
||||||
|
@ -123,6 +124,7 @@ class Setup extends Component {
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
<Toasts />
|
<Toasts />
|
||||||
|
<Icons />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import * as actionCreators from '../../actions/login';
|
||||||
import logo from '../../components/ui/svg/logo.svg';
|
import logo from '../../components/ui/svg/logo.svg';
|
||||||
import Toasts from '../../components/Toasts';
|
import Toasts from '../../components/Toasts';
|
||||||
import Footer from '../../components/ui/Footer';
|
import Footer from '../../components/ui/Footer';
|
||||||
|
import Icons from '../../components/ui/Icons';
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
|
|
||||||
import './Login.css';
|
import './Login.css';
|
||||||
|
@ -69,6 +70,7 @@ class Login extends Component {
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
<Toasts />
|
<Toasts />
|
||||||
|
<Icons />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ const dashboard = handleActions(
|
||||||
dns_port: dnsPort,
|
dns_port: dnsPort,
|
||||||
dns_addresses: dnsAddresses,
|
dns_addresses: dnsAddresses,
|
||||||
protection_enabled: protectionEnabled,
|
protection_enabled: protectionEnabled,
|
||||||
|
protection_disabled_duration: protectionDisabledDuration,
|
||||||
http_port: httpPort,
|
http_port: httpPort,
|
||||||
language,
|
language,
|
||||||
} = payload;
|
} = payload;
|
||||||
|
@ -36,9 +37,11 @@ const dashboard = handleActions(
|
||||||
dnsPort,
|
dnsPort,
|
||||||
dnsAddresses,
|
dnsAddresses,
|
||||||
protectionEnabled,
|
protectionEnabled,
|
||||||
|
protectionDisabledDuration,
|
||||||
language,
|
language,
|
||||||
httpPort,
|
httpPort,
|
||||||
};
|
};
|
||||||
|
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -103,15 +106,22 @@ const dashboard = handleActions(
|
||||||
...state,
|
...state,
|
||||||
processingProtection: false,
|
processingProtection: false,
|
||||||
}),
|
}),
|
||||||
[actions.toggleProtectionSuccess]: (state) => {
|
[actions.toggleProtectionSuccess]: (state, { payload }) => {
|
||||||
const newState = {
|
const newState = {
|
||||||
...state,
|
...state,
|
||||||
protectionEnabled: !state.protectionEnabled,
|
protectionEnabled: !state.protectionEnabled,
|
||||||
processingProtection: false,
|
processingProtection: false,
|
||||||
|
protectionDisabledDuration: payload.disabledDuration,
|
||||||
};
|
};
|
||||||
|
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[actions.setDisableDurationTime]: (state, { payload }) => ({
|
||||||
|
...state,
|
||||||
|
protectionDisabledDuration: payload.timeToEnableProtection,
|
||||||
|
}),
|
||||||
|
|
||||||
[actions.getClientsRequest]: (state) => ({
|
[actions.getClientsRequest]: (state) => ({
|
||||||
...state,
|
...state,
|
||||||
processingClients: true,
|
processingClients: true,
|
||||||
|
@ -156,6 +166,8 @@ const dashboard = handleActions(
|
||||||
processingUpdate: false,
|
processingUpdate: false,
|
||||||
processingProfile: true,
|
processingProfile: true,
|
||||||
protectionEnabled: false,
|
protectionEnabled: false,
|
||||||
|
protectionDisabledDuration: null,
|
||||||
|
protectionCountdownActive: false,
|
||||||
processingProtection: false,
|
processingProtection: false,
|
||||||
httpPort: STANDARD_WEB_PORT,
|
httpPort: STANDARD_WEB_PORT,
|
||||||
dnsPort: STANDARD_DNS_PORT,
|
dnsPort: STANDARD_DNS_PORT,
|
||||||
|
|
|
@ -124,10 +124,11 @@ const dhcp = handleActions(
|
||||||
staticLeases: [],
|
staticLeases: [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[actions.toggleLeaseModal]: (state) => {
|
[actions.toggleLeaseModal]: (state, { payload }) => {
|
||||||
const newState = {
|
const newState = {
|
||||||
...state,
|
...state,
|
||||||
isModalOpen: !state.isModalOpen,
|
isModalOpen: !state.isModalOpen,
|
||||||
|
leaseModalConfig: payload,
|
||||||
};
|
};
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
@ -200,6 +201,7 @@ const dhcp = handleActions(
|
||||||
leases: [],
|
leases: [],
|
||||||
staticLeases: [],
|
staticLeases: [],
|
||||||
isModalOpen: false,
|
isModalOpen: false,
|
||||||
|
leaseModalConfig: undefined,
|
||||||
dhcp_available: false,
|
dhcp_available: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { handleActions } from 'redux-actions';
|
import { handleActions } from 'redux-actions';
|
||||||
|
|
||||||
import * as actions from '../actions/queryLogs';
|
import * as actions from '../actions/queryLogs';
|
||||||
import { DEFAULT_LOGS_FILTER } from '../helpers/constants';
|
import { DEFAULT_LOGS_FILTER, DAY } from '../helpers/constants';
|
||||||
|
|
||||||
const queryLogs = handleActions(
|
const queryLogs = handleActions(
|
||||||
{
|
{
|
||||||
|
@ -86,7 +86,7 @@ const queryLogs = handleActions(
|
||||||
processingGetConfig: false,
|
processingGetConfig: false,
|
||||||
processingSetConfig: false,
|
processingSetConfig: false,
|
||||||
processingAdditionalLogs: false,
|
processingAdditionalLogs: false,
|
||||||
interval: 1,
|
interval: DAY,
|
||||||
logs: [],
|
logs: [],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
oldest: '',
|
oldest: '',
|
||||||
|
|
|
@ -22,11 +22,11 @@ const settings = handleActions(
|
||||||
},
|
},
|
||||||
[actions.toggleSettingStatus]: (state, { payload }) => {
|
[actions.toggleSettingStatus]: (state, { payload }) => {
|
||||||
const { settingsList } = state;
|
const { settingsList } = state;
|
||||||
const { settingKey } = payload;
|
const { settingKey, value } = payload;
|
||||||
|
|
||||||
const setting = settingsList[settingKey];
|
const setting = settingsList[settingKey];
|
||||||
|
|
||||||
const newSetting = {
|
const newSetting = value || {
|
||||||
...setting,
|
...setting,
|
||||||
enabled: !setting.enabled,
|
enabled: !setting.enabled,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { handleActions } from 'redux-actions';
|
import { handleActions } from 'redux-actions';
|
||||||
import { normalizeTopClients } from '../helpers/helpers';
|
import { normalizeTopClients } from '../helpers/helpers';
|
||||||
|
import { DAY } from '../helpers/constants';
|
||||||
|
|
||||||
import * as actions from '../actions/stats';
|
import * as actions from '../actions/stats';
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ const stats = handleActions(
|
||||||
[actions.getStatsConfigFailure]: (state) => ({ ...state, processingGetConfig: false }),
|
[actions.getStatsConfigFailure]: (state) => ({ ...state, processingGetConfig: false }),
|
||||||
[actions.getStatsConfigSuccess]: (state, { payload }) => ({
|
[actions.getStatsConfigSuccess]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
interval: payload.interval,
|
...payload,
|
||||||
processingGetConfig: false,
|
processingGetConfig: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ const stats = handleActions(
|
||||||
[actions.setStatsConfigFailure]: (state) => ({ ...state, processingSetConfig: false }),
|
[actions.setStatsConfigFailure]: (state) => ({ ...state, processingSetConfig: false }),
|
||||||
[actions.setStatsConfigSuccess]: (state, { payload }) => ({
|
[actions.setStatsConfigSuccess]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
interval: payload.interval,
|
...payload,
|
||||||
processingSetConfig: false,
|
processingSetConfig: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -91,7 +92,7 @@ const stats = handleActions(
|
||||||
processingSetConfig: false,
|
processingSetConfig: false,
|
||||||
processingStats: true,
|
processingStats: true,
|
||||||
processingReset: false,
|
processingReset: false,
|
||||||
interval: 1,
|
interval: DAY,
|
||||||
...defaultStats,
|
...defaultStats,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -25,6 +25,8 @@ RUN apk --no-cache add ca-certificates libcap tzdata && \
|
||||||
mkdir -p /opt/adguardhome/conf /opt/adguardhome/work && \
|
mkdir -p /opt/adguardhome/conf /opt/adguardhome/work && \
|
||||||
chown -R nobody: /opt/adguardhome
|
chown -R nobody: /opt/adguardhome
|
||||||
|
|
||||||
|
RUN apk --no-cache add tini
|
||||||
|
|
||||||
ARG DIST_DIR
|
ARG DIST_DIR
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
|
@ -52,14 +54,32 @@ RUN setcap 'cap_net_bind_service=+eip' /opt/adguardhome/AdGuardHome
|
||||||
# TODO(a.garipov): Remove the old, non-standard 784 and 8853 ports for
|
# TODO(a.garipov): Remove the old, non-standard 784 and 8853 ports for
|
||||||
# DNS-over-QUIC in a future release.
|
# DNS-over-QUIC in a future release.
|
||||||
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 443/udp 784/udp\
|
EXPOSE 53/tcp 53/udp 67/udp 68/udp 80/tcp 443/tcp 443/udp 784/udp\
|
||||||
853/tcp 853/udp 3000/tcp 3000/udp 3001/tcp 3001/udp 5443/tcp\
|
853/tcp 853/udp 3000/tcp 3000/udp 5443/tcp\
|
||||||
5443/udp 6060/tcp 8853/udp
|
5443/udp 6060/tcp 8853/udp
|
||||||
|
|
||||||
WORKDIR /opt/adguardhome/work
|
WORKDIR /opt/adguardhome/work
|
||||||
|
|
||||||
ENTRYPOINT ["/opt/adguardhome/AdGuardHome"]
|
# Install helpers for healthcheck.
|
||||||
|
COPY --chown=nobody:nogroup\
|
||||||
|
./${DIST_DIR}/docker/scripts\
|
||||||
|
/opt/adguardhome/scripts
|
||||||
|
|
||||||
|
HEALTHCHECK \
|
||||||
|
--interval=30s \
|
||||||
|
--timeout=10s \
|
||||||
|
--retries=3 \
|
||||||
|
CMD [ "/opt/adguardhome/scripts/healthcheck.sh" ]
|
||||||
|
|
||||||
|
# It seems that the healthckech script sometimes spawns zombie processes, so we
|
||||||
|
# need a way to handle them, since AdGuard Home doesn't know how to keep track
|
||||||
|
# of the processes delegated to it by the OS. Use tini as entry point because
|
||||||
|
# it needs the PID=1 to be the default parent for orphaned processes.
|
||||||
|
#
|
||||||
|
# See https://github.com/adguardTeam/adGuardHome/issues/3290.
|
||||||
|
ENTRYPOINT [ "/sbin/tini", "--" ]
|
||||||
|
|
||||||
CMD [ \
|
CMD [ \
|
||||||
|
"/opt/adguardhome/AdGuardHome", \
|
||||||
"--no-check-update", \
|
"--no-check-update", \
|
||||||
"-c", "/opt/adguardhome/conf/AdGuardHome.yaml", \
|
"-c", "/opt/adguardhome/conf/AdGuardHome.yaml", \
|
||||||
"-h", "0.0.0.0", \
|
"-h", "0.0.0.0", \
|
|
@ -0,0 +1,22 @@
|
||||||
|
/^[^[:space:]]/ { is_dns = /^dns:/ }
|
||||||
|
|
||||||
|
/^[[:space:]]+bind_hosts:/ { if (is_dns) prev_line = FNR }
|
||||||
|
|
||||||
|
/^[[:space:]]+- .+/ {
|
||||||
|
if (FNR - prev_line == 1) {
|
||||||
|
addrs[addrsnum++] = $2
|
||||||
|
prev_line = FNR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/^[[:space:]]+port:/ { if (is_dns) port = $2 }
|
||||||
|
|
||||||
|
END {
|
||||||
|
for (i in addrs) {
|
||||||
|
if (match(addrs[i], ":")) {
|
||||||
|
print "[" addrs[i] "]:" port
|
||||||
|
} else {
|
||||||
|
print addrs[i] ":" port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# AdGuard Home Docker healthcheck script
|
||||||
|
|
||||||
|
# Exit the script if a pipeline fails (-e), prevent accidental filename
|
||||||
|
# expansion (-f), and consider undefined variables as errors (-u).
|
||||||
|
set -e -f -u
|
||||||
|
|
||||||
|
# Function error_exit is an echo wrapper that writes to stderr and stops the
|
||||||
|
# script execution with code 1.
|
||||||
|
error_exit() {
|
||||||
|
echo "$1" 1>&2
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
agh_dir="/opt/adguardhome"
|
||||||
|
readonly agh_dir
|
||||||
|
|
||||||
|
filename="${agh_dir}/conf/AdGuardHome.yaml"
|
||||||
|
readonly filename
|
||||||
|
|
||||||
|
if ! [ -f "$filename" ]
|
||||||
|
then
|
||||||
|
wget "http://127.0.0.1:3000" -O /dev/null -q || exit 1
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
help_dir="${agh_dir}/scripts"
|
||||||
|
readonly help_dir
|
||||||
|
|
||||||
|
# Parse web host
|
||||||
|
|
||||||
|
web_url="$( awk -f "${help_dir}/web-bind.awk" "$filename" )"
|
||||||
|
readonly web_url
|
||||||
|
|
||||||
|
if [ "$web_url" = '' ]
|
||||||
|
then
|
||||||
|
error_exit "no web bindings could be retrieved from $filename"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TODO(e.burkov): Deal with 0 port.
|
||||||
|
case "$web_url"
|
||||||
|
in
|
||||||
|
(*':0')
|
||||||
|
error_exit '0 in web port is not supported by healthcheck'
|
||||||
|
;;
|
||||||
|
(*)
|
||||||
|
# Go on.
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Parse DNS hosts
|
||||||
|
|
||||||
|
dns_hosts="$( awk -f "${help_dir}/dns-bind.awk" "$filename" )"
|
||||||
|
readonly dns_hosts
|
||||||
|
|
||||||
|
if [ "$dns_hosts" = '' ]
|
||||||
|
then
|
||||||
|
error_exit "no DNS bindings could be retrieved from $filename"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TODO(e.burkov): Deal with 0 port.
|
||||||
|
case "$( echo "$dns_hosts" | head -n 1 )"
|
||||||
|
in
|
||||||
|
(*':0')
|
||||||
|
error_exit '0 in DNS port is not supported by healthcheck'
|
||||||
|
;;
|
||||||
|
(*)
|
||||||
|
# Go on.
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check
|
||||||
|
|
||||||
|
# Skip SSL certificate validation since there is no guarantee the container
|
||||||
|
# trusts the one used. It should be safe to drop the SSL validation since the
|
||||||
|
# current script intended to be used from inside the container and only checks
|
||||||
|
# the endpoint availability, ignoring the content of the response.
|
||||||
|
#
|
||||||
|
# See https://github.com/AdguardTeam/AdGuardHome/issues/5642.
|
||||||
|
wget --no-check-certificate "$web_url" -O /dev/null -q || exit 1
|
||||||
|
|
||||||
|
echo "$dns_hosts" | while read -r host
|
||||||
|
do
|
||||||
|
nslookup -type=a healthcheck.adguardhome.test. "$host" > /dev/null ||\
|
||||||
|
error_exit "nslookup failed for $host"
|
||||||
|
done
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Don't consider the HTTPS hostname since the enforced HTTPS redirection should
|
||||||
|
# work if the SSL check skipped. See file docker/healthcheck.sh.
|
||||||
|
/^bind_host:/ { host = $2 }
|
||||||
|
|
||||||
|
/^bind_port:/ { port = $2 }
|
||||||
|
|
||||||
|
END {
|
||||||
|
if (match(host, ":")) {
|
||||||
|
print "http://[" host "]:" port
|
||||||
|
} else {
|
||||||
|
print "http://" host ":" port
|
||||||
|
}
|
||||||
|
}
|
33
go.mod
33
go.mod
|
@ -4,11 +4,12 @@ go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AdguardTeam/dnsproxy v0.48.3
|
github.com/AdguardTeam/dnsproxy v0.48.3
|
||||||
github.com/AdguardTeam/golibs v0.13.1
|
github.com/AdguardTeam/golibs v0.13.2
|
||||||
github.com/AdguardTeam/urlfilter v0.16.1
|
github.com/AdguardTeam/urlfilter v0.16.1
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
github.com/ameshkov/dnscrypt/v2 v2.2.6
|
github.com/ameshkov/dnscrypt/v2 v2.2.6
|
||||||
github.com/digineo/go-ipset/v2 v2.2.1
|
github.com/digineo/go-ipset/v2 v2.2.1
|
||||||
|
github.com/dimfeld/httptreemux/v5 v5.5.0
|
||||||
github.com/fsnotify/fsnotify v1.6.0
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
github.com/go-ping/ping v1.1.0
|
github.com/go-ping/ping v1.1.0
|
||||||
github.com/google/go-cmp v0.5.9
|
github.com/google/go-cmp v0.5.9
|
||||||
|
@ -16,21 +17,20 @@ require (
|
||||||
github.com/google/renameio v1.0.1
|
github.com/google/renameio v1.0.1
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8
|
github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8
|
||||||
|
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
|
||||||
github.com/kardianos/service v1.2.2
|
github.com/kardianos/service v1.2.2
|
||||||
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
|
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
|
||||||
github.com/mdlayher/netlink v1.7.1
|
github.com/mdlayher/netlink v1.7.1
|
||||||
// TODO(a.garipov): This package is deprecated; find a new one or use
|
github.com/mdlayher/packet v1.1.1
|
||||||
// our own code for that. Perhaps, use gopacket.
|
github.com/miekg/dns v1.1.53
|
||||||
github.com/mdlayher/raw v0.1.0
|
|
||||||
github.com/miekg/dns v1.1.50
|
|
||||||
github.com/quic-go/quic-go v0.33.0
|
github.com/quic-go/quic-go v0.33.0
|
||||||
github.com/stretchr/testify v1.8.2
|
github.com/stretchr/testify v1.8.2
|
||||||
github.com/ti-mo/netfilter v0.5.0
|
github.com/ti-mo/netfilter v0.5.0
|
||||||
go.etcd.io/bbolt v1.3.7
|
go.etcd.io/bbolt v1.3.7
|
||||||
golang.org/x/crypto v0.6.0
|
golang.org/x/crypto v0.7.0
|
||||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
|
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
|
||||||
golang.org/x/net v0.8.0
|
golang.org/x/net v0.8.0
|
||||||
golang.org/x/sys v0.6.0
|
golang.org/x/sys v0.7.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
|
@ -43,23 +43,22 @@ require (
|
||||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
|
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
|
||||||
github.com/bluele/gcache v0.0.2 // indirect
|
github.com/bluele/gcache v0.0.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/mdlayher/raw v0.1.0 // indirect
|
||||||
github.com/mdlayher/packet v1.1.1 // indirect
|
|
||||||
github.com/mdlayher/socket v0.4.0 // indirect
|
github.com/mdlayher/socket v0.4.0 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.8.3 // indirect
|
github.com/onsi/ginkgo/v2 v2.9.2 // indirect
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/quic-go/qpack v0.4.0 // indirect
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
github.com/quic-go/qtls-go1-19 v0.2.1 // indirect
|
github.com/quic-go/qtls-go1-19 v0.3.0 // indirect
|
||||||
github.com/quic-go/qtls-go1-20 v0.1.1 // indirect
|
github.com/quic-go/qtls-go1-20 v0.2.0 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
|
||||||
golang.org/x/mod v0.8.0 // indirect
|
golang.org/x/mod v0.9.0 // indirect
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.org/x/text v0.8.0 // indirect
|
golang.org/x/text v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.6.0 // indirect
|
golang.org/x/tools v0.7.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
62
go.sum
62
go.sum
|
@ -2,8 +2,8 @@ github.com/AdguardTeam/dnsproxy v0.48.3 h1:h9xgDSmd1MqsPFNApyaPVXolmSTtzOWOcfWvP
|
||||||
github.com/AdguardTeam/dnsproxy v0.48.3/go.mod h1:Y7g7jRTd/u7+KJ/QvnGI2PCE8vnisp6EsW47/Sz0DZw=
|
github.com/AdguardTeam/dnsproxy v0.48.3/go.mod h1:Y7g7jRTd/u7+KJ/QvnGI2PCE8vnisp6EsW47/Sz0DZw=
|
||||||
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||||
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
||||||
github.com/AdguardTeam/golibs v0.13.1 h1:x6ChoXk2jborbCWJ01TyBAEY3SilHts0SCG7yjnf6Sc=
|
github.com/AdguardTeam/golibs v0.13.2 h1:BPASsyQKmb+b8VnvsNOHp7bKfcZl9Z+Z2UhPjOiupSc=
|
||||||
github.com/AdguardTeam/golibs v0.13.1/go.mod h1:7ylQLv2Lqsc3UW3jHoITynYk6Y1tYtgEMkR09ppfsN8=
|
github.com/AdguardTeam/golibs v0.13.2/go.mod h1:7ylQLv2Lqsc3UW3jHoITynYk6Y1tYtgEMkR09ppfsN8=
|
||||||
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
|
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
|
||||||
github.com/AdguardTeam/urlfilter v0.16.1 h1:ZPi0rjqo8cQf2FVdzo6cqumNoHZx2KPXj2yZa1A5BBw=
|
github.com/AdguardTeam/urlfilter v0.16.1 h1:ZPi0rjqo8cQf2FVdzo6cqumNoHZx2KPXj2yZa1A5BBw=
|
||||||
github.com/AdguardTeam/urlfilter v0.16.1/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI=
|
github.com/AdguardTeam/urlfilter v0.16.1/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI=
|
||||||
|
@ -29,6 +29,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1MX77g=
|
github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1MX77g=
|
||||||
github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU=
|
github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU=
|
||||||
|
github.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2PccwOFQ=
|
||||||
|
github.com/dimfeld/httptreemux/v5 v5.5.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw=
|
||||||
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
|
github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
|
@ -37,11 +39,11 @@ github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
|
||||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
|
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
|
||||||
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
@ -53,8 +55,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
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/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
|
||||||
github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU=
|
github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU=
|
||||||
github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
|
github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
|
||||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
@ -68,8 +70,8 @@ github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe0
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||||
|
@ -102,13 +104,13 @@ github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL
|
||||||
github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
|
github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
|
||||||
github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
|
github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
|
||||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
|
||||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/onsi/ginkgo/v2 v2.8.3 h1:RpbK1G8nWPNaCVFBWsOGnEQQGgASi6b8fxcWBvDYjxQ=
|
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||||
github.com/onsi/ginkgo/v2 v2.8.3/go.mod h1:6OaUA8BCi0aZfmzYT/q9AacwTzDpNbxILUT+TlBq6MY=
|
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
|
||||||
github.com/onsi/gomega v1.27.0 h1:QLidEla4bXUuZVFa4KX6JHCsuGgbi85LC/pCHrt/O08=
|
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
@ -121,10 +123,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||||
github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
|
github.com/quic-go/qtls-go1-19 v0.3.0 h1:aUBoQdpHzUWtPw5tQZbsD2GnrWCNu7/RIX1PtqGeLYY=
|
||||||
github.com/quic-go/qtls-go1-19 v0.2.1/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
github.com/quic-go/qtls-go1-19 v0.3.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
||||||
github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk=
|
github.com/quic-go/qtls-go1-20 v0.2.0 h1:jUHn+obJ6WI5JudqBO0Iy1ra5Vh5vsitQ1gXQvkmN+E=
|
||||||
github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
github.com/quic-go/qtls-go1-20 v0.2.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
||||||
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
|
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
|
||||||
github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA=
|
github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA=
|
||||||
github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA=
|
github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA=
|
||||||
|
@ -151,23 +153,23 @@ github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ
|
||||||
github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ=
|
github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ=
|
||||||
github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
|
github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
|
||||||
github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E=
|
github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E=
|
||||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
|
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg=
|
||||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||||
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
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.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
@ -182,7 +184,6 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
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-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
@ -211,15 +212,15 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
@ -233,9 +234,8 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/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/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ const textPlainDeprMsg = `using this api with the text/plain content-type is dep
|
||||||
// deprecation and removal of a plain-text API if the request is made with the
|
// deprecation and removal of a plain-text API if the request is made with the
|
||||||
// "text/plain" content-type.
|
// "text/plain" content-type.
|
||||||
func WriteTextPlainDeprecated(w http.ResponseWriter, r *http.Request) (isPlainText bool) {
|
func WriteTextPlainDeprecated(w http.ResponseWriter, r *http.Request) (isPlainText bool) {
|
||||||
if r.Header.Get(HdrNameContentType) != HdrValTextPlain {
|
if r.Header.Get(httphdr.ContentType) != HdrValTextPlain {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ func WriteJSONResponse(w http.ResponseWriter, r *http.Request, resp any) (err er
|
||||||
// redefine the status code.
|
// redefine the status code.
|
||||||
func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) {
|
func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) {
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
w.Header().Set(HdrNameContentType, HdrValApplicationJSON)
|
w.Header().Set(httphdr.ContentType, HdrValApplicationJSON)
|
||||||
err = json.NewEncoder(w).Encode(resp)
|
err = json.NewEncoder(w).Encode(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err)
|
Error(r, w, http.StatusInternalServerError, "encoding resp: %s", err)
|
||||||
|
|
|
@ -1,22 +1,6 @@
|
||||||
package aghhttp
|
package aghhttp
|
||||||
|
|
||||||
// HTTP Headers
|
// HTTP headers
|
||||||
|
|
||||||
// HTTP header name constants.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Remove unused.
|
|
||||||
const (
|
|
||||||
HdrNameAcceptEncoding = "Accept-Encoding"
|
|
||||||
HdrNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
|
|
||||||
HdrNameAltSvc = "Alt-Svc"
|
|
||||||
HdrNameContentEncoding = "Content-Encoding"
|
|
||||||
HdrNameContentType = "Content-Type"
|
|
||||||
HdrNameOrigin = "Origin"
|
|
||||||
HdrNameServer = "Server"
|
|
||||||
HdrNameTrailer = "Trailer"
|
|
||||||
HdrNameUserAgent = "User-Agent"
|
|
||||||
HdrNameVary = "Vary"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTP header value constants.
|
// HTTP header value constants.
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -1,47 +1,14 @@
|
||||||
package aghnet
|
package aghnet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"fmt"
|
||||||
"strconv"
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The maximum lengths of generated hostnames for different IP versions.
|
|
||||||
const (
|
|
||||||
ipv4HostnameMaxLen = len("192-168-100-100")
|
|
||||||
ipv6HostnameMaxLen = len("ff80-f076-0000-0000-0000-0000-0000-0010")
|
|
||||||
)
|
|
||||||
|
|
||||||
// generateIPv4Hostname generates the hostname by IP address version 4.
|
|
||||||
func generateIPv4Hostname(ipv4 net.IP) (hostname string) {
|
|
||||||
hnData := make([]byte, 0, ipv4HostnameMaxLen)
|
|
||||||
for i, part := range ipv4 {
|
|
||||||
if i > 0 {
|
|
||||||
hnData = append(hnData, '-')
|
|
||||||
}
|
|
||||||
hnData = strconv.AppendUint(hnData, uint64(part), 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(hnData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateIPv6Hostname generates the hostname by IP address version 6.
|
|
||||||
func generateIPv6Hostname(ipv6 net.IP) (hostname string) {
|
|
||||||
hnData := make([]byte, 0, ipv6HostnameMaxLen)
|
|
||||||
for i, partsNum := 0, net.IPv6len/2; i < partsNum; i++ {
|
|
||||||
if i > 0 {
|
|
||||||
hnData = append(hnData, '-')
|
|
||||||
}
|
|
||||||
for _, val := range ipv6[i*2 : i*2+2] {
|
|
||||||
if val < 10 {
|
|
||||||
hnData = append(hnData, '0')
|
|
||||||
}
|
|
||||||
hnData = strconv.AppendUint(hnData, uint64(val), 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(hnData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateHostname generates the hostname from ip. In case of using IPv4 the
|
// GenerateHostname generates the hostname from ip. In case of using IPv4 the
|
||||||
// result should be like:
|
// result should be like:
|
||||||
//
|
//
|
||||||
|
@ -52,10 +19,42 @@ func generateIPv6Hostname(ipv6 net.IP) (hostname string) {
|
||||||
// ff80-f076-0000-0000-0000-0000-0000-0010
|
// ff80-f076-0000-0000-0000-0000-0000-0010
|
||||||
//
|
//
|
||||||
// ip must be either an IPv4 or an IPv6.
|
// ip must be either an IPv4 or an IPv6.
|
||||||
func GenerateHostname(ip net.IP) (hostname string) {
|
func GenerateHostname(ip netip.Addr) (hostname string) {
|
||||||
if ipv4 := ip.To4(); ipv4 != nil {
|
if !ip.IsValid() {
|
||||||
return generateIPv4Hostname(ipv4)
|
// TODO(s.chzhen): Get rid of it.
|
||||||
|
panic("aghnet generate hostname: invalid ip")
|
||||||
}
|
}
|
||||||
|
|
||||||
return generateIPv6Hostname(ip)
|
ip = ip.Unmap()
|
||||||
|
hostname = ip.StringExpanded()
|
||||||
|
|
||||||
|
if ip.Is4() {
|
||||||
|
return strings.Replace(hostname, ".", "-", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Replace(hostname, ":", "-", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDomainNameSet returns nil and error, if list has duplicate or empty
|
||||||
|
// domain name. Otherwise returns a set, which contains non-FQDN domain names,
|
||||||
|
// and nil error.
|
||||||
|
func NewDomainNameSet(list []string) (set *stringutil.Set, err error) {
|
||||||
|
set = stringutil.NewSet()
|
||||||
|
|
||||||
|
for i, v := range list {
|
||||||
|
host := strings.ToLower(strings.TrimSuffix(v, "."))
|
||||||
|
// TODO(a.garipov): Think about ignoring empty (".") names in the
|
||||||
|
// future.
|
||||||
|
if host == "" {
|
||||||
|
return nil, errors.Error("host name is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if set.Has(host) {
|
||||||
|
return nil, fmt.Errorf("duplicate host name %q at index %d", host, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
set.Add(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
return set, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package aghnet
|
package aghnet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -12,19 +12,19 @@ func TestGenerateHostName(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
want string
|
want string
|
||||||
ip net.IP
|
ip netip.Addr
|
||||||
}{{
|
}{{
|
||||||
name: "good_ipv4",
|
name: "good_ipv4",
|
||||||
want: "127-0-0-1",
|
want: "127-0-0-1",
|
||||||
ip: net.IP{127, 0, 0, 1},
|
ip: netip.MustParseAddr("127.0.0.1"),
|
||||||
}, {
|
}, {
|
||||||
name: "good_ipv6",
|
name: "good_ipv6",
|
||||||
want: "fe00-0000-0000-0000-0000-0000-0000-0001",
|
want: "fe00-0000-0000-0000-0000-0000-0000-0001",
|
||||||
ip: net.ParseIP("fe00::1"),
|
ip: netip.MustParseAddr("fe00::1"),
|
||||||
}, {
|
}, {
|
||||||
name: "4to6",
|
name: "4to6",
|
||||||
want: "1-2-3-4",
|
want: "1-2-3-4",
|
||||||
ip: net.ParseIP("::ffff:1.2.3.4"),
|
ip: netip.MustParseAddr("::ffff:1.2.3.4"),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
@ -36,29 +36,6 @@ func TestGenerateHostName(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid", func(t *testing.T) {
|
t.Run("invalid", func(t *testing.T) {
|
||||||
testCases := []struct {
|
assert.Panics(t, func() { GenerateHostname(netip.Addr{}) })
|
||||||
name string
|
|
||||||
ip net.IP
|
|
||||||
}{{
|
|
||||||
name: "bad_ipv4",
|
|
||||||
ip: net.IP{127, 0, 0, 1, 0},
|
|
||||||
}, {
|
|
||||||
name: "bad_ipv6",
|
|
||||||
ip: net.IP{
|
|
||||||
0xff, 0xff, 0xff, 0xff,
|
|
||||||
0xff, 0xff, 0xff, 0xff,
|
|
||||||
0xff, 0xff, 0xff, 0xff,
|
|
||||||
0xff, 0xff, 0xff,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
name: "nil",
|
|
||||||
ip: nil,
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
assert.Panics(t, func() { GenerateHostname(tc.ip) })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ func (rm *requestMatcher) MatchRequest(
|
||||||
) (res *urlfilter.DNSResult, ok bool) {
|
) (res *urlfilter.DNSResult, ok bool) {
|
||||||
switch req.DNSType {
|
switch req.DNSType {
|
||||||
case dns.TypeA, dns.TypeAAAA, dns.TypePTR:
|
case dns.TypeA, dns.TypeAAAA, dns.TypePTR:
|
||||||
log.Debug("%s: handling the request for %s", hostsContainerPref, req.Hostname)
|
log.Debug("%s: handling the request for %s", hostsContainerPrefix, req.Hostname)
|
||||||
default:
|
default:
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
@ -90,9 +90,9 @@ func (rm *requestMatcher) resetEng(rulesStrg *filterlist.RuleStorage, tr map[str
|
||||||
rm.translator = tr
|
rm.translator = tr
|
||||||
}
|
}
|
||||||
|
|
||||||
// hostsContainerPref is a prefix for logging and wrapping errors in
|
// hostsContainerPrefix is a prefix for logging and wrapping errors in
|
||||||
// HostsContainer's methods.
|
// HostsContainer's methods.
|
||||||
const hostsContainerPref = "hosts container"
|
const hostsContainerPrefix = "hosts container"
|
||||||
|
|
||||||
// HostsContainer stores the relevant hosts database provided by the OS and
|
// HostsContainer stores the relevant hosts database provided by the OS and
|
||||||
// processes both A/AAAA and PTR DNS requests for those.
|
// processes both A/AAAA and PTR DNS requests for those.
|
||||||
|
@ -115,8 +115,8 @@ type HostsContainer struct {
|
||||||
// fsys is the working file system to read hosts files from.
|
// fsys is the working file system to read hosts files from.
|
||||||
fsys fs.FS
|
fsys fs.FS
|
||||||
|
|
||||||
// w tracks the changes in specified files and directories.
|
// watcher tracks the changes in specified files and directories.
|
||||||
w aghos.FSWatcher
|
watcher aghos.FSWatcher
|
||||||
|
|
||||||
// patterns stores specified paths in the fs.Glob-compatible form.
|
// patterns stores specified paths in the fs.Glob-compatible form.
|
||||||
patterns []string
|
patterns []string
|
||||||
|
@ -160,7 +160,7 @@ func NewHostsContainer(
|
||||||
w aghos.FSWatcher,
|
w aghos.FSWatcher,
|
||||||
paths ...string,
|
paths ...string,
|
||||||
) (hc *HostsContainer, err error) {
|
) (hc *HostsContainer, err error) {
|
||||||
defer func() { err = errors.Annotate(err, "%s: %w", hostsContainerPref) }()
|
defer func() { err = errors.Annotate(err, "%s: %w", hostsContainerPrefix) }()
|
||||||
|
|
||||||
if len(paths) == 0 {
|
if len(paths) == 0 {
|
||||||
return nil, ErrNoHostsPaths
|
return nil, ErrNoHostsPaths
|
||||||
|
@ -182,11 +182,11 @@ func NewHostsContainer(
|
||||||
done: make(chan struct{}, 1),
|
done: make(chan struct{}, 1),
|
||||||
updates: make(chan HostsRecords, 1),
|
updates: make(chan HostsRecords, 1),
|
||||||
fsys: fsys,
|
fsys: fsys,
|
||||||
w: w,
|
watcher: w,
|
||||||
patterns: patterns,
|
patterns: patterns,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("%s: starting", hostsContainerPref)
|
log.Debug("%s: starting", hostsContainerPrefix)
|
||||||
|
|
||||||
// Load initially.
|
// Load initially.
|
||||||
if err = hc.refresh(); err != nil {
|
if err = hc.refresh(); err != nil {
|
||||||
|
@ -199,7 +199,7 @@ func NewHostsContainer(
|
||||||
return nil, fmt.Errorf("adding path: %w", err)
|
return nil, fmt.Errorf("adding path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("%s: %s is expected to exist but doesn't", hostsContainerPref, p)
|
log.Debug("%s: %s is expected to exist but doesn't", hostsContainerPrefix, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,14 +208,21 @@ func NewHostsContainer(
|
||||||
return hc, nil
|
return hc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close implements the io.Closer interface for *HostsContainer. Close must
|
// Close implements the [io.Closer] interface for *HostsContainer. It closes
|
||||||
// only be called once. The returned err is always nil.
|
// both itself and its [aghos.FSWatcher]. Close must only be called once.
|
||||||
func (hc *HostsContainer) Close() (err error) {
|
func (hc *HostsContainer) Close() (err error) {
|
||||||
log.Debug("%s: closing", hostsContainerPref)
|
log.Debug("%s: closing", hostsContainerPrefix)
|
||||||
|
|
||||||
|
err = hc.watcher.Close()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("closing fs watcher: %w", err)
|
||||||
|
|
||||||
|
// Go on and close the container either way.
|
||||||
|
}
|
||||||
|
|
||||||
close(hc.done)
|
close(hc.done)
|
||||||
|
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upd returns the channel into which the updates are sent.
|
// Upd returns the channel into which the updates are sent.
|
||||||
|
@ -251,22 +258,22 @@ func pathsToPatterns(fsys fs.FS, paths []string) (patterns []string, err error)
|
||||||
// update channel of HostsContainer when finishes. It's used to be called
|
// update channel of HostsContainer when finishes. It's used to be called
|
||||||
// within a separate goroutine.
|
// within a separate goroutine.
|
||||||
func (hc *HostsContainer) handleEvents() {
|
func (hc *HostsContainer) handleEvents() {
|
||||||
defer log.OnPanic(fmt.Sprintf("%s: handling events", hostsContainerPref))
|
defer log.OnPanic(fmt.Sprintf("%s: handling events", hostsContainerPrefix))
|
||||||
|
|
||||||
defer close(hc.updates)
|
defer close(hc.updates)
|
||||||
|
|
||||||
ok, eventsCh := true, hc.w.Events()
|
ok, eventsCh := true, hc.watcher.Events()
|
||||||
for ok {
|
for ok {
|
||||||
select {
|
select {
|
||||||
case _, ok = <-eventsCh:
|
case _, ok = <-eventsCh:
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug("%s: watcher closed the events channel", hostsContainerPref)
|
log.Debug("%s: watcher closed the events channel", hostsContainerPrefix)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := hc.refresh(); err != nil {
|
if err := hc.refresh(); err != nil {
|
||||||
log.Error("%s: %s", hostsContainerPref, err)
|
log.Error("%s: %s", hostsContainerPrefix, err)
|
||||||
}
|
}
|
||||||
case _, ok = <-hc.done:
|
case _, ok = <-hc.done:
|
||||||
// Go on.
|
// Go on.
|
||||||
|
@ -345,7 +352,7 @@ func (hp *hostsParser) parseLine(line string) (ip netip.Addr, hosts []string) {
|
||||||
// TODO(e.burkov): Investigate if hosts may contain DNS-SD domains.
|
// TODO(e.burkov): Investigate if hosts may contain DNS-SD domains.
|
||||||
err = netutil.ValidateHostname(f)
|
err = netutil.ValidateHostname(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("%s: host %q is invalid, ignoring", hostsContainerPref, f)
|
log.Error("%s: host %q is invalid, ignoring", hostsContainerPrefix, f)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -389,7 +396,7 @@ func (hp *hostsParser) addRules(ip netip.Addr, host, line string) {
|
||||||
rule, rulePtr := hp.writeRules(host, ip)
|
rule, rulePtr := hp.writeRules(host, ip)
|
||||||
hp.translations[rule], hp.translations[rulePtr] = line, line
|
hp.translations[rule], hp.translations[rulePtr] = line, line
|
||||||
|
|
||||||
log.Debug("%s: added ip-host pair %q-%q", hostsContainerPref, ip, host)
|
log.Debug("%s: added ip-host pair %q-%q", hostsContainerPrefix, ip, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeRules writes the actual rule for the qtype and the PTR for the host-ip
|
// writeRules writes the actual rule for the qtype and the PTR for the host-ip
|
||||||
|
@ -443,7 +450,7 @@ func (hp *hostsParser) writeRules(host string, ip netip.Addr) (rule, rulePtr str
|
||||||
|
|
||||||
// sendUpd tries to send the parsed data to the ch.
|
// sendUpd tries to send the parsed data to the ch.
|
||||||
func (hp *hostsParser) sendUpd(ch chan HostsRecords) {
|
func (hp *hostsParser) sendUpd(ch chan HostsRecords) {
|
||||||
log.Debug("%s: sending upd", hostsContainerPref)
|
log.Debug("%s: sending upd", hostsContainerPrefix)
|
||||||
|
|
||||||
upd := hp.table
|
upd := hp.table
|
||||||
select {
|
select {
|
||||||
|
@ -451,11 +458,11 @@ func (hp *hostsParser) sendUpd(ch chan HostsRecords) {
|
||||||
// Updates are delivered. Go on.
|
// Updates are delivered. Go on.
|
||||||
case <-ch:
|
case <-ch:
|
||||||
ch <- upd
|
ch <- upd
|
||||||
log.Debug("%s: replaced the last update", hostsContainerPref)
|
log.Debug("%s: replaced the last update", hostsContainerPrefix)
|
||||||
case ch <- upd:
|
case ch <- upd:
|
||||||
// The previous update was just read and the next one pushed. Go on.
|
// The previous update was just read and the next one pushed. Go on.
|
||||||
default:
|
default:
|
||||||
log.Error("%s: the updates channel is broken", hostsContainerPref)
|
log.Error("%s: the updates channel is broken", hostsContainerPrefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -473,7 +480,7 @@ func (hp *hostsParser) newStrg(id int) (s *filterlist.RuleStorage, err error) {
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Accept a parameter to specify the files to refresh.
|
// TODO(e.burkov): Accept a parameter to specify the files to refresh.
|
||||||
func (hc *HostsContainer) refresh() (err error) {
|
func (hc *HostsContainer) refresh() (err error) {
|
||||||
log.Debug("%s: refreshing", hostsContainerPref)
|
log.Debug("%s: refreshing", hostsContainerPrefix)
|
||||||
|
|
||||||
hp := hc.newHostsParser()
|
hp := hc.newHostsParser()
|
||||||
if _, err = aghos.FileWalker(hp.parseFile).Walk(hc.fsys, hc.patterns...); err != nil {
|
if _, err = aghos.FileWalker(hp.parseFile).Walk(hc.fsys, hc.patterns...); err != nil {
|
||||||
|
@ -482,7 +489,7 @@ func (hc *HostsContainer) refresh() (err error) {
|
||||||
|
|
||||||
// hc.last is nil on the first refresh, so let that one through.
|
// hc.last is nil on the first refresh, so let that one through.
|
||||||
if hc.last != nil && maps.EqualFunc(hp.table, hc.last, (*HostsRecord).equal) {
|
if hc.last != nil && maps.EqualFunc(hp.table, hc.last, (*HostsRecord).equal) {
|
||||||
log.Debug("%s: no changes detected", hostsContainerPref)
|
log.Debug("%s: no changes detected", hostsContainerPrefix)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
//go:build mips || mips64
|
|
||||||
|
|
||||||
// This file is an adapted version of github.com/josharian/native.
|
|
||||||
|
|
||||||
package aghos
|
|
||||||
|
|
||||||
import "encoding/binary"
|
|
||||||
|
|
||||||
// NativeEndian is the native endianness of this system.
|
|
||||||
var NativeEndian = binary.BigEndian
|
|
|
@ -1,10 +0,0 @@
|
||||||
//go:build amd64 || 386 || arm || arm64 || mipsle || mips64le || ppc64le
|
|
||||||
|
|
||||||
// This file is an adapted version of github.com/josharian/native.
|
|
||||||
|
|
||||||
package aghos
|
|
||||||
|
|
||||||
import "encoding/binary"
|
|
||||||
|
|
||||||
// NativeEndian is the native endianness of this system.
|
|
||||||
var NativeEndian = binary.LittleEndian
|
|
|
@ -15,18 +15,16 @@ import (
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
||||||
"github.com/mdlayher/ethernet"
|
"github.com/mdlayher/ethernet"
|
||||||
|
"github.com/mdlayher/packet"
|
||||||
//lint:ignore SA1019 See the TODO in go.mod.
|
|
||||||
"github.com/mdlayher/raw"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// dhcpUnicastAddr is the combination of MAC and IP addresses for responding to
|
// dhcpUnicastAddr is the combination of MAC and IP addresses for responding to
|
||||||
// the unconfigured host.
|
// the unconfigured host.
|
||||||
type dhcpUnicastAddr struct {
|
type dhcpUnicastAddr struct {
|
||||||
// raw.Addr is embedded here to make *dhcpUcastAddr a net.Addr without
|
// packet.Addr is embedded here to make *dhcpUcastAddr a net.Addr without
|
||||||
// actually implementing all methods. It also contains the client's
|
// actually implementing all methods. It also contains the client's
|
||||||
// hardware address.
|
// hardware address.
|
||||||
raw.Addr
|
packet.Addr
|
||||||
|
|
||||||
// yiaddr is an IP address just allocated by server for the host.
|
// yiaddr is an IP address just allocated by server for the host.
|
||||||
yiaddr net.IP
|
yiaddr net.IP
|
||||||
|
@ -52,7 +50,7 @@ type dhcpConn struct {
|
||||||
// newDHCPConn creates the special connection for DHCP server.
|
// newDHCPConn creates the special connection for DHCP server.
|
||||||
func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err error) {
|
func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err error) {
|
||||||
var ucast net.PacketConn
|
var ucast net.PacketConn
|
||||||
if ucast, err = raw.ListenPacket(iface, uint16(ethernet.EtherTypeIPv4), nil); err != nil {
|
if ucast, err = packet.Listen(iface, packet.Raw, int(ethernet.EtherTypeIPv4), nil); err != nil {
|
||||||
return nil, fmt.Errorf("creating raw udp connection: %w", err)
|
return nil, fmt.Errorf("creating raw udp connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,9 @@ import (
|
||||||
"github.com/google/gopacket"
|
"github.com/google/gopacket"
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
|
"github.com/mdlayher/packet"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
//lint:ignore SA1019 See the TODO in go.mod.
|
|
||||||
"github.com/mdlayher/raw"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDHCPConn_WriteTo_common(t *testing.T) {
|
func TestDHCPConn_WriteTo_common(t *testing.T) {
|
||||||
|
@ -57,7 +55,7 @@ func TestBuildEtherPkt(t *testing.T) {
|
||||||
srcIP: net.IP{1, 2, 3, 4},
|
srcIP: net.IP{1, 2, 3, 4},
|
||||||
}
|
}
|
||||||
peer := &dhcpUnicastAddr{
|
peer := &dhcpUnicastAddr{
|
||||||
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}},
|
Addr: packet.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}},
|
||||||
yiaddr: net.IP{4, 3, 2, 1},
|
yiaddr: net.IP{4, 3, 2, 1},
|
||||||
}
|
}
|
||||||
payload := (&dhcpv4.DHCPv4{}).ToBytes()
|
payload := (&dhcpv4.DHCPv4{}).ToBytes()
|
||||||
|
@ -102,7 +100,7 @@ func TestBuildEtherPkt(t *testing.T) {
|
||||||
t.Run("serializing_error", func(t *testing.T) {
|
t.Run("serializing_error", func(t *testing.T) {
|
||||||
// Create a peer with invalid MAC.
|
// Create a peer with invalid MAC.
|
||||||
badPeer := &dhcpUnicastAddr{
|
badPeer := &dhcpUnicastAddr{
|
||||||
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}},
|
Addr: packet.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}},
|
||||||
yiaddr: net.IP{4, 3, 2, 1},
|
yiaddr: net.IP{4, 3, 2, 1},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -32,6 +33,8 @@ func normalizeIP(ip net.IP) net.IP {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load lease table from DB
|
// Load lease table from DB
|
||||||
|
//
|
||||||
|
// TODO(s.chzhen): Decrease complexity.
|
||||||
func (s *server) dbLoad() (err error) {
|
func (s *server) dbLoad() (err error) {
|
||||||
dynLeases := []*Lease{}
|
dynLeases := []*Lease{}
|
||||||
staticLeases := []*Lease{}
|
staticLeases := []*Lease{}
|
||||||
|
@ -57,26 +60,28 @@ func (s *server) dbLoad() (err error) {
|
||||||
for i := range obj {
|
for i := range obj {
|
||||||
obj[i].IP = normalizeIP(obj[i].IP)
|
obj[i].IP = normalizeIP(obj[i].IP)
|
||||||
|
|
||||||
if !(len(obj[i].IP) == 4 || len(obj[i].IP) == 16) {
|
ip, ok := netip.AddrFromSlice(obj[i].IP)
|
||||||
|
if !ok {
|
||||||
log.Info("dhcp: invalid IP: %s", obj[i].IP)
|
log.Info("dhcp: invalid IP: %s", obj[i].IP)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
lease := Lease{
|
lease := Lease{
|
||||||
HWAddr: obj[i].HWAddr,
|
HWAddr: obj[i].HWAddr,
|
||||||
IP: obj[i].IP,
|
IP: ip,
|
||||||
Hostname: obj[i].Hostname,
|
Hostname: obj[i].Hostname,
|
||||||
Expiry: time.Unix(obj[i].Expiry, 0),
|
Expiry: time.Unix(obj[i].Expiry, 0),
|
||||||
|
IsStatic: obj[i].Expiry == leaseExpireStatic,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(obj[i].IP) == 16 {
|
if len(obj[i].IP) == 16 {
|
||||||
if obj[i].Expiry == leaseExpireStatic {
|
if lease.IsStatic {
|
||||||
v6StaticLeases = append(v6StaticLeases, &lease)
|
v6StaticLeases = append(v6StaticLeases, &lease)
|
||||||
} else {
|
} else {
|
||||||
v6DynLeases = append(v6DynLeases, &lease)
|
v6DynLeases = append(v6DynLeases, &lease)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if obj[i].Expiry == leaseExpireStatic {
|
if lease.IsStatic {
|
||||||
staticLeases = append(staticLeases, &lease)
|
staticLeases = append(staticLeases, &lease)
|
||||||
} else {
|
} else {
|
||||||
dynLeases = append(dynLeases, &lease)
|
dynLeases = append(dynLeases, &lease)
|
||||||
|
@ -145,7 +150,7 @@ func (s *server) dbStore() (err error) {
|
||||||
|
|
||||||
lease := leaseJSON{
|
lease := leaseJSON{
|
||||||
HWAddr: l.HWAddr,
|
HWAddr: l.HWAddr,
|
||||||
IP: l.IP,
|
IP: l.IP.AsSlice(),
|
||||||
Hostname: l.Hostname,
|
Hostname: l.Hostname,
|
||||||
Expiry: l.Expiry.Unix(),
|
Expiry: l.Expiry.Unix(),
|
||||||
}
|
}
|
||||||
|
@ -162,7 +167,7 @@ func (s *server) dbStore() (err error) {
|
||||||
|
|
||||||
lease := leaseJSON{
|
lease := leaseJSON{
|
||||||
HWAddr: l.HWAddr,
|
HWAddr: l.HWAddr,
|
||||||
IP: l.IP,
|
IP: l.IP.AsSlice(),
|
||||||
Hostname: l.Hostname,
|
Hostname: l.Hostname,
|
||||||
Expiry: l.Expiry.Unix(),
|
Expiry: l.Expiry.Unix(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,13 +41,19 @@ type Lease struct {
|
||||||
// of 1 means that this is a static lease.
|
// of 1 means that this is a static lease.
|
||||||
Expiry time.Time `json:"expires"`
|
Expiry time.Time `json:"expires"`
|
||||||
|
|
||||||
Hostname string `json:"hostname"`
|
// Hostname of the client.
|
||||||
HWAddr net.HardwareAddr `json:"mac"`
|
Hostname string `json:"hostname"`
|
||||||
|
|
||||||
|
// HWAddr is the physical hardware address (MAC address).
|
||||||
|
HWAddr net.HardwareAddr `json:"mac"`
|
||||||
|
|
||||||
// IP is the IP address leased to the client.
|
// IP is the IP address leased to the client.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Migrate leases.db and use netip.Addr.
|
// TODO(a.garipov): Migrate leases.db.
|
||||||
IP net.IP `json:"ip"`
|
IP netip.Addr `json:"ip"`
|
||||||
|
|
||||||
|
// IsStatic defines if the lease is static.
|
||||||
|
IsStatic bool `json:"static"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone returns a deep copy of l.
|
// Clone returns a deep copy of l.
|
||||||
|
@ -60,7 +66,8 @@ func (l *Lease) Clone() (clone *Lease) {
|
||||||
Expiry: l.Expiry,
|
Expiry: l.Expiry,
|
||||||
Hostname: l.Hostname,
|
Hostname: l.Hostname,
|
||||||
HWAddr: slices.Clone(l.HWAddr),
|
HWAddr: slices.Clone(l.HWAddr),
|
||||||
IP: slices.Clone(l.IP),
|
IP: l.IP,
|
||||||
|
IsStatic: l.IsStatic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,17 +88,10 @@ func (l *Lease) IsBlocklisted() (ok bool) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsStatic returns true if the lease is static.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Just make it a boolean field.
|
|
||||||
func (l *Lease) IsStatic() (ok bool) {
|
|
||||||
return l != nil && l.Expiry.Unix() == leaseExpireStatic
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface for Lease.
|
// MarshalJSON implements the json.Marshaler interface for Lease.
|
||||||
func (l Lease) MarshalJSON() ([]byte, error) {
|
func (l Lease) MarshalJSON() ([]byte, error) {
|
||||||
var expiryStr string
|
var expiryStr string
|
||||||
if !l.IsStatic() {
|
if !l.IsStatic {
|
||||||
// The front-end is waiting for RFC 3999 format of the time
|
// The front-end is waiting for RFC 3999 format of the time
|
||||||
// value. It also shouldn't got an Expiry field for static
|
// value. It also shouldn't got an Expiry field for static
|
||||||
// leases.
|
// leases.
|
||||||
|
|
|
@ -48,11 +48,11 @@ func TestDB(t *testing.T) {
|
||||||
Expiry: time.Now().Add(time.Hour),
|
Expiry: time.Now().Add(time.Hour),
|
||||||
Hostname: "static-1.local",
|
Hostname: "static-1.local",
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
IP: net.IP{192, 168, 10, 100},
|
IP: netip.MustParseAddr("192.168.10.100"),
|
||||||
}, {
|
}, {
|
||||||
Hostname: "static-2.local",
|
Hostname: "static-2.local",
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xBB},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xBB},
|
||||||
IP: net.IP{192, 168, 10, 101},
|
IP: netip.MustParseAddr("192.168.10.101"),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
srv4, ok := s.srv4.(*v4Server)
|
srv4, ok := s.srv4.(*v4Server)
|
||||||
|
@ -80,7 +80,7 @@ func TestDB(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, leases[1].HWAddr, ll[0].HWAddr)
|
assert.Equal(t, leases[1].HWAddr, ll[0].HWAddr)
|
||||||
assert.Equal(t, leases[1].IP, ll[0].IP)
|
assert.Equal(t, leases[1].IP, ll[0].IP)
|
||||||
assert.True(t, ll[0].IsStatic())
|
assert.True(t, ll[0].IsStatic)
|
||||||
|
|
||||||
assert.Equal(t, leases[0].HWAddr, ll[1].HWAddr)
|
assert.Equal(t, leases[0].HWAddr, ll[1].HWAddr)
|
||||||
assert.Equal(t, leases[0].IP, ll[1].IP)
|
assert.Equal(t, leases[0].IP, ll[1].IP)
|
||||||
|
@ -96,7 +96,7 @@ func TestNormalizeLeases(t *testing.T) {
|
||||||
|
|
||||||
staticLeases := []*Lease{{
|
staticLeases := []*Lease{{
|
||||||
HWAddr: net.HardwareAddr{1, 2, 3, 4},
|
HWAddr: net.HardwareAddr{1, 2, 3, 4},
|
||||||
IP: net.IP{0, 2, 3, 4},
|
IP: netip.MustParseAddr("0.2.3.4"),
|
||||||
}, {
|
}, {
|
||||||
HWAddr: net.HardwareAddr{2, 2, 3, 4},
|
HWAddr: net.HardwareAddr{2, 2, 3, 4},
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
|
@ -57,12 +58,77 @@ func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {
|
||||||
|
|
||||||
// dhcpStatusResponse is the response for /control/dhcp/status endpoint.
|
// dhcpStatusResponse is the response for /control/dhcp/status endpoint.
|
||||||
type dhcpStatusResponse struct {
|
type dhcpStatusResponse struct {
|
||||||
IfaceName string `json:"interface_name"`
|
IfaceName string `json:"interface_name"`
|
||||||
V4 V4ServerConf `json:"v4"`
|
V4 V4ServerConf `json:"v4"`
|
||||||
V6 V6ServerConf `json:"v6"`
|
V6 V6ServerConf `json:"v6"`
|
||||||
Leases []*Lease `json:"leases"`
|
Leases []*leaseDynamic `json:"leases"`
|
||||||
StaticLeases []*Lease `json:"static_leases"`
|
StaticLeases []*leaseStatic `json:"static_leases"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// leaseStatic is the JSON form of static DHCP lease.
|
||||||
|
type leaseStatic struct {
|
||||||
|
HWAddr string `json:"mac"`
|
||||||
|
IP netip.Addr `json:"ip"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// leasesToStatic converts list of leases to their JSON form.
|
||||||
|
func leasesToStatic(leases []*Lease) (static []*leaseStatic) {
|
||||||
|
static = make([]*leaseStatic, len(leases))
|
||||||
|
|
||||||
|
for i, l := range leases {
|
||||||
|
static[i] = &leaseStatic{
|
||||||
|
HWAddr: l.HWAddr.String(),
|
||||||
|
IP: l.IP,
|
||||||
|
Hostname: l.Hostname,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return static
|
||||||
|
}
|
||||||
|
|
||||||
|
// toLease converts leaseStatic to Lease or returns error.
|
||||||
|
func (l *leaseStatic) toLease() (lease *Lease, err error) {
|
||||||
|
addr, err := net.ParseMAC(l.HWAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't parse MAC address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Lease{
|
||||||
|
HWAddr: addr,
|
||||||
|
IP: l.IP,
|
||||||
|
Hostname: l.Hostname,
|
||||||
|
IsStatic: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// leaseDynamic is the JSON form of dynamic DHCP lease.
|
||||||
|
type leaseDynamic struct {
|
||||||
|
HWAddr string `json:"mac"`
|
||||||
|
IP netip.Addr `json:"ip"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Expiry string `json:"expires"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// leasesToDynamic converts list of leases to their JSON form.
|
||||||
|
func leasesToDynamic(leases []*Lease) (dynamic []*leaseDynamic) {
|
||||||
|
dynamic = make([]*leaseDynamic, len(leases))
|
||||||
|
|
||||||
|
for i, l := range leases {
|
||||||
|
dynamic[i] = &leaseDynamic{
|
||||||
|
HWAddr: l.HWAddr.String(),
|
||||||
|
IP: l.IP,
|
||||||
|
Hostname: l.Hostname,
|
||||||
|
// The front-end is waiting for RFC 3999 format of the time
|
||||||
|
// value.
|
||||||
|
//
|
||||||
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/2692.
|
||||||
|
Expiry: l.Expiry.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -76,8 +142,8 @@ func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
s.srv4.WriteDiskConfig4(&status.V4)
|
s.srv4.WriteDiskConfig4(&status.V4)
|
||||||
s.srv6.WriteDiskConfig6(&status.V6)
|
s.srv6.WriteDiskConfig6(&status.V6)
|
||||||
|
|
||||||
status.Leases = s.Leases(LeasesDynamic)
|
status.Leases = leasesToDynamic(s.Leases(LeasesDynamic))
|
||||||
status.StaticLeases = s.Leases(LeasesStatic)
|
status.StaticLeases = leasesToStatic(s.Leases(LeasesStatic))
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, status)
|
_ = aghhttp.WriteJSONResponse(w, r, status)
|
||||||
}
|
}
|
||||||
|
@ -488,7 +554,7 @@ func setOtherDHCPResult(ifaceName string, result *dhcpSearchResult) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
|
func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
|
||||||
l := &Lease{}
|
l := &leaseStatic{}
|
||||||
err := json.NewDecoder(r.Body).Decode(l)
|
err := json.NewDecoder(r.Body).Decode(l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||||
|
@ -496,22 +562,29 @@ func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.IP == nil {
|
if !l.IP.IsValid() {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "invalid IP")
|
aghhttp.Error(r, w, http.StatusBadRequest, "invalid IP")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l.IP = l.IP.Unmap()
|
||||||
|
|
||||||
var srv DHCPServer
|
var srv DHCPServer
|
||||||
if ip4 := l.IP.To4(); ip4 != nil {
|
if l.IP.Is4() {
|
||||||
l.IP = ip4
|
|
||||||
srv = s.srv4
|
srv = s.srv4
|
||||||
} else {
|
} else {
|
||||||
l.IP = l.IP.To16()
|
|
||||||
srv = s.srv6
|
srv = s.srv6
|
||||||
}
|
}
|
||||||
|
|
||||||
err = srv.AddStaticLease(l)
|
lease, err := l.toLease()
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "parsing: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = srv.AddStaticLease(lease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
|
|
||||||
|
@ -520,7 +593,7 @@ func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {
|
func (s *server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {
|
||||||
l := &Lease{}
|
l := &leaseStatic{}
|
||||||
err := json.NewDecoder(r.Body).Decode(l)
|
err := json.NewDecoder(r.Body).Decode(l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||||
|
@ -528,27 +601,29 @@ func (s *server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.IP == nil {
|
if !l.IP.IsValid() {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "invalid IP")
|
aghhttp.Error(r, w, http.StatusBadRequest, "invalid IP")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ip4 := l.IP.To4()
|
l.IP = l.IP.Unmap()
|
||||||
|
|
||||||
if ip4 == nil {
|
var srv DHCPServer
|
||||||
l.IP = l.IP.To16()
|
if l.IP.Is4() {
|
||||||
|
srv = s.srv4
|
||||||
|
} else {
|
||||||
|
srv = s.srv6
|
||||||
|
}
|
||||||
|
|
||||||
err = s.srv6.RemoveStaticLease(l)
|
lease, err := l.toLease()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "parsing: %s", err)
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.IP = ip4
|
err = srv.RemoveStaticLease(lease)
|
||||||
err = s.srv4.RemoveStaticLease(l)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
//go:build darwin || freebsd || linux || openbsd
|
||||||
|
|
||||||
|
package dhcpd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServer_handleDHCPStatus(t *testing.T) {
|
||||||
|
const (
|
||||||
|
staticName = "static-client"
|
||||||
|
staticMAC = "aa:aa:aa:aa:aa:aa"
|
||||||
|
)
|
||||||
|
|
||||||
|
staticIP := netip.MustParseAddr("192.168.10.10")
|
||||||
|
|
||||||
|
staticLease := &leaseStatic{
|
||||||
|
HWAddr: staticMAC,
|
||||||
|
IP: staticIP,
|
||||||
|
Hostname: staticName,
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := Create(&ServerConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Conf4: *defaultV4ServerConf(),
|
||||||
|
WorkDir: t.TempDir(),
|
||||||
|
DBFilePath: dbFilename,
|
||||||
|
ConfigModified: func() {},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// checkStatus is a helper that asserts the response of
|
||||||
|
// [*server.handleDHCPStatus].
|
||||||
|
checkStatus := func(t *testing.T, want *dhcpStatusResponse) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
var req *http.Request
|
||||||
|
req, err = http.NewRequest(http.MethodGet, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
b := &bytes.Buffer{}
|
||||||
|
err = json.NewEncoder(b).Encode(&want)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.handleDHCPStatus(w, req)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
assert.JSONEq(t, b.String(), w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultResponse is a helper that returs the response with default
|
||||||
|
// configuration.
|
||||||
|
defaultResponse := func() *dhcpStatusResponse {
|
||||||
|
conf4 := defaultV4ServerConf()
|
||||||
|
conf4.LeaseDuration = 86400
|
||||||
|
|
||||||
|
resp := &dhcpStatusResponse{
|
||||||
|
V4: *conf4,
|
||||||
|
V6: V6ServerConf{},
|
||||||
|
Leases: []*leaseDynamic{},
|
||||||
|
StaticLeases: []*leaseStatic{},
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := t.Run("status", func(t *testing.T) {
|
||||||
|
resp := defaultResponse()
|
||||||
|
|
||||||
|
checkStatus(t, resp)
|
||||||
|
})
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
ok = t.Run("add_static_lease", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
b := &bytes.Buffer{}
|
||||||
|
err = json.NewEncoder(b).Encode(staticLease)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var r *http.Request
|
||||||
|
r, err = http.NewRequest(http.MethodPost, "", b)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.handleDHCPAddStaticLease(w, r)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
resp := defaultResponse()
|
||||||
|
resp.StaticLeases = []*leaseStatic{staticLease}
|
||||||
|
|
||||||
|
checkStatus(t, resp)
|
||||||
|
})
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
ok = t.Run("add_invalid_lease", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
b := &bytes.Buffer{}
|
||||||
|
|
||||||
|
err = json.NewEncoder(b).Encode(&leaseStatic{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var r *http.Request
|
||||||
|
r, err = http.NewRequest(http.MethodPost, "", b)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.handleDHCPAddStaticLease(w, r)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
})
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
ok = t.Run("remove_static_lease", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
b := &bytes.Buffer{}
|
||||||
|
err = json.NewEncoder(b).Encode(staticLease)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var r *http.Request
|
||||||
|
r, err = http.NewRequest(http.MethodPost, "", b)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.handleDHCPRemoveStaticLease(w, r)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
resp := defaultResponse()
|
||||||
|
|
||||||
|
checkStatus(t, resp)
|
||||||
|
})
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
ok = t.Run("set_config", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
resp := defaultResponse()
|
||||||
|
resp.Enabled = false
|
||||||
|
|
||||||
|
b := &bytes.Buffer{}
|
||||||
|
err = json.NewEncoder(b).Encode(&resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var r *http.Request
|
||||||
|
r, err = http.NewRequest(http.MethodPost, "", b)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.handleDHCPSetConfig(w, r)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
checkStatus(t, resp)
|
||||||
|
})
|
||||||
|
require.True(t, ok)
|
||||||
|
}
|
|
@ -16,6 +16,8 @@ import (
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Perhaps create an optimized version with uint32 for IPv4
|
// TODO(a.garipov): Perhaps create an optimized version with uint32 for IPv4
|
||||||
// ranges? Or use one of uint128 packages?
|
// ranges? Or use one of uint128 packages?
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Use netip.Addr.
|
||||||
type ipRange struct {
|
type ipRange struct {
|
||||||
start *big.Int
|
start *big.Int
|
||||||
end *big.Int
|
end *big.Int
|
||||||
|
@ -27,8 +29,6 @@ const maxRangeLen = math.MaxUint32
|
||||||
|
|
||||||
// newIPRange creates a new IP address range. start must be less than end. The
|
// newIPRange creates a new IP address range. start must be less than end. The
|
||||||
// resulting range must not be greater than maxRangeLen.
|
// resulting range must not be greater than maxRangeLen.
|
||||||
//
|
|
||||||
// TODO(e.burkov): Use netip.Addr.
|
|
||||||
func newIPRange(start, end net.IP) (r *ipRange, err error) {
|
func newIPRange(start, end net.IP) (r *ipRange, err error) {
|
||||||
defer func() { err = errors.Annotate(err, "invalid ip range: %w") }()
|
defer func() { err = errors.Annotate(err, "invalid ip range: %w") }()
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,8 @@ import (
|
||||||
"github.com/go-ping/ping"
|
"github.com/go-ping/ping"
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
||||||
|
"github.com/mdlayher/packet"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
//lint:ignore SA1019 See the TODO in go.mod.
|
|
||||||
"github.com/mdlayher/raw"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// v4Server is a DHCPv4 server.
|
// v4Server is a DHCPv4 server.
|
||||||
|
@ -98,7 +96,7 @@ func normalizeHostname(hostname string) (norm string, err error) {
|
||||||
// validHostnameForClient accepts the hostname sent by the client and its IP and
|
// validHostnameForClient accepts the hostname sent by the client and its IP and
|
||||||
// returns either a normalized version of that hostname, or a new hostname
|
// returns either a normalized version of that hostname, or a new hostname
|
||||||
// generated from the IP address, or an empty string.
|
// generated from the IP address, or an empty string.
|
||||||
func (s *v4Server) validHostnameForClient(cliHostname string, ip net.IP) (hostname string) {
|
func (s *v4Server) validHostnameForClient(cliHostname string, ip netip.Addr) (hostname string) {
|
||||||
hostname, err := normalizeHostname(cliHostname)
|
hostname, err := normalizeHostname(cliHostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Info("dhcpv4: %s", err)
|
log.Info("dhcpv4: %s", err)
|
||||||
|
@ -130,7 +128,7 @@ func (s *v4Server) ResetLeases(leases []*Lease) (err error) {
|
||||||
s.leases = nil
|
s.leases = nil
|
||||||
|
|
||||||
for _, l := range leases {
|
for _, l := range leases {
|
||||||
if !l.IsStatic() {
|
if !l.IsStatic {
|
||||||
l.Hostname = s.validHostnameForClient(l.Hostname, l.IP)
|
l.Hostname = s.validHostnameForClient(l.Hostname, l.IP)
|
||||||
}
|
}
|
||||||
err = s.addLease(l)
|
err = s.addLease(l)
|
||||||
|
@ -192,7 +190,7 @@ func (s *v4Server) GetLeases(flags GetLeasesFlags) (leases []*Lease) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if getStatic && l.IsStatic() {
|
if getStatic && l.IsStatic {
|
||||||
leases = append(leases, l.Clone())
|
leases = append(leases, l.Clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,10 +209,9 @@ func (s *v4Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
netIP := ip.AsSlice()
|
|
||||||
for _, l := range s.leases {
|
for _, l := range s.leases {
|
||||||
if l.IP.Equal(netIP) {
|
if l.IP == ip {
|
||||||
if l.Expiry.After(now) || l.IsStatic() {
|
if l.IsStatic || l.Expiry.After(now) {
|
||||||
return l.HWAddr
|
return l.HWAddr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,7 +244,8 @@ func (s *v4Server) rmLeaseByIndex(i int) {
|
||||||
s.leases = append(s.leases[:i], s.leases[i+1:]...)
|
s.leases = append(s.leases[:i], s.leases[i+1:]...)
|
||||||
|
|
||||||
r := s.conf.ipRange
|
r := s.conf.ipRange
|
||||||
offset, ok := r.offset(l.IP)
|
leaseIP := net.IP(l.IP.AsSlice())
|
||||||
|
offset, ok := r.offset(leaseIP)
|
||||||
if ok {
|
if ok {
|
||||||
s.leasedOffsets.set(offset, false)
|
s.leasedOffsets.set(offset, false)
|
||||||
}
|
}
|
||||||
|
@ -261,9 +259,9 @@ func (s *v4Server) rmLeaseByIndex(i int) {
|
||||||
// Return error if a static lease is found
|
// Return error if a static lease is found
|
||||||
func (s *v4Server) rmDynamicLease(lease *Lease) (err error) {
|
func (s *v4Server) rmDynamicLease(lease *Lease) (err error) {
|
||||||
for i, l := range s.leases {
|
for i, l := range s.leases {
|
||||||
isStatic := l.IsStatic()
|
isStatic := l.IsStatic
|
||||||
|
|
||||||
if bytes.Equal(l.HWAddr, lease.HWAddr) || l.IP.Equal(lease.IP) {
|
if bytes.Equal(l.HWAddr, lease.HWAddr) || l.IP == lease.IP {
|
||||||
if isStatic {
|
if isStatic {
|
||||||
return errors.Error("static lease already exists")
|
return errors.Error("static lease already exists")
|
||||||
}
|
}
|
||||||
|
@ -291,13 +289,13 @@ const ErrDupHostname = errors.Error("hostname is not unique")
|
||||||
// addLease adds a dynamic or static lease.
|
// addLease adds a dynamic or static lease.
|
||||||
func (s *v4Server) addLease(l *Lease) (err error) {
|
func (s *v4Server) addLease(l *Lease) (err error) {
|
||||||
r := s.conf.ipRange
|
r := s.conf.ipRange
|
||||||
offset, inOffset := r.offset(l.IP)
|
leaseIP := net.IP(l.IP.AsSlice())
|
||||||
|
offset, inOffset := r.offset(leaseIP)
|
||||||
|
|
||||||
if l.IsStatic() {
|
if l.IsStatic {
|
||||||
// TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is
|
// TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is
|
||||||
// disabled.
|
// disabled.
|
||||||
addr := netip.AddrFrom4(*(*[4]byte)(l.IP.To4()))
|
if sn := s.conf.subnet; !sn.Contains(l.IP) {
|
||||||
if sn := s.conf.subnet; !sn.Contains(addr) {
|
|
||||||
return fmt.Errorf("subnet %s does not contain the ip %q", sn, l.IP)
|
return fmt.Errorf("subnet %s does not contain the ip %q", sn, l.IP)
|
||||||
}
|
}
|
||||||
} else if !inOffset {
|
} else if !inOffset {
|
||||||
|
@ -325,7 +323,7 @@ func (s *v4Server) rmLease(lease *Lease) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, l := range s.leases {
|
for i, l := range s.leases {
|
||||||
if l.IP.Equal(lease.IP) {
|
if l.IP == lease.IP {
|
||||||
if !bytes.Equal(l.HWAddr, lease.HWAddr) || l.Hostname != lease.Hostname {
|
if !bytes.Equal(l.HWAddr, lease.HWAddr) || l.Hostname != lease.Hostname {
|
||||||
return fmt.Errorf("lease for ip %s is different: %+v", lease.IP, l)
|
return fmt.Errorf("lease for ip %s is different: %+v", lease.IP, l)
|
||||||
}
|
}
|
||||||
|
@ -352,14 +350,16 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
|
||||||
return ErrUnconfigured
|
return ErrUnconfigured
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := l.IP.To4()
|
l.IP = l.IP.Unmap()
|
||||||
if ip == nil {
|
|
||||||
|
if !l.IP.Is4() {
|
||||||
return fmt.Errorf("invalid ip %q, only ipv4 is supported", l.IP)
|
return fmt.Errorf("invalid ip %q, only ipv4 is supported", l.IP)
|
||||||
} else if gwIP := s.conf.GatewayIP; gwIP == netip.AddrFrom4(*(*[4]byte)(ip)) {
|
} else if gwIP := s.conf.GatewayIP; gwIP == l.IP {
|
||||||
return fmt.Errorf("can't assign the gateway IP %s to the lease", gwIP)
|
return fmt.Errorf("can't assign the gateway IP %s to the lease", gwIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Expiry = time.Unix(leaseExpireStatic, 0)
|
l.Expiry = time.Unix(leaseExpireStatic, 0)
|
||||||
|
l.IsStatic = true
|
||||||
|
|
||||||
err = netutil.ValidateMAC(l.HWAddr)
|
err = netutil.ValidateMAC(l.HWAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -396,7 +396,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf(
|
err = fmt.Errorf(
|
||||||
"removing dynamic leases for %s (%s): %w",
|
"removing dynamic leases for %s (%s): %w",
|
||||||
ip,
|
l.IP,
|
||||||
l.HWAddr,
|
l.HWAddr,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
@ -406,7 +406,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
|
||||||
|
|
||||||
err = s.addLease(l)
|
err = s.addLease(l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("adding static lease for %s (%s): %w", ip, l.HWAddr, err)
|
err = fmt.Errorf("adding static lease for %s (%s): %w", l.IP, l.HWAddr, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -429,7 +429,7 @@ func (s *v4Server) RemoveStaticLease(l *Lease) (err error) {
|
||||||
return ErrUnconfigured
|
return ErrUnconfigured
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(l.IP) != 4 {
|
if !l.IP.Is4() {
|
||||||
return fmt.Errorf("invalid IP")
|
return fmt.Errorf("invalid IP")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -529,7 +529,7 @@ func (s *v4Server) nextIP() (ip net.IP) {
|
||||||
func (s *v4Server) findExpiredLease() int {
|
func (s *v4Server) findExpiredLease() int {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for i, lease := range s.leases {
|
for i, lease := range s.leases {
|
||||||
if !lease.IsStatic() && lease.Expiry.Before(now) {
|
if !lease.IsStatic && lease.Expiry.Before(now) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -542,8 +542,8 @@ func (s *v4Server) findExpiredLease() int {
|
||||||
func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) {
|
func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) {
|
||||||
l = &Lease{HWAddr: slices.Clone(mac)}
|
l = &Lease{HWAddr: slices.Clone(mac)}
|
||||||
|
|
||||||
l.IP = s.nextIP()
|
nextIP := s.nextIP()
|
||||||
if l.IP == nil {
|
if nextIP == nil {
|
||||||
i := s.findExpiredLease()
|
i := s.findExpiredLease()
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -554,6 +554,13 @@ func (s *v4Server) reserveLease(mac net.HardwareAddr) (l *Lease, err error) {
|
||||||
return s.leases[i], nil
|
return s.leases[i], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
netIP, ok := netip.AddrFromSlice(nextIP)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Error("invalid ip")
|
||||||
|
}
|
||||||
|
|
||||||
|
l.IP = netIP
|
||||||
|
|
||||||
err = s.addLease(l)
|
err = s.addLease(l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -603,7 +610,8 @@ func (s *v4Server) allocateLease(mac net.HardwareAddr) (l *Lease, err error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.addrAvailable(l.IP) {
|
leaseIP := l.IP.AsSlice()
|
||||||
|
if s.addrAvailable(leaseIP) {
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -623,8 +631,9 @@ func (s *v4Server) handleDiscover(req, resp *dhcpv4.DHCPv4) (l *Lease, err error
|
||||||
l = s.findLease(mac)
|
l = s.findLease(mac)
|
||||||
if l != nil {
|
if l != nil {
|
||||||
reqIP := req.RequestedIPAddress()
|
reqIP := req.RequestedIPAddress()
|
||||||
if len(reqIP) != 0 && !reqIP.Equal(l.IP) {
|
leaseIP := net.IP(l.IP.AsSlice())
|
||||||
log.Debug("dhcpv4: different RequestedIP: %s != %s", reqIP, l.IP)
|
if len(reqIP) != 0 && !reqIP.Equal(leaseIP) {
|
||||||
|
log.Debug("dhcpv4: different RequestedIP: %s != %s", reqIP, leaseIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
|
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
|
||||||
|
@ -674,12 +683,19 @@ func (s *v4Server) checkLease(mac net.HardwareAddr, ip net.IP) (lease *Lease, mi
|
||||||
s.leasesLock.Lock()
|
s.leasesLock.Lock()
|
||||||
defer s.leasesLock.Unlock()
|
defer s.leasesLock.Unlock()
|
||||||
|
|
||||||
|
netIP, ok := netip.AddrFromSlice(ip)
|
||||||
|
if !ok {
|
||||||
|
log.Info("check lease: invalid IP: %s", ip)
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
for _, l := range s.leases {
|
for _, l := range s.leases {
|
||||||
if !bytes.Equal(l.HWAddr, mac) {
|
if !bytes.Equal(l.HWAddr, mac) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.IP.Equal(ip) {
|
if l.IP == netIP {
|
||||||
return l, false
|
return l, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -845,7 +861,7 @@ func (s *v4Server) handleRequest(req, resp *dhcpv4.DHCPv4) (lease *Lease, needsR
|
||||||
s.leasesLock.Lock()
|
s.leasesLock.Lock()
|
||||||
defer s.leasesLock.Unlock()
|
defer s.leasesLock.Unlock()
|
||||||
|
|
||||||
if lease.IsStatic() {
|
if lease.IsStatic {
|
||||||
if lease.Hostname != "" {
|
if lease.Hostname != "" {
|
||||||
// TODO(e.burkov): This option is used to update the server's DNS
|
// TODO(e.burkov): This option is used to update the server's DNS
|
||||||
// mapping. The option should only be answered when it has been
|
// mapping. The option should only be answered when it has been
|
||||||
|
@ -878,9 +894,16 @@ func (s *v4Server) handleDecline(req, resp *dhcpv4.DHCPv4) (err error) {
|
||||||
reqIP = req.ClientIPAddr
|
reqIP = req.ClientIPAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
netIP, ok := netip.AddrFromSlice(reqIP)
|
||||||
|
if !ok {
|
||||||
|
log.Info("dhcpv4: invalid IP: %s", reqIP)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var oldLease *Lease
|
var oldLease *Lease
|
||||||
for _, l := range s.leases {
|
for _, l := range s.leases {
|
||||||
if bytes.Equal(l.HWAddr, mac) && l.IP.Equal(reqIP) {
|
if bytes.Equal(l.HWAddr, mac) && l.IP == netIP {
|
||||||
oldLease = l
|
oldLease = l
|
||||||
|
|
||||||
break
|
break
|
||||||
|
@ -920,8 +943,7 @@ func (s *v4Server) handleDecline(req, resp *dhcpv4.DHCPv4) (err error) {
|
||||||
|
|
||||||
log.Info("dhcpv4: changed ip from %s to %s for %s", reqIP, newLease.IP, mac)
|
log.Info("dhcpv4: changed ip from %s to %s for %s", reqIP, newLease.IP, mac)
|
||||||
|
|
||||||
resp.YourIPAddr = make([]byte, 4)
|
resp.YourIPAddr = net.IP(newLease.IP.AsSlice())
|
||||||
copy(resp.YourIPAddr, newLease.IP)
|
|
||||||
|
|
||||||
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
|
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
|
||||||
|
|
||||||
|
@ -944,8 +966,15 @@ func (s *v4Server) handleRelease(req, resp *dhcpv4.DHCPv4) (err error) {
|
||||||
s.leasesLock.Lock()
|
s.leasesLock.Lock()
|
||||||
defer s.leasesLock.Unlock()
|
defer s.leasesLock.Unlock()
|
||||||
|
|
||||||
|
netIP, ok := netip.AddrFromSlice(reqIP)
|
||||||
|
if !ok {
|
||||||
|
log.Info("dhcpv4: invalid IP: %s", reqIP)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, l := range s.leases {
|
for _, l := range s.leases {
|
||||||
if !bytes.Equal(l.HWAddr, mac) || !l.IP.Equal(reqIP) {
|
if !bytes.Equal(l.HWAddr, mac) || l.IP != netIP {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1018,7 +1047,7 @@ func (s *v4Server) handle(req, resp *dhcpv4.DHCPv4) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
if l != nil {
|
if l != nil {
|
||||||
resp.YourIPAddr = slices.Clone(l.IP)
|
resp.YourIPAddr = net.IP(l.IP.AsSlice())
|
||||||
}
|
}
|
||||||
|
|
||||||
s.updateOptions(req, resp)
|
s.updateOptions(req, resp)
|
||||||
|
@ -1136,7 +1165,7 @@ func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DH
|
||||||
// Unicast DHCPOFFER and DHCPACK messages to the client's
|
// Unicast DHCPOFFER and DHCPACK messages to the client's
|
||||||
// hardware address and yiaddr.
|
// hardware address and yiaddr.
|
||||||
peer = &dhcpUnicastAddr{
|
peer = &dhcpUnicastAddr{
|
||||||
Addr: raw.Addr{HardwareAddr: req.ClientHWAddr},
|
Addr: packet.Addr{HardwareAddr: req.ClientHWAddr},
|
||||||
yiaddr: resp.YourIPAddr,
|
yiaddr: resp.YourIPAddr,
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -15,11 +15,9 @@ import (
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
|
"github.com/mdlayher/packet"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
//lint:ignore SA1019 See the TODO in go.mod.
|
|
||||||
"github.com/mdlayher/raw"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -62,7 +60,7 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
anotherName = "another-client"
|
anotherName = "another-client"
|
||||||
)
|
)
|
||||||
|
|
||||||
staticIP := net.IP{192, 168, 10, 10}
|
staticIP := netip.MustParseAddr("192.168.10.10")
|
||||||
anotherIP := DefaultRangeStart
|
anotherIP := DefaultRangeStart
|
||||||
staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||||
anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB}
|
anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB}
|
||||||
|
@ -75,6 +73,7 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
Hostname: staticName,
|
Hostname: staticName,
|
||||||
HWAddr: staticMAC,
|
HWAddr: staticMAC,
|
||||||
IP: staticIP,
|
IP: staticIP,
|
||||||
|
IsStatic: true,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -83,7 +82,8 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
Expiry: time.Unix(leaseExpireStatic, 0),
|
Expiry: time.Unix(leaseExpireStatic, 0),
|
||||||
Hostname: staticName,
|
Hostname: staticName,
|
||||||
HWAddr: anotherMAC,
|
HWAddr: anotherMAC,
|
||||||
IP: anotherIP.AsSlice(),
|
IP: anotherIP,
|
||||||
|
IsStatic: true,
|
||||||
})
|
})
|
||||||
assert.ErrorIs(t, err, ErrDupHostname)
|
assert.ErrorIs(t, err, ErrDupHostname)
|
||||||
})
|
})
|
||||||
|
@ -97,7 +97,8 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
Expiry: time.Unix(leaseExpireStatic, 0),
|
Expiry: time.Unix(leaseExpireStatic, 0),
|
||||||
Hostname: anotherName,
|
Hostname: anotherName,
|
||||||
HWAddr: staticMAC,
|
HWAddr: staticMAC,
|
||||||
IP: anotherIP.AsSlice(),
|
IP: anotherIP,
|
||||||
|
IsStatic: true,
|
||||||
})
|
})
|
||||||
testutil.AssertErrorMsg(t, wantErrMsg, err)
|
testutil.AssertErrorMsg(t, wantErrMsg, err)
|
||||||
})
|
})
|
||||||
|
@ -112,6 +113,7 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
Hostname: anotherName,
|
Hostname: anotherName,
|
||||||
HWAddr: anotherMAC,
|
HWAddr: anotherMAC,
|
||||||
IP: staticIP,
|
IP: staticIP,
|
||||||
|
IsStatic: true,
|
||||||
})
|
})
|
||||||
testutil.AssertErrorMsg(t, wantErrMsg, err)
|
testutil.AssertErrorMsg(t, wantErrMsg, err)
|
||||||
})
|
})
|
||||||
|
@ -124,13 +126,14 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
discoverAnOffer := func(
|
discoverAnOffer := func(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
name string,
|
name string,
|
||||||
ip net.IP,
|
netIP netip.Addr,
|
||||||
mac net.HardwareAddr,
|
mac net.HardwareAddr,
|
||||||
) (resp *dhcpv4.DHCPv4) {
|
) (resp *dhcpv4.DHCPv4) {
|
||||||
testutil.CleanupAndRequireSuccess(t, func() (err error) {
|
testutil.CleanupAndRequireSuccess(t, func() (err error) {
|
||||||
return s.ResetLeases(s.GetLeases(LeasesStatic))
|
return s.ResetLeases(s.GetLeases(LeasesStatic))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ip := net.IP(netIP.AsSlice())
|
||||||
req, err := dhcpv4.NewDiscovery(
|
req, err := dhcpv4.NewDiscovery(
|
||||||
mac,
|
mac,
|
||||||
dhcpv4.WithOption(dhcpv4.OptHostName(name)),
|
dhcpv4.WithOption(dhcpv4.OptHostName(name)),
|
||||||
|
@ -151,7 +154,7 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("same_name", func(t *testing.T) {
|
t.Run("same_name", func(t *testing.T) {
|
||||||
resp := discoverAnOffer(t, staticName, anotherIP.AsSlice(), anotherMAC)
|
resp := discoverAnOffer(t, staticName, anotherIP, anotherMAC)
|
||||||
|
|
||||||
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
|
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
|
||||||
dhcpv4.OptHostName(staticName),
|
dhcpv4.OptHostName(staticName),
|
||||||
|
@ -161,11 +164,15 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
res := s4.handle(req, resp)
|
res := s4.handle(req, resp)
|
||||||
require.Positive(t, res)
|
require.Positive(t, res)
|
||||||
|
|
||||||
assert.Equal(t, aghnet.GenerateHostname(resp.YourIPAddr), resp.HostName())
|
var netIP netip.Addr
|
||||||
|
netIP, ok = netip.AddrFromSlice(resp.YourIPAddr)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
assert.Equal(t, aghnet.GenerateHostname(netIP), resp.HostName())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("same_mac", func(t *testing.T) {
|
t.Run("same_mac", func(t *testing.T) {
|
||||||
resp := discoverAnOffer(t, anotherName, anotherIP.AsSlice(), staticMAC)
|
resp := discoverAnOffer(t, anotherName, anotherIP, staticMAC)
|
||||||
|
|
||||||
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
|
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
|
||||||
dhcpv4.OptHostName(anotherName),
|
dhcpv4.OptHostName(anotherName),
|
||||||
|
@ -179,7 +186,8 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
require.Len(t, fqdnOptData, 3+len(staticName))
|
require.Len(t, fqdnOptData, 3+len(staticName))
|
||||||
assert.Equal(t, []uint8(staticName), fqdnOptData[3:])
|
assert.Equal(t, []uint8(staticName), fqdnOptData[3:])
|
||||||
|
|
||||||
assert.Equal(t, staticIP, resp.YourIPAddr)
|
ip := net.IP(staticIP.AsSlice())
|
||||||
|
assert.Equal(t, ip, resp.YourIPAddr)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("same_ip", func(t *testing.T) {
|
t.Run("same_ip", func(t *testing.T) {
|
||||||
|
@ -212,7 +220,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
|
||||||
lease: &Lease{
|
lease: &Lease{
|
||||||
Hostname: "success.local",
|
Hostname: "success.local",
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
IP: net.IP{192, 168, 10, 150},
|
IP: netip.MustParseAddr("192.168.10.150"),
|
||||||
},
|
},
|
||||||
name: "success",
|
name: "success",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
|
@ -220,7 +228,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
|
||||||
lease: &Lease{
|
lease: &Lease{
|
||||||
Hostname: "probably-router.local",
|
Hostname: "probably-router.local",
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
IP: DefaultGatewayIP.AsSlice(),
|
IP: DefaultGatewayIP,
|
||||||
},
|
},
|
||||||
name: "with_gateway_ip",
|
name: "with_gateway_ip",
|
||||||
wantErrMsg: "dhcpv4: adding static lease: " +
|
wantErrMsg: "dhcpv4: adding static lease: " +
|
||||||
|
@ -229,7 +237,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
|
||||||
lease: &Lease{
|
lease: &Lease{
|
||||||
Hostname: "ip6.local",
|
Hostname: "ip6.local",
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
IP: net.ParseIP("ffff::1"),
|
IP: netip.MustParseAddr("ffff::1"),
|
||||||
},
|
},
|
||||||
name: "ipv6",
|
name: "ipv6",
|
||||||
wantErrMsg: `dhcpv4: adding static lease: ` +
|
wantErrMsg: `dhcpv4: adding static lease: ` +
|
||||||
|
@ -238,7 +246,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
|
||||||
lease: &Lease{
|
lease: &Lease{
|
||||||
Hostname: "bad-mac.local",
|
Hostname: "bad-mac.local",
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA},
|
||||||
IP: net.IP{192, 168, 10, 150},
|
IP: netip.MustParseAddr("192.168.10.150"),
|
||||||
},
|
},
|
||||||
name: "bad_mac",
|
name: "bad_mac",
|
||||||
wantErrMsg: `dhcpv4: adding static lease: bad mac address "aa:aa": ` +
|
wantErrMsg: `dhcpv4: adding static lease: bad mac address "aa:aa": ` +
|
||||||
|
@ -247,7 +255,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
|
||||||
lease: &Lease{
|
lease: &Lease{
|
||||||
Hostname: "bad-lbl-.local",
|
Hostname: "bad-lbl-.local",
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
IP: net.IP{192, 168, 10, 150},
|
IP: netip.MustParseAddr("192.168.10.150"),
|
||||||
},
|
},
|
||||||
name: "bad_hostname",
|
name: "bad_hostname",
|
||||||
wantErrMsg: `dhcpv4: adding static lease: validating hostname: ` +
|
wantErrMsg: `dhcpv4: adding static lease: validating hostname: ` +
|
||||||
|
@ -289,11 +297,11 @@ func TestV4_AddReplace(t *testing.T) {
|
||||||
dynLeases := []Lease{{
|
dynLeases := []Lease{{
|
||||||
Hostname: "dynamic-1.local",
|
Hostname: "dynamic-1.local",
|
||||||
HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
IP: net.IP{192, 168, 10, 150},
|
IP: netip.MustParseAddr("192.168.10.150"),
|
||||||
}, {
|
}, {
|
||||||
Hostname: "dynamic-2.local",
|
Hostname: "dynamic-2.local",
|
||||||
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
IP: net.IP{192, 168, 10, 151},
|
IP: netip.MustParseAddr("192.168.10.151"),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for i := range dynLeases {
|
for i := range dynLeases {
|
||||||
|
@ -304,11 +312,11 @@ func TestV4_AddReplace(t *testing.T) {
|
||||||
stLeases := []*Lease{{
|
stLeases := []*Lease{{
|
||||||
Hostname: "static-1.local",
|
Hostname: "static-1.local",
|
||||||
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
IP: net.IP{192, 168, 10, 150},
|
IP: netip.MustParseAddr("192.168.10.150"),
|
||||||
}, {
|
}, {
|
||||||
Hostname: "static-2.local",
|
Hostname: "static-2.local",
|
||||||
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
IP: net.IP{192, 168, 10, 152},
|
IP: netip.MustParseAddr("192.168.10.152"),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, l := range stLeases {
|
for _, l := range stLeases {
|
||||||
|
@ -320,9 +328,9 @@ func TestV4_AddReplace(t *testing.T) {
|
||||||
require.Len(t, ls, 2)
|
require.Len(t, ls, 2)
|
||||||
|
|
||||||
for i, l := range ls {
|
for i, l := range ls {
|
||||||
assert.True(t, stLeases[i].IP.Equal(l.IP))
|
assert.Equal(t, stLeases[i].IP, l.IP)
|
||||||
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
|
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
|
||||||
assert.True(t, l.IsStatic())
|
assert.True(t, l.IsStatic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -513,7 +521,7 @@ func TestV4StaticLease_Get(t *testing.T) {
|
||||||
l := &Lease{
|
l := &Lease{
|
||||||
Hostname: "static-1.local",
|
Hostname: "static-1.local",
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
IP: net.IP{192, 168, 10, 150},
|
IP: netip.MustParseAddr("192.168.10.150"),
|
||||||
}
|
}
|
||||||
err := s.AddStaticLease(l)
|
err := s.AddStaticLease(l)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -539,7 +547,9 @@ func TestV4StaticLease_Get(t *testing.T) {
|
||||||
t.Run("offer", func(t *testing.T) {
|
t.Run("offer", func(t *testing.T) {
|
||||||
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
||||||
assert.Equal(t, mac, resp.ClientHWAddr)
|
assert.Equal(t, mac, resp.ClientHWAddr)
|
||||||
assert.True(t, l.IP.Equal(resp.YourIPAddr))
|
|
||||||
|
ip := net.IP(l.IP.AsSlice())
|
||||||
|
assert.True(t, ip.Equal(resp.YourIPAddr))
|
||||||
|
|
||||||
assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
|
assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
|
assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
|
@ -564,7 +574,9 @@ func TestV4StaticLease_Get(t *testing.T) {
|
||||||
t.Run("ack", func(t *testing.T) {
|
t.Run("ack", func(t *testing.T) {
|
||||||
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
||||||
assert.Equal(t, mac, resp.ClientHWAddr)
|
assert.Equal(t, mac, resp.ClientHWAddr)
|
||||||
assert.True(t, l.IP.Equal(resp.YourIPAddr))
|
|
||||||
|
ip := net.IP(l.IP.AsSlice())
|
||||||
|
assert.True(t, ip.Equal(resp.YourIPAddr))
|
||||||
|
|
||||||
assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
|
assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
|
assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
|
@ -583,7 +595,7 @@ func TestV4StaticLease_Get(t *testing.T) {
|
||||||
ls := s.GetLeases(LeasesStatic)
|
ls := s.GetLeases(LeasesStatic)
|
||||||
require.Len(t, ls, 1)
|
require.Len(t, ls, 1)
|
||||||
|
|
||||||
assert.True(t, l.IP.Equal(ls[0].IP))
|
assert.Equal(t, l.IP, ls[0].IP)
|
||||||
assert.Equal(t, mac, ls[0].HWAddr)
|
assert.Equal(t, mac, ls[0].HWAddr)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -681,7 +693,8 @@ func TestV4DynamicLease_Get(t *testing.T) {
|
||||||
ls := s.GetLeases(LeasesDynamic)
|
ls := s.GetLeases(LeasesDynamic)
|
||||||
require.Len(t, ls, 1)
|
require.Len(t, ls, 1)
|
||||||
|
|
||||||
assert.True(t, net.IP{192, 168, 10, 100}.Equal(ls[0].IP))
|
ip := netip.MustParseAddr("192.168.10.100")
|
||||||
|
assert.Equal(t, ip, ls[0].IP)
|
||||||
assert.Equal(t, mac, ls[0].HWAddr)
|
assert.Equal(t, mac, ls[0].HWAddr)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -810,7 +823,7 @@ func TestV4Server_Send(t *testing.T) {
|
||||||
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
|
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
|
||||||
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
|
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
|
||||||
want: &dhcpUnicastAddr{
|
want: &dhcpUnicastAddr{
|
||||||
Addr: raw.Addr{HardwareAddr: knownMAC},
|
Addr: packet.Addr{HardwareAddr: knownMAC},
|
||||||
yiaddr: knownIP,
|
yiaddr: knownIP,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
@ -862,3 +875,144 @@ func TestV4Server_Send(t *testing.T) {
|
||||||
assert.True(t, resp.IsBroadcast())
|
assert.True(t, resp.IsBroadcast())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestV4Server_FindMACbyIP(t *testing.T) {
|
||||||
|
const (
|
||||||
|
staticName = "static-client"
|
||||||
|
anotherName = "another-client"
|
||||||
|
)
|
||||||
|
|
||||||
|
staticIP := netip.MustParseAddr("192.168.10.10")
|
||||||
|
staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||||
|
|
||||||
|
anotherIP := netip.MustParseAddr("192.168.100.100")
|
||||||
|
anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB}
|
||||||
|
|
||||||
|
s := &v4Server{
|
||||||
|
leases: []*Lease{{
|
||||||
|
Expiry: time.Unix(leaseExpireStatic, 0),
|
||||||
|
Hostname: staticName,
|
||||||
|
HWAddr: staticMAC,
|
||||||
|
IP: staticIP,
|
||||||
|
IsStatic: true,
|
||||||
|
}, {
|
||||||
|
Expiry: time.Unix(10, 0),
|
||||||
|
Hostname: anotherName,
|
||||||
|
HWAddr: anotherMAC,
|
||||||
|
IP: anotherIP,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
want net.HardwareAddr
|
||||||
|
ip netip.Addr
|
||||||
|
name string
|
||||||
|
}{{
|
||||||
|
name: "basic",
|
||||||
|
ip: staticIP,
|
||||||
|
want: staticMAC,
|
||||||
|
}, {
|
||||||
|
name: "not_found",
|
||||||
|
ip: netip.MustParseAddr("1.2.3.4"),
|
||||||
|
want: nil,
|
||||||
|
}, {
|
||||||
|
name: "expired",
|
||||||
|
ip: anotherIP,
|
||||||
|
want: nil,
|
||||||
|
}, {
|
||||||
|
name: "v6",
|
||||||
|
ip: netip.MustParseAddr("ffff::1"),
|
||||||
|
want: nil,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
mac := s.FindMACbyIP(tc.ip)
|
||||||
|
|
||||||
|
require.Equal(t, tc.want, mac)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV4Server_handleDecline(t *testing.T) {
|
||||||
|
const (
|
||||||
|
dynamicName = "dynamic-client"
|
||||||
|
anotherName = "another-client"
|
||||||
|
)
|
||||||
|
|
||||||
|
dynamicIP := netip.MustParseAddr("192.168.10.200")
|
||||||
|
dynamicMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||||
|
|
||||||
|
s := defaultSrv(t)
|
||||||
|
|
||||||
|
s4, ok := s.(*v4Server)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
s4.leases = []*Lease{{
|
||||||
|
Hostname: dynamicName,
|
||||||
|
HWAddr: dynamicMAC,
|
||||||
|
IP: dynamicIP,
|
||||||
|
}}
|
||||||
|
|
||||||
|
req, err := dhcpv4.New(
|
||||||
|
dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(net.IP(dynamicIP.AsSlice()))),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req.ClientIPAddr = net.IP(dynamicIP.AsSlice())
|
||||||
|
req.ClientHWAddr = dynamicMAC
|
||||||
|
|
||||||
|
resp := &dhcpv4.DHCPv4{}
|
||||||
|
err = s4.handleDecline(req, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
wantResp := &dhcpv4.DHCPv4{
|
||||||
|
YourIPAddr: net.IP(s4.conf.RangeStart.AsSlice()),
|
||||||
|
Options: dhcpv4.OptionsFromList(
|
||||||
|
dhcpv4.OptMessageType(dhcpv4.MessageTypeAck),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, wantResp, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV4Server_handleRelease(t *testing.T) {
|
||||||
|
const (
|
||||||
|
dynamicName = "dymamic-client"
|
||||||
|
anotherName = "another-client"
|
||||||
|
)
|
||||||
|
|
||||||
|
dynamicIP := netip.MustParseAddr("192.168.10.200")
|
||||||
|
dynamicMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||||
|
|
||||||
|
s := defaultSrv(t)
|
||||||
|
|
||||||
|
s4, ok := s.(*v4Server)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
s4.leases = []*Lease{{
|
||||||
|
Hostname: dynamicName,
|
||||||
|
HWAddr: dynamicMAC,
|
||||||
|
IP: dynamicIP,
|
||||||
|
}}
|
||||||
|
|
||||||
|
req, err := dhcpv4.New(
|
||||||
|
dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(net.IP(dynamicIP.AsSlice()))),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req.ClientIPAddr = net.IP(dynamicIP.AsSlice())
|
||||||
|
req.ClientHWAddr = dynamicMAC
|
||||||
|
|
||||||
|
resp := &dhcpv4.DHCPv4{}
|
||||||
|
err = s4.handleRelease(req, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
wantResp := &dhcpv4.DHCPv4{
|
||||||
|
Options: dhcpv4.OptionsFromList(
|
||||||
|
dhcpv4.OptMessageType(dhcpv4.MessageTypeAck),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, wantResp, resp)
|
||||||
|
}
|
||||||
|
|
|
@ -61,13 +61,13 @@ func ip6InRange(start, ip net.IP) bool {
|
||||||
|
|
||||||
// ResetLeases resets leases.
|
// ResetLeases resets leases.
|
||||||
func (s *v6Server) ResetLeases(leases []*Lease) (err error) {
|
func (s *v6Server) ResetLeases(leases []*Lease) (err error) {
|
||||||
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
|
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
|
||||||
|
|
||||||
s.leases = nil
|
s.leases = nil
|
||||||
for _, l := range leases {
|
for _, l := range leases {
|
||||||
|
ip := net.IP(l.IP.AsSlice())
|
||||||
if l.Expiry.Unix() != leaseExpireStatic &&
|
if l.Expiry.Unix() != leaseExpireStatic &&
|
||||||
!ip6InRange(s.conf.ipStart, l.IP) {
|
!ip6InRange(s.conf.ipStart, ip) {
|
||||||
|
|
||||||
log.Debug("dhcpv6: skipping a lease with IP %v: not within current IP range", l.IP)
|
log.Debug("dhcpv6: skipping a lease with IP %v: not within current IP range", l.IP)
|
||||||
|
|
||||||
|
@ -119,10 +119,9 @@ func (s *v6Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
netIP := ip.AsSlice()
|
|
||||||
for _, l := range s.leases {
|
for _, l := range s.leases {
|
||||||
if l.IP.Equal(netIP) {
|
if l.IP == ip {
|
||||||
if l.Expiry.After(now) || l.IsStatic() {
|
if l.IsStatic || l.Expiry.After(now) {
|
||||||
return l.HWAddr
|
return l.HWAddr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +132,8 @@ func (s *v6Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
|
||||||
|
|
||||||
// Remove (swap) lease by index
|
// Remove (swap) lease by index
|
||||||
func (s *v6Server) leaseRemoveSwapByIndex(i int) {
|
func (s *v6Server) leaseRemoveSwapByIndex(i int) {
|
||||||
s.ipAddrs[s.leases[i].IP[15]] = 0
|
leaseIP := s.leases[i].IP.As16()
|
||||||
|
s.ipAddrs[leaseIP[15]] = 0
|
||||||
log.Debug("dhcpv6: removed lease %s", s.leases[i].HWAddr)
|
log.Debug("dhcpv6: removed lease %s", s.leases[i].HWAddr)
|
||||||
|
|
||||||
n := len(s.leases)
|
n := len(s.leases)
|
||||||
|
@ -162,7 +162,7 @@ func (s *v6Server) rmDynamicLease(lease *Lease) (err error) {
|
||||||
l = s.leases[i]
|
l = s.leases[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
if net.IP.Equal(l.IP, lease.IP) {
|
if l.IP == lease.IP {
|
||||||
if l.Expiry.Unix() == leaseExpireStatic {
|
if l.Expiry.Unix() == leaseExpireStatic {
|
||||||
return fmt.Errorf("static lease already exists")
|
return fmt.Errorf("static lease already exists")
|
||||||
}
|
}
|
||||||
|
@ -178,7 +178,7 @@ func (s *v6Server) rmDynamicLease(lease *Lease) (err error) {
|
||||||
func (s *v6Server) AddStaticLease(l *Lease) (err error) {
|
func (s *v6Server) AddStaticLease(l *Lease) (err error) {
|
||||||
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
|
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
|
||||||
|
|
||||||
if len(l.IP) != net.IPv6len {
|
if !l.IP.Is6() {
|
||||||
return fmt.Errorf("invalid IP")
|
return fmt.Errorf("invalid IP")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ func (s *v6Server) AddStaticLease(l *Lease) (err error) {
|
||||||
func (s *v6Server) RemoveStaticLease(l *Lease) (err error) {
|
func (s *v6Server) RemoveStaticLease(l *Lease) (err error) {
|
||||||
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
|
defer func() { err = errors.Annotate(err, "dhcpv6: %w") }()
|
||||||
|
|
||||||
if len(l.IP) != 16 {
|
if !l.IP.Is6() {
|
||||||
return fmt.Errorf("invalid IP")
|
return fmt.Errorf("invalid IP")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,14 +234,15 @@ func (s *v6Server) RemoveStaticLease(l *Lease) (err error) {
|
||||||
// Add a lease
|
// Add a lease
|
||||||
func (s *v6Server) addLease(l *Lease) {
|
func (s *v6Server) addLease(l *Lease) {
|
||||||
s.leases = append(s.leases, l)
|
s.leases = append(s.leases, l)
|
||||||
s.ipAddrs[l.IP[15]] = 1
|
ip := l.IP.As16()
|
||||||
|
s.ipAddrs[ip[15]] = 1
|
||||||
log.Debug("dhcpv6: added lease %s <-> %s", l.IP, l.HWAddr)
|
log.Debug("dhcpv6: added lease %s <-> %s", l.IP, l.HWAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a lease with the same properties
|
// Remove a lease with the same properties
|
||||||
func (s *v6Server) rmLease(lease *Lease) (err error) {
|
func (s *v6Server) rmLease(lease *Lease) (err error) {
|
||||||
for i, l := range s.leases {
|
for i, l := range s.leases {
|
||||||
if net.IP.Equal(l.IP, lease.IP) {
|
if l.IP == lease.IP {
|
||||||
if !bytes.Equal(l.HWAddr, lease.HWAddr) ||
|
if !bytes.Equal(l.HWAddr, lease.HWAddr) ||
|
||||||
l.Hostname != lease.Hostname {
|
l.Hostname != lease.Hostname {
|
||||||
return fmt.Errorf("lease not found")
|
return fmt.Errorf("lease not found")
|
||||||
|
@ -308,18 +309,27 @@ func (s *v6Server) reserveLease(mac net.HardwareAddr) *Lease {
|
||||||
s.leasesLock.Lock()
|
s.leasesLock.Lock()
|
||||||
defer s.leasesLock.Unlock()
|
defer s.leasesLock.Unlock()
|
||||||
|
|
||||||
copy(l.IP, s.conf.ipStart)
|
ip := s.findFreeIP()
|
||||||
l.IP = s.findFreeIP()
|
if ip == nil {
|
||||||
if l.IP == nil {
|
|
||||||
i := s.findExpiredLease()
|
i := s.findExpiredLease()
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(s.leases[i].HWAddr, mac)
|
copy(s.leases[i].HWAddr, mac)
|
||||||
|
|
||||||
return s.leases[i]
|
return s.leases[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
netIP, ok := netip.AddrFromSlice(ip)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
l.IP = netIP
|
||||||
|
|
||||||
s.addLease(&l)
|
s.addLease(&l)
|
||||||
|
|
||||||
return &l
|
return &l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -388,7 +398,8 @@ func (s *v6Server) checkIA(msg *dhcpv6.Message, lease *Lease) error {
|
||||||
return fmt.Errorf("no IANA.Addr option in %s", msg.Type().String())
|
return fmt.Errorf("no IANA.Addr option in %s", msg.Type().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if !oiaAddr.IPv6Addr.Equal(lease.IP) {
|
leaseIP := net.IP(lease.IP.AsSlice())
|
||||||
|
if !oiaAddr.IPv6Addr.Equal(leaseIP) {
|
||||||
return fmt.Errorf("invalid IANA.Addr option in %s", msg.Type().String())
|
return fmt.Errorf("invalid IANA.Addr option in %s", msg.Type().String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -475,7 +486,7 @@ func (s *v6Server) process(msg *dhcpv6.Message, req, resp dhcpv6.DHCPv6) bool {
|
||||||
copy(oia.IaId[:], []byte(valueIAID))
|
copy(oia.IaId[:], []byte(valueIAID))
|
||||||
}
|
}
|
||||||
oiaAddr := &dhcpv6.OptIAAddress{
|
oiaAddr := &dhcpv6.OptIAAddress{
|
||||||
IPv6Addr: lease.IP,
|
IPv6Addr: net.IP(lease.IP.AsSlice()),
|
||||||
PreferredLifetime: lifetime,
|
PreferredLifetime: lifetime,
|
||||||
ValidLifetime: lifetime,
|
ValidLifetime: lifetime,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,9 @@ package dhcpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||||
"github.com/insomniacslk/dhcp/iana"
|
"github.com/insomniacslk/dhcp/iana"
|
||||||
|
@ -27,7 +29,7 @@ func TestV6_AddRemove_static(t *testing.T) {
|
||||||
|
|
||||||
// Add static lease.
|
// Add static lease.
|
||||||
l := &Lease{
|
l := &Lease{
|
||||||
IP: net.ParseIP("2001::1"),
|
IP: netip.MustParseAddr("2001::1"),
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
}
|
}
|
||||||
err = s.AddStaticLease(l)
|
err = s.AddStaticLease(l)
|
||||||
|
@ -46,7 +48,7 @@ func TestV6_AddRemove_static(t *testing.T) {
|
||||||
|
|
||||||
// Try to remove non-existent static lease.
|
// Try to remove non-existent static lease.
|
||||||
err = s.RemoveStaticLease(&Lease{
|
err = s.RemoveStaticLease(&Lease{
|
||||||
IP: net.ParseIP("2001::2"),
|
IP: netip.MustParseAddr("2001::2"),
|
||||||
HWAddr: l.HWAddr,
|
HWAddr: l.HWAddr,
|
||||||
})
|
})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
@ -71,10 +73,10 @@ func TestV6_AddReplace(t *testing.T) {
|
||||||
|
|
||||||
// Add dynamic leases.
|
// Add dynamic leases.
|
||||||
dynLeases := []*Lease{{
|
dynLeases := []*Lease{{
|
||||||
IP: net.ParseIP("2001::1"),
|
IP: netip.MustParseAddr("2001::1"),
|
||||||
HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0x11, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
}, {
|
}, {
|
||||||
IP: net.ParseIP("2001::2"),
|
IP: netip.MustParseAddr("2001::2"),
|
||||||
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
@ -83,10 +85,10 @@ func TestV6_AddReplace(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
stLeases := []*Lease{{
|
stLeases := []*Lease{{
|
||||||
IP: net.ParseIP("2001::1"),
|
IP: netip.MustParseAddr("2001::1"),
|
||||||
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0x33, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
}, {
|
}, {
|
||||||
IP: net.ParseIP("2001::3"),
|
IP: netip.MustParseAddr("2001::3"),
|
||||||
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0x22, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
@ -99,7 +101,7 @@ func TestV6_AddReplace(t *testing.T) {
|
||||||
require.Len(t, ls, 2)
|
require.Len(t, ls, 2)
|
||||||
|
|
||||||
for i, l := range ls {
|
for i, l := range ls {
|
||||||
assert.True(t, stLeases[i].IP.Equal(l.IP))
|
assert.Equal(t, stLeases[i].IP, l.IP)
|
||||||
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
|
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
|
||||||
assert.EqualValues(t, leaseExpireStatic, l.Expiry.Unix())
|
assert.EqualValues(t, leaseExpireStatic, l.Expiry.Unix())
|
||||||
}
|
}
|
||||||
|
@ -126,7 +128,7 @@ func TestV6GetLease(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
l := &Lease{
|
l := &Lease{
|
||||||
IP: net.ParseIP("2001::1"),
|
IP: netip.MustParseAddr("2001::1"),
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
}
|
}
|
||||||
err = s.AddStaticLease(l)
|
err = s.AddStaticLease(l)
|
||||||
|
@ -158,7 +160,8 @@ func TestV6GetLease(t *testing.T) {
|
||||||
oia = resp.Options.OneIANA()
|
oia = resp.Options.OneIANA()
|
||||||
oiaAddr = oia.Options.OneAddress()
|
oiaAddr = oia.Options.OneAddress()
|
||||||
|
|
||||||
assert.Equal(t, l.IP, oiaAddr.IPv6Addr)
|
ip := net.IP(l.IP.AsSlice())
|
||||||
|
assert.Equal(t, ip, oiaAddr.IPv6Addr)
|
||||||
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
|
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -182,7 +185,8 @@ func TestV6GetLease(t *testing.T) {
|
||||||
oia = resp.Options.OneIANA()
|
oia = resp.Options.OneIANA()
|
||||||
oiaAddr = oia.Options.OneAddress()
|
oiaAddr = oia.Options.OneAddress()
|
||||||
|
|
||||||
assert.Equal(t, l.IP, oiaAddr.IPv6Addr)
|
ip := net.IP(l.IP.AsSlice())
|
||||||
|
assert.Equal(t, ip, oiaAddr.IPv6Addr)
|
||||||
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
|
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -308,3 +312,74 @@ func TestIP6InRange(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestV6_FindMACbyIP(t *testing.T) {
|
||||||
|
const (
|
||||||
|
staticName = "static-client"
|
||||||
|
anotherName = "another-client"
|
||||||
|
)
|
||||||
|
|
||||||
|
staticIP := netip.MustParseAddr("2001::1")
|
||||||
|
staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||||
|
|
||||||
|
anotherIP := netip.MustParseAddr("2001::100")
|
||||||
|
anotherMAC := net.HardwareAddr{0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB}
|
||||||
|
|
||||||
|
s := &v6Server{
|
||||||
|
leases: []*Lease{{
|
||||||
|
Expiry: time.Unix(leaseExpireStatic, 0),
|
||||||
|
Hostname: staticName,
|
||||||
|
HWAddr: staticMAC,
|
||||||
|
IP: staticIP,
|
||||||
|
IsStatic: true,
|
||||||
|
}, {
|
||||||
|
Expiry: time.Unix(10, 0),
|
||||||
|
Hostname: anotherName,
|
||||||
|
HWAddr: anotherMAC,
|
||||||
|
IP: anotherIP,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
s.leases = []*Lease{{
|
||||||
|
Expiry: time.Unix(leaseExpireStatic, 0),
|
||||||
|
Hostname: staticName,
|
||||||
|
HWAddr: staticMAC,
|
||||||
|
IP: staticIP,
|
||||||
|
IsStatic: true,
|
||||||
|
}, {
|
||||||
|
Expiry: time.Unix(10, 0),
|
||||||
|
Hostname: anotherName,
|
||||||
|
HWAddr: anotherMAC,
|
||||||
|
IP: anotherIP,
|
||||||
|
}}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
want net.HardwareAddr
|
||||||
|
ip netip.Addr
|
||||||
|
name string
|
||||||
|
}{{
|
||||||
|
name: "basic",
|
||||||
|
ip: staticIP,
|
||||||
|
want: staticMAC,
|
||||||
|
}, {
|
||||||
|
name: "not_found",
|
||||||
|
ip: netip.MustParseAddr("ffff::1"),
|
||||||
|
want: nil,
|
||||||
|
}, {
|
||||||
|
name: "expired",
|
||||||
|
ip: anotherIP,
|
||||||
|
want: nil,
|
||||||
|
}, {
|
||||||
|
name: "v4",
|
||||||
|
ip: netip.MustParseAddr("1.2.3.4"),
|
||||||
|
want: nil,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
mac := s.FindMACbyIP(tc.ip)
|
||||||
|
|
||||||
|
require.Equal(t, tc.want, mac)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -81,6 +81,10 @@ type FilteringConfig struct {
|
||||||
// 0, then default value is used (3600).
|
// 0, then default value is used (3600).
|
||||||
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"`
|
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"`
|
||||||
|
|
||||||
|
// ProtectionDisabledUntil is the timestamp until when the protection is
|
||||||
|
// disabled.
|
||||||
|
ProtectionDisabledUntil *time.Time `yaml:"protection_disabled_until"`
|
||||||
|
|
||||||
// ParentalBlockHost is the IP (or domain name) which is used to respond to
|
// ParentalBlockHost is the IP (or domain name) which is used to respond to
|
||||||
// DNS requests blocked by parental control.
|
// DNS requests blocked by parental control.
|
||||||
ParentalBlockHost string `yaml:"parental_block_host"`
|
ParentalBlockHost string `yaml:"parental_block_host"`
|
||||||
|
@ -195,12 +199,16 @@ type FilteringConfig struct {
|
||||||
// IpsetListFileName, if set, points to the file with ipset configuration.
|
// IpsetListFileName, if set, points to the file with ipset configuration.
|
||||||
// The format is the same as in [IpsetList].
|
// The format is the same as in [IpsetList].
|
||||||
IpsetListFileName string `yaml:"ipset_file"`
|
IpsetListFileName string `yaml:"ipset_file"`
|
||||||
|
|
||||||
|
// BootstrapPreferIPv6, if true, instructs the bootstrapper to prefer IPv6
|
||||||
|
// addresses to IPv4 ones for DoH, DoQ, and DoT.
|
||||||
|
BootstrapPreferIPv6 bool `yaml:"bootstrap_prefer_ipv6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EDNSClientSubnet is the settings list for EDNS Client Subnet.
|
// EDNSClientSubnet is the settings list for EDNS Client Subnet.
|
||||||
type EDNSClientSubnet struct {
|
type EDNSClientSubnet struct {
|
||||||
// CustomIP for EDNS Client Subnet.
|
// CustomIP for EDNS Client Subnet.
|
||||||
CustomIP string `yaml:"custom_ip"`
|
CustomIP netip.Addr `yaml:"custom_ip"`
|
||||||
|
|
||||||
// Enabled defines if EDNS Client Subnet is enabled.
|
// Enabled defines if EDNS Client Subnet is enabled.
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
|
@ -340,15 +348,8 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if srvConf.EDNSClientSubnet.UseCustom {
|
if srvConf.EDNSClientSubnet.UseCustom {
|
||||||
// TODO(s.chzhen): Add wrapper around netip.Addr.
|
|
||||||
var ip net.IP
|
|
||||||
ip, err = netutil.ParseIP(srvConf.EDNSClientSubnet.CustomIP)
|
|
||||||
if err != nil {
|
|
||||||
return conf, fmt.Errorf("edns: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(s.chzhen): Use netip.Addr instead of net.IP inside dnsproxy.
|
// TODO(s.chzhen): Use netip.Addr instead of net.IP inside dnsproxy.
|
||||||
conf.EDNSAddr = ip
|
conf.EDNSAddr = net.IP(srvConf.EDNSClientSubnet.CustomIP.AsSlice())
|
||||||
}
|
}
|
||||||
|
|
||||||
if srvConf.CacheSize != 0 {
|
if srvConf.CacheSize != 0 {
|
||||||
|
@ -377,7 +378,7 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) {
|
||||||
|
|
||||||
err = s.prepareTLS(&conf)
|
err = s.prepareTLS(&conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return conf, fmt.Errorf("validating tls: %w", err)
|
return proxy.Config{}, fmt.Errorf("validating tls: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c := srvConf.DNSCryptConfig; c.Enabled {
|
if c := srvConf.DNSCryptConfig; c.Enabled {
|
||||||
|
@ -388,7 +389,7 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.UpstreamConfig == nil || len(conf.UpstreamConfig.Upstreams) == 0 {
|
if conf.UpstreamConfig == nil || len(conf.UpstreamConfig.Upstreams) == 0 {
|
||||||
return conf, errors.Error("no default upstream servers configured")
|
return proxy.Config{}, errors.Error("no default upstream servers configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
return conf, nil
|
return conf, nil
|
||||||
|
@ -482,6 +483,7 @@ func (s *Server) prepareUpstreamSettings() error {
|
||||||
Bootstrap: s.conf.BootstrapDNS,
|
Bootstrap: s.conf.BootstrapDNS,
|
||||||
Timeout: s.conf.UpstreamTimeout,
|
Timeout: s.conf.UpstreamTimeout,
|
||||||
HTTPVersions: httpVersions,
|
HTTPVersions: httpVersions,
|
||||||
|
PreferIPv6: s.conf.BootstrapPreferIPv6,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -497,6 +499,7 @@ func (s *Server) prepareUpstreamSettings() error {
|
||||||
Bootstrap: s.conf.BootstrapDNS,
|
Bootstrap: s.conf.BootstrapDNS,
|
||||||
Timeout: s.conf.UpstreamTimeout,
|
Timeout: s.conf.UpstreamTimeout,
|
||||||
HTTPVersions: httpVersions,
|
HTTPVersions: httpVersions,
|
||||||
|
PreferIPv6: s.conf.BootstrapPreferIPv6,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -584,11 +587,11 @@ func (s *Server) prepareTLS(proxyConfig *proxy.Config) (err error) {
|
||||||
if s.conf.StrictSNICheck {
|
if s.conf.StrictSNICheck {
|
||||||
if len(cert.DNSNames) != 0 {
|
if len(cert.DNSNames) != 0 {
|
||||||
s.conf.dnsNames = cert.DNSNames
|
s.conf.dnsNames = cert.DNSNames
|
||||||
log.Debug("dnsforward: using certificate's SAN as DNS names: %v", cert.DNSNames)
|
log.Debug("dns: using certificate's SAN as DNS names: %v", cert.DNSNames)
|
||||||
slices.Sort(s.conf.dnsNames)
|
slices.Sort(s.conf.dnsNames)
|
||||||
} else {
|
} else {
|
||||||
s.conf.dnsNames = append(s.conf.dnsNames, cert.Subject.CommonName)
|
s.conf.dnsNames = append(s.conf.dnsNames, cert.Subject.CommonName)
|
||||||
log.Debug("dnsforward: using certificate's CN as DNS name: %s", cert.Subject.CommonName)
|
log.Debug("dns: using certificate's CN as DNS name: %s", cert.Subject.CommonName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -642,3 +645,49 @@ func (s *Server) onGetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, er
|
||||||
}
|
}
|
||||||
return &s.conf.cert, nil
|
return &s.conf.cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdatedProtectionStatus updates protection state, if the protection was
|
||||||
|
// disabled temporarily. Returns the updated state of protection.
|
||||||
|
func (s *Server) UpdatedProtectionStatus() (enabled bool, disabledUntil *time.Time) {
|
||||||
|
s.serverLock.RLock()
|
||||||
|
defer s.serverLock.RUnlock()
|
||||||
|
|
||||||
|
disabledUntil = s.conf.ProtectionDisabledUntil
|
||||||
|
if disabledUntil == nil {
|
||||||
|
return s.conf.ProtectionEnabled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().Before(*disabledUntil) {
|
||||||
|
return false, disabledUntil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the values in a separate goroutine, unless an update is already in
|
||||||
|
// progress. Since this method is called very often, and this update is a
|
||||||
|
// relatively rare situation, do not lock s.serverLock for writing, as that
|
||||||
|
// can lead to freezes.
|
||||||
|
//
|
||||||
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/5661.
|
||||||
|
if s.protectionUpdateInProgress.CompareAndSwap(false, true) {
|
||||||
|
go s.enableProtectionAfterPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// enableProtectionAfterPause sets the protection configuration to enabled
|
||||||
|
// values. It is intended to be used as a goroutine.
|
||||||
|
func (s *Server) enableProtectionAfterPause() {
|
||||||
|
defer log.OnPanic("dns: enabling protection after pause")
|
||||||
|
|
||||||
|
defer s.protectionUpdateInProgress.Store(false)
|
||||||
|
|
||||||
|
defer s.conf.ConfigModified()
|
||||||
|
|
||||||
|
s.serverLock.Lock()
|
||||||
|
defer s.serverLock.Unlock()
|
||||||
|
|
||||||
|
s.conf.ProtectionEnabled = true
|
||||||
|
s.conf.ProtectionDisabledUntil = nil
|
||||||
|
|
||||||
|
log.Info("dns: protection is restarted after pause")
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ import (
|
||||||
// To transfer information between modules
|
// To transfer information between modules
|
||||||
//
|
//
|
||||||
// TODO(s.chzhen): Add lowercased, non-FQDN version of the hostname from the
|
// TODO(s.chzhen): Add lowercased, non-FQDN version of the hostname from the
|
||||||
// question of the request.
|
// question of the request. Add persistent client.
|
||||||
type dnsContext struct {
|
type dnsContext struct {
|
||||||
proxyCtx *proxy.DNSContext
|
proxyCtx *proxy.DNSContext
|
||||||
|
|
||||||
|
@ -37,6 +38,8 @@ type dnsContext struct {
|
||||||
// was parsed successfully and belongs to one of the locally served IP
|
// was parsed successfully and belongs to one of the locally served IP
|
||||||
// ranges. It is also filled with unmapped version of the address if it's
|
// ranges. It is also filled with unmapped version of the address if it's
|
||||||
// within DNS64 prefixes.
|
// within DNS64 prefixes.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Use netip.Addr when we switch to netip more fully.
|
||||||
unreversedReqIP net.IP
|
unreversedReqIP net.IP
|
||||||
|
|
||||||
// err is the error returned from a processing function.
|
// err is the error returned from a processing function.
|
||||||
|
@ -181,6 +184,21 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
|
||||||
return resultCodeFinish
|
return resultCodeFinish
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle a reserved domain healthcheck.adguardhome.test.
|
||||||
|
//
|
||||||
|
// [Section 6.2 of RFC 6761] states that DNS Registries/Registrars must not
|
||||||
|
// grant requests to register test names in the normal way to any person or
|
||||||
|
// entity, making domain names under test. TLD free to use in internal
|
||||||
|
// purposes.
|
||||||
|
//
|
||||||
|
// [Section 6.2 of RFC 6761]: https://www.rfc-editor.org/rfc/rfc6761.html#section-6.2
|
||||||
|
if q.Name == "healthcheck.adguardhome.test." {
|
||||||
|
// Generate a NODATA negative response to make nslookup exit with 0.
|
||||||
|
pctx.Res = s.makeResponse(pctx.Req)
|
||||||
|
|
||||||
|
return resultCodeFinish
|
||||||
|
}
|
||||||
|
|
||||||
// Get the ClientID, if any, before getting client-specific filtering
|
// Get the ClientID, if any, before getting client-specific filtering
|
||||||
// settings.
|
// settings.
|
||||||
var key [8]byte
|
var key [8]byte
|
||||||
|
@ -188,7 +206,7 @@ func (s *Server) processInitial(dctx *dnsContext) (rc resultCode) {
|
||||||
dctx.clientID = string(s.clientIDCache.Get(key[:]))
|
dctx.clientID = string(s.clientIDCache.Get(key[:]))
|
||||||
|
|
||||||
// Get the client-specific filtering settings.
|
// Get the client-specific filtering settings.
|
||||||
dctx.protectionEnabled = s.conf.ProtectionEnabled
|
dctx.protectionEnabled, _ = s.UpdatedProtectionStatus()
|
||||||
dctx.setts = s.getClientRequestFilteringSettings(dctx)
|
dctx.setts = s.getClientRequestFilteringSettings(dctx)
|
||||||
|
|
||||||
return resultCodeSuccess
|
return resultCodeSuccess
|
||||||
|
@ -240,17 +258,16 @@ func (s *Server) onDHCPLeaseChanged(flags int) {
|
||||||
lowhost := strings.ToLower(l.Hostname + "." + s.localDomainSuffix)
|
lowhost := strings.ToLower(l.Hostname + "." + s.localDomainSuffix)
|
||||||
|
|
||||||
// Assume that we only process IPv4 now.
|
// Assume that we only process IPv4 now.
|
||||||
//
|
if !l.IP.Is4() {
|
||||||
// TODO(a.garipov): Remove once we switch to netip.Addr more fully.
|
log.Debug("dnsforward: skipping invalid ip from dhcp: bad ipv4 net.IP %v", l.IP)
|
||||||
ip, err := netutil.IPToAddr(l.IP, netutil.AddrFamilyIPv4)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("dnsforward: skipping invalid ip %v from dhcp: %s", l.IP, err)
|
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ipToHost[ip] = lowhost
|
leaseIP := l.IP
|
||||||
hostToIP[lowhost] = ip
|
|
||||||
|
ipToHost[leaseIP] = lowhost
|
||||||
|
hostToIP[lowhost] = leaseIP
|
||||||
}
|
}
|
||||||
|
|
||||||
s.setTableHostToIP(hostToIP)
|
s.setTableHostToIP(hostToIP)
|
||||||
|
@ -442,6 +459,88 @@ func (s *Server) processDHCPHosts(dctx *dnsContext) (rc resultCode) {
|
||||||
return resultCodeSuccess
|
return resultCodeSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// indexFirstV4Label returns the index at which the reversed IPv4 address
|
||||||
|
// starts, assuming the domain is pre-validated ARPA domain having in-addr and
|
||||||
|
// arpa labels removed.
|
||||||
|
func indexFirstV4Label(domain string) (idx int) {
|
||||||
|
idx = len(domain)
|
||||||
|
for labelsNum := 0; labelsNum < net.IPv4len && idx > 0; labelsNum++ {
|
||||||
|
curIdx := strings.LastIndexByte(domain[:idx-1], '.') + 1
|
||||||
|
_, parseErr := strconv.ParseUint(domain[curIdx:idx-1], 10, 8)
|
||||||
|
if parseErr != nil {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
idx = curIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexFirstV6Label returns the index at which the reversed IPv6 address
|
||||||
|
// starts, assuming the domain is pre-validated ARPA domain having ip6 and arpa
|
||||||
|
// labels removed.
|
||||||
|
func indexFirstV6Label(domain string) (idx int) {
|
||||||
|
idx = len(domain)
|
||||||
|
for labelsNum := 0; labelsNum < net.IPv6len*2 && idx > 0; labelsNum++ {
|
||||||
|
curIdx := idx - len("a.")
|
||||||
|
if curIdx > 1 && domain[curIdx-1] != '.' {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
nibble := domain[curIdx]
|
||||||
|
if (nibble < '0' || nibble > '9') && (nibble < 'a' || nibble > 'f') {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
idx = curIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractARPASubnet tries to convert a reversed ARPA address being a part of
|
||||||
|
// domain to an IP network. domain must be an FQDN.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Move to golibs.
|
||||||
|
func extractARPASubnet(domain string) (pref netip.Prefix, err error) {
|
||||||
|
err = netutil.ValidateDomainName(strings.TrimSuffix(domain, "."))
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error since it's informative enough as is.
|
||||||
|
return netip.Prefix{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
v4Suffix = "in-addr.arpa."
|
||||||
|
v6Suffix = "ip6.arpa."
|
||||||
|
)
|
||||||
|
|
||||||
|
domain = strings.ToLower(domain)
|
||||||
|
|
||||||
|
var idx int
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(domain, v4Suffix):
|
||||||
|
idx = indexFirstV4Label(domain[:len(domain)-len(v4Suffix)])
|
||||||
|
case strings.HasSuffix(domain, v6Suffix):
|
||||||
|
idx = indexFirstV6Label(domain[:len(domain)-len(v6Suffix)])
|
||||||
|
default:
|
||||||
|
return netip.Prefix{}, &netutil.AddrError{
|
||||||
|
Err: netutil.ErrNotAReversedSubnet,
|
||||||
|
Kind: netutil.AddrKindARPA,
|
||||||
|
Addr: domain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var subnet *net.IPNet
|
||||||
|
subnet, err = netutil.SubnetFromReversedAddr(domain[idx:])
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error since it's informative enough as is.
|
||||||
|
return netip.Prefix{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return netutil.IPNetToPrefixNoMapped(subnet)
|
||||||
|
}
|
||||||
|
|
||||||
// processRestrictLocal responds with NXDOMAIN to PTR requests for IP addresses
|
// processRestrictLocal responds with NXDOMAIN to PTR requests for IP addresses
|
||||||
// in locally served network from external clients.
|
// in locally served network from external clients.
|
||||||
func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
|
func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
|
||||||
|
@ -453,34 +552,29 @@ func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
|
||||||
return resultCodeSuccess
|
return resultCodeSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := netutil.IPFromReversedAddr(q.Name)
|
subnet, err := extractARPASubnet(q.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("dnsforward: parsing reversed addr: %s", err)
|
if errors.Is(err, netutil.ErrNotAReversedSubnet) {
|
||||||
|
log.Debug("dnsforward: request is not for arpa domain")
|
||||||
|
|
||||||
// DNS-Based Service Discovery uses PTR records having not an ARPA
|
return resultCodeSuccess
|
||||||
// format of the domain name in question. Those shouldn't be
|
|
||||||
// invalidated. See http://www.dns-sd.org/ServerStaticSetup.html and
|
|
||||||
// RFC 2782.
|
|
||||||
name := strings.TrimSuffix(q.Name, ".")
|
|
||||||
if err = netutil.ValidateSRVDomainName(name); err != nil {
|
|
||||||
log.Debug("dnsforward: validating service domain: %s", err)
|
|
||||||
|
|
||||||
return resultCodeError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("dnsforward: request is not for arpa domain")
|
log.Debug("dnsforward: parsing reversed addr: %s", err)
|
||||||
|
|
||||||
return resultCodeSuccess
|
return resultCodeError
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restrict an access to local addresses for external clients. We also
|
// Restrict an access to local addresses for external clients. We also
|
||||||
// assume that all the DHCP leases we give are locally served or at least
|
// assume that all the DHCP leases we give are locally served or at least
|
||||||
// shouldn't be accessible externally.
|
// shouldn't be accessible externally.
|
||||||
if !s.privateNets.Contains(ip) {
|
subnetAddr := subnet.Addr()
|
||||||
|
addrData := subnetAddr.AsSlice()
|
||||||
|
if !s.privateNets.Contains(addrData) {
|
||||||
return resultCodeSuccess
|
return resultCodeSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("dnsforward: addr %s is from locally served network", ip)
|
log.Debug("dnsforward: addr %s is from locally served network", subnetAddr)
|
||||||
|
|
||||||
if !dctx.isLocalClient {
|
if !dctx.isLocalClient {
|
||||||
log.Debug("dnsforward: %q requests an internal ip", pctx.Addr)
|
log.Debug("dnsforward: %q requests an internal ip", pctx.Addr)
|
||||||
|
@ -491,7 +585,7 @@ func (s *Server) processRestrictLocal(dctx *dnsContext) (rc resultCode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not perform unreversing ever again.
|
// Do not perform unreversing ever again.
|
||||||
dctx.unreversedReqIP = ip
|
dctx.unreversedReqIP = addrData
|
||||||
|
|
||||||
// There is no need to filter request from external addresses since this
|
// There is no need to filter request from external addresses since this
|
||||||
// code is only executed when the request is for locally served ARPA
|
// code is only executed when the request is for locally served ARPA
|
||||||
|
|
|
@ -36,8 +36,6 @@ func (s *Server) setupDNS64() {
|
||||||
// valid IPv4. It panics, if there are no configured DNS64 prefixes, because
|
// valid IPv4. It panics, if there are no configured DNS64 prefixes, because
|
||||||
// synthesis should not be performed unless DNS64 function enabled.
|
// synthesis should not be performed unless DNS64 function enabled.
|
||||||
func (s *Server) mapDNS64(ip netip.Addr) (mapped net.IP) {
|
func (s *Server) mapDNS64(ip netip.Addr) (mapped net.IP) {
|
||||||
// Don't mask the address here since it should have already been masked on
|
|
||||||
// initialization stage.
|
|
||||||
pref := s.dns64Pref.Masked().Addr().As16()
|
pref := s.dns64Pref.Masked().Addr().As16()
|
||||||
ipData := ip.As4()
|
ipData := ip.As4()
|
||||||
|
|
||||||
|
|
|
@ -605,3 +605,129 @@ func TestIPStringFromAddr(t *testing.T) {
|
||||||
assert.Empty(t, ipStringFromAddr(nil))
|
assert.Empty(t, ipStringFromAddr(nil))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(e.burkov): Add fuzzing when moving to golibs.
|
||||||
|
func TestExtractARPASubnet(t *testing.T) {
|
||||||
|
const (
|
||||||
|
v4Suf = `in-addr.arpa.`
|
||||||
|
v4Part = `2.1.` + v4Suf
|
||||||
|
v4Whole = `4.3.` + v4Part
|
||||||
|
|
||||||
|
v6Suf = `ip6.arpa.`
|
||||||
|
v6Part = `4.3.2.1.0.0.0.0.0.0.0.0.0.0.0.0.` + v6Suf
|
||||||
|
v6Whole = `f.e.d.c.0.0.0.0.0.0.0.0.0.0.0.0.` + v6Part
|
||||||
|
)
|
||||||
|
|
||||||
|
v4Pref := netip.MustParsePrefix("1.2.3.4/32")
|
||||||
|
v4PrefPart := netip.MustParsePrefix("1.2.0.0/16")
|
||||||
|
v6Pref := netip.MustParsePrefix("::1234:0:0:0:cdef/128")
|
||||||
|
v6PrefPart := netip.MustParsePrefix("0:0:0:1234::/64")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
want netip.Prefix
|
||||||
|
name string
|
||||||
|
domain string
|
||||||
|
wantErr string
|
||||||
|
}{{
|
||||||
|
want: netip.Prefix{},
|
||||||
|
name: "not_an_arpa",
|
||||||
|
domain: "some.domain.name.",
|
||||||
|
wantErr: `bad arpa domain name "some.domain.name.": ` +
|
||||||
|
`not a reversed ip network`,
|
||||||
|
}, {
|
||||||
|
want: netip.Prefix{},
|
||||||
|
name: "bad_domain_name",
|
||||||
|
domain: "abc.123.",
|
||||||
|
wantErr: `bad domain name "abc.123": ` +
|
||||||
|
`bad top-level domain name label "123": all octets are numeric`,
|
||||||
|
}, {
|
||||||
|
want: v4Pref,
|
||||||
|
name: "whole_v4",
|
||||||
|
domain: v4Whole,
|
||||||
|
wantErr: "",
|
||||||
|
}, {
|
||||||
|
want: v4PrefPart,
|
||||||
|
name: "partial_v4",
|
||||||
|
domain: v4Part,
|
||||||
|
wantErr: "",
|
||||||
|
}, {
|
||||||
|
want: v4Pref,
|
||||||
|
name: "whole_v4_within_domain",
|
||||||
|
domain: "a." + v4Whole,
|
||||||
|
wantErr: "",
|
||||||
|
}, {
|
||||||
|
want: v4Pref,
|
||||||
|
name: "whole_v4_additional_label",
|
||||||
|
domain: "5." + v4Whole,
|
||||||
|
wantErr: "",
|
||||||
|
}, {
|
||||||
|
want: v4PrefPart,
|
||||||
|
name: "partial_v4_within_domain",
|
||||||
|
domain: "a." + v4Part,
|
||||||
|
wantErr: "",
|
||||||
|
}, {
|
||||||
|
want: v4PrefPart,
|
||||||
|
name: "overflow_v4",
|
||||||
|
domain: "256." + v4Part,
|
||||||
|
wantErr: "",
|
||||||
|
}, {
|
||||||
|
want: v4PrefPart,
|
||||||
|
name: "overflow_v4_within_domain",
|
||||||
|
domain: "a.256." + v4Part,
|
||||||
|
wantErr: "",
|
||||||
|
}, {
|
||||||
|
want: netip.Prefix{},
|
||||||
|
name: "empty_v4",
|
||||||
|
domain: v4Suf,
|
||||||
|
wantErr: `bad arpa domain name "in-addr.arpa": ` +
|
||||||
|
`not a reversed ip network`,
|
||||||
|
}, {
|
||||||
|
want: netip.Prefix{},
|
||||||
|
name: "empty_v4_within_domain",
|
||||||
|
domain: "a." + v4Suf,
|
||||||
|
wantErr: `bad arpa domain name "in-addr.arpa": ` +
|
||||||
|
`not a reversed ip network`,
|
||||||
|
}, {
|
||||||
|
want: v6Pref,
|
||||||
|
name: "whole_v6",
|
||||||
|
domain: v6Whole,
|
||||||
|
wantErr: "",
|
||||||
|
}, {
|
||||||
|
want: v6PrefPart,
|
||||||
|
name: "partial_v6",
|
||||||
|
domain: v6Part,
|
||||||
|
}, {
|
||||||
|
want: v6Pref,
|
||||||
|
name: "whole_v6_within_domain",
|
||||||
|
domain: "g." + v6Whole,
|
||||||
|
wantErr: "",
|
||||||
|
}, {
|
||||||
|
want: v6Pref,
|
||||||
|
name: "whole_v6_additional_label",
|
||||||
|
domain: "1." + v6Whole,
|
||||||
|
wantErr: "",
|
||||||
|
}, {
|
||||||
|
want: v6PrefPart,
|
||||||
|
name: "partial_v6_within_domain",
|
||||||
|
domain: "label." + v6Part,
|
||||||
|
wantErr: "",
|
||||||
|
}, {
|
||||||
|
want: netip.Prefix{},
|
||||||
|
name: "empty_v6",
|
||||||
|
domain: v6Suf,
|
||||||
|
wantErr: `bad arpa domain name "ip6.arpa": not a reversed ip network`,
|
||||||
|
}, {
|
||||||
|
want: netip.Prefix{},
|
||||||
|
name: "empty_v6_within_domain",
|
||||||
|
domain: "g." + v6Suf,
|
||||||
|
wantErr: `bad arpa domain name "ip6.arpa": not a reversed ip network`,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
subnet, err := extractARPASubnet(tc.domain)
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErr, err)
|
||||||
|
assert.Equal(t, tc.want, subnet)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
|
@ -111,6 +112,10 @@ type Server struct {
|
||||||
|
|
||||||
isRunning bool
|
isRunning bool
|
||||||
|
|
||||||
|
// protectionUpdateInProgress is used to make sure that only one goroutine
|
||||||
|
// updating the protection configuration after a pause is running at a time.
|
||||||
|
protectionUpdateInProgress atomic.Bool
|
||||||
|
|
||||||
conf ServerConfig
|
conf ServerConfig
|
||||||
// serverLock protects Server.
|
// serverLock protects Server.
|
||||||
serverLock sync.RWMutex
|
serverLock sync.RWMutex
|
||||||
|
@ -447,6 +452,8 @@ func (s *Server) setupResolvers(localAddrs []string) (err error) {
|
||||||
Bootstrap: bootstraps,
|
Bootstrap: bootstraps,
|
||||||
Timeout: defaultLocalTimeout,
|
Timeout: defaultLocalTimeout,
|
||||||
// TODO(e.burkov): Should we verify server's certificates?
|
// TODO(e.burkov): Should we verify server's certificates?
|
||||||
|
|
||||||
|
PreferIPv6: s.conf.BootstrapPreferIPv6,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
@ -412,7 +413,7 @@ func TestServerRace(t *testing.T) {
|
||||||
filterConf := &filtering.Config{
|
filterConf := &filtering.Config{
|
||||||
SafeBrowsingEnabled: true,
|
SafeBrowsingEnabled: true,
|
||||||
SafeBrowsingCacheSize: 1000,
|
SafeBrowsingCacheSize: 1000,
|
||||||
SafeSearchEnabled: true,
|
SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
|
||||||
SafeSearchCacheSize: 1000,
|
SafeSearchCacheSize: 1000,
|
||||||
ParentalCacheSize: 1000,
|
ParentalCacheSize: 1000,
|
||||||
CacheTime: 30,
|
CacheTime: 30,
|
||||||
|
@ -440,12 +441,27 @@ func TestServerRace(t *testing.T) {
|
||||||
|
|
||||||
func TestSafeSearch(t *testing.T) {
|
func TestSafeSearch(t *testing.T) {
|
||||||
resolver := &aghtest.TestResolver{}
|
resolver := &aghtest.TestResolver{}
|
||||||
|
safeSearchConf := filtering.SafeSearchConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Google: true,
|
||||||
|
Yandex: true,
|
||||||
|
CustomResolver: resolver,
|
||||||
|
}
|
||||||
|
|
||||||
filterConf := &filtering.Config{
|
filterConf := &filtering.Config{
|
||||||
SafeSearchEnabled: true,
|
SafeSearchConf: safeSearchConf,
|
||||||
SafeSearchCacheSize: 1000,
|
SafeSearchCacheSize: 1000,
|
||||||
CacheTime: 30,
|
CacheTime: 30,
|
||||||
CustomResolver: resolver,
|
|
||||||
}
|
}
|
||||||
|
safeSearch, err := safesearch.NewDefault(
|
||||||
|
safeSearchConf,
|
||||||
|
"",
|
||||||
|
filterConf.SafeSearchCacheSize,
|
||||||
|
time.Minute*time.Duration(filterConf.CacheTime),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
filterConf.SafeSearch = safeSearch
|
||||||
forwardConf := ServerConfig{
|
forwardConf := ServerConfig{
|
||||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||||
|
@ -498,7 +514,8 @@ func TestSafeSearch(t *testing.T) {
|
||||||
t.Run(tc.host, func(t *testing.T) {
|
t.Run(tc.host, func(t *testing.T) {
|
||||||
req := createTestMessage(tc.host)
|
req := createTestMessage(tc.host)
|
||||||
|
|
||||||
reply, _, err := client.Exchange(req, addr)
|
var reply *dns.Msg
|
||||||
|
reply, _, err = client.Exchange(req, addr)
|
||||||
require.NoErrorf(t, err, "couldn't talk to server %s: %s", addr, err)
|
require.NoErrorf(t, err, "couldn't talk to server %s: %s", addr, err)
|
||||||
assertResponse(t, reply, tc.want)
|
assertResponse(t, reply, tc.want)
|
||||||
})
|
})
|
||||||
|
@ -1057,7 +1074,7 @@ var testDHCP = &dhcpd.MockInterface{
|
||||||
OnEnabled: func() (ok bool) { return true },
|
OnEnabled: func() (ok bool) { return true },
|
||||||
OnLeases: func(flags dhcpd.GetLeasesFlags) (leases []*dhcpd.Lease) {
|
OnLeases: func(flags dhcpd.GetLeasesFlags) (leases []*dhcpd.Lease) {
|
||||||
return []*dhcpd.Lease{{
|
return []*dhcpd.Lease{{
|
||||||
IP: net.IP{192, 168, 12, 34},
|
IP: netip.MustParseAddr("192.168.12.34"),
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
Hostname: "myhost",
|
Hostname: "myhost",
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -23,41 +23,101 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// jsonDNSConfig is the JSON representation of the DNS server configuration.
|
// jsonDNSConfig is the JSON representation of the DNS server configuration.
|
||||||
|
//
|
||||||
|
// TODO(s.chzhen): Split it into smaller pieces. Use aghalg.NullBool instead
|
||||||
|
// of *bool.
|
||||||
type jsonDNSConfig struct {
|
type jsonDNSConfig struct {
|
||||||
Upstreams *[]string `json:"upstream_dns"`
|
// Upstreams is the list of upstream DNS servers.
|
||||||
UpstreamsFile *string `json:"upstream_dns_file"`
|
Upstreams *[]string `json:"upstream_dns"`
|
||||||
Bootstraps *[]string `json:"bootstrap_dns"`
|
|
||||||
ProtectionEnabled *bool `json:"protection_enabled"`
|
// UpstreamsFile is the file containing upstream DNS servers.
|
||||||
RateLimit *uint32 `json:"ratelimit"`
|
UpstreamsFile *string `json:"upstream_dns_file"`
|
||||||
BlockingMode *BlockingMode `json:"blocking_mode"`
|
|
||||||
EDNSCSEnabled *bool `json:"edns_cs_enabled"`
|
// Bootstraps is the list of DNS servers resolving IP addresses of the
|
||||||
DNSSECEnabled *bool `json:"dnssec_enabled"`
|
// upstream DoH/DoT resolvers.
|
||||||
DisableIPv6 *bool `json:"disable_ipv6"`
|
Bootstraps *[]string `json:"bootstrap_dns"`
|
||||||
UpstreamMode *string `json:"upstream_mode"`
|
|
||||||
CacheSize *uint32 `json:"cache_size"`
|
// ProtectionEnabled defines if protection is enabled.
|
||||||
CacheMinTTL *uint32 `json:"cache_ttl_min"`
|
ProtectionEnabled *bool `json:"protection_enabled"`
|
||||||
CacheMaxTTL *uint32 `json:"cache_ttl_max"`
|
|
||||||
CacheOptimistic *bool `json:"cache_optimistic"`
|
// RateLimit is the number of requests per second allowed per client.
|
||||||
ResolveClients *bool `json:"resolve_clients"`
|
RateLimit *uint32 `json:"ratelimit"`
|
||||||
UsePrivateRDNS *bool `json:"use_private_ptr_resolvers"`
|
|
||||||
LocalPTRUpstreams *[]string `json:"local_ptr_upstreams"`
|
// BlockingMode defines the way blocked responses are constructed.
|
||||||
BlockingIPv4 net.IP `json:"blocking_ipv4"`
|
BlockingMode *BlockingMode `json:"blocking_mode"`
|
||||||
BlockingIPv6 net.IP `json:"blocking_ipv6"`
|
|
||||||
|
// EDNSCSEnabled defines if EDNS Client Subnet is enabled.
|
||||||
|
EDNSCSEnabled *bool `json:"edns_cs_enabled"`
|
||||||
|
|
||||||
|
// EDNSCSUseCustom defines if EDNSCSCustomIP should be used.
|
||||||
|
EDNSCSUseCustom *bool `json:"edns_cs_use_custom"`
|
||||||
|
|
||||||
|
// DNSSECEnabled defines if DNSSEC is enabled.
|
||||||
|
DNSSECEnabled *bool `json:"dnssec_enabled"`
|
||||||
|
|
||||||
|
// DisableIPv6 defines if IPv6 addresses should be dropped.
|
||||||
|
DisableIPv6 *bool `json:"disable_ipv6"`
|
||||||
|
|
||||||
|
// UpstreamMode defines the way DNS requests are constructed.
|
||||||
|
UpstreamMode *string `json:"upstream_mode"`
|
||||||
|
|
||||||
|
// CacheSize in bytes.
|
||||||
|
CacheSize *uint32 `json:"cache_size"`
|
||||||
|
|
||||||
|
// CacheMinTTL is custom minimum TTL for cached DNS responses.
|
||||||
|
CacheMinTTL *uint32 `json:"cache_ttl_min"`
|
||||||
|
|
||||||
|
// CacheMaxTTL is custom maximum TTL for cached DNS responses.
|
||||||
|
CacheMaxTTL *uint32 `json:"cache_ttl_max"`
|
||||||
|
|
||||||
|
// CacheOptimistic defines if expired entries should be served.
|
||||||
|
CacheOptimistic *bool `json:"cache_optimistic"`
|
||||||
|
|
||||||
|
// ResolveClients defines if clients IPs should be resolved into hostnames.
|
||||||
|
ResolveClients *bool `json:"resolve_clients"`
|
||||||
|
|
||||||
|
// UsePrivateRDNS defines if privates DNS resolvers should be used.
|
||||||
|
UsePrivateRDNS *bool `json:"use_private_ptr_resolvers"`
|
||||||
|
|
||||||
|
// LocalPTRUpstreams is the list of local private DNS resolvers.
|
||||||
|
LocalPTRUpstreams *[]string `json:"local_ptr_upstreams"`
|
||||||
|
|
||||||
|
// BlockingIPv4 is custom IPv4 address for blocked A requests.
|
||||||
|
BlockingIPv4 net.IP `json:"blocking_ipv4"`
|
||||||
|
|
||||||
|
// BlockingIPv6 is custom IPv6 address for blocked AAAA requests.
|
||||||
|
BlockingIPv6 net.IP `json:"blocking_ipv6"`
|
||||||
|
|
||||||
|
// DisabledUntil is a timestamp until when the protection is disabled.
|
||||||
|
DisabledUntil *time.Time `json:"protection_disabled_until"`
|
||||||
|
|
||||||
|
// EDNSCSCustomIP is custom IP for EDNS Client Subnet.
|
||||||
|
EDNSCSCustomIP netip.Addr `json:"edns_cs_custom_ip"`
|
||||||
|
|
||||||
|
// DefaultLocalPTRUpstreams is used to pass the addresses from
|
||||||
|
// systemResolvers to the front-end. It's not a pointer to the slice since
|
||||||
|
// there is no need to omit it while decoding from JSON.
|
||||||
|
DefaultLocalPTRUpstreams []string `json:"default_local_ptr_upstreams,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
||||||
|
protectionEnabled, protectionDisabledUntil := s.UpdatedProtectionStatus()
|
||||||
|
|
||||||
s.serverLock.RLock()
|
s.serverLock.RLock()
|
||||||
defer s.serverLock.RUnlock()
|
defer s.serverLock.RUnlock()
|
||||||
|
|
||||||
upstreams := stringutil.CloneSliceOrEmpty(s.conf.UpstreamDNS)
|
upstreams := stringutil.CloneSliceOrEmpty(s.conf.UpstreamDNS)
|
||||||
upstreamFile := s.conf.UpstreamDNSFileName
|
upstreamFile := s.conf.UpstreamDNSFileName
|
||||||
bootstraps := stringutil.CloneSliceOrEmpty(s.conf.BootstrapDNS)
|
bootstraps := stringutil.CloneSliceOrEmpty(s.conf.BootstrapDNS)
|
||||||
protectionEnabled := s.conf.ProtectionEnabled
|
|
||||||
blockingMode := s.conf.BlockingMode
|
blockingMode := s.conf.BlockingMode
|
||||||
blockingIPv4 := s.conf.BlockingIPv4
|
blockingIPv4 := s.conf.BlockingIPv4
|
||||||
blockingIPv6 := s.conf.BlockingIPv6
|
blockingIPv6 := s.conf.BlockingIPv6
|
||||||
ratelimit := s.conf.Ratelimit
|
ratelimit := s.conf.Ratelimit
|
||||||
|
|
||||||
|
customIP := s.conf.EDNSClientSubnet.CustomIP
|
||||||
enableEDNSClientSubnet := s.conf.EDNSClientSubnet.Enabled
|
enableEDNSClientSubnet := s.conf.EDNSClientSubnet.Enabled
|
||||||
|
useCustom := s.conf.EDNSClientSubnet.UseCustom
|
||||||
|
|
||||||
enableDNSSEC := s.conf.EnableDNSSEC
|
enableDNSSEC := s.conf.EnableDNSSEC
|
||||||
aaaaDisabled := s.conf.AAAADisabled
|
aaaaDisabled := s.conf.AAAADisabled
|
||||||
cacheSize := s.conf.CacheSize
|
cacheSize := s.conf.CacheSize
|
||||||
|
@ -67,6 +127,7 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
||||||
resolveClients := s.conf.ResolveClients
|
resolveClients := s.conf.ResolveClients
|
||||||
usePrivateRDNS := s.conf.UsePrivateRDNS
|
usePrivateRDNS := s.conf.UsePrivateRDNS
|
||||||
localPTRUpstreams := stringutil.CloneSliceOrEmpty(s.conf.LocalPTRResolvers)
|
localPTRUpstreams := stringutil.CloneSliceOrEmpty(s.conf.LocalPTRResolvers)
|
||||||
|
|
||||||
var upstreamMode string
|
var upstreamMode string
|
||||||
if s.conf.FastestAddr {
|
if s.conf.FastestAddr {
|
||||||
upstreamMode = "fastest_addr"
|
upstreamMode = "fastest_addr"
|
||||||
|
@ -74,46 +135,41 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
||||||
upstreamMode = "parallel"
|
upstreamMode = "parallel"
|
||||||
}
|
}
|
||||||
|
|
||||||
return &jsonDNSConfig{
|
|
||||||
Upstreams: &upstreams,
|
|
||||||
UpstreamsFile: &upstreamFile,
|
|
||||||
Bootstraps: &bootstraps,
|
|
||||||
ProtectionEnabled: &protectionEnabled,
|
|
||||||
BlockingMode: &blockingMode,
|
|
||||||
BlockingIPv4: blockingIPv4,
|
|
||||||
BlockingIPv6: blockingIPv6,
|
|
||||||
RateLimit: &ratelimit,
|
|
||||||
EDNSCSEnabled: &enableEDNSClientSubnet,
|
|
||||||
DNSSECEnabled: &enableDNSSEC,
|
|
||||||
DisableIPv6: &aaaaDisabled,
|
|
||||||
CacheSize: &cacheSize,
|
|
||||||
CacheMinTTL: &cacheMinTTL,
|
|
||||||
CacheMaxTTL: &cacheMaxTTL,
|
|
||||||
CacheOptimistic: &cacheOptimistic,
|
|
||||||
UpstreamMode: &upstreamMode,
|
|
||||||
ResolveClients: &resolveClients,
|
|
||||||
UsePrivateRDNS: &usePrivateRDNS,
|
|
||||||
LocalPTRUpstreams: &localPTRUpstreams,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defLocalPTRUps, err := s.filterOurDNSAddrs(s.sysResolvers.Get())
|
defLocalPTRUps, err := s.filterOurDNSAddrs(s.sysResolvers.Get())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("getting dns configuration: %s", err)
|
log.Debug("getting dns configuration: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := struct {
|
return &jsonDNSConfig{
|
||||||
jsonDNSConfig
|
Upstreams: &upstreams,
|
||||||
// DefautLocalPTRUpstreams is used to pass the addresses from
|
UpstreamsFile: &upstreamFile,
|
||||||
// systemResolvers to the front-end. It's not a pointer to the slice
|
Bootstraps: &bootstraps,
|
||||||
// since there is no need to omit it while decoding from JSON.
|
ProtectionEnabled: &protectionEnabled,
|
||||||
DefautLocalPTRUpstreams []string `json:"default_local_ptr_upstreams,omitempty"`
|
BlockingMode: &blockingMode,
|
||||||
}{
|
BlockingIPv4: blockingIPv4,
|
||||||
jsonDNSConfig: *s.getDNSConfig(),
|
BlockingIPv6: blockingIPv6,
|
||||||
DefautLocalPTRUpstreams: defLocalPTRUps,
|
RateLimit: &ratelimit,
|
||||||
|
EDNSCSCustomIP: customIP,
|
||||||
|
EDNSCSEnabled: &enableEDNSClientSubnet,
|
||||||
|
EDNSCSUseCustom: &useCustom,
|
||||||
|
DNSSECEnabled: &enableDNSSEC,
|
||||||
|
DisableIPv6: &aaaaDisabled,
|
||||||
|
CacheSize: &cacheSize,
|
||||||
|
CacheMinTTL: &cacheMinTTL,
|
||||||
|
CacheMaxTTL: &cacheMaxTTL,
|
||||||
|
CacheOptimistic: &cacheOptimistic,
|
||||||
|
UpstreamMode: &upstreamMode,
|
||||||
|
ResolveClients: &resolveClients,
|
||||||
|
UsePrivateRDNS: &usePrivateRDNS,
|
||||||
|
LocalPTRUpstreams: &localPTRUpstreams,
|
||||||
|
DefaultLocalPTRUpstreams: defLocalPTRUps,
|
||||||
|
DisabledUntil: protectionDisabledUntil,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetConfig handles requests to the GET /control/dns_info endpoint.
|
||||||
|
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
resp := s.getDNSConfig()
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,6 +260,7 @@ func (req *jsonDNSConfig) checkCacheTTL() bool {
|
||||||
return min <= max
|
return min <= max
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleSetConfig handles requests to the POST /control/dns_config endpoint.
|
||||||
func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
req := &jsonDNSConfig{}
|
req := &jsonDNSConfig{}
|
||||||
err := json.NewDecoder(r.Body).Decode(req)
|
err := json.NewDecoder(r.Body).Decode(req)
|
||||||
|
@ -231,8 +288,8 @@ func (s *Server) handleSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setConfigRestartable sets the server parameters. shouldRestart is true if
|
// setConfig sets the server parameters. shouldRestart is true if the server
|
||||||
// the server should be restarted to apply changes.
|
// should be restarted to apply changes.
|
||||||
func (s *Server) setConfig(dc *jsonDNSConfig) (shouldRestart bool) {
|
func (s *Server) setConfig(dc *jsonDNSConfig) (shouldRestart bool) {
|
||||||
s.serverLock.Lock()
|
s.serverLock.Lock()
|
||||||
defer s.serverLock.Unlock()
|
defer s.serverLock.Unlock()
|
||||||
|
@ -250,6 +307,10 @@ func (s *Server) setConfig(dc *jsonDNSConfig) (shouldRestart bool) {
|
||||||
s.conf.FastestAddr = *dc.UpstreamMode == "fastest_addr"
|
s.conf.FastestAddr = *dc.UpstreamMode == "fastest_addr"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dc.EDNSCSUseCustom != nil && *dc.EDNSCSUseCustom {
|
||||||
|
s.conf.EDNSClientSubnet.CustomIP = dc.EDNSCSCustomIP
|
||||||
|
}
|
||||||
|
|
||||||
setIfNotNil(&s.conf.ProtectionEnabled, dc.ProtectionEnabled)
|
setIfNotNil(&s.conf.ProtectionEnabled, dc.ProtectionEnabled)
|
||||||
setIfNotNil(&s.conf.EnableDNSSEC, dc.DNSSECEnabled)
|
setIfNotNil(&s.conf.EnableDNSSEC, dc.DNSSECEnabled)
|
||||||
setIfNotNil(&s.conf.AAAADisabled, dc.DisableIPv6)
|
setIfNotNil(&s.conf.AAAADisabled, dc.DisableIPv6)
|
||||||
|
@ -281,6 +342,7 @@ func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) {
|
||||||
setIfNotNil(&s.conf.UpstreamDNSFileName, dc.UpstreamsFile),
|
setIfNotNil(&s.conf.UpstreamDNSFileName, dc.UpstreamsFile),
|
||||||
setIfNotNil(&s.conf.BootstrapDNS, dc.Bootstraps),
|
setIfNotNil(&s.conf.BootstrapDNS, dc.Bootstraps),
|
||||||
setIfNotNil(&s.conf.EDNSClientSubnet.Enabled, dc.EDNSCSEnabled),
|
setIfNotNil(&s.conf.EDNSClientSubnet.Enabled, dc.EDNSCSEnabled),
|
||||||
|
setIfNotNil(&s.conf.EDNSClientSubnet.UseCustom, dc.EDNSCSUseCustom),
|
||||||
setIfNotNil(&s.conf.CacheSize, dc.CacheSize),
|
setIfNotNil(&s.conf.CacheSize, dc.CacheSize),
|
||||||
setIfNotNil(&s.conf.CacheMinTTL, dc.CacheMinTTL),
|
setIfNotNil(&s.conf.CacheMinTTL, dc.CacheMinTTL),
|
||||||
setIfNotNil(&s.conf.CacheMaxTTL, dc.CacheMaxTTL),
|
setIfNotNil(&s.conf.CacheMaxTTL, dc.CacheMaxTTL),
|
||||||
|
@ -388,15 +450,15 @@ func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet)
|
||||||
|
|
||||||
var errs []error
|
var errs []error
|
||||||
for _, domain := range keys {
|
for _, domain := range keys {
|
||||||
var subnet *net.IPNet
|
var subnet netip.Prefix
|
||||||
subnet, err = netutil.SubnetFromReversedAddr(domain)
|
subnet, err = extractARPASubnet(domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !privateNets.Contains(subnet.IP) {
|
if !privateNets.Contains(subnet.Addr().AsSlice()) {
|
||||||
errs = append(
|
errs = append(
|
||||||
errs,
|
errs,
|
||||||
fmt.Errorf("arpa domain %q should point to a locally-served network", domain),
|
fmt.Errorf("arpa domain %q should point to a locally-served network", domain),
|
||||||
|
@ -577,6 +639,7 @@ func (err domainSpecificTestError) Error() (msg string) {
|
||||||
func checkDNS(
|
func checkDNS(
|
||||||
upstreamConfigStr string,
|
upstreamConfigStr string,
|
||||||
bootstrap []string,
|
bootstrap []string,
|
||||||
|
bootstrapPrefIPv6 bool,
|
||||||
timeout time.Duration,
|
timeout time.Duration,
|
||||||
healthCheck healthCheckFunc,
|
healthCheck healthCheckFunc,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
|
@ -604,8 +667,9 @@ func checkDNS(
|
||||||
log.Debug("dnsforward: checking if upstream %q works", upstreamAddr)
|
log.Debug("dnsforward: checking if upstream %q works", upstreamAddr)
|
||||||
|
|
||||||
u, err := upstream.AddressToUpstream(upstreamAddr, &upstream.Options{
|
u, err := upstream.AddressToUpstream(upstreamAddr, &upstream.Options{
|
||||||
Bootstrap: bootstrap,
|
Bootstrap: bootstrap,
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
|
PreferIPv6: bootstrapPrefIPv6,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to choose upstream for %q: %w", upstreamAddr, err)
|
return fmt.Errorf("failed to choose upstream for %q: %w", upstreamAddr, err)
|
||||||
|
@ -637,6 +701,7 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
result := map[string]string{}
|
result := map[string]string{}
|
||||||
bootstraps := req.BootstrapDNS
|
bootstraps := req.BootstrapDNS
|
||||||
|
bootstrapPrefIPv6 := s.conf.BootstrapPreferIPv6
|
||||||
timeout := s.conf.UpstreamTimeout
|
timeout := s.conf.UpstreamTimeout
|
||||||
|
|
||||||
type upsCheckResult = struct {
|
type upsCheckResult = struct {
|
||||||
|
@ -653,7 +718,7 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
defer func() { resCh <- res }()
|
defer func() { resCh <- res }()
|
||||||
|
|
||||||
checkErr := checkDNS(ups, bootstraps, timeout, healthCheck)
|
checkErr := checkDNS(ups, bootstraps, bootstrapPrefIPv6, timeout, healthCheck)
|
||||||
if checkErr != nil {
|
if checkErr != nil {
|
||||||
res.res = checkErr.Error()
|
res.res = checkErr.Error()
|
||||||
} else {
|
} else {
|
||||||
|
@ -685,6 +750,52 @@ func (s *Server) handleCacheClear(w http.ResponseWriter, _ *http.Request) {
|
||||||
_, _ = io.WriteString(w, "OK")
|
_, _ = io.WriteString(w, "OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// protectionJSON is an object for /control/protection endpoint.
|
||||||
|
type protectionJSON struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Duration uint `json:"duration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSetProtection is a handler for the POST /control/protection HTTP API.
|
||||||
|
func (s *Server) handleSetProtection(w http.ResponseWriter, r *http.Request) {
|
||||||
|
protectionReq := &protectionJSON{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(protectionReq)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var disabledUntil *time.Time
|
||||||
|
if protectionReq.Duration > 0 {
|
||||||
|
if protectionReq.Enabled {
|
||||||
|
aghhttp.Error(
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"Setting a duration is only allowed with protection disabling",
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
calcTime := time.Now().Add(time.Duration(protectionReq.Duration) * time.Millisecond)
|
||||||
|
disabledUntil = &calcTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
s.serverLock.Lock()
|
||||||
|
defer s.serverLock.Unlock()
|
||||||
|
|
||||||
|
s.conf.ProtectionEnabled = protectionReq.Enabled
|
||||||
|
s.conf.ProtectionDisabledUntil = disabledUntil
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.conf.ConfigModified()
|
||||||
|
|
||||||
|
aghhttp.OK(w)
|
||||||
|
}
|
||||||
|
|
||||||
// handleDoH is the DNS-over-HTTPs handler.
|
// handleDoH is the DNS-over-HTTPs handler.
|
||||||
//
|
//
|
||||||
// Control flow:
|
// Control flow:
|
||||||
|
@ -719,6 +830,7 @@ func (s *Server) registerHandlers() {
|
||||||
s.conf.HTTPRegister(http.MethodGet, "/control/dns_info", s.handleGetConfig)
|
s.conf.HTTPRegister(http.MethodGet, "/control/dns_info", s.handleGetConfig)
|
||||||
s.conf.HTTPRegister(http.MethodPost, "/control/dns_config", s.handleSetConfig)
|
s.conf.HTTPRegister(http.MethodPost, "/control/dns_config", s.handleSetConfig)
|
||||||
s.conf.HTTPRegister(http.MethodPost, "/control/test_upstream_dns", s.handleTestUpstreamDNS)
|
s.conf.HTTPRegister(http.MethodPost, "/control/test_upstream_dns", s.handleTestUpstreamDNS)
|
||||||
|
s.conf.HTTPRegister(http.MethodPost, "/control/protection", s.handleSetProtection)
|
||||||
|
|
||||||
s.conf.HTTPRegister(http.MethodGet, "/control/access/list", s.handleAccessList)
|
s.conf.HTTPRegister(http.MethodGet, "/control/access/list", s.handleAccessList)
|
||||||
s.conf.HTTPRegister(http.MethodPost, "/control/access/set", s.handleAccessSet)
|
s.conf.HTTPRegister(http.MethodPost, "/control/access/set", s.handleAccessSet)
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
@ -57,7 +58,7 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
|
||||||
filterConf := &filtering.Config{
|
filterConf := &filtering.Config{
|
||||||
SafeBrowsingEnabled: true,
|
SafeBrowsingEnabled: true,
|
||||||
SafeBrowsingCacheSize: 1000,
|
SafeBrowsingCacheSize: 1000,
|
||||||
SafeSearchEnabled: true,
|
SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
|
||||||
SafeSearchCacheSize: 1000,
|
SafeSearchCacheSize: 1000,
|
||||||
ParentalCacheSize: 1000,
|
ParentalCacheSize: 1000,
|
||||||
CacheTime: 30,
|
CacheTime: 30,
|
||||||
|
@ -122,7 +123,7 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
|
||||||
s.conf = tc.conf()
|
s.conf = tc.conf()
|
||||||
s.handleGetConfig(w, nil)
|
s.handleGetConfig(w, nil)
|
||||||
|
|
||||||
cType := w.Header().Get(aghhttp.HdrNameContentType)
|
cType := w.Header().Get(httphdr.ContentType)
|
||||||
assert.Equal(t, aghhttp.HdrValApplicationJSON, cType)
|
assert.Equal(t, aghhttp.HdrValApplicationJSON, cType)
|
||||||
assert.JSONEq(t, string(caseWant), w.Body.String())
|
assert.JSONEq(t, string(caseWant), w.Body.String())
|
||||||
})
|
})
|
||||||
|
@ -133,7 +134,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||||
filterConf := &filtering.Config{
|
filterConf := &filtering.Config{
|
||||||
SafeBrowsingEnabled: true,
|
SafeBrowsingEnabled: true,
|
||||||
SafeBrowsingCacheSize: 1000,
|
SafeBrowsingCacheSize: 1000,
|
||||||
SafeSearchEnabled: true,
|
SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
|
||||||
SafeSearchCacheSize: 1000,
|
SafeSearchCacheSize: 1000,
|
||||||
ParentalCacheSize: 1000,
|
ParentalCacheSize: 1000,
|
||||||
CacheTime: 30,
|
CacheTime: 30,
|
||||||
|
@ -181,6 +182,12 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||||
}, {
|
}, {
|
||||||
name: "edns_cs_enabled",
|
name: "edns_cs_enabled",
|
||||||
wantSet: "",
|
wantSet: "",
|
||||||
|
}, {
|
||||||
|
name: "edns_cs_use_custom",
|
||||||
|
wantSet: "",
|
||||||
|
}, {
|
||||||
|
name: "edns_cs_use_custom_bad_ip",
|
||||||
|
wantSet: "decoding request: ParseAddr(\"bad.ip\"): unexpected character (at \"bad.ip\")",
|
||||||
}, {
|
}, {
|
||||||
name: "dnssec_enabled",
|
name: "dnssec_enabled",
|
||||||
wantSet: "",
|
wantSet: "",
|
||||||
|
@ -212,7 +219,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||||
}, {
|
}, {
|
||||||
name: "local_ptr_upstreams_bad",
|
name: "local_ptr_upstreams_bad",
|
||||||
wantSet: `validating private upstream servers: checking domain-specific upstreams: ` +
|
wantSet: `validating private upstream servers: checking domain-specific upstreams: ` +
|
||||||
`bad arpa domain name "non.arpa": not a reversed ip network`,
|
`bad arpa domain name "non.arpa.": not a reversed ip network`,
|
||||||
}, {
|
}, {
|
||||||
name: "local_ptr_upstreams_null",
|
name: "local_ptr_upstreams_null",
|
||||||
wantSet: "",
|
wantSet: "",
|
||||||
|
@ -222,16 +229,20 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||||
Req json.RawMessage `json:"req"`
|
Req json.RawMessage `json:"req"`
|
||||||
Want json.RawMessage `json:"want"`
|
Want json.RawMessage `json:"want"`
|
||||||
}
|
}
|
||||||
loadTestData(t, t.Name()+jsonExt, &data)
|
|
||||||
|
testData := t.Name() + jsonExt
|
||||||
|
loadTestData(t, testData, &data)
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
// NOTE: Do not use require.Contains, because the size of the data
|
||||||
|
// prevents it from printing a meaningful error message.
|
||||||
caseData, ok := data[tc.name]
|
caseData, ok := data[tc.name]
|
||||||
require.True(t, ok)
|
require.Truef(t, ok, "%q does not contain test data for test case %s", testData, tc.name)
|
||||||
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
s.conf = defaultConf
|
s.conf = defaultConf
|
||||||
s.conf.FilteringConfig.EDNSClientSubnet.Enabled = false
|
s.conf.FilteringConfig.EDNSClientSubnet = &EDNSClientSubnet{}
|
||||||
})
|
})
|
||||||
|
|
||||||
rBody := io.NopCloser(bytes.NewReader(caseData.Req))
|
rBody := io.NopCloser(bytes.NewReader(caseData.Req))
|
||||||
|
@ -373,7 +384,7 @@ func TestValidateUpstreamsPrivate(t *testing.T) {
|
||||||
}, {
|
}, {
|
||||||
name: "not_arpa_subnet",
|
name: "not_arpa_subnet",
|
||||||
wantErr: `checking domain-specific upstreams: ` +
|
wantErr: `checking domain-specific upstreams: ` +
|
||||||
`bad arpa domain name "hello.world": not a reversed ip network`,
|
`bad arpa domain name "hello.world.": not a reversed ip network`,
|
||||||
u: "[/hello.world/]#",
|
u: "[/hello.world/]#",
|
||||||
}, {
|
}, {
|
||||||
name: "non-private_arpa_address",
|
name: "non-private_arpa_address",
|
||||||
|
@ -389,8 +400,12 @@ func TestValidateUpstreamsPrivate(t *testing.T) {
|
||||||
name: "several_bad",
|
name: "several_bad",
|
||||||
wantErr: `checking domain-specific upstreams: 2 errors: ` +
|
wantErr: `checking domain-specific upstreams: 2 errors: ` +
|
||||||
`"arpa domain \"1.2.3.4.in-addr.arpa.\" should point to a locally-served network", ` +
|
`"arpa domain \"1.2.3.4.in-addr.arpa.\" should point to a locally-served network", ` +
|
||||||
`"bad arpa domain name \"non.arpa\": not a reversed ip network"`,
|
`"bad arpa domain name \"non.arpa.\": not a reversed ip network"`,
|
||||||
u: "[/non.arpa/1.2.3.4.in-addr.arpa/127.in-addr.arpa/]#",
|
u: "[/non.arpa/1.2.3.4.in-addr.arpa/127.in-addr.arpa/]#",
|
||||||
|
}, {
|
||||||
|
name: "partial_good",
|
||||||
|
wantErr: "",
|
||||||
|
u: "[/a.1.2.3.10.in-addr.arpa/a.10.in-addr.arpa/]#",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue