Compare commits
32 Commits
v0.108.0-b
...
AG-21485
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
521aedc5bc | ||
|
|
1842f7d888 | ||
|
|
40ff26ea21 | ||
|
|
4afd39b22f | ||
|
|
e43ba17884 | ||
|
|
6d402dc86c | ||
|
|
18acdf9b09 | ||
|
|
5da7751463 | ||
|
|
7631ca4ab3 | ||
|
|
c6d4f2317e | ||
|
|
0ea224a9e4 | ||
|
|
d78a3edb22 | ||
|
|
bbbdea2635 | ||
|
|
6c8d89a4da | ||
|
|
f082312e49 | ||
|
|
ea03d1af93 | ||
|
|
67d8b7df90 | ||
|
|
b23ea0a690 | ||
|
|
a186b5c436 | ||
|
|
6da7392345 | ||
|
|
c1924a8b8a | ||
|
|
0376afb38e | ||
|
|
230d7b8c17 | ||
|
|
950ecb1f5e | ||
|
|
9e14d5f99f | ||
|
|
89bf3721b5 | ||
|
|
15bba281ee | ||
|
|
f9fe3172c4 | ||
|
|
5d5a729569 | ||
|
|
b1120221c7 | ||
|
|
f553eee842 | ||
|
|
61b4043775 |
114
CHANGELOG.md
114
CHANGELOG.md
@@ -14,17 +14,53 @@ and this project adheres to
|
|||||||
<!--
|
<!--
|
||||||
## [v0.108.0] - TBA
|
## [v0.108.0] - TBA
|
||||||
|
|
||||||
## [v0.107.28] - 2023-04-12 (APPROX.)
|
## [v0.107.29] - 2023-04-26 (APPROX.)
|
||||||
|
|
||||||
See also the [v0.107.28 GitHub milestone][ms-v0.107.28].
|
See also the [v0.107.29 GitHub milestone][ms-v0.107.29].
|
||||||
|
|
||||||
[ms-v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/milestone/64?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.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- The ability to exclude client activity from the query log or statistics by
|
||||||
|
editing client's settings on the Clients settings page in the UI ([#1717],
|
||||||
|
[#4299]).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The `github.com/mdlayher/raw` dependency has been temporarily returned to
|
||||||
|
support raw connections on Darwin ([#5712]).
|
||||||
|
- Incorrect recording of blocked results as “Blocked by CNAME or IP” in the
|
||||||
|
query log ([#5725]).
|
||||||
|
- All Safe Search services being unchecked by default.
|
||||||
|
- Panic when a DNSCrypt stamp is invalid ([#5721]).
|
||||||
|
|
||||||
|
[#1717]: https://github.com/AdguardTeam/AdGuardHome/issues/1717
|
||||||
|
[#4299]: https://github.com/AdguardTeam/AdGuardHome/issues/4299
|
||||||
|
[#5721]: https://github.com/AdguardTeam/AdGuardHome/issues/5721
|
||||||
|
[#5725]: https://github.com/AdguardTeam/AdGuardHome/issues/5725
|
||||||
|
|
||||||
|
<!--
|
||||||
|
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
|
- The ability to make bootstrap DNS lookups prefer IPv6 addresses to IPv4 ones
|
||||||
using the new `dns.bootstrap_prefer_ipv6` configuration file property
|
using the new `dns.bootstrap_prefer_ipv6` configuration file property
|
||||||
([#4262]).
|
([#4262]).
|
||||||
@@ -32,20 +68,20 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
|||||||
- The new HTTP API `POST /control/protection`, that updates protection state
|
- The new HTTP API `POST /control/protection`, that updates protection state
|
||||||
and adds an optional pause duration ([#1333]). The format of request body
|
and adds an optional pause duration ([#1333]). The format of request body
|
||||||
is described in `openapi/openapi.yaml`. The duration of this pause could
|
is described in `openapi/openapi.yaml`. The duration of this pause could
|
||||||
also be set with the config field `protection_disabled_until` in `dns`
|
also be set with the property `protection_disabled_until` in the `dns` object
|
||||||
section of the YAML configuration file.
|
of the YAML configuration file.
|
||||||
- The ability to create a static DHCP lease from a dynamic one more easily
|
- The ability to create a static DHCP lease from a dynamic one more easily
|
||||||
([#3459]).
|
([#3459]).
|
||||||
- Two new HTTP APIs, `PUT /control/stats/config/update` and `GET
|
- 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
|
control/stats/config`, which can be used to set and receive the query log
|
||||||
configuration. See openapi/openapi.yaml for the full description.
|
configuration. See `openapi/openapi.yaml` for the full description.
|
||||||
- Two new HTTP APIs, `PUT /control/querylog/config/update` and `GET
|
- Two new HTTP APIs, `PUT /control/querylog/config/update` and `GET
|
||||||
control/querylog/config`, which can be used to set and receive the statistics
|
control/querylog/config`, which can be used to set and receive the statistics
|
||||||
configuration. See openapi/openapi.yaml for the full description.
|
configuration. See `openapi/openapi.yaml` for the full description.
|
||||||
- The ability to set custom IP for EDNS Client Subnet by using the DNS-server
|
- 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]).
|
configuration section on the DNS settings page in the UI ([#1472]).
|
||||||
- The ability to manage safesearch for each service by using the new
|
- The ability to manage Safe Search for each service by using the new
|
||||||
`safe_search` field ([#1163]).
|
`safe_search` property ([#1163]).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -74,9 +110,9 @@ In this release, the schema version has changed from 17 to 20.
|
|||||||
|
|
||||||
To rollback this change, convert the property back into days and change the
|
To rollback this change, convert the property back into days and change the
|
||||||
`schema_version` back to `19`.
|
`schema_version` back to `19`.
|
||||||
- The `dns.safesearch_enabled` field has been replaced with `safe_search`
|
- The `dns.safesearch_enabled` property has been replaced with `safe_search`
|
||||||
object containing per-service settings.
|
object containing per-service settings.
|
||||||
- The `clients.persistent.safesearch_enabled` field has been replaced with
|
- The `clients.persistent.safesearch_enabled` property has been replaced with
|
||||||
`safe_search` object containing per-service settings.
|
`safe_search` object containing per-service settings.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -95,7 +131,7 @@ In this release, the schema version has changed from 17 to 20.
|
|||||||
```
|
```
|
||||||
|
|
||||||
To rollback this change, move the value of `dns.safe_search.enabled` into the
|
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
|
`dns.safesearch_enabled`, then remove `dns.safe_search` property. Do the same
|
||||||
client's specific `clients.persistent.safesearch` and then change the
|
client's specific `clients.persistent.safesearch` and then change the
|
||||||
`schema_version` back to `17`.
|
`schema_version` back to `17`.
|
||||||
|
|
||||||
@@ -105,7 +141,7 @@ In this release, the schema version has changed from 17 to 20.
|
|||||||
`PUT /control/safesearch/settings` API.
|
`PUT /control/safesearch/settings` API.
|
||||||
- The `POST /control/safesearch/disable` HTTP API is deprecated. Use the new
|
- The `POST /control/safesearch/disable` HTTP API is deprecated. Use the new
|
||||||
`PUT /control/safesearch/settings` API
|
`PUT /control/safesearch/settings` API
|
||||||
- The `safesearch_enabled` field is deprecated in the following HTTP APIs:
|
- The `safesearch_enabled` property is deprecated in the following HTTP APIs:
|
||||||
- `GET /control/clients`;
|
- `GET /control/clients`;
|
||||||
- `POST /control/clients/add`;
|
- `POST /control/clients/add`;
|
||||||
- `POST /control/clients/update`;
|
- `POST /control/clients/update`;
|
||||||
@@ -116,30 +152,35 @@ In this release, the schema version has changed from 17 to 20.
|
|||||||
/control/stats/config` API instead.
|
/control/stats/config` API instead.
|
||||||
|
|
||||||
**NOTE:** If interval is custom then it will be equal to `90` days for
|
**NOTE:** If interval is custom then it will be equal to `90` days for
|
||||||
compatibility reasons. See openapi/openapi.yaml and `openapi/CHANGELOG.md`.
|
compatibility reasons. See `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.
|
||||||
- The `POST /control/stats_config` HTTP API; use the new `PUT
|
- The `POST /control/stats_config` HTTP API; use the new `PUT
|
||||||
/control/stats/config/update` API instead.
|
/control/stats/config/update` API instead.
|
||||||
- The `GET /control/querylog_info` HTTP API; use the new `GET
|
- The `GET /control/querylog_info` HTTP API; use the new `GET
|
||||||
/control/querylog/config` API instead.
|
/control/querylog/config` API instead.
|
||||||
|
|
||||||
**NOTE:** If interval is custom then it will be equal to `90` days for
|
**NOTE:** If interval is custom then it will be equal to `90` days for
|
||||||
compatibility reasons. See openapi/openapi.yaml and `openapi/CHANGELOG.md`.
|
compatibility reasons. See `openapi/openapi.yaml` and `openapi/CHANGELOG.md`.
|
||||||
- The `POST /control/querylog_config` HTTP API; use the new `PUT
|
- The `POST /control/querylog_config` HTTP API; use the new `PUT
|
||||||
/control/querylog/config/update` API instead.
|
/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
|
[#1163]: https://github.com/AdguardTeam/AdGuardHome/issues/1163
|
||||||
[#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333
|
[#1333]: https://github.com/AdguardTeam/AdGuardHome/issues/1333
|
||||||
[#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472
|
[#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472
|
||||||
|
[#1717]: https://github.com/AdguardTeam/AdGuardHome/issues/1717
|
||||||
[#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290
|
[#3290]: https://github.com/AdguardTeam/AdGuardHome/issues/3290
|
||||||
[#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459
|
[#3459]: https://github.com/AdguardTeam/AdGuardHome/issues/3459
|
||||||
[#4262]: https://github.com/AdguardTeam/AdGuardHome/issues/4262
|
[#4262]: https://github.com/AdguardTeam/AdGuardHome/issues/4262
|
||||||
|
[#4299]: https://github.com/AdguardTeam/AdGuardHome/issues/4299
|
||||||
[#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567
|
[#5567]: https://github.com/AdguardTeam/AdGuardHome/issues/5567
|
||||||
|
[#5701]: https://github.com/AdguardTeam/AdGuardHome/issues/5701
|
||||||
|
|
||||||
[rfc6761]: https://www.rfc-editor.org/rfc/rfc6761
|
[ms-v0.107.28]: https://github.com/AdguardTeam/AdGuardHome/milestone/64?closed=1
|
||||||
|
[rfc6761]: https://www.rfc-editor.org/rfc/rfc6761
|
||||||
|
|
||||||
<!--
|
|
||||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -183,7 +224,7 @@ See also the [v0.107.26 GitHub milestone][ms-v0.107.26].
|
|||||||
|
|
||||||
- The ability to set custom IP for EDNS Client Subnet by using the new
|
- The ability to set custom IP for EDNS Client Subnet by using the new
|
||||||
`dns.edns_client_subnet.use_custom` and `dns.edns_client_subnet.custom_ip`
|
`dns.edns_client_subnet.use_custom` and `dns.edns_client_subnet.custom_ip`
|
||||||
fields ([#1472]). The UI changes are coming in the upcoming releases.
|
properties ([#1472]). The UI changes are coming in the upcoming releases.
|
||||||
- The ability to use `dnstype` rules in the disallowed domains list ([#5468]).
|
- The ability to use `dnstype` rules in the disallowed domains list ([#5468]).
|
||||||
This allows dropping requests based on their question types.
|
This allows dropping requests based on their question types.
|
||||||
|
|
||||||
@@ -211,7 +252,7 @@ See also the [v0.107.26 GitHub milestone][ms-v0.107.26].
|
|||||||
```
|
```
|
||||||
|
|
||||||
To rollback this change, move the value of `dns.edns_client_subnet.enabled`
|
To rollback this change, move the value of `dns.edns_client_subnet.enabled`
|
||||||
into the `dns.edns_client_subnet`, remove the fields
|
into the `dns.edns_client_subnet`, remove the properties
|
||||||
`dns.edns_client_subnet.enabled`, `dns.edns_client_subnet.use_custom`,
|
`dns.edns_client_subnet.enabled`, `dns.edns_client_subnet.use_custom`,
|
||||||
`dns.edns_client_subnet.custom_ip`, and change the `schema_version` back to
|
`dns.edns_client_subnet.custom_ip`, and change the `schema_version` back to
|
||||||
`16`.
|
`16`.
|
||||||
@@ -271,11 +312,11 @@ See also the [v0.107.24 GitHub milestone][ms-v0.107.24].
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- The ability to disable statistics by using the new `statistics.enabled`
|
- The ability to disable statistics by using the new `statistics.enabled`
|
||||||
field. Previously it was necessary to set the `statistics_interval` to 0,
|
property. Previously it was necessary to set the `statistics_interval` to 0,
|
||||||
losing the previous value ([#1717], [#4299]).
|
losing the previous value ([#1717], [#4299]).
|
||||||
- The ability to exclude domain names from the query log or statistics by using
|
- The ability to exclude domain names from the query log or statistics by using
|
||||||
the new `querylog.ignored` or `statistics.ignored` fields ([#1717], [#4299]).
|
the new `querylog.ignored` or `statistics.ignored` properties ([#1717],
|
||||||
The UI changes are coming in the upcoming releases.
|
[#4299]). The UI changes are coming in the upcoming releases.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@@ -300,7 +341,7 @@ In this release, the schema version has changed from 14 to 16.
|
|||||||
|
|
||||||
To rollback this change, move the property back into the `dns` object and
|
To rollback this change, move the property back into the `dns` object and
|
||||||
change the `schema_version` back to `15`.
|
change the `schema_version` back to `15`.
|
||||||
- The fields `dns.querylog_enabled`, `dns.querylog_file_enabled`,
|
- The properties `dns.querylog_enabled`, `dns.querylog_file_enabled`,
|
||||||
`dns.querylog_interval`, and `dns.querylog_size_memory` have been moved to the
|
`dns.querylog_interval`, and `dns.querylog_size_memory` have been moved to the
|
||||||
new `querylog` object.
|
new `querylog` object.
|
||||||
|
|
||||||
@@ -358,8 +399,8 @@ See also the [v0.107.23 GitHub milestone][ms-v0.107.23].
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- DNS64 support ([#5117]). The function may be enabled with new `use_dns64`
|
- DNS64 support ([#5117]). The function may be enabled with new `use_dns64`
|
||||||
field under `dns` object in the configuration along with `dns64_prefixes`, the
|
property under `dns` object in the configuration along with `dns64_prefixes`,
|
||||||
set of exclusion prefixes to filter AAAA responses. The Well-Known Prefix
|
the set of exclusion prefixes to filter AAAA responses. The Well-Known Prefix
|
||||||
(`64:ff9b::/96`) is used if no custom prefixes are specified.
|
(`64:ff9b::/96`) is used if no custom prefixes are specified.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -1017,7 +1058,7 @@ In this release, the schema version has changed from 12 to 14.
|
|||||||
hosts: true
|
hosts: true
|
||||||
```
|
```
|
||||||
|
|
||||||
The value for `clients.runtime_sources.rdns` field is taken from
|
The value for `clients.runtime_sources.rdns` property is taken from
|
||||||
`dns.resolve_clients` property. To rollback this change, remove the
|
`dns.resolve_clients` property. To rollback this change, remove the
|
||||||
`runtime_sources` property, move the contents of `persistent` into the
|
`runtime_sources` property, move the contents of `persistent` into the
|
||||||
`clients` itself, the value of `clients.runtime_sources.rdns` into the
|
`clients` itself, the value of `clients.runtime_sources.rdns` into the
|
||||||
@@ -1267,7 +1308,7 @@ See also the [v0.107.0 GitHub milestone][ms-v0.107.0].
|
|||||||
log entries concerning cached responses won't include that information.
|
log entries concerning cached responses won't include that information.
|
||||||
- Finnish and Ukrainian localizations.
|
- Finnish and Ukrainian localizations.
|
||||||
- Setting the timeout for IP address pinging in the "Fastest IP address" mode
|
- Setting the timeout for IP address pinging in the "Fastest IP address" mode
|
||||||
through the new `fastest_timeout` field in the configuration file ([#1992]).
|
through the new `fastest_timeout` property in the configuration file ([#1992]).
|
||||||
- Static IP address detection on FreeBSD ([#3289]).
|
- Static IP address detection on FreeBSD ([#3289]).
|
||||||
- Optimistic cache ([#2145]).
|
- Optimistic cache ([#2145]).
|
||||||
- New possible value of `6h` for `querylog_interval` property ([#2504]).
|
- New possible value of `6h` for `querylog_interval` property ([#2504]).
|
||||||
@@ -1683,7 +1724,7 @@ See also the [v0.105.2 GitHub milestone][ms-v0.105.2].
|
|||||||
- Inconsistent responses for messages with EDNS0 and AD when DNS caching is
|
- Inconsistent responses for messages with EDNS0 and AD when DNS caching is
|
||||||
enabled ([#2600]).
|
enabled ([#2600]).
|
||||||
- Incomplete OpenWrt detection ([#2757]).
|
- Incomplete OpenWrt detection ([#2757]).
|
||||||
- DHCP lease's `expired` field incorrect time format ([#2692]).
|
- DHCP lease's `expired` property incorrect time format ([#2692]).
|
||||||
- Incomplete DNS upstreams validation ([#2674]).
|
- Incomplete DNS upstreams validation ([#2674]).
|
||||||
- Wrong parsing of DHCP options of the `ip` type ([#2688]).
|
- Wrong parsing of DHCP options of the `ip` type ([#2688]).
|
||||||
|
|
||||||
@@ -1720,8 +1761,8 @@ See also the [v0.105.1 GitHub milestone][ms-v0.105.1].
|
|||||||
the machine has a static IP.
|
the machine has a static IP.
|
||||||
- Optical issue on custom rules ([#2641]).
|
- Optical issue on custom rules ([#2641]).
|
||||||
- Occasional crashes during startup.
|
- Occasional crashes during startup.
|
||||||
- The field `"range_start"` in the `GET /control/dhcp/status` HTTP API response
|
- The property `"range_start"` in the `GET /control/dhcp/status` HTTP API
|
||||||
is now correctly named again ([#2678]).
|
response is now correctly named again ([#2678]).
|
||||||
- DHCPv6 server's `ra_slaac_only` and `ra_allow_slaac` properties aren't reset
|
- DHCPv6 server's `ra_slaac_only` and `ra_allow_slaac` properties aren't reset
|
||||||
to `false` on update anymore ([#2653]).
|
to `false` on update anymore ([#2653]).
|
||||||
- The `Vary` header is now added along with `Access-Control-Allow-Origin` to
|
- The `Vary` header is now added along with `Access-Control-Allow-Origin` to
|
||||||
@@ -1791,7 +1832,7 @@ See also the [v0.105.0 GitHub milestone][ms-v0.105.0].
|
|||||||
|
|
||||||
- Go 1.14 support. v0.106.0 will require at least Go 1.15 to build.
|
- Go 1.14 support. v0.106.0 will require at least Go 1.15 to build.
|
||||||
- The `darwin/386` port. It will be removed in v0.106.0.
|
- The `darwin/386` port. It will be removed in v0.106.0.
|
||||||
- The `"rule"` and `"filter_id"` fields in `GET /filtering/check_host` and
|
- The `"rule"` and `"filter_id"` property in `GET /filtering/check_host` and
|
||||||
`GET /querylog` responses. They will be removed in v0.106.0 ([#2102]).
|
`GET /querylog` responses. They will be removed in v0.106.0 ([#2102]).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -1899,11 +1940,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
|
||||||
|
|||||||
2
client/dev.eslintrc
vendored
2
client/dev.eslintrc
vendored
@@ -3,4 +3,4 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"no-debugger":"warn",
|
"no-debugger":"warn",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,40 @@
|
|||||||
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279">
|
<link rel="mask-icon" href="assets/safari-pinned-tab.svg" color="#67B279">
|
||||||
<link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48">
|
<link rel="icon" type="image/png" href="assets/favicon.png" sizes="48x48">
|
||||||
<title>AdGuard Home</title>
|
<title>AdGuard Home</title>
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="DARK"] .wrapper {
|
||||||
|
background-color: #f5f7fb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
You need to enable JavaScript to run this app.
|
You need to enable JavaScript to run this app.
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="root"></div>
|
<div id="root">
|
||||||
|
<div class="wrapper"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var LOCAL_STORAGE_THEME_KEY = 'account_theme';
|
||||||
|
var theme = 'light';
|
||||||
|
|
||||||
|
try {
|
||||||
|
theme = window.localStorage.getItem(LOCAL_STORAGE_THEME_KEY);
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.dataset.theme = theme;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -17,5 +17,12 @@
|
|||||||
You need to enable JavaScript to run this app.
|
You need to enable JavaScript to run this app.
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
var currentTheme = prefersDark ? 'dark' : 'light';
|
||||||
|
document.body.dataset.theme = currentTheme;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -257,12 +257,12 @@
|
|||||||
"query_log_cleared": "The query log has been successfully cleared",
|
"query_log_cleared": "The query log has been successfully cleared",
|
||||||
"query_log_updated": "The query log has been successfully updated",
|
"query_log_updated": "The query log has been successfully updated",
|
||||||
"query_log_clear": "Clear query logs",
|
"query_log_clear": "Clear query logs",
|
||||||
"query_log_retention": "Query logs retention",
|
"query_log_retention": "Query logs rotation",
|
||||||
"query_log_enable": "Enable log",
|
"query_log_enable": "Enable log",
|
||||||
"query_log_configuration": "Logs configuration",
|
"query_log_configuration": "Logs configuration",
|
||||||
"query_log_disabled": "The query log is disabled and can be configured in the <0>settings</0>",
|
"query_log_disabled": "The query log is disabled and can be configured in the <0>settings</0>",
|
||||||
"query_log_strict_search": "Use double quotes for strict search",
|
"query_log_strict_search": "Use double quotes for strict search",
|
||||||
"query_log_retention_confirm": "Are you sure you want to change query log retention? If you decrease the interval value, some data will be lost",
|
"query_log_retention_confirm": "Are you sure you want to change query log rotation? If you decrease the interval value, some data will be lost",
|
||||||
"anonymize_client_ip": "Anonymize client IP",
|
"anonymize_client_ip": "Anonymize client IP",
|
||||||
"anonymize_client_ip_desc": "Don't save the client's full IP address to logs or statistics",
|
"anonymize_client_ip_desc": "Don't save the client's full IP address to logs or statistics",
|
||||||
"dns_config": "DNS server configuration",
|
"dns_config": "DNS server configuration",
|
||||||
@@ -668,5 +668,11 @@
|
|||||||
"disable_notify_for_hours": "Disable protection for {{count}} hour",
|
"disable_notify_for_hours": "Disable protection for {{count}} hour",
|
||||||
"disable_notify_for_hours_plural": "Disable protection for {{count}} hours",
|
"disable_notify_for_hours_plural": "Disable protection for {{count}} hours",
|
||||||
"disable_notify_until_tomorrow": "Disable protection until tomorrow",
|
"disable_notify_until_tomorrow": "Disable protection until tomorrow",
|
||||||
"enable_protection_timer": "Protection will be enabled in {{time}}"
|
"enable_protection_timer": "Protection will be enabled in {{time}}",
|
||||||
|
"custom_retention_input": "Enter retention in hours",
|
||||||
|
"custom_rotation_input": "Enter rotation in hours",
|
||||||
|
"protection_section_label": "Protection",
|
||||||
|
"log_and_stats_section_label": "Query log and statistics",
|
||||||
|
"ignore_query_log": "Ignore this client in query log",
|
||||||
|
"ignore_statistics": "Ignore this client in statistics"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,6 @@ import { createAction } from 'redux-actions';
|
|||||||
import apiClient from '../api/Api';
|
import apiClient from '../api/Api';
|
||||||
import { addErrorToast, addSuccessToast } from './toasts';
|
import { addErrorToast, addSuccessToast } from './toasts';
|
||||||
|
|
||||||
export const getBlockedServicesAvailableServicesRequest = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_REQUEST');
|
|
||||||
export const getBlockedServicesAvailableServicesFailure = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_FAILURE');
|
|
||||||
export const getBlockedServicesAvailableServicesSuccess = createAction('GET_BLOCKED_SERVICES_AVAILABLE_SERVICES_SUCCESS');
|
|
||||||
|
|
||||||
export const getBlockedServicesAvailableServices = () => async (dispatch) => {
|
|
||||||
dispatch(getBlockedServicesAvailableServicesRequest());
|
|
||||||
try {
|
|
||||||
const data = await apiClient.getBlockedServicesAvailableServices();
|
|
||||||
dispatch(getBlockedServicesAvailableServicesSuccess(data));
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(addErrorToast({ error }));
|
|
||||||
dispatch(getBlockedServicesAvailableServicesFailure());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST');
|
export const getBlockedServicesRequest = createAction('GET_BLOCKED_SERVICES_REQUEST');
|
||||||
export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE');
|
export const getBlockedServicesFailure = createAction('GET_BLOCKED_SERVICES_FAILURE');
|
||||||
export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS');
|
export const getBlockedServicesSuccess = createAction('GET_BLOCKED_SERVICES_SUCCESS');
|
||||||
|
|||||||
@@ -479,19 +479,12 @@ class Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Blocked services
|
// Blocked services
|
||||||
BLOCKED_SERVICES_SERVICES = { path: 'blocked_services/services', method: 'GET' };
|
|
||||||
|
|
||||||
BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' };
|
BLOCKED_SERVICES_LIST = { path: 'blocked_services/list', method: 'GET' };
|
||||||
|
|
||||||
BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
|
BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
|
||||||
|
|
||||||
BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };
|
BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };
|
||||||
|
|
||||||
getBlockedServicesAvailableServices() {
|
|
||||||
const { path, method } = this.BLOCKED_SERVICES_SERVICES;
|
|
||||||
return this.makeRequest(path, method);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllBlockedServices() {
|
getAllBlockedServices() {
|
||||||
const { path, method } = this.BLOCKED_SERVICES_ALL;
|
const { path, method } = this.BLOCKED_SERVICES_ALL;
|
||||||
return this.makeRequest(path, method);
|
return this.makeRequest(path, method);
|
||||||
|
|||||||
@@ -165,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) => {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -41,6 +41,17 @@ const settingsCheckboxes = [
|
|||||||
placeholder: 'use_adguard_parental',
|
placeholder: 'use_adguard_parental',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const logAndStatsCheckboxes = [
|
||||||
|
{
|
||||||
|
name: 'ignore_querylog',
|
||||||
|
placeholder: 'ignore_query_log',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ignore_statistics',
|
||||||
|
placeholder: 'ignore_statistics',
|
||||||
|
},
|
||||||
|
];
|
||||||
const validate = (values) => {
|
const validate = (values) => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
const { name, ids } = values;
|
const { name, ids } = values;
|
||||||
@@ -148,6 +159,9 @@ let Form = (props) => {
|
|||||||
settings: {
|
settings: {
|
||||||
title: 'settings',
|
title: 'settings',
|
||||||
component: <div label="settings" title={props.t('main_settings')}>
|
component: <div label="settings" title={props.t('main_settings')}>
|
||||||
|
<div className="form__label--bot form__label--bold">
|
||||||
|
{t('protection_section_label')}
|
||||||
|
</div>
|
||||||
{settingsCheckboxes.map((setting) => (
|
{settingsCheckboxes.map((setting) => (
|
||||||
<div className="form__group" key={setting.name}>
|
<div className="form__group" key={setting.name}>
|
||||||
<Field
|
<Field
|
||||||
@@ -185,6 +199,19 @@ let Form = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form__label--bold form__label--top form__label--bot">
|
||||||
|
{t('log_and_stats_section_label')}
|
||||||
|
</div>
|
||||||
|
{logAndStatsCheckboxes.map((setting) => (
|
||||||
|
<div className="form__group" key={setting.name}>
|
||||||
|
<Field
|
||||||
|
name={setting.name}
|
||||||
|
type="checkbox"
|
||||||
|
component={CheckboxField}
|
||||||
|
placeholder={t(setting.placeholder)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>,
|
</div>,
|
||||||
},
|
},
|
||||||
block_services: {
|
block_services: {
|
||||||
|
|||||||
@@ -1,25 +1,37 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import {
|
||||||
|
change,
|
||||||
|
Field,
|
||||||
|
formValueSelector,
|
||||||
|
reduxForm,
|
||||||
|
} from 'redux-form';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import { Trans, withTranslation } from 'react-i18next';
|
import { Trans, withTranslation } from 'react-i18next';
|
||||||
import flow from 'lodash/flow';
|
import flow from 'lodash/flow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CheckboxField,
|
CheckboxField,
|
||||||
renderRadioField,
|
|
||||||
toFloatNumber,
|
toFloatNumber,
|
||||||
renderTextareaField,
|
renderTextareaField, renderInputField, renderRadioField,
|
||||||
} from '../../../helpers/form';
|
} from '../../../helpers/form';
|
||||||
import {
|
import {
|
||||||
FORM_NAME,
|
FORM_NAME,
|
||||||
QUERY_LOG_INTERVALS_DAYS,
|
QUERY_LOG_INTERVALS_DAYS,
|
||||||
HOUR,
|
HOUR,
|
||||||
DAY,
|
DAY,
|
||||||
|
RETENTION_CUSTOM,
|
||||||
|
RETENTION_CUSTOM_INPUT,
|
||||||
|
RETENTION_RANGE,
|
||||||
|
CUSTOM_INTERVAL,
|
||||||
} from '../../../helpers/constants';
|
} from '../../../helpers/constants';
|
||||||
import '../FormButton.css';
|
import '../FormButton.css';
|
||||||
|
|
||||||
|
|
||||||
const getIntervalTitle = (interval, t) => {
|
const getIntervalTitle = (interval, t) => {
|
||||||
switch (interval) {
|
switch (interval) {
|
||||||
|
case RETENTION_CUSTOM:
|
||||||
|
return t('settings_custom');
|
||||||
case 6 * HOUR:
|
case 6 * HOUR:
|
||||||
return t('interval_6_hour');
|
return t('interval_6_hour');
|
||||||
case DAY:
|
case DAY:
|
||||||
@@ -42,11 +54,26 @@ const getIntervalFields = (processing, t, toNumber) => QUERY_LOG_INTERVALS_DAYS.
|
|||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
const Form = (props) => {
|
let Form = (props) => {
|
||||||
const {
|
const {
|
||||||
handleSubmit, submitting, invalid, processing, processingClear, handleClear, t,
|
handleSubmit,
|
||||||
|
submitting,
|
||||||
|
invalid,
|
||||||
|
processing,
|
||||||
|
processingClear,
|
||||||
|
handleClear,
|
||||||
|
t,
|
||||||
|
interval,
|
||||||
|
customInterval,
|
||||||
|
dispatch,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (QUERY_LOG_INTERVALS_DAYS.includes(interval)) {
|
||||||
|
dispatch(change(FORM_NAME.LOG_CONFIG, CUSTOM_INTERVAL, null));
|
||||||
|
}
|
||||||
|
}, [interval]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
@@ -73,6 +100,37 @@ const Form = (props) => {
|
|||||||
</label>
|
</label>
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<div className="custom-controls-stacked">
|
<div className="custom-controls-stacked">
|
||||||
|
<Field
|
||||||
|
key={RETENTION_CUSTOM}
|
||||||
|
name="interval"
|
||||||
|
type="radio"
|
||||||
|
component={renderRadioField}
|
||||||
|
value={QUERY_LOG_INTERVALS_DAYS.includes(interval)
|
||||||
|
? RETENTION_CUSTOM
|
||||||
|
: interval
|
||||||
|
}
|
||||||
|
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
|
||||||
|
normalize={toFloatNumber}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
{!QUERY_LOG_INTERVALS_DAYS.includes(interval) && (
|
||||||
|
<div className="form__group--input">
|
||||||
|
<div className="form__desc form__desc--top">
|
||||||
|
{t('custom_rotation_input')}
|
||||||
|
</div>
|
||||||
|
<Field
|
||||||
|
key={RETENTION_CUSTOM_INPUT}
|
||||||
|
name={CUSTOM_INTERVAL}
|
||||||
|
type="number"
|
||||||
|
className="form-control"
|
||||||
|
component={renderInputField}
|
||||||
|
disabled={processing}
|
||||||
|
normalize={toFloatNumber}
|
||||||
|
min={RETENTION_RANGE.MIN}
|
||||||
|
max={RETENTION_RANGE.MAX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{getIntervalFields(processing, t, toFloatNumber)}
|
{getIntervalFields(processing, t, toFloatNumber)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +154,12 @@ const Form = (props) => {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-success btn-standard btn-large"
|
className="btn btn-success btn-standard btn-large"
|
||||||
disabled={submitting || invalid || processing}
|
disabled={
|
||||||
|
submitting
|
||||||
|
|| invalid
|
||||||
|
|| processing
|
||||||
|
|| (!QUERY_LOG_INTERVALS_DAYS.includes(interval) && !customInterval)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Trans>save_btn</Trans>
|
<Trans>save_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
@@ -121,8 +184,22 @@ Form.propTypes = {
|
|||||||
processing: PropTypes.bool.isRequired,
|
processing: PropTypes.bool.isRequired,
|
||||||
processingClear: PropTypes.bool.isRequired,
|
processingClear: PropTypes.bool.isRequired,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
|
interval: PropTypes.number,
|
||||||
|
customInterval: PropTypes.number,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selector = formValueSelector(FORM_NAME.LOG_CONFIG);
|
||||||
|
|
||||||
|
Form = connect((state) => {
|
||||||
|
const interval = selector(state, 'interval');
|
||||||
|
const customInterval = selector(state, CUSTOM_INTERVAL);
|
||||||
|
return {
|
||||||
|
interval,
|
||||||
|
customInterval,
|
||||||
|
};
|
||||||
|
})(Form);
|
||||||
|
|
||||||
export default flow([
|
export default flow([
|
||||||
withTranslation(),
|
withTranslation(),
|
||||||
reduxForm({ form: FORM_NAME.LOG_CONFIG }),
|
reduxForm({ form: FORM_NAME.LOG_CONFIG }),
|
||||||
|
|||||||
@@ -4,15 +4,22 @@ import { withTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
|
import { HOUR } from '../../../helpers/constants';
|
||||||
|
|
||||||
class LogsConfig extends Component {
|
class LogsConfig extends Component {
|
||||||
handleFormSubmit = (values) => {
|
handleFormSubmit = (values) => {
|
||||||
const { t, interval: prevInterval } = this.props;
|
const { t, interval: prevInterval } = this.props;
|
||||||
const { interval } = values;
|
const { interval, customInterval, ...rest } = values;
|
||||||
|
|
||||||
const data = { ...values, ignored: values.ignored ? values.ignored.split('\n') : [] };
|
const newInterval = customInterval ? customInterval * HOUR : interval;
|
||||||
|
|
||||||
if (interval !== prevInterval) {
|
const data = {
|
||||||
|
...rest,
|
||||||
|
ignored: values.ignored ? values.ignored.split('\n') : [],
|
||||||
|
interval: newInterval,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newInterval < 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(data);
|
this.props.setLogsConfig(data);
|
||||||
@@ -32,7 +39,14 @@ class LogsConfig extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
t, enabled, interval, processing, processingClear, anonymize_client_ip, ignored,
|
t,
|
||||||
|
enabled,
|
||||||
|
interval,
|
||||||
|
processing,
|
||||||
|
processingClear,
|
||||||
|
anonymize_client_ip,
|
||||||
|
ignored,
|
||||||
|
customInterval,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,6 +60,7 @@ class LogsConfig extends Component {
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
enabled,
|
enabled,
|
||||||
interval,
|
interval,
|
||||||
|
customInterval,
|
||||||
anonymize_client_ip,
|
anonymize_client_ip,
|
||||||
ignored: ignored.join('\n'),
|
ignored: ignored.join('\n'),
|
||||||
}}
|
}}
|
||||||
@@ -62,6 +77,7 @@ class LogsConfig extends Component {
|
|||||||
|
|
||||||
LogsConfig.propTypes = {
|
LogsConfig.propTypes = {
|
||||||
interval: PropTypes.number.isRequired,
|
interval: PropTypes.number.isRequired,
|
||||||
|
customInterval: PropTypes.number,
|
||||||
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,
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form__group--input {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 1.5rem 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.form__group--checkbox {
|
.form__group--checkbox {
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
@@ -100,6 +105,14 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form__label--bot {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form__label--top {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.form__status {
|
.form__status {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -1,32 +1,44 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import {
|
||||||
|
change, Field, formValueSelector, 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 { connect } from 'react-redux';
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
renderRadioField,
|
renderRadioField,
|
||||||
toNumber,
|
toNumber,
|
||||||
CheckboxField,
|
CheckboxField,
|
||||||
renderTextareaField,
|
renderTextareaField,
|
||||||
|
toFloatNumber,
|
||||||
|
renderInputField,
|
||||||
} from '../../../helpers/form';
|
} from '../../../helpers/form';
|
||||||
import {
|
import {
|
||||||
FORM_NAME,
|
FORM_NAME,
|
||||||
STATS_INTERVALS_DAYS,
|
STATS_INTERVALS_DAYS,
|
||||||
DAY,
|
DAY,
|
||||||
|
RETENTION_CUSTOM,
|
||||||
|
RETENTION_CUSTOM_INPUT,
|
||||||
|
CUSTOM_INTERVAL,
|
||||||
|
RETENTION_RANGE,
|
||||||
} from '../../../helpers/constants';
|
} from '../../../helpers/constants';
|
||||||
import '../FormButton.css';
|
import '../FormButton.css';
|
||||||
|
|
||||||
const getIntervalTitle = (intervalMs, t) => {
|
const getIntervalTitle = (intervalMs, t) => {
|
||||||
switch (intervalMs / DAY) {
|
switch (intervalMs) {
|
||||||
case 1:
|
case RETENTION_CUSTOM:
|
||||||
|
return t('settings_custom');
|
||||||
|
case DAY:
|
||||||
return t('interval_24_hour');
|
return t('interval_24_hour');
|
||||||
default:
|
default:
|
||||||
return t('interval_days', { count: intervalMs / DAY });
|
return t('interval_days', { count: intervalMs / DAY });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Form = (props) => {
|
let Form = (props) => {
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
processing,
|
processing,
|
||||||
@@ -35,8 +47,17 @@ const Form = (props) => {
|
|||||||
handleReset,
|
handleReset,
|
||||||
processingReset,
|
processingReset,
|
||||||
t,
|
t,
|
||||||
|
interval,
|
||||||
|
customInterval,
|
||||||
|
dispatch,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (STATS_INTERVALS_DAYS.includes(interval)) {
|
||||||
|
dispatch(change(FORM_NAME.STATS_CONFIG, CUSTOM_INTERVAL, null));
|
||||||
|
}
|
||||||
|
}, [interval]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
@@ -56,6 +77,37 @@ const Form = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="form__group form__group--settings mt-2">
|
<div className="form__group form__group--settings mt-2">
|
||||||
<div className="custom-controls-stacked">
|
<div className="custom-controls-stacked">
|
||||||
|
<Field
|
||||||
|
key={RETENTION_CUSTOM}
|
||||||
|
name="interval"
|
||||||
|
type="radio"
|
||||||
|
component={renderRadioField}
|
||||||
|
value={STATS_INTERVALS_DAYS.includes(interval)
|
||||||
|
? RETENTION_CUSTOM
|
||||||
|
: interval
|
||||||
|
}
|
||||||
|
placeholder={getIntervalTitle(RETENTION_CUSTOM, t)}
|
||||||
|
normalize={toFloatNumber}
|
||||||
|
disabled={processing}
|
||||||
|
/>
|
||||||
|
{!STATS_INTERVALS_DAYS.includes(interval) && (
|
||||||
|
<div className="form__group--input">
|
||||||
|
<div className="form__desc form__desc--top">
|
||||||
|
{t('custom_retention_input')}
|
||||||
|
</div>
|
||||||
|
<Field
|
||||||
|
key={RETENTION_CUSTOM_INPUT}
|
||||||
|
name={CUSTOM_INTERVAL}
|
||||||
|
type="number"
|
||||||
|
className="form-control"
|
||||||
|
component={renderInputField}
|
||||||
|
disabled={processing}
|
||||||
|
normalize={toFloatNumber}
|
||||||
|
min={RETENTION_RANGE.MIN}
|
||||||
|
max={RETENTION_RANGE.MAX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{STATS_INTERVALS_DAYS.map((interval) => (
|
{STATS_INTERVALS_DAYS.map((interval) => (
|
||||||
<Field
|
<Field
|
||||||
key={interval}
|
key={interval}
|
||||||
@@ -90,7 +142,12 @@ const Form = (props) => {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-success btn-standard btn-large"
|
className="btn btn-success btn-standard btn-large"
|
||||||
disabled={submitting || invalid || processing}
|
disabled={
|
||||||
|
submitting
|
||||||
|
|| invalid
|
||||||
|
|| processing
|
||||||
|
|| (!STATS_INTERVALS_DAYS.includes(interval) && !customInterval)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Trans>save_btn</Trans>
|
<Trans>save_btn</Trans>
|
||||||
</button>
|
</button>
|
||||||
@@ -116,8 +173,22 @@ Form.propTypes = {
|
|||||||
processing: PropTypes.bool.isRequired,
|
processing: PropTypes.bool.isRequired,
|
||||||
processingReset: PropTypes.bool.isRequired,
|
processingReset: PropTypes.bool.isRequired,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
|
interval: PropTypes.number,
|
||||||
|
customInterval: PropTypes.number,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selector = formValueSelector(FORM_NAME.STATS_CONFIG);
|
||||||
|
|
||||||
|
Form = connect((state) => {
|
||||||
|
const interval = selector(state, 'interval');
|
||||||
|
const customInterval = selector(state, CUSTOM_INTERVAL);
|
||||||
|
return {
|
||||||
|
interval,
|
||||||
|
customInterval,
|
||||||
|
};
|
||||||
|
})(Form);
|
||||||
|
|
||||||
export default flow([
|
export default flow([
|
||||||
withTranslation(),
|
withTranslation(),
|
||||||
reduxForm({ form: FORM_NAME.STATS_CONFIG }),
|
reduxForm({ form: FORM_NAME.STATS_CONFIG }),
|
||||||
|
|||||||
@@ -4,13 +4,18 @@ import { withTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
|
import { HOUR } from '../../../helpers/constants';
|
||||||
|
|
||||||
class StatsConfig extends Component {
|
class StatsConfig extends Component {
|
||||||
handleFormSubmit = ({ enabled, interval, ignored }) => {
|
handleFormSubmit = ({
|
||||||
|
enabled, interval, ignored, customInterval,
|
||||||
|
}) => {
|
||||||
const { t, interval: prevInterval } = this.props;
|
const { t, interval: prevInterval } = this.props;
|
||||||
|
const newInterval = customInterval ? customInterval * HOUR : interval;
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
enabled,
|
enabled,
|
||||||
interval,
|
interval: newInterval,
|
||||||
ignored: ignored ? ignored.split('\n') : [],
|
ignored: ignored ? ignored.split('\n') : [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,7 +38,13 @@ class StatsConfig extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
t, interval, processing, processingReset, ignored, enabled,
|
t,
|
||||||
|
interval,
|
||||||
|
customInterval,
|
||||||
|
processing,
|
||||||
|
processingReset,
|
||||||
|
ignored,
|
||||||
|
enabled,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,6 +57,7 @@ class StatsConfig extends Component {
|
|||||||
<Form
|
<Form
|
||||||
initialValues={{
|
initialValues={{
|
||||||
interval,
|
interval,
|
||||||
|
customInterval,
|
||||||
enabled,
|
enabled,
|
||||||
ignored: ignored.join('\n'),
|
ignored: ignored.join('\n'),
|
||||||
}}
|
}}
|
||||||
@@ -62,6 +74,7 @@ class StatsConfig extends Component {
|
|||||||
|
|
||||||
StatsConfig.propTypes = {
|
StatsConfig.propTypes = {
|
||||||
interval: PropTypes.number.isRequired,
|
interval: PropTypes.number.isRequired,
|
||||||
|
customInterval: PropTypes.number,
|
||||||
ignored: PropTypes.array.isRequired,
|
ignored: PropTypes.array.isRequired,
|
||||||
enabled: PropTypes.bool.isRequired,
|
enabled: PropTypes.bool.isRequired,
|
||||||
processing: PropTypes.bool.isRequired,
|
processing: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ class Settings extends Component {
|
|||||||
enabled={queryLogs.enabled}
|
enabled={queryLogs.enabled}
|
||||||
ignored={queryLogs.ignored}
|
ignored={queryLogs.ignored}
|
||||||
interval={queryLogs.interval}
|
interval={queryLogs.interval}
|
||||||
|
customInterval={queryLogs.customInterval}
|
||||||
anonymize_client_ip={queryLogs.anonymize_client_ip}
|
anonymize_client_ip={queryLogs.anonymize_client_ip}
|
||||||
processing={queryLogs.processingSetConfig}
|
processing={queryLogs.processingSetConfig}
|
||||||
processingClear={queryLogs.processingClear}
|
processingClear={queryLogs.processingClear}
|
||||||
@@ -134,6 +135,7 @@ class Settings extends Component {
|
|||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
<StatsConfig
|
<StatsConfig
|
||||||
interval={stats.interval}
|
interval={stats.interval}
|
||||||
|
customInterval={stats.customInterval}
|
||||||
ignored={stats.ignored}
|
ignored={stats.ignored}
|
||||||
enabled={stats.enabled}
|
enabled={stats.enabled}
|
||||||
processing={stats.processingSetConfig}
|
processing={stats.processingSetConfig}
|
||||||
@@ -166,6 +168,7 @@ Settings.propTypes = {
|
|||||||
stats: PropTypes.shape({
|
stats: PropTypes.shape({
|
||||||
processingGetConfig: PropTypes.bool,
|
processingGetConfig: PropTypes.bool,
|
||||||
interval: PropTypes.number,
|
interval: PropTypes.number,
|
||||||
|
customInterval: PropTypes.number,
|
||||||
enabled: PropTypes.bool,
|
enabled: PropTypes.bool,
|
||||||
ignored: PropTypes.array,
|
ignored: PropTypes.array,
|
||||||
processingSetConfig: PropTypes.bool,
|
processingSetConfig: PropTypes.bool,
|
||||||
@@ -174,6 +177,7 @@ Settings.propTypes = {
|
|||||||
queryLogs: PropTypes.shape({
|
queryLogs: PropTypes.shape({
|
||||||
enabled: PropTypes.bool,
|
enabled: PropTypes.bool,
|
||||||
interval: PropTypes.number,
|
interval: PropTypes.number,
|
||||||
|
customInterval: PropTypes.number,
|
||||||
anonymize_client_ip: PropTypes.bool,
|
anonymize_client_ip: PropTypes.bool,
|
||||||
processingSetConfig: PropTypes.bool,
|
processingSetConfig: PropTypes.bool,
|
||||||
processingClear: PropTypes.bool,
|
processingClear: PropTypes.bool,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } 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 cn from 'classnames';
|
import cn from 'classnames';
|
||||||
@@ -33,16 +33,14 @@ 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(() => {
|
|
||||||
if (!isLoggedIn) {
|
|
||||||
setUITheme(window.matchMedia('(prefers-color-scheme: dark)').matches ? THEMES.dark : THEMES.light);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getYear = () => {
|
const getYear = () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
background-color: var(--rt-nodata-bgcolor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay--visible {
|
.overlay--visible {
|
||||||
|
|||||||
@@ -220,6 +220,12 @@ 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 QUERY_LOG_INTERVALS_DAYS = [HOUR * 6, DAY, DAY * 7, DAY * 30, DAY * 90];
|
||||||
|
|
||||||
|
export const RETENTION_CUSTOM = 1;
|
||||||
|
|
||||||
|
export const RETENTION_CUSTOM_INPUT = 'custom_retention_input';
|
||||||
|
|
||||||
|
export const CUSTOM_INTERVAL = 'customInterval';
|
||||||
|
|
||||||
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];
|
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];
|
||||||
|
|
||||||
// Note that translation strings contain these modes (blocking_mode_CONSTANT)
|
// Note that translation strings contain these modes (blocking_mode_CONSTANT)
|
||||||
@@ -462,6 +468,11 @@ export const UINT32_RANGE = {
|
|||||||
MAX: 4294967295,
|
MAX: 4294967295,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const RETENTION_RANGE = {
|
||||||
|
MIN: 1,
|
||||||
|
MAX: 365 * 24,
|
||||||
|
};
|
||||||
|
|
||||||
export const DHCP_VALUES_PLACEHOLDERS = {
|
export const DHCP_VALUES_PLACEHOLDERS = {
|
||||||
ipv4: {
|
ipv4: {
|
||||||
subnet_mask: '255.255.255.0',
|
subnet_mask: '255.255.255.0',
|
||||||
@@ -537,3 +548,5 @@ export const DISABLE_PROTECTION_TIMINGS = {
|
|||||||
HOUR: 60 * 60 * 1000,
|
HOUR: 60 * 60 * 1000,
|
||||||
TOMORROW: 24 * 60 * 60 * 1000,
|
TOMORROW: 24 * 60 * 60 * 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const LOCAL_STORAGE_THEME_KEY = 'account_theme';
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import {
|
|||||||
STANDARD_HTTPS_PORT,
|
STANDARD_HTTPS_PORT,
|
||||||
STANDARD_WEB_PORT,
|
STANDARD_WEB_PORT,
|
||||||
SPECIAL_FILTER_ID,
|
SPECIAL_FILTER_ID,
|
||||||
|
THEMES,
|
||||||
|
LOCAL_STORAGE_THEME_KEY,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -678,13 +680,61 @@ export const setHtmlLangAttr = (language) => {
|
|||||||
window.document.documentElement.lang = language;
|
window.document.documentElement.lang = language;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set local storage field
|
||||||
|
*
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const setStorageItem = (key, value) => {
|
||||||
|
if (window.localStorage) {
|
||||||
|
window.localStorage.setItem(key, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get local storage field
|
||||||
|
*
|
||||||
|
* @param {string} key
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getStorageItem = (key) => (window.localStorage
|
||||||
|
? window.localStorage.getItem(key)
|
||||||
|
: null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set local storage theme field
|
||||||
|
*
|
||||||
|
* @param {string} theme
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const setTheme = (theme) => {
|
||||||
|
setStorageItem(LOCAL_STORAGE_THEME_KEY, theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get local storage theme field
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getTheme = () => getStorageItem(LOCAL_STORAGE_THEME_KEY) || THEMES.light;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets UI theme.
|
* Sets UI theme.
|
||||||
*
|
*
|
||||||
* @param theme
|
* @param theme
|
||||||
*/
|
*/
|
||||||
export const setUITheme = (theme) => {
|
export const setUITheme = (theme) => {
|
||||||
document.body.dataset.theme = theme;
|
let currentTheme = theme || getTheme();
|
||||||
|
|
||||||
|
if (currentTheme === THEMES.auto) {
|
||||||
|
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
currentTheme = prefersDark ? THEMES.dark : THEMES.light;
|
||||||
|
}
|
||||||
|
setTheme(currentTheme);
|
||||||
|
document.body.dataset.theme = currentTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"timeUpdated": "2023-03-29T00:10:35.167Z",
|
"timeUpdated": "2023-04-06T10:46:09.881Z",
|
||||||
"categories": {
|
"categories": {
|
||||||
"0": "audio_video_player",
|
"0": "audio_video_player",
|
||||||
"1": "comments",
|
"1": "comments",
|
||||||
@@ -24192,4 +24192,4 @@
|
|||||||
"3gppnetwork.org": "3gpp",
|
"3gppnetwork.org": "3gpp",
|
||||||
"3gpp.org": "3gpp"
|
"3gpp.org": "3gpp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ const dashboard = handleActions(
|
|||||||
autoClients: [],
|
autoClients: [],
|
||||||
supportedTags: [],
|
supportedTags: [],
|
||||||
name: '',
|
name: '',
|
||||||
theme: 'auto',
|
theme: undefined,
|
||||||
checkUpdateFlag: false,
|
checkUpdateFlag: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
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, DAY } from '../helpers/constants';
|
import {
|
||||||
|
DEFAULT_LOGS_FILTER, DAY, QUERY_LOG_INTERVALS_DAYS, HOUR,
|
||||||
|
} from '../helpers/constants';
|
||||||
|
|
||||||
const queryLogs = handleActions(
|
const queryLogs = handleActions(
|
||||||
{
|
{
|
||||||
@@ -59,6 +61,9 @@ const queryLogs = handleActions(
|
|||||||
[actions.getLogsConfigSuccess]: (state, { payload }) => ({
|
[actions.getLogsConfigSuccess]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
...payload,
|
...payload,
|
||||||
|
customInterval: !QUERY_LOG_INTERVALS_DAYS.includes(payload.interval)
|
||||||
|
? payload.interval / HOUR
|
||||||
|
: null,
|
||||||
processingGetConfig: false,
|
processingGetConfig: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -95,6 +100,7 @@ const queryLogs = handleActions(
|
|||||||
anonymize_client_ip: false,
|
anonymize_client_ip: false,
|
||||||
isDetailed: true,
|
isDetailed: true,
|
||||||
isEntireLog: false,
|
isEntireLog: false,
|
||||||
|
customInterval: null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +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 { DAY, HOUR, STATS_INTERVALS_DAYS } from '../helpers/constants';
|
||||||
|
|
||||||
import * as actions from '../actions/stats';
|
import * as actions from '../actions/stats';
|
||||||
|
|
||||||
@@ -27,6 +27,9 @@ const stats = handleActions(
|
|||||||
[actions.getStatsConfigSuccess]: (state, { payload }) => ({
|
[actions.getStatsConfigSuccess]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
...payload,
|
...payload,
|
||||||
|
customInterval: !STATS_INTERVALS_DAYS.includes(payload.interval)
|
||||||
|
? payload.interval / HOUR
|
||||||
|
: null,
|
||||||
processingGetConfig: false,
|
processingGetConfig: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -93,6 +96,7 @@ const stats = handleActions(
|
|||||||
processingStats: true,
|
processingStats: true,
|
||||||
processingReset: false,
|
processingReset: false,
|
||||||
interval: DAY,
|
interval: DAY,
|
||||||
|
customInterval: null,
|
||||||
...defaultStats,
|
...defaultStats,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,19 +4,27 @@
|
|||||||
|
|
||||||
/^[[:space:]]+- .+/ {
|
/^[[:space:]]+- .+/ {
|
||||||
if (FNR - prev_line == 1) {
|
if (FNR - prev_line == 1) {
|
||||||
addrs[addrsnum++] = $2
|
addrs[$2] = true
|
||||||
prev_line = FNR
|
prev_line = FNR
|
||||||
|
|
||||||
|
if ($2 == "0.0.0.0" || $2 == "::") {
|
||||||
|
delete addrs
|
||||||
|
addrs["localhost"] = true
|
||||||
|
|
||||||
|
# Drop all the other addresses.
|
||||||
|
prev_line = -1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/^[[:space:]]+port:/ { if (is_dns) port = $2 }
|
/^[[:space:]]+port:/ { if (is_dns) port = $2 }
|
||||||
|
|
||||||
END {
|
END {
|
||||||
for (i in addrs) {
|
for (addr in addrs) {
|
||||||
if (match(addrs[i], ":")) {
|
if (match(addr, ":")) {
|
||||||
print "[" addrs[i] "]:" port
|
print "[" addr "]:" port
|
||||||
} else {
|
} else {
|
||||||
print addrs[i] ":" port
|
print addr ":" port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ END {
|
|||||||
} else {
|
} else {
|
||||||
print "http://" host ":" port
|
print "http://" host ":" port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
go.mod
28
go.mod
@@ -4,10 +4,10 @@ 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.7
|
||||||
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/dimfeld/httptreemux/v5 v5.5.0
|
||||||
github.com/fsnotify/fsnotify v1.6.0
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
@@ -17,19 +17,23 @@ 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
|
||||||
github.com/mdlayher/packet v1.1.1
|
github.com/mdlayher/packet v1.1.1
|
||||||
|
// TODO(a.garipov): This package is deprecated; find a new one or use our
|
||||||
|
// own code for that. Perhaps, use gopacket.
|
||||||
|
github.com/mdlayher/raw v0.1.0
|
||||||
github.com/miekg/dns v1.1.53
|
github.com/miekg/dns v1.1.53
|
||||||
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.7.0
|
golang.org/x/crypto v0.8.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.9.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
|
||||||
@@ -44,9 +48,7 @@ require (
|
|||||||
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-20230315185526-52ccab3ef572 // 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-20230323073829-e72429f035bd // indirect
|
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
|
||||||
github.com/mdlayher/raw v0.1.0 // indirect
|
|
||||||
github.com/mdlayher/socket v0.4.0 // indirect
|
github.com/mdlayher/socket v0.4.0 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.9.2 // 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
|
||||||
@@ -54,11 +56,11 @@ require (
|
|||||||
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.3.0 // indirect
|
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
|
||||||
github.com/quic-go/qtls-go1-20 v0.2.0 // indirect
|
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
|
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
|
||||||
golang.org/x/mod v0.9.0 // indirect
|
golang.org/x/mod v0.10.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.9.0 // indirect
|
||||||
golang.org/x/tools v0.7.0 // indirect
|
golang.org/x/tools v0.8.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
49
go.sum
49
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=
|
||||||
@@ -15,8 +15,8 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH
|
|||||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
|
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
|
||||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
|
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
|
||||||
github.com/ameshkov/dnscrypt/v2 v2.2.6 h1:rE7AFbPWebq7me7RVS66Cipd1m7ef1yf2+C8QzjQXXE=
|
github.com/ameshkov/dnscrypt/v2 v2.2.7 h1:aEitLIR8HcxVodZ79mgRcCiC0A0I5kZPBuWGFwwulAw=
|
||||||
github.com/ameshkov/dnscrypt/v2 v2.2.6/go.mod h1:qPWhwz6FdSmuK7W4sMyvogrez4MWdtzosdqlr0Rg3ow=
|
github.com/ameshkov/dnscrypt/v2 v2.2.7/go.mod h1:qPWhwz6FdSmuK7W4sMyvogrez4MWdtzosdqlr0Rg3ow=
|
||||||
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
|
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
|
||||||
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
|
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
|
||||||
@@ -55,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-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
|
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ=
|
||||||
github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
|
github.com/google/pprof v0.0.0-20230406165453-00490a63f317/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=
|
||||||
@@ -70,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=
|
||||||
@@ -123,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.3.0 h1:aUBoQdpHzUWtPw5tQZbsD2GnrWCNu7/RIX1PtqGeLYY=
|
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
|
||||||
github.com/quic-go/qtls-go1-19 v0.3.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
||||||
github.com/quic-go/qtls-go1-20 v0.2.0 h1:jUHn+obJ6WI5JudqBO0Iy1ra5Vh5vsitQ1gXQvkmN+E=
|
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
|
||||||
github.com/quic-go/qtls-go1-20 v0.2.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
github.com/quic-go/qtls-go1-20 v0.2.2/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=
|
||||||
@@ -161,15 +161,15 @@ 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.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
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.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.10.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=
|
||||||
@@ -185,8 +185,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
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-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.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
@@ -218,23 +218,24 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
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=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
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.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
||||||
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 (
|
||||||
|
|||||||
2
internal/aghnet/testdata/etc_hosts
vendored
2
internal/aghnet/testdata/etc_hosts
vendored
@@ -35,4 +35,4 @@
|
|||||||
1.3.5.7 domain4 domain4.alias
|
1.3.5.7 domain4 domain4.alias
|
||||||
7.5.3.1 domain4.alias domain4
|
7.5.3.1 domain4.alias domain4
|
||||||
::13 domain6 domain6.alias
|
::13 domain6 domain6.alias
|
||||||
::31 domain6.alias domain6
|
::31 domain6.alias domain6
|
||||||
|
|||||||
2
internal/aghnet/testdata/ifaces
vendored
2
internal/aghnet/testdata/ifaces
vendored
@@ -1 +1 @@
|
|||||||
iface sample_name inet static
|
iface sample_name inet static
|
||||||
|
|||||||
2
internal/aghnet/testdata/include-subsources
vendored
2
internal/aghnet/testdata/include-subsources
vendored
@@ -2,4 +2,4 @@
|
|||||||
# parent directory. Real interface files usually contain only absolute paths.
|
# parent directory. Real interface files usually contain only absolute paths.
|
||||||
|
|
||||||
source ./testdata/ifaces
|
source ./testdata/ifaces
|
||||||
source ./testdata/*
|
source ./testdata/*
|
||||||
|
|||||||
2
internal/aghnet/testdata/proc_net_arp
vendored
2
internal/aghnet/testdata/proc_net_arp
vendored
@@ -3,4 +3,4 @@ IP address HW type Flags HW address Mask Device
|
|||||||
::ffff:ffff 0x1 0x0 ef:cd:ab:ef:cd:ab * br-lan
|
::ffff:ffff 0x1 0x0 ef:cd:ab:ef:cd:ab * br-lan
|
||||||
0.0.0.0 0x0 0x0 00:00:00:00:00:00 * unspec
|
0.0.0.0 0x0 0x0 00:00:00:00:00:00 * unspec
|
||||||
1.2.3.4.5 0x1 0x2 aa:bb:cc:dd:ee:ff * wan
|
1.2.3.4.5 0x1 0x2 aa:bb:cc:dd:ee:ff * wan
|
||||||
1.2.3.4 0x1 0x2 12:34:56:78:910 * wan
|
1.2.3.4 0x1 0x2 12:34:56:78:910 * wan
|
||||||
|
|||||||
@@ -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
|
|
||||||
293
internal/dhcpd/conn_darwin.go
Normal file
293
internal/dhcpd/conn_darwin.go
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package dhcpd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
"github.com/google/gopacket"
|
||||||
|
"github.com/google/gopacket/layers"
|
||||||
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
|
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
||||||
|
"github.com/mdlayher/ethernet"
|
||||||
|
|
||||||
|
//lint:ignore SA1019 See the TODO in go.mod.
|
||||||
|
"github.com/mdlayher/raw"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dhcpUnicastAddr is the combination of MAC and IP addresses for responding to
|
||||||
|
// the unconfigured host.
|
||||||
|
type dhcpUnicastAddr struct {
|
||||||
|
// raw.Addr is embedded here to make *dhcpUcastAddr a net.Addr without
|
||||||
|
// actually implementing all methods. It also contains the client's
|
||||||
|
// hardware address.
|
||||||
|
raw.Addr
|
||||||
|
|
||||||
|
// yiaddr is an IP address just allocated by server for the host.
|
||||||
|
yiaddr net.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
// dhcpConn is the net.PacketConn capable of handling both net.UDPAddr and
|
||||||
|
// net.HardwareAddr.
|
||||||
|
type dhcpConn struct {
|
||||||
|
// udpConn is the connection for UDP addresses.
|
||||||
|
udpConn net.PacketConn
|
||||||
|
// bcastIP is the broadcast address specific for the configured
|
||||||
|
// interface's subnet.
|
||||||
|
bcastIP net.IP
|
||||||
|
|
||||||
|
// rawConn is the connection for MAC addresses.
|
||||||
|
rawConn net.PacketConn
|
||||||
|
// srcMAC is the hardware address of the configured network interface.
|
||||||
|
srcMAC net.HardwareAddr
|
||||||
|
// srcIP is the IP address of the configured network interface.
|
||||||
|
srcIP net.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDHCPConn creates the special connection for DHCP server.
|
||||||
|
func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err error) {
|
||||||
|
var ucast net.PacketConn
|
||||||
|
if ucast, err = raw.ListenPacket(iface, uint16(ethernet.EtherTypeIPv4), nil); err != nil {
|
||||||
|
return nil, fmt.Errorf("creating raw udp connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the UDP connection.
|
||||||
|
var bcast net.PacketConn
|
||||||
|
bcast, err = server4.NewIPv4UDPConn(iface.Name, &net.UDPAddr{
|
||||||
|
// TODO(e.burkov): Listening on zeroes makes the server handle
|
||||||
|
// requests from all the interfaces. Inspect the ways to
|
||||||
|
// specify the interface-specific listening addresses.
|
||||||
|
//
|
||||||
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.
|
||||||
|
IP: net.IP{0, 0, 0, 0},
|
||||||
|
Port: dhcpv4.ServerPort,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating ipv4 udp connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dhcpConn{
|
||||||
|
udpConn: bcast,
|
||||||
|
bcastIP: s.conf.broadcastIP.AsSlice(),
|
||||||
|
rawConn: ucast,
|
||||||
|
srcMAC: iface.HardwareAddr,
|
||||||
|
srcIP: s.conf.dnsIPAddrs[0].AsSlice(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapErrs is a helper to wrap the errors from two independent underlying
|
||||||
|
// connections.
|
||||||
|
func (*dhcpConn) wrapErrs(action string, udpConnErr, rawConnErr error) (err error) {
|
||||||
|
switch {
|
||||||
|
case udpConnErr != nil && rawConnErr != nil:
|
||||||
|
return errors.List(fmt.Sprintf("%s both connections", action), udpConnErr, rawConnErr)
|
||||||
|
case udpConnErr != nil:
|
||||||
|
return fmt.Errorf("%s udp connection: %w", action, udpConnErr)
|
||||||
|
case rawConnErr != nil:
|
||||||
|
return fmt.Errorf("%s raw connection: %w", action, rawConnErr)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTo implements net.PacketConn for *dhcpConn. It selects the underlying
|
||||||
|
// connection to write to based on the type of addr.
|
||||||
|
func (c *dhcpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||||
|
switch addr := addr.(type) {
|
||||||
|
case *dhcpUnicastAddr:
|
||||||
|
// Unicast the message to the client's MAC address. Use the raw
|
||||||
|
// connection.
|
||||||
|
//
|
||||||
|
// Note: unicasting is performed on the only network interface
|
||||||
|
// that is configured. For now it may be not what users expect
|
||||||
|
// so additionally broadcast the message via UDP connection.
|
||||||
|
//
|
||||||
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/3539.
|
||||||
|
var rerr error
|
||||||
|
n, rerr = c.unicast(p, addr)
|
||||||
|
|
||||||
|
_, uerr := c.broadcast(p, &net.UDPAddr{
|
||||||
|
IP: netutil.IPv4bcast(),
|
||||||
|
Port: dhcpv4.ClientPort,
|
||||||
|
})
|
||||||
|
|
||||||
|
return n, c.wrapErrs("writing to", uerr, rerr)
|
||||||
|
case *net.UDPAddr:
|
||||||
|
if addr.IP.Equal(net.IPv4bcast) {
|
||||||
|
// Broadcast the message for the client which supports
|
||||||
|
// it. Use the UDP connection.
|
||||||
|
return c.broadcast(p, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unicast the message to the client's IP address. Use the UDP
|
||||||
|
// connection.
|
||||||
|
return c.udpConn.WriteTo(p, addr)
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("addr has an unexpected type %T", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFrom implements net.PacketConn for *dhcpConn.
|
||||||
|
func (c *dhcpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
|
return c.udpConn.ReadFrom(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unicast wraps respData with required frames and writes it to the peer.
|
||||||
|
func (c *dhcpConn) unicast(respData []byte, peer *dhcpUnicastAddr) (n int, err error) {
|
||||||
|
var data []byte
|
||||||
|
data, err = c.buildEtherPkt(respData, peer)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.rawConn.WriteTo(data, &peer.Addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements net.PacketConn for *dhcpConn.
|
||||||
|
func (c *dhcpConn) Close() (err error) {
|
||||||
|
rerr := c.rawConn.Close()
|
||||||
|
if errors.Is(rerr, os.ErrClosed) {
|
||||||
|
// Ignore the error since the actual file is closed already.
|
||||||
|
rerr = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.wrapErrs("closing", c.udpConn.Close(), rerr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalAddr implements net.PacketConn for *dhcpConn.
|
||||||
|
func (c *dhcpConn) LocalAddr() (a net.Addr) {
|
||||||
|
return c.udpConn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDeadline implements net.PacketConn for *dhcpConn.
|
||||||
|
func (c *dhcpConn) SetDeadline(t time.Time) (err error) {
|
||||||
|
return c.wrapErrs("setting deadline on", c.udpConn.SetDeadline(t), c.rawConn.SetDeadline(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReadDeadline implements net.PacketConn for *dhcpConn.
|
||||||
|
func (c *dhcpConn) SetReadDeadline(t time.Time) error {
|
||||||
|
return c.wrapErrs(
|
||||||
|
"setting reading deadline on",
|
||||||
|
c.udpConn.SetReadDeadline(t),
|
||||||
|
c.rawConn.SetReadDeadline(t),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWriteDeadline implements net.PacketConn for *dhcpConn.
|
||||||
|
func (c *dhcpConn) SetWriteDeadline(t time.Time) error {
|
||||||
|
return c.wrapErrs(
|
||||||
|
"setting writing deadline on",
|
||||||
|
c.udpConn.SetWriteDeadline(t),
|
||||||
|
c.rawConn.SetWriteDeadline(t),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ipv4DefaultTTL is the default Time to Live value in seconds as recommended by
|
||||||
|
// RFC-1700.
|
||||||
|
//
|
||||||
|
// See https://datatracker.ietf.org/doc/html/rfc1700.
|
||||||
|
const ipv4DefaultTTL = 64
|
||||||
|
|
||||||
|
// buildEtherPkt wraps the payload with IPv4, UDP and Ethernet frames.
|
||||||
|
// Validation of the payload is a caller's responsibility.
|
||||||
|
func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []byte, err error) {
|
||||||
|
udpLayer := &layers.UDP{
|
||||||
|
SrcPort: dhcpv4.ServerPort,
|
||||||
|
DstPort: dhcpv4.ClientPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
ipv4Layer := &layers.IPv4{
|
||||||
|
Version: uint8(layers.IPProtocolIPv4),
|
||||||
|
Flags: layers.IPv4DontFragment,
|
||||||
|
TTL: ipv4DefaultTTL,
|
||||||
|
Protocol: layers.IPProtocolUDP,
|
||||||
|
SrcIP: c.srcIP,
|
||||||
|
DstIP: peer.yiaddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore the error since it's only returned for invalid network layer's
|
||||||
|
// type.
|
||||||
|
_ = udpLayer.SetNetworkLayerForChecksum(ipv4Layer)
|
||||||
|
|
||||||
|
ethLayer := &layers.Ethernet{
|
||||||
|
SrcMAC: c.srcMAC,
|
||||||
|
DstMAC: peer.HardwareAddr,
|
||||||
|
EthernetType: layers.EthernetTypeIPv4,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := gopacket.NewSerializeBuffer()
|
||||||
|
setts := gopacket.SerializeOptions{
|
||||||
|
FixLengths: true,
|
||||||
|
ComputeChecksums: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gopacket.SerializeLayers(
|
||||||
|
buf,
|
||||||
|
setts,
|
||||||
|
ethLayer,
|
||||||
|
ipv4Layer,
|
||||||
|
udpLayer,
|
||||||
|
gopacket.Payload(payload),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("serializing layers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// send writes resp for peer to conn considering the req's parameters according
|
||||||
|
// to RFC-2131.
|
||||||
|
//
|
||||||
|
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
|
||||||
|
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
|
||||||
|
switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
|
||||||
|
case giaddr != nil && !giaddr.IsUnspecified():
|
||||||
|
// Send any return messages to the server port on the BOOTP
|
||||||
|
// relay agent whose address appears in giaddr.
|
||||||
|
peer = &net.UDPAddr{
|
||||||
|
IP: giaddr,
|
||||||
|
Port: dhcpv4.ServerPort,
|
||||||
|
}
|
||||||
|
if mtype == dhcpv4.MessageTypeNak {
|
||||||
|
// Set the broadcast bit in the DHCPNAK, so that the relay agent
|
||||||
|
// broadcasts it to the client, because the client may not have
|
||||||
|
// a correct network address or subnet mask, and the client may not
|
||||||
|
// be answering ARP requests.
|
||||||
|
resp.SetBroadcast()
|
||||||
|
}
|
||||||
|
case mtype == dhcpv4.MessageTypeNak:
|
||||||
|
// Broadcast any DHCPNAK messages to 0xffffffff.
|
||||||
|
case ciaddr != nil && !ciaddr.IsUnspecified():
|
||||||
|
// Unicast DHCPOFFER and DHCPACK messages to the address in
|
||||||
|
// ciaddr.
|
||||||
|
peer = &net.UDPAddr{
|
||||||
|
IP: ciaddr,
|
||||||
|
Port: dhcpv4.ClientPort,
|
||||||
|
}
|
||||||
|
case !req.IsBroadcast() && req.ClientHWAddr != nil:
|
||||||
|
// Unicast DHCPOFFER and DHCPACK messages to the client's
|
||||||
|
// hardware address and yiaddr.
|
||||||
|
peer = &dhcpUnicastAddr{
|
||||||
|
Addr: raw.Addr{HardwareAddr: req.ClientHWAddr},
|
||||||
|
yiaddr: resp.YourIPAddr,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Go on since peer is already set to broadcast.
|
||||||
|
}
|
||||||
|
|
||||||
|
pktData := resp.ToBytes()
|
||||||
|
|
||||||
|
log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary())
|
||||||
|
|
||||||
|
_, err := conn.WriteTo(pktData, peer)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
219
internal/dhcpd/conn_darwin_internal_test.go
Normal file
219
internal/dhcpd/conn_darwin_internal_test.go
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package dhcpd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/google/gopacket"
|
||||||
|
"github.com/google/gopacket/layers"
|
||||||
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
//lint:ignore SA1019 See the TODO in go.mod.
|
||||||
|
"github.com/mdlayher/raw"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDHCPConn_WriteTo_common(t *testing.T) {
|
||||||
|
respData := (&dhcpv4.DHCPv4{}).ToBytes()
|
||||||
|
udpAddr := &net.UDPAddr{
|
||||||
|
IP: net.IP{1, 2, 3, 4},
|
||||||
|
Port: dhcpv4.ClientPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("unicast_ip", func(t *testing.T) {
|
||||||
|
writeTo := func(_ []byte, addr net.Addr) (_ int, _ error) {
|
||||||
|
assert.Equal(t, udpAddr, addr)
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &dhcpConn{udpConn: &fakePacketConn{writeTo: writeTo}}
|
||||||
|
|
||||||
|
_, err := conn.WriteTo(respData, udpAddr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unexpected_addr_type", func(t *testing.T) {
|
||||||
|
type unexpectedAddrType struct {
|
||||||
|
net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &dhcpConn{}
|
||||||
|
n, err := conn.WriteTo(nil, &unexpectedAddrType{})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
testutil.AssertErrorMsg(t, "addr has an unexpected type *dhcpd.unexpectedAddrType", err)
|
||||||
|
assert.Zero(t, n)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildEtherPkt(t *testing.T) {
|
||||||
|
conn := &dhcpConn{
|
||||||
|
srcMAC: net.HardwareAddr{1, 2, 3, 4, 5, 6},
|
||||||
|
srcIP: net.IP{1, 2, 3, 4},
|
||||||
|
}
|
||||||
|
peer := &dhcpUnicastAddr{
|
||||||
|
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{6, 5, 4, 3, 2, 1}},
|
||||||
|
yiaddr: net.IP{4, 3, 2, 1},
|
||||||
|
}
|
||||||
|
payload := (&dhcpv4.DHCPv4{}).ToBytes()
|
||||||
|
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
pkt, err := conn.buildEtherPkt(payload, peer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, pkt)
|
||||||
|
|
||||||
|
actualPkt := gopacket.NewPacket(pkt, layers.LayerTypeEthernet, gopacket.DecodeOptions{
|
||||||
|
NoCopy: true,
|
||||||
|
})
|
||||||
|
require.NotNil(t, actualPkt)
|
||||||
|
|
||||||
|
wantTypes := []gopacket.LayerType{
|
||||||
|
layers.LayerTypeEthernet,
|
||||||
|
layers.LayerTypeIPv4,
|
||||||
|
layers.LayerTypeUDP,
|
||||||
|
layers.LayerTypeDHCPv4,
|
||||||
|
}
|
||||||
|
actualLayers := actualPkt.Layers()
|
||||||
|
require.Len(t, actualLayers, len(wantTypes))
|
||||||
|
|
||||||
|
for i, wantType := range wantTypes {
|
||||||
|
layer := actualLayers[i]
|
||||||
|
require.NotNil(t, layer)
|
||||||
|
|
||||||
|
assert.Equal(t, wantType, layer.LayerType())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad_payload", func(t *testing.T) {
|
||||||
|
// Create an invalid DHCP packet.
|
||||||
|
invalidPayload := []byte{1, 2, 3, 4}
|
||||||
|
pkt, err := conn.buildEtherPkt(invalidPayload, peer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, pkt)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("serializing_error", func(t *testing.T) {
|
||||||
|
// Create a peer with invalid MAC.
|
||||||
|
badPeer := &dhcpUnicastAddr{
|
||||||
|
Addr: raw.Addr{HardwareAddr: net.HardwareAddr{5, 4, 3, 2, 1}},
|
||||||
|
yiaddr: net.IP{4, 3, 2, 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt, err := conn.buildEtherPkt(payload, badPeer)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.Empty(t, pkt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV4Server_Send(t *testing.T) {
|
||||||
|
s := &v4Server{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultIP = net.IP{99, 99, 99, 99}
|
||||||
|
knownIP = net.IP{4, 2, 4, 2}
|
||||||
|
knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
defaultPeer := &net.UDPAddr{
|
||||||
|
IP: defaultIP,
|
||||||
|
// Use neither client nor server port to check it actually
|
||||||
|
// changed.
|
||||||
|
Port: dhcpv4.ClientPort + dhcpv4.ServerPort,
|
||||||
|
}
|
||||||
|
defaultResp := &dhcpv4.DHCPv4{}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
want net.Addr
|
||||||
|
req *dhcpv4.DHCPv4
|
||||||
|
resp *dhcpv4.DHCPv4
|
||||||
|
name string
|
||||||
|
}{{
|
||||||
|
name: "giaddr",
|
||||||
|
req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},
|
||||||
|
resp: defaultResp,
|
||||||
|
want: &net.UDPAddr{
|
||||||
|
IP: knownIP,
|
||||||
|
Port: dhcpv4.ServerPort,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "nak",
|
||||||
|
req: &dhcpv4.DHCPv4{},
|
||||||
|
resp: &dhcpv4.DHCPv4{
|
||||||
|
Options: dhcpv4.OptionsFromList(
|
||||||
|
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
want: defaultPeer,
|
||||||
|
}, {
|
||||||
|
name: "ciaddr",
|
||||||
|
req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP},
|
||||||
|
resp: &dhcpv4.DHCPv4{},
|
||||||
|
want: &net.UDPAddr{
|
||||||
|
IP: knownIP,
|
||||||
|
Port: dhcpv4.ClientPort,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "chaddr",
|
||||||
|
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
|
||||||
|
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
|
||||||
|
want: &dhcpUnicastAddr{
|
||||||
|
Addr: raw.Addr{HardwareAddr: knownMAC},
|
||||||
|
yiaddr: knownIP,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "who_are_you",
|
||||||
|
req: &dhcpv4.DHCPv4{},
|
||||||
|
resp: &dhcpv4.DHCPv4{},
|
||||||
|
want: defaultPeer,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
conn := &fakePacketConn{
|
||||||
|
writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) {
|
||||||
|
assert.Equal(t, tc.want, addr)
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("giaddr_nak", func(t *testing.T) {
|
||||||
|
req := &dhcpv4.DHCPv4{
|
||||||
|
GatewayIPAddr: knownIP,
|
||||||
|
}
|
||||||
|
// Ensure the request is for unicast.
|
||||||
|
req.SetUnicast()
|
||||||
|
resp := &dhcpv4.DHCPv4{
|
||||||
|
Options: dhcpv4.OptionsFromList(
|
||||||
|
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
want := &net.UDPAddr{
|
||||||
|
IP: req.GatewayIPAddr,
|
||||||
|
Port: dhcpv4.ServerPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &fakePacketConn{
|
||||||
|
writeTo: func(_ []byte, addr net.Addr) (n int, err error) {
|
||||||
|
assert.Equal(t, want, addr)
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s.send(cloneUDPAddr(defaultPeer), conn, req, resp)
|
||||||
|
assert.True(t, resp.IsBroadcast())
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build darwin || freebsd || linux || openbsd
|
//go:build freebsd || linux || openbsd
|
||||||
|
|
||||||
package dhcpd
|
package dhcpd
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/google/gopacket"
|
"github.com/google/gopacket"
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
@@ -238,3 +239,53 @@ func (c *dhcpConn) buildEtherPkt(payload []byte, peer *dhcpUnicastAddr) (pkt []b
|
|||||||
|
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send writes resp for peer to conn considering the req's parameters according
|
||||||
|
// to RFC-2131.
|
||||||
|
//
|
||||||
|
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
|
||||||
|
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
|
||||||
|
switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
|
||||||
|
case giaddr != nil && !giaddr.IsUnspecified():
|
||||||
|
// Send any return messages to the server port on the BOOTP
|
||||||
|
// relay agent whose address appears in giaddr.
|
||||||
|
peer = &net.UDPAddr{
|
||||||
|
IP: giaddr,
|
||||||
|
Port: dhcpv4.ServerPort,
|
||||||
|
}
|
||||||
|
if mtype == dhcpv4.MessageTypeNak {
|
||||||
|
// Set the broadcast bit in the DHCPNAK, so that the relay agent
|
||||||
|
// broadcasts it to the client, because the client may not have
|
||||||
|
// a correct network address or subnet mask, and the client may not
|
||||||
|
// be answering ARP requests.
|
||||||
|
resp.SetBroadcast()
|
||||||
|
}
|
||||||
|
case mtype == dhcpv4.MessageTypeNak:
|
||||||
|
// Broadcast any DHCPNAK messages to 0xffffffff.
|
||||||
|
case ciaddr != nil && !ciaddr.IsUnspecified():
|
||||||
|
// Unicast DHCPOFFER and DHCPACK messages to the address in
|
||||||
|
// ciaddr.
|
||||||
|
peer = &net.UDPAddr{
|
||||||
|
IP: ciaddr,
|
||||||
|
Port: dhcpv4.ClientPort,
|
||||||
|
}
|
||||||
|
case !req.IsBroadcast() && req.ClientHWAddr != nil:
|
||||||
|
// Unicast DHCPOFFER and DHCPACK messages to the client's
|
||||||
|
// hardware address and yiaddr.
|
||||||
|
peer = &dhcpUnicastAddr{
|
||||||
|
Addr: packet.Addr{HardwareAddr: req.ClientHWAddr},
|
||||||
|
yiaddr: resp.YourIPAddr,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Go on since peer is already set to broadcast.
|
||||||
|
}
|
||||||
|
|
||||||
|
pktData := resp.ToBytes()
|
||||||
|
|
||||||
|
log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary())
|
||||||
|
|
||||||
|
_, err := conn.WriteTo(pktData, peer)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build darwin || freebsd || linux || openbsd
|
//go:build freebsd || linux || openbsd
|
||||||
|
|
||||||
package dhcpd
|
package dhcpd
|
||||||
|
|
||||||
@@ -110,3 +110,108 @@ func TestBuildEtherPkt(t *testing.T) {
|
|||||||
assert.Empty(t, pkt)
|
assert.Empty(t, pkt)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestV4Server_Send(t *testing.T) {
|
||||||
|
s := &v4Server{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultIP = net.IP{99, 99, 99, 99}
|
||||||
|
knownIP = net.IP{4, 2, 4, 2}
|
||||||
|
knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
defaultPeer := &net.UDPAddr{
|
||||||
|
IP: defaultIP,
|
||||||
|
// Use neither client nor server port to check it actually
|
||||||
|
// changed.
|
||||||
|
Port: dhcpv4.ClientPort + dhcpv4.ServerPort,
|
||||||
|
}
|
||||||
|
defaultResp := &dhcpv4.DHCPv4{}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
want net.Addr
|
||||||
|
req *dhcpv4.DHCPv4
|
||||||
|
resp *dhcpv4.DHCPv4
|
||||||
|
name string
|
||||||
|
}{{
|
||||||
|
name: "giaddr",
|
||||||
|
req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},
|
||||||
|
resp: defaultResp,
|
||||||
|
want: &net.UDPAddr{
|
||||||
|
IP: knownIP,
|
||||||
|
Port: dhcpv4.ServerPort,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "nak",
|
||||||
|
req: &dhcpv4.DHCPv4{},
|
||||||
|
resp: &dhcpv4.DHCPv4{
|
||||||
|
Options: dhcpv4.OptionsFromList(
|
||||||
|
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
want: defaultPeer,
|
||||||
|
}, {
|
||||||
|
name: "ciaddr",
|
||||||
|
req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP},
|
||||||
|
resp: &dhcpv4.DHCPv4{},
|
||||||
|
want: &net.UDPAddr{
|
||||||
|
IP: knownIP,
|
||||||
|
Port: dhcpv4.ClientPort,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "chaddr",
|
||||||
|
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
|
||||||
|
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
|
||||||
|
want: &dhcpUnicastAddr{
|
||||||
|
Addr: packet.Addr{HardwareAddr: knownMAC},
|
||||||
|
yiaddr: knownIP,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "who_are_you",
|
||||||
|
req: &dhcpv4.DHCPv4{},
|
||||||
|
resp: &dhcpv4.DHCPv4{},
|
||||||
|
want: defaultPeer,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
conn := &fakePacketConn{
|
||||||
|
writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) {
|
||||||
|
assert.Equal(t, tc.want, addr)
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("giaddr_nak", func(t *testing.T) {
|
||||||
|
req := &dhcpv4.DHCPv4{
|
||||||
|
GatewayIPAddr: knownIP,
|
||||||
|
}
|
||||||
|
// Ensure the request is for unicast.
|
||||||
|
req.SetUnicast()
|
||||||
|
resp := &dhcpv4.DHCPv4{
|
||||||
|
Options: dhcpv4.OptionsFromList(
|
||||||
|
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
want := &net.UDPAddr{
|
||||||
|
IP: req.GatewayIPAddr,
|
||||||
|
Port: dhcpv4.ServerPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &fakePacketConn{
|
||||||
|
writeTo: func(_ []byte, addr net.Addr) (n int, err error) {
|
||||||
|
assert.Equal(t, want, addr)
|
||||||
|
|
||||||
|
return 0, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
s.send(cloneUDPAddr(defaultPeer), conn, req, resp)
|
||||||
|
assert.True(t, resp.IsBroadcast())
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -71,16 +71,17 @@ func (s *server) dbLoad() (err error) {
|
|||||||
IP: 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)
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ type Lease struct {
|
|||||||
//
|
//
|
||||||
// TODO(a.garipov): Migrate leases.db.
|
// TODO(a.garipov): Migrate leases.db.
|
||||||
IP netip.Addr `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.
|
||||||
@@ -64,6 +67,7 @@ func (l *Lease) Clone() (clone *Lease) {
|
|||||||
Hostname: l.Hostname,
|
Hostname: l.Hostname,
|
||||||
HWAddr: slices.Clone(l.HWAddr),
|
HWAddr: slices.Clone(l.HWAddr),
|
||||||
IP: l.IP,
|
IP: l.IP,
|
||||||
|
IsStatic: l.IsStatic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -511,7 +577,14 @@ func (s *server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
|
|||||||
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)
|
||||||
@@ -543,7 +616,14 @@ func (s *server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ
|
|||||||
srv = s.srv6
|
srv = s.srv6
|
||||||
}
|
}
|
||||||
|
|
||||||
err = srv.RemoveStaticLease(l)
|
lease, err := l.toLease()
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "parsing: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = srv.RemoveStaticLease(lease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
|
|
||||||
|
|||||||
@@ -5,28 +5,27 @@ package dhcpd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServer_handleDHCPStatus(t *testing.T) {
|
func TestServer_handleDHCPStatus(t *testing.T) {
|
||||||
const staticName = "static-client"
|
const (
|
||||||
|
staticName = "static-client"
|
||||||
|
staticMAC = "aa:aa:aa:aa:aa:aa"
|
||||||
|
)
|
||||||
|
|
||||||
staticIP := netip.MustParseAddr("192.168.10.10")
|
staticIP := netip.MustParseAddr("192.168.10.10")
|
||||||
staticMAC := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
|
||||||
|
|
||||||
staticLease := &Lease{
|
staticLease := &leaseStatic{
|
||||||
Expiry: time.Unix(leaseExpireStatic, 0),
|
|
||||||
Hostname: staticName,
|
|
||||||
HWAddr: staticMAC,
|
HWAddr: staticMAC,
|
||||||
IP: staticIP,
|
IP: staticIP,
|
||||||
|
Hostname: staticName,
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := Create(&ServerConfig{
|
s, err := Create(&ServerConfig{
|
||||||
@@ -65,8 +64,8 @@ func TestServer_handleDHCPStatus(t *testing.T) {
|
|||||||
resp := &dhcpStatusResponse{
|
resp := &dhcpStatusResponse{
|
||||||
V4: *conf4,
|
V4: *conf4,
|
||||||
V6: V6ServerConf{},
|
V6: V6ServerConf{},
|
||||||
Leases: []*Lease{},
|
Leases: []*leaseDynamic{},
|
||||||
StaticLeases: []*Lease{},
|
StaticLeases: []*leaseStatic{},
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +94,7 @@ func TestServer_handleDHCPStatus(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusOK, w.Code)
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
resp := defaultResponse()
|
resp := defaultResponse()
|
||||||
resp.StaticLeases = []*Lease{staticLease}
|
resp.StaticLeases = []*leaseStatic{staticLease}
|
||||||
|
|
||||||
checkStatus(t, resp)
|
checkStatus(t, resp)
|
||||||
})
|
})
|
||||||
@@ -106,7 +105,7 @@ func TestServer_handleDHCPStatus(t *testing.T) {
|
|||||||
|
|
||||||
b := &bytes.Buffer{}
|
b := &bytes.Buffer{}
|
||||||
|
|
||||||
err = json.NewEncoder(b).Encode(&Lease{})
|
err = json.NewEncoder(b).Encode(&leaseStatic{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var r *http.Request
|
var r *http.Request
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,7 +127,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)
|
||||||
@@ -190,7 +189,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,7 +210,7 @@ func (s *v4Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
|
|||||||
|
|
||||||
for _, l := range s.leases {
|
for _, l := range s.leases {
|
||||||
if l.IP == ip {
|
if l.IP == ip {
|
||||||
if l.Expiry.After(now) || l.IsStatic() {
|
if l.IsStatic || l.Expiry.After(now) {
|
||||||
return l.HWAddr
|
return l.HWAddr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +258,7 @@ 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 == lease.IP {
|
if bytes.Equal(l.HWAddr, lease.HWAddr) || l.IP == lease.IP {
|
||||||
if isStatic {
|
if isStatic {
|
||||||
@@ -292,7 +291,7 @@ func (s *v4Server) addLease(l *Lease) (err error) {
|
|||||||
leaseIP := net.IP(l.IP.AsSlice())
|
leaseIP := net.IP(l.IP.AsSlice())
|
||||||
offset, inOffset := r.offset(leaseIP)
|
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.
|
||||||
if sn := s.conf.subnet; !sn.Contains(l.IP) {
|
if sn := s.conf.subnet; !sn.Contains(l.IP) {
|
||||||
@@ -359,6 +358,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -528,7 +528,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -860,7 +860,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
|
||||||
@@ -1131,56 +1131,6 @@ func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4
|
|||||||
s.send(peer, conn, req, resp)
|
s.send(peer, conn, req, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// send writes resp for peer to conn considering the req's parameters according
|
|
||||||
// to RFC-2131.
|
|
||||||
//
|
|
||||||
// See https://datatracker.ietf.org/doc/html/rfc2131#section-4.1.
|
|
||||||
func (s *v4Server) send(peer net.Addr, conn net.PacketConn, req, resp *dhcpv4.DHCPv4) {
|
|
||||||
switch giaddr, ciaddr, mtype := req.GatewayIPAddr, req.ClientIPAddr, resp.MessageType(); {
|
|
||||||
case giaddr != nil && !giaddr.IsUnspecified():
|
|
||||||
// Send any return messages to the server port on the BOOTP
|
|
||||||
// relay agent whose address appears in giaddr.
|
|
||||||
peer = &net.UDPAddr{
|
|
||||||
IP: giaddr,
|
|
||||||
Port: dhcpv4.ServerPort,
|
|
||||||
}
|
|
||||||
if mtype == dhcpv4.MessageTypeNak {
|
|
||||||
// Set the broadcast bit in the DHCPNAK, so that the relay agent
|
|
||||||
// broadcasts it to the client, because the client may not have
|
|
||||||
// a correct network address or subnet mask, and the client may not
|
|
||||||
// be answering ARP requests.
|
|
||||||
resp.SetBroadcast()
|
|
||||||
}
|
|
||||||
case mtype == dhcpv4.MessageTypeNak:
|
|
||||||
// Broadcast any DHCPNAK messages to 0xffffffff.
|
|
||||||
case ciaddr != nil && !ciaddr.IsUnspecified():
|
|
||||||
// Unicast DHCPOFFER and DHCPACK messages to the address in
|
|
||||||
// ciaddr.
|
|
||||||
peer = &net.UDPAddr{
|
|
||||||
IP: ciaddr,
|
|
||||||
Port: dhcpv4.ClientPort,
|
|
||||||
}
|
|
||||||
case !req.IsBroadcast() && req.ClientHWAddr != nil:
|
|
||||||
// Unicast DHCPOFFER and DHCPACK messages to the client's
|
|
||||||
// hardware address and yiaddr.
|
|
||||||
peer = &dhcpUnicastAddr{
|
|
||||||
Addr: packet.Addr{HardwareAddr: req.ClientHWAddr},
|
|
||||||
yiaddr: resp.YourIPAddr,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// Go on since peer is already set to broadcast.
|
|
||||||
}
|
|
||||||
|
|
||||||
pktData := resp.ToBytes()
|
|
||||||
|
|
||||||
log.Debug("dhcpv4: sending %d bytes to %s: %s", len(pktData), peer, resp.Summary())
|
|
||||||
|
|
||||||
_, err := conn.WriteTo(pktData, peer)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("dhcpv4: conn.Write to %s failed: %s", peer, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the IPv4 DHCP server.
|
// Start starts the IPv4 DHCP server.
|
||||||
func (s *v4Server) Start() (err error) {
|
func (s *v4Server) Start() (err error) {
|
||||||
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
|
defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ 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"
|
||||||
)
|
)
|
||||||
@@ -73,6 +72,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)
|
||||||
|
|
||||||
@@ -82,6 +82,7 @@ func TestV4Server_leasing(t *testing.T) {
|
|||||||
Hostname: staticName,
|
Hostname: staticName,
|
||||||
HWAddr: anotherMAC,
|
HWAddr: anotherMAC,
|
||||||
IP: anotherIP,
|
IP: anotherIP,
|
||||||
|
IsStatic: true,
|
||||||
})
|
})
|
||||||
assert.ErrorIs(t, err, ErrDupHostname)
|
assert.ErrorIs(t, err, ErrDupHostname)
|
||||||
})
|
})
|
||||||
@@ -96,6 +97,7 @@ func TestV4Server_leasing(t *testing.T) {
|
|||||||
Hostname: anotherName,
|
Hostname: anotherName,
|
||||||
HWAddr: staticMAC,
|
HWAddr: staticMAC,
|
||||||
IP: anotherIP,
|
IP: anotherIP,
|
||||||
|
IsStatic: true,
|
||||||
})
|
})
|
||||||
testutil.AssertErrorMsg(t, wantErrMsg, err)
|
testutil.AssertErrorMsg(t, wantErrMsg, err)
|
||||||
})
|
})
|
||||||
@@ -110,6 +112,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)
|
||||||
})
|
})
|
||||||
@@ -326,7 +329,7 @@ func TestV4_AddReplace(t *testing.T) {
|
|||||||
for i, l := range ls {
|
for i, l := range ls {
|
||||||
assert.Equal(t, stLeases[i].IP, l.IP)
|
assert.Equal(t, stLeases[i].IP, l.IP)
|
||||||
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
|
assert.Equal(t, stLeases[i].HWAddr, l.HWAddr)
|
||||||
assert.True(t, l.IsStatic())
|
assert.True(t, l.IsStatic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,111 +770,6 @@ func (fc *fakePacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
|||||||
return fc.writeTo(p, addr)
|
return fc.writeTo(p, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestV4Server_Send(t *testing.T) {
|
|
||||||
s := &v4Server{}
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultIP = net.IP{99, 99, 99, 99}
|
|
||||||
knownIP = net.IP{4, 2, 4, 2}
|
|
||||||
knownMAC = net.HardwareAddr{6, 5, 4, 3, 2, 1}
|
|
||||||
)
|
|
||||||
|
|
||||||
defaultPeer := &net.UDPAddr{
|
|
||||||
IP: defaultIP,
|
|
||||||
// Use neither client nor server port to check it actually
|
|
||||||
// changed.
|
|
||||||
Port: dhcpv4.ClientPort + dhcpv4.ServerPort,
|
|
||||||
}
|
|
||||||
defaultResp := &dhcpv4.DHCPv4{}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
want net.Addr
|
|
||||||
req *dhcpv4.DHCPv4
|
|
||||||
resp *dhcpv4.DHCPv4
|
|
||||||
name string
|
|
||||||
}{{
|
|
||||||
name: "giaddr",
|
|
||||||
req: &dhcpv4.DHCPv4{GatewayIPAddr: knownIP},
|
|
||||||
resp: defaultResp,
|
|
||||||
want: &net.UDPAddr{
|
|
||||||
IP: knownIP,
|
|
||||||
Port: dhcpv4.ServerPort,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
name: "nak",
|
|
||||||
req: &dhcpv4.DHCPv4{},
|
|
||||||
resp: &dhcpv4.DHCPv4{
|
|
||||||
Options: dhcpv4.OptionsFromList(
|
|
||||||
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
want: defaultPeer,
|
|
||||||
}, {
|
|
||||||
name: "ciaddr",
|
|
||||||
req: &dhcpv4.DHCPv4{ClientIPAddr: knownIP},
|
|
||||||
resp: &dhcpv4.DHCPv4{},
|
|
||||||
want: &net.UDPAddr{
|
|
||||||
IP: knownIP,
|
|
||||||
Port: dhcpv4.ClientPort,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
name: "chaddr",
|
|
||||||
req: &dhcpv4.DHCPv4{ClientHWAddr: knownMAC},
|
|
||||||
resp: &dhcpv4.DHCPv4{YourIPAddr: knownIP},
|
|
||||||
want: &dhcpUnicastAddr{
|
|
||||||
Addr: packet.Addr{HardwareAddr: knownMAC},
|
|
||||||
yiaddr: knownIP,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
name: "who_are_you",
|
|
||||||
req: &dhcpv4.DHCPv4{},
|
|
||||||
resp: &dhcpv4.DHCPv4{},
|
|
||||||
want: defaultPeer,
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
conn := &fakePacketConn{
|
|
||||||
writeTo: func(_ []byte, addr net.Addr) (_ int, _ error) {
|
|
||||||
assert.Equal(t, tc.want, addr)
|
|
||||||
|
|
||||||
return 0, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s.send(cloneUDPAddr(defaultPeer), conn, tc.req, tc.resp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("giaddr_nak", func(t *testing.T) {
|
|
||||||
req := &dhcpv4.DHCPv4{
|
|
||||||
GatewayIPAddr: knownIP,
|
|
||||||
}
|
|
||||||
// Ensure the request is for unicast.
|
|
||||||
req.SetUnicast()
|
|
||||||
resp := &dhcpv4.DHCPv4{
|
|
||||||
Options: dhcpv4.OptionsFromList(
|
|
||||||
dhcpv4.OptMessageType(dhcpv4.MessageTypeNak),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
want := &net.UDPAddr{
|
|
||||||
IP: req.GatewayIPAddr,
|
|
||||||
Port: dhcpv4.ServerPort,
|
|
||||||
}
|
|
||||||
|
|
||||||
conn := &fakePacketConn{
|
|
||||||
writeTo: func(_ []byte, addr net.Addr) (n int, err error) {
|
|
||||||
assert.Equal(t, want, addr)
|
|
||||||
|
|
||||||
return 0, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
s.send(cloneUDPAddr(defaultPeer), conn, req, resp)
|
|
||||||
assert.True(t, resp.IsBroadcast())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestV4Server_FindMACbyIP(t *testing.T) {
|
func TestV4Server_FindMACbyIP(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
staticName = "static-client"
|
staticName = "static-client"
|
||||||
@@ -890,6 +788,7 @@ func TestV4Server_FindMACbyIP(t *testing.T) {
|
|||||||
Hostname: staticName,
|
Hostname: staticName,
|
||||||
HWAddr: staticMAC,
|
HWAddr: staticMAC,
|
||||||
IP: staticIP,
|
IP: staticIP,
|
||||||
|
IsStatic: true,
|
||||||
}, {
|
}, {
|
||||||
Expiry: time.Unix(10, 0),
|
Expiry: time.Unix(10, 0),
|
||||||
Hostname: anotherName,
|
Hostname: anotherName,
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func (s *v6Server) FindMACbyIP(ip netip.Addr) (mac net.HardwareAddr) {
|
|||||||
|
|
||||||
for _, l := range s.leases {
|
for _, l := range s.leases {
|
||||||
if l.IP == ip {
|
if l.IP == ip {
|
||||||
if l.Expiry.After(now) || l.IsStatic() {
|
if l.IsStatic || l.Expiry.After(now) {
|
||||||
return l.HWAddr
|
return l.HWAddr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ func TestV6_FindMACbyIP(t *testing.T) {
|
|||||||
Hostname: staticName,
|
Hostname: staticName,
|
||||||
HWAddr: staticMAC,
|
HWAddr: staticMAC,
|
||||||
IP: staticIP,
|
IP: staticIP,
|
||||||
|
IsStatic: true,
|
||||||
}, {
|
}, {
|
||||||
Expiry: time.Unix(10, 0),
|
Expiry: time.Unix(10, 0),
|
||||||
Hostname: anotherName,
|
Hostname: anotherName,
|
||||||
@@ -344,6 +345,7 @@ func TestV6_FindMACbyIP(t *testing.T) {
|
|||||||
Hostname: staticName,
|
Hostname: staticName,
|
||||||
HWAddr: staticMAC,
|
HWAddr: staticMAC,
|
||||||
IP: staticIP,
|
IP: staticIP,
|
||||||
|
IsStatic: true,
|
||||||
}, {
|
}, {
|
||||||
Expiry: time.Unix(10, 0),
|
Expiry: time.Unix(10, 0),
|
||||||
Hostname: anotherName,
|
Hostname: anotherName,
|
||||||
|
|||||||
@@ -587,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,30 +648,46 @@ func (s *Server) onGetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, er
|
|||||||
|
|
||||||
// UpdatedProtectionStatus updates protection state, if the protection was
|
// UpdatedProtectionStatus updates protection state, if the protection was
|
||||||
// disabled temporarily. Returns the updated state of protection.
|
// disabled temporarily. Returns the updated state of protection.
|
||||||
func (s *Server) UpdatedProtectionStatus() (enabled bool) {
|
func (s *Server) UpdatedProtectionStatus() (enabled bool, disabledUntil *time.Time) {
|
||||||
changed := false
|
s.serverLock.RLock()
|
||||||
defer func() {
|
defer s.serverLock.RUnlock()
|
||||||
if changed {
|
|
||||||
log.Info("dns: protection is restarted after pause")
|
disabledUntil = s.conf.ProtectionDisabledUntil
|
||||||
s.conf.ConfigModified()
|
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()
|
s.serverLock.Lock()
|
||||||
defer s.serverLock.Unlock()
|
defer s.serverLock.Unlock()
|
||||||
|
|
||||||
disabledUntil := s.conf.ProtectionDisabledUntil
|
|
||||||
if disabledUntil == nil {
|
|
||||||
return s.conf.ProtectionEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Now().Before(*disabledUntil) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
s.conf.ProtectionEnabled = true
|
s.conf.ProtectionEnabled = true
|
||||||
s.conf.ProtectionDisabledUntil = nil
|
s.conf.ProtectionDisabledUntil = nil
|
||||||
changed = true
|
|
||||||
|
|
||||||
return true
|
log.Info("dns: protection is restarted after pause")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,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
|
||||||
|
|
||||||
@@ -206,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.UpdatedProtectionStatus()
|
dctx.protectionEnabled, _ = s.UpdatedProtectionStatus()
|
||||||
dctx.setts = s.getClientRequestFilteringSettings(dctx)
|
dctx.setts = s.getClientRequestFilteringSettings(dctx)
|
||||||
|
|
||||||
return resultCodeSuccess
|
return resultCodeSuccess
|
||||||
@@ -460,7 +460,7 @@ func (s *Server) processDHCPHosts(dctx *dnsContext) (rc resultCode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// indexFirstV4Label returns the index at which the reversed IPv4 address
|
// indexFirstV4Label returns the index at which the reversed IPv4 address
|
||||||
// starts, assuiming the domain is pre-validated ARPA domain having in-addr and
|
// starts, assuming the domain is pre-validated ARPA domain having in-addr and
|
||||||
// arpa labels removed.
|
// arpa labels removed.
|
||||||
func indexFirstV4Label(domain string) (idx int) {
|
func indexFirstV4Label(domain string) (idx int) {
|
||||||
idx = len(domain)
|
idx = len(domain)
|
||||||
@@ -478,7 +478,7 @@ func indexFirstV4Label(domain string) (idx int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// indexFirstV6Label returns the index at which the reversed IPv6 address
|
// indexFirstV6Label returns the index at which the reversed IPv6 address
|
||||||
// starts, assuiming the domain is pre-validated ARPA domain having ip6 and arpa
|
// starts, assuming the domain is pre-validated ARPA domain having ip6 and arpa
|
||||||
// labels removed.
|
// labels removed.
|
||||||
func indexFirstV6Label(domain string) (idx int) {
|
func indexFirstV6Label(domain string) (idx int) {
|
||||||
idx = len(domain)
|
idx = len(domain)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -453,8 +453,9 @@ func TestSafeSearch(t *testing.T) {
|
|||||||
SafeSearchCacheSize: 1000,
|
SafeSearchCacheSize: 1000,
|
||||||
CacheTime: 30,
|
CacheTime: 30,
|
||||||
}
|
}
|
||||||
safeSearch, err := safesearch.NewDefaultSafeSearch(
|
safeSearch, err := safesearch.NewDefault(
|
||||||
safeSearchConf,
|
safeSearchConf,
|
||||||
|
"",
|
||||||
filterConf.SafeSearchCacheSize,
|
filterConf.SafeSearchCacheSize,
|
||||||
time.Minute*time.Duration(filterConf.CacheTime),
|
time.Minute*time.Duration(filterConf.CacheTime),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ type jsonDNSConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
||||||
protectionEnabled := s.UpdatedProtectionStatus()
|
protectionEnabled, protectionDisabledUntil := s.UpdatedProtectionStatus()
|
||||||
|
|
||||||
s.serverLock.RLock()
|
s.serverLock.RLock()
|
||||||
defer s.serverLock.RUnlock()
|
defer s.serverLock.RUnlock()
|
||||||
@@ -128,12 +128,6 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
|||||||
usePrivateRDNS := s.conf.UsePrivateRDNS
|
usePrivateRDNS := s.conf.UsePrivateRDNS
|
||||||
localPTRUpstreams := stringutil.CloneSliceOrEmpty(s.conf.LocalPTRResolvers)
|
localPTRUpstreams := stringutil.CloneSliceOrEmpty(s.conf.LocalPTRResolvers)
|
||||||
|
|
||||||
var disabledUntil *time.Time
|
|
||||||
if s.conf.ProtectionDisabledUntil != nil {
|
|
||||||
t := *s.conf.ProtectionDisabledUntil
|
|
||||||
disabledUntil = &t
|
|
||||||
}
|
|
||||||
|
|
||||||
var upstreamMode string
|
var upstreamMode string
|
||||||
if s.conf.FastestAddr {
|
if s.conf.FastestAddr {
|
||||||
upstreamMode = "fastest_addr"
|
upstreamMode = "fastest_addr"
|
||||||
@@ -169,7 +163,7 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
|||||||
UsePrivateRDNS: &usePrivateRDNS,
|
UsePrivateRDNS: &usePrivateRDNS,
|
||||||
LocalPTRUpstreams: &localPTRUpstreams,
|
LocalPTRUpstreams: &localPTRUpstreams,
|
||||||
DefaultLocalPTRUpstreams: defLocalPTRUps,
|
DefaultLocalPTRUpstreams: defLocalPTRUps,
|
||||||
DisabledUntil: disabledUntil,
|
DisabledUntil: protectionDisabledUntil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -40,12 +40,17 @@ func (s *Server) processQueryLogsAndStats(dctx *dnsContext) (rc resultCode) {
|
|||||||
|
|
||||||
log.Debug("client ip: %s", ip)
|
log.Debug("client ip: %s", ip)
|
||||||
|
|
||||||
|
ipStr := ip.String()
|
||||||
|
ids := []string{ipStr, dctx.clientID}
|
||||||
|
|
||||||
// Synchronize access to s.queryLog and s.stats so they won't be suddenly
|
// Synchronize access to s.queryLog and s.stats so they won't be suddenly
|
||||||
// uninitialized while in use. This can happen after proxy server has been
|
// uninitialized while in use. This can happen after proxy server has been
|
||||||
// stopped, but its workers haven't yet exited.
|
// stopped, but its workers haven't yet exited.
|
||||||
if shouldLog &&
|
if shouldLog &&
|
||||||
s.queryLog != nil &&
|
s.queryLog != nil &&
|
||||||
s.queryLog.ShouldLog(host, q.Qtype, q.Qclass) {
|
// TODO(s.chzhen): Use dnsforward.dnsContext when it will start
|
||||||
|
// containing persistent client.
|
||||||
|
s.queryLog.ShouldLog(host, q.Qtype, q.Qclass, ids) {
|
||||||
s.logQuery(dctx, pctx, elapsed, ip)
|
s.logQuery(dctx, pctx, elapsed, ip)
|
||||||
} else {
|
} else {
|
||||||
log.Debug(
|
log.Debug(
|
||||||
@@ -56,8 +61,11 @@ func (s *Server) processQueryLogsAndStats(dctx *dnsContext) (rc resultCode) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.stats != nil && s.stats.ShouldCount(host, q.Qtype, q.Qclass) {
|
if s.stats != nil &&
|
||||||
s.updateStats(dctx, elapsed, *dctx.result, ip)
|
// TODO(s.chzhen): Use dnsforward.dnsContext when it will start
|
||||||
|
// containing persistent client.
|
||||||
|
s.stats.ShouldCount(host, q.Qtype, q.Qclass, ids) {
|
||||||
|
s.updateStats(dctx, elapsed, *dctx.result, ipStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultCodeSuccess
|
return resultCodeSuccess
|
||||||
@@ -110,7 +118,7 @@ func (s *Server) updateStats(
|
|||||||
ctx *dnsContext,
|
ctx *dnsContext,
|
||||||
elapsed time.Duration,
|
elapsed time.Duration,
|
||||||
res filtering.Result,
|
res filtering.Result,
|
||||||
clientIP net.IP,
|
clientIP string,
|
||||||
) {
|
) {
|
||||||
pctx := ctx.proxyCtx
|
pctx := ctx.proxyCtx
|
||||||
e := stats.Entry{}
|
e := stats.Entry{}
|
||||||
@@ -119,8 +127,8 @@ func (s *Server) updateStats(
|
|||||||
|
|
||||||
if clientID := ctx.clientID; clientID != "" {
|
if clientID := ctx.clientID; clientID != "" {
|
||||||
e.Client = clientID
|
e.Client = clientID
|
||||||
} else if clientIP != nil {
|
} else {
|
||||||
e.Client = clientIP.String()
|
e.Client = clientIP
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Time = uint32(elapsed / 1000)
|
e.Time = uint32(elapsed / 1000)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func (l *testQueryLog) Add(p *querylog.AddParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ShouldLog implements the [querylog.QueryLog] interface for *testQueryLog.
|
// ShouldLog implements the [querylog.QueryLog] interface for *testQueryLog.
|
||||||
func (l *testQueryLog) ShouldLog(string, uint16, uint16) bool {
|
func (l *testQueryLog) ShouldLog(string, uint16, uint16, []string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ func (l *testStats) Update(e stats.Entry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ShouldCount implements the [stats.Interface] interface for *testStats.
|
// ShouldCount implements the [stats.Interface] interface for *testStats.
|
||||||
func (l *testStats) ShouldCount(string, uint16, uint16) bool {
|
func (l *testStats) ShouldCount(string, uint16, uint16, []string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package filtering
|
package filtering
|
||||||
|
|
||||||
import (
|
import "github.com/miekg/dns"
|
||||||
"github.com/AdguardTeam/urlfilter/rules"
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SafeSearch interface describes a service for search engines hosts rewrites.
|
// SafeSearch interface describes a service for search engines hosts rewrites.
|
||||||
type SafeSearch interface {
|
type SafeSearch interface {
|
||||||
// SearchHost returns a replacement address for the search engine host.
|
// CheckHost checks host with safe search filter. CheckHost must be safe
|
||||||
SearchHost(host string, qtype uint16) (res *rules.DNSRewrite)
|
// for concurrent use. qtype must be either [dns.TypeA] or [dns.TypeAAAA].
|
||||||
|
|
||||||
// CheckHost checks host with safe search engine.
|
|
||||||
CheckHost(host string, qtype uint16) (res Result, err error)
|
CheckHost(host string, qtype uint16) (res Result, err error)
|
||||||
|
|
||||||
|
// Update updates the configuration of the safe search filter. Update must
|
||||||
|
// be safe for concurrent use. An implementation of Update may ignore some
|
||||||
|
// fields, but it must document which.
|
||||||
|
Update(conf SafeSearchConfig) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SafeSearchConfig is a struct with safe search related settings.
|
// SafeSearchConfig is a struct with safe search related settings.
|
||||||
@@ -37,10 +37,12 @@ type SafeSearchConfig struct {
|
|||||||
// [hostChecker.check].
|
// [hostChecker.check].
|
||||||
func (d *DNSFilter) checkSafeSearch(
|
func (d *DNSFilter) checkSafeSearch(
|
||||||
host string,
|
host string,
|
||||||
_ uint16,
|
qtype uint16,
|
||||||
setts *Settings,
|
setts *Settings,
|
||||||
) (res Result, err error) {
|
) (res Result, err error) {
|
||||||
if !setts.ProtectionEnabled || !setts.SafeSearchEnabled {
|
if !setts.ProtectionEnabled ||
|
||||||
|
!setts.SafeSearchEnabled ||
|
||||||
|
(qtype != dns.TypeA && qtype != dns.TypeAAAA) {
|
||||||
return Result{}, nil
|
return Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +52,8 @@ func (d *DNSFilter) checkSafeSearch(
|
|||||||
|
|
||||||
clientSafeSearch := setts.ClientSafeSearch
|
clientSafeSearch := setts.ClientSafeSearch
|
||||||
if clientSafeSearch != nil {
|
if clientSafeSearch != nil {
|
||||||
return clientSafeSearch.CheckHost(host, dns.TypeA)
|
return clientSafeSearch.CheckHost(host, qtype)
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.safeSearch.CheckHost(host, dns.TypeA)
|
return d.safeSearch.CheckHost(host, qtype)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|
|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
|duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
|duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
||||||
|start.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
|start.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
||||||
|www.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
|www.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
||||||
|
|||||||
@@ -188,4 +188,4 @@
|
|||||||
|www.google.tt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
|www.google.tt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|www.google.vg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
|www.google.vg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|www.google.vu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
|www.google.vu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|www.google.ws^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
|www.google.ws^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
|pixabay.com^$dnsrewrite=NOERROR;CNAME;safesearch.pixabay.com
|
|pixabay.com^$dnsrewrite=NOERROR;CNAME;safesearch.pixabay.com
|
||||||
|
|||||||
@@ -49,4 +49,4 @@
|
|||||||
|yandex.ru^$dnsrewrite=NOERROR;A;213.180.193.56
|
|yandex.ru^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||||
|yandex.tj^$dnsrewrite=NOERROR;A;213.180.193.56
|
|yandex.tj^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||||
|yandex.tm^$dnsrewrite=NOERROR;A;213.180.193.56
|
|yandex.tm^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||||
|yandex.uz^$dnsrewrite=NOERROR;A;213.180.193.56
|
|yandex.uz^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|m.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
|m.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||||
|youtubei.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
|youtubei.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||||
|youtube.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
|youtube.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||||
|www.youtube-nocookie.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
|www.youtube-nocookie.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
@@ -53,44 +54,85 @@ func isServiceProtected(s filtering.SafeSearchConfig, service Service) (ok bool)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultSafeSearch is the default safesearch struct.
|
// Default is the default safe search filter that uses filtering rules with the
|
||||||
type DefaultSafeSearch struct {
|
// dnsrewrite modifier.
|
||||||
engine *urlfilter.DNSEngine
|
type Default struct {
|
||||||
safeSearchCache cache.Cache
|
// mu protects engine.
|
||||||
resolver filtering.Resolver
|
mu *sync.RWMutex
|
||||||
cacheTime time.Duration
|
|
||||||
|
// engine is the filtering engine that contains the DNS rewrite rules.
|
||||||
|
// engine may be nil, which means that this safe search filter is disabled.
|
||||||
|
engine *urlfilter.DNSEngine
|
||||||
|
|
||||||
|
cache cache.Cache
|
||||||
|
resolver filtering.Resolver
|
||||||
|
logPrefix string
|
||||||
|
cacheTTL time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultSafeSearch returns new safesearch struct. CacheTime is an element
|
// NewDefault returns an initialized default safe search filter. name is used
|
||||||
// TTL (in minutes).
|
// for logging.
|
||||||
func NewDefaultSafeSearch(
|
func NewDefault(
|
||||||
conf filtering.SafeSearchConfig,
|
conf filtering.SafeSearchConfig,
|
||||||
|
name string,
|
||||||
cacheSize uint,
|
cacheSize uint,
|
||||||
cacheTime time.Duration,
|
cacheTTL time.Duration,
|
||||||
) (ss *DefaultSafeSearch, err error) {
|
) (ss *Default, err error) {
|
||||||
engine, err := newEngine(filtering.SafeSearchListID, conf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolver filtering.Resolver = net.DefaultResolver
|
var resolver filtering.Resolver = net.DefaultResolver
|
||||||
if conf.CustomResolver != nil {
|
if conf.CustomResolver != nil {
|
||||||
resolver = conf.CustomResolver
|
resolver = conf.CustomResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DefaultSafeSearch{
|
ss = &Default{
|
||||||
engine: engine,
|
mu: &sync.RWMutex{},
|
||||||
safeSearchCache: cache.New(cache.Config{
|
|
||||||
|
cache: cache.New(cache.Config{
|
||||||
EnableLRU: true,
|
EnableLRU: true,
|
||||||
MaxSize: cacheSize,
|
MaxSize: cacheSize,
|
||||||
}),
|
}),
|
||||||
cacheTime: cacheTime,
|
resolver: resolver,
|
||||||
resolver: resolver,
|
// Use %s, because the client safe-search names already contain double
|
||||||
}, nil
|
// quotes.
|
||||||
|
logPrefix: fmt.Sprintf("safesearch %s: ", name),
|
||||||
|
cacheTTL: cacheTTL,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ss.resetEngine(filtering.SafeSearchListID, conf)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newEngine creates new engine for provided safe search configuration.
|
// log is a helper for logging that includes the name of the safe search
|
||||||
func newEngine(listID int, conf filtering.SafeSearchConfig) (engine *urlfilter.DNSEngine, err error) {
|
// filter. level must be one of [log.DEBUG], [log.INFO], and [log.ERROR].
|
||||||
|
func (ss *Default) log(level log.Level, msg string, args ...any) {
|
||||||
|
switch level {
|
||||||
|
case log.DEBUG:
|
||||||
|
log.Debug(ss.logPrefix+msg, args...)
|
||||||
|
case log.INFO:
|
||||||
|
log.Info(ss.logPrefix+msg, args...)
|
||||||
|
case log.ERROR:
|
||||||
|
log.Error(ss.logPrefix+msg, args...)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("safesearch: unsupported logging level %d", level))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetEngine creates new engine for provided safe search configuration and
|
||||||
|
// sets it in ss.
|
||||||
|
func (ss *Default) resetEngine(
|
||||||
|
listID int,
|
||||||
|
conf filtering.SafeSearchConfig,
|
||||||
|
) (err error) {
|
||||||
|
if !conf.Enabled {
|
||||||
|
ss.log(log.INFO, "disabled")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for service, serviceRules := range safeSearchRules {
|
for service, serviceRules := range safeSearchRules {
|
||||||
if isServiceProtected(conf, service) {
|
if isServiceProtected(conf, service) {
|
||||||
@@ -106,20 +148,73 @@ func newEngine(listID int, conf filtering.SafeSearchConfig) (engine *urlfilter.D
|
|||||||
|
|
||||||
rs, err := filterlist.NewRuleStorage([]filterlist.RuleList{strList})
|
rs, err := filterlist.NewRuleStorage([]filterlist.RuleList{strList})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating rule storage: %w", err)
|
return fmt.Errorf("creating rule storage: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
engine = urlfilter.NewDNSEngine(rs)
|
ss.engine = urlfilter.NewDNSEngine(rs)
|
||||||
log.Info("safesearch: filter %d: reset %d rules", listID, engine.RulesCount)
|
|
||||||
|
|
||||||
return engine, nil
|
ss.log(log.INFO, "reset %d rules", ss.engine.RulesCount)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ filtering.SafeSearch = (*DefaultSafeSearch)(nil)
|
var _ filtering.SafeSearch = (*Default)(nil)
|
||||||
|
|
||||||
|
// CheckHost implements the [filtering.SafeSearch] interface for
|
||||||
|
// *DefaultSafeSearch.
|
||||||
|
func (ss *Default) CheckHost(
|
||||||
|
host string,
|
||||||
|
qtype rules.RRType,
|
||||||
|
) (res filtering.Result, err error) {
|
||||||
|
start := time.Now()
|
||||||
|
defer func() {
|
||||||
|
ss.log(log.DEBUG, "lookup for %q finished in %s", host, time.Since(start))
|
||||||
|
}()
|
||||||
|
|
||||||
|
if qtype != dns.TypeA && qtype != dns.TypeAAAA {
|
||||||
|
return filtering.Result{}, fmt.Errorf("unsupported question type %s", dns.Type(qtype))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache. Return cached result if it was found
|
||||||
|
cachedValue, isFound := ss.getCachedResult(host, qtype)
|
||||||
|
if isFound {
|
||||||
|
ss.log(log.DEBUG, "found in cache: %q", host)
|
||||||
|
|
||||||
|
return cachedValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rewrite := ss.searchHost(host, qtype)
|
||||||
|
if rewrite == nil {
|
||||||
|
return filtering.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fltRes, err := ss.newResult(rewrite, qtype)
|
||||||
|
if err != nil {
|
||||||
|
ss.log(log.DEBUG, "looking up addresses for %q: %s", host, err)
|
||||||
|
|
||||||
|
return filtering.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fltRes != nil {
|
||||||
|
res = *fltRes
|
||||||
|
ss.setCacheResult(host, qtype, res)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtering.Result{}, fmt.Errorf("no ipv4 addresses for %q", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchHost looks up DNS rewrites in the internal DNS filtering engine.
|
||||||
|
func (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRewrite) {
|
||||||
|
ss.mu.RLock()
|
||||||
|
defer ss.mu.RUnlock()
|
||||||
|
|
||||||
|
if ss.engine == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SearchHost implements the [filtering.SafeSearch] interface for *DefaultSafeSearch.
|
|
||||||
func (ss *DefaultSafeSearch) SearchHost(host string, qtype uint16) (res *rules.DNSRewrite) {
|
|
||||||
r, _ := ss.engine.MatchRequest(&urlfilter.DNSRequest{
|
r, _ := ss.engine.MatchRequest(&urlfilter.DNSRequest{
|
||||||
Hostname: strings.ToLower(host),
|
Hostname: strings.ToLower(host),
|
||||||
DNSType: qtype,
|
DNSType: qtype,
|
||||||
@@ -133,51 +228,11 @@ func (ss *DefaultSafeSearch) SearchHost(host string, qtype uint16) (res *rules.D
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckHost implements the [filtering.SafeSearch] interface for
|
// newResult creates Result object from rewrite rule. qtype must be either
|
||||||
// *DefaultSafeSearch.
|
// [dns.TypeA] or [dns.TypeAAAA].
|
||||||
func (ss *DefaultSafeSearch) CheckHost(
|
func (ss *Default) newResult(
|
||||||
host string,
|
|
||||||
qtype uint16,
|
|
||||||
) (res filtering.Result, err error) {
|
|
||||||
if log.GetLevel() >= log.DEBUG {
|
|
||||||
timer := log.StartTimer()
|
|
||||||
defer timer.LogElapsed("safesearch: lookup for %s", host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check cache. Return cached result if it was found
|
|
||||||
cachedValue, isFound := ss.getCachedResult(host)
|
|
||||||
if isFound {
|
|
||||||
log.Debug("safesearch: found in cache: %s", host)
|
|
||||||
|
|
||||||
return cachedValue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rewrite := ss.SearchHost(host, qtype)
|
|
||||||
if rewrite == nil {
|
|
||||||
return filtering.Result{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dRes, err := ss.newResult(rewrite, qtype)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("safesearch: failed to lookup addresses for %s: %s", host, err)
|
|
||||||
|
|
||||||
return filtering.Result{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if dRes != nil {
|
|
||||||
res = *dRes
|
|
||||||
ss.setCacheResult(host, res)
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtering.Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newResult creates Result object from rewrite rule.
|
|
||||||
func (ss *DefaultSafeSearch) newResult(
|
|
||||||
rewrite *rules.DNSRewrite,
|
rewrite *rules.DNSRewrite,
|
||||||
qtype uint16,
|
qtype rules.RRType,
|
||||||
) (res *filtering.Result, err error) {
|
) (res *filtering.Result, err error) {
|
||||||
res = &filtering.Result{
|
res = &filtering.Result{
|
||||||
Rules: []*filtering.ResultRule{{
|
Rules: []*filtering.ResultRule{{
|
||||||
@@ -187,7 +242,7 @@ func (ss *DefaultSafeSearch) newResult(
|
|||||||
IsFiltered: true,
|
IsFiltered: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if rewrite.RRType == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) {
|
if rewrite.RRType == qtype {
|
||||||
ip, ok := rewrite.Value.(net.IP)
|
ip, ok := rewrite.Value.(net.IP)
|
||||||
if !ok || ip == nil {
|
if !ok || ip == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -198,17 +253,25 @@ func (ss *DefaultSafeSearch) newResult(
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if rewrite.NewCNAME == "" {
|
host := rewrite.NewCNAME
|
||||||
|
if host == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ips, err := ss.resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME)
|
ss.log(log.DEBUG, "resolving %q", host)
|
||||||
|
|
||||||
|
ips, err := ss.resolver.LookupIP(context.Background(), qtypeToProto(qtype), host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ss.log(log.DEBUG, "resolved %s", ips)
|
||||||
|
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
if ip = ip.To4(); ip == nil {
|
// TODO(a.garipov): Remove this filtering once the resolver we use
|
||||||
|
// actually learns about network.
|
||||||
|
ip = fitToProto(ip, qtype)
|
||||||
|
if ip == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,38 +283,71 @@ func (ss *DefaultSafeSearch) newResult(
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// setCacheResult stores data in cache for host.
|
// qtypeToProto returns "ip4" for [dns.TypeA] and "ip6" for [dns.TypeAAAA].
|
||||||
func (ss *DefaultSafeSearch) setCacheResult(host string, res filtering.Result) {
|
// It panics for other types.
|
||||||
expire := uint32(time.Now().Add(ss.cacheTime).Unix())
|
func qtypeToProto(qtype rules.RRType) (proto string) {
|
||||||
|
switch qtype {
|
||||||
|
case dns.TypeA:
|
||||||
|
return "ip4"
|
||||||
|
case dns.TypeAAAA:
|
||||||
|
return "ip6"
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("safesearch: unsupported question type %s", dns.Type(qtype)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fitToProto returns a non-nil IP address if ip is the correct protocol version
|
||||||
|
// for qtype. qtype is expected to be either [dns.TypeA] or [dns.TypeAAAA].
|
||||||
|
func fitToProto(ip net.IP, qtype rules.RRType) (res net.IP) {
|
||||||
|
ip4 := ip.To4()
|
||||||
|
if qtype == dns.TypeA {
|
||||||
|
return ip4
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip4 == nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setCacheResult stores data in cache for host. qtype is expected to be either
|
||||||
|
// [dns.TypeA] or [dns.TypeAAAA].
|
||||||
|
func (ss *Default) setCacheResult(host string, qtype rules.RRType, res filtering.Result) {
|
||||||
|
expire := uint32(time.Now().Add(ss.cacheTTL).Unix())
|
||||||
exp := make([]byte, 4)
|
exp := make([]byte, 4)
|
||||||
binary.BigEndian.PutUint32(exp, expire)
|
binary.BigEndian.PutUint32(exp, expire)
|
||||||
buf := bytes.NewBuffer(exp)
|
buf := bytes.NewBuffer(exp)
|
||||||
|
|
||||||
err := gob.NewEncoder(buf).Encode(res)
|
err := gob.NewEncoder(buf).Encode(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("safesearch: cache encoding: %s", err)
|
ss.log(log.ERROR, "cache encoding: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val := buf.Bytes()
|
val := buf.Bytes()
|
||||||
_ = ss.safeSearchCache.Set([]byte(host), val)
|
_ = ss.cache.Set([]byte(dns.Type(qtype).String()+" "+host), val)
|
||||||
|
|
||||||
log.Debug("safesearch: stored in cache: %s (%d bytes)", host, len(val))
|
ss.log(log.DEBUG, "stored in cache: %q, %d bytes", host, len(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCachedResult returns stored data from cache for host.
|
// getCachedResult returns stored data from cache for host. qtype is expected
|
||||||
func (ss *DefaultSafeSearch) getCachedResult(host string) (res filtering.Result, ok bool) {
|
// to be either [dns.TypeA] or [dns.TypeAAAA].
|
||||||
|
func (ss *Default) getCachedResult(
|
||||||
|
host string,
|
||||||
|
qtype rules.RRType,
|
||||||
|
) (res filtering.Result, ok bool) {
|
||||||
res = filtering.Result{}
|
res = filtering.Result{}
|
||||||
|
|
||||||
data := ss.safeSearchCache.Get([]byte(host))
|
data := ss.cache.Get([]byte(dns.Type(qtype).String() + " " + host))
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return res, false
|
return res, false
|
||||||
}
|
}
|
||||||
|
|
||||||
exp := binary.BigEndian.Uint32(data[:4])
|
exp := binary.BigEndian.Uint32(data[:4])
|
||||||
if exp <= uint32(time.Now().Unix()) {
|
if exp <= uint32(time.Now().Unix()) {
|
||||||
ss.safeSearchCache.Del([]byte(host))
|
ss.cache.Del([]byte(host))
|
||||||
|
|
||||||
return res, false
|
return res, false
|
||||||
}
|
}
|
||||||
@@ -260,10 +356,27 @@ func (ss *DefaultSafeSearch) getCachedResult(host string) (res filtering.Result,
|
|||||||
|
|
||||||
err := gob.NewDecoder(buf).Decode(&res)
|
err := gob.NewDecoder(buf).Decode(&res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("safesearch: cache decoding: %s", err)
|
ss.log(log.ERROR, "cache decoding: %s", err)
|
||||||
|
|
||||||
return filtering.Result{}, false
|
return filtering.Result{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, true
|
return res, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update implements the [filtering.SafeSearch] interface for *Default. Update
|
||||||
|
// ignores the CustomResolver and Enabled fields.
|
||||||
|
func (ss *Default) Update(conf filtering.SafeSearchConfig) (err error) {
|
||||||
|
ss.mu.Lock()
|
||||||
|
defer ss.mu.Unlock()
|
||||||
|
|
||||||
|
err = ss.resetEngine(filtering.SafeSearchListID, conf)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ss.cache.Clear()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
137
internal/filtering/safesearch/safesearch_internal_test.go
Normal file
137
internal/filtering/safesearch/safesearch_internal_test.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package safesearch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
|
"github.com/AdguardTeam/urlfilter/rules"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO(a.garipov): Move as much of this as possible into proper external tests.
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TODO(a.garipov): Add IPv6 tests.
|
||||||
|
testQType = dns.TypeA
|
||||||
|
testCacheSize = 5000
|
||||||
|
testCacheTTL = 30 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultSafeSearchConf = filtering.SafeSearchConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Bing: true,
|
||||||
|
DuckDuckGo: true,
|
||||||
|
Google: true,
|
||||||
|
Pixabay: true,
|
||||||
|
Yandex: true,
|
||||||
|
YouTube: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var yandexIP = net.IPv4(213, 180, 193, 56)
|
||||||
|
|
||||||
|
func newForTest(t testing.TB, ssConf filtering.SafeSearchConfig) (ss *Default) {
|
||||||
|
ss, err := NewDefault(ssConf, "", testCacheSize, testCacheTTL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafeSearch(t *testing.T) {
|
||||||
|
ss := newForTest(t, defaultSafeSearchConf)
|
||||||
|
val := ss.searchHost("www.google.com", testQType)
|
||||||
|
|
||||||
|
assert.Equal(t, &rules.DNSRewrite{NewCNAME: "forcesafesearch.google.com"}, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafeSearchCacheYandex(t *testing.T) {
|
||||||
|
const domain = "yandex.ru"
|
||||||
|
|
||||||
|
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
|
||||||
|
|
||||||
|
// Check host with disabled safesearch.
|
||||||
|
res, err := ss.CheckHost(domain, testQType)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.False(t, res.IsFiltered)
|
||||||
|
assert.Empty(t, res.Rules)
|
||||||
|
|
||||||
|
ss = newForTest(t, defaultSafeSearchConf)
|
||||||
|
res, err = ss.CheckHost(domain, testQType)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// For yandex we already know valid IP.
|
||||||
|
require.Len(t, res.Rules, 1)
|
||||||
|
|
||||||
|
assert.Equal(t, res.Rules[0].IP, yandexIP)
|
||||||
|
|
||||||
|
// Check cache.
|
||||||
|
cachedValue, isFound := ss.getCachedResult(domain, testQType)
|
||||||
|
require.True(t, isFound)
|
||||||
|
require.Len(t, cachedValue.Rules, 1)
|
||||||
|
|
||||||
|
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafeSearchCacheGoogle(t *testing.T) {
|
||||||
|
const domain = "www.google.ru"
|
||||||
|
|
||||||
|
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
|
||||||
|
|
||||||
|
res, err := ss.CheckHost(domain, testQType)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.False(t, res.IsFiltered)
|
||||||
|
assert.Empty(t, res.Rules)
|
||||||
|
|
||||||
|
resolver := &aghtest.TestResolver{}
|
||||||
|
ss = newForTest(t, defaultSafeSearchConf)
|
||||||
|
ss.resolver = resolver
|
||||||
|
|
||||||
|
// Lookup for safesearch domain.
|
||||||
|
rewrite := ss.searchHost(domain, testQType)
|
||||||
|
|
||||||
|
ips, err := resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var foundIP net.IP
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.To4() != nil {
|
||||||
|
foundIP = ip
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err = ss.CheckHost(domain, testQType)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, res.Rules, 1)
|
||||||
|
|
||||||
|
assert.True(t, res.Rules[0].IP.Equal(foundIP))
|
||||||
|
|
||||||
|
// Check cache.
|
||||||
|
cachedValue, isFound := ss.getCachedResult(domain, testQType)
|
||||||
|
require.True(t, isFound)
|
||||||
|
require.Len(t, cachedValue.Rules, 1)
|
||||||
|
|
||||||
|
assert.True(t, cachedValue.Rules[0].IP.Equal(foundIP))
|
||||||
|
}
|
||||||
|
|
||||||
|
const googleHost = "www.google.com"
|
||||||
|
|
||||||
|
var dnsRewriteSink *rules.DNSRewrite
|
||||||
|
|
||||||
|
func BenchmarkSafeSearch(b *testing.B) {
|
||||||
|
ss := newForTest(b, defaultSafeSearchConf)
|
||||||
|
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
dnsRewriteSink = ss.searchHost(googleHost, testQType)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(b, "forcesafesearch.google.com", dnsRewriteSink.NewCNAME)
|
||||||
|
}
|
||||||
@@ -1,26 +1,37 @@
|
|||||||
package safesearch
|
package safesearch_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/urlfilter/rules"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
testutil.DiscardLogOutput(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common test constants.
|
||||||
const (
|
const (
|
||||||
safeSearchCacheSize = 5000
|
// TODO(a.garipov): Add IPv6 tests.
|
||||||
cacheTime = 30 * time.Minute
|
testQType = dns.TypeA
|
||||||
|
testCacheSize = 5000
|
||||||
|
testCacheTTL = 30 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultSafeSearchConf = filtering.SafeSearchConfig{
|
// testConf is the default safe search configuration for tests.
|
||||||
Enabled: true,
|
var testConf = filtering.SafeSearchConfig{
|
||||||
|
CustomResolver: nil,
|
||||||
|
|
||||||
|
Enabled: true,
|
||||||
|
|
||||||
Bing: true,
|
Bing: true,
|
||||||
DuckDuckGo: true,
|
DuckDuckGo: true,
|
||||||
Google: true,
|
Google: true,
|
||||||
@@ -29,25 +40,15 @@ var defaultSafeSearchConf = filtering.SafeSearchConfig{
|
|||||||
YouTube: true,
|
YouTube: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// yandexIP is the expected IP address of Yandex safe search results. Keep in
|
||||||
|
// sync with the rules data.
|
||||||
var yandexIP = net.IPv4(213, 180, 193, 56)
|
var yandexIP = net.IPv4(213, 180, 193, 56)
|
||||||
|
|
||||||
func newForTest(t testing.TB, ssConf filtering.SafeSearchConfig) (ss *DefaultSafeSearch) {
|
func TestDefault_CheckHost_yandex(t *testing.T) {
|
||||||
ss, err := NewDefaultSafeSearch(ssConf, safeSearchCacheSize, cacheTime)
|
conf := testConf
|
||||||
|
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return ss
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSafeSearch(t *testing.T) {
|
|
||||||
ss := newForTest(t, defaultSafeSearchConf)
|
|
||||||
val := ss.SearchHost("www.google.com", dns.TypeA)
|
|
||||||
|
|
||||||
assert.Equal(t, &rules.DNSRewrite{NewCNAME: "forcesafesearch.google.com"}, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckHostSafeSearchYandex(t *testing.T) {
|
|
||||||
ss := newForTest(t, defaultSafeSearchConf)
|
|
||||||
|
|
||||||
// Check host for each domain.
|
// Check host for each domain.
|
||||||
for _, host := range []string{
|
for _, host := range []string{
|
||||||
"yandex.ru",
|
"yandex.ru",
|
||||||
@@ -57,7 +58,8 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
|
|||||||
"yandex.kz",
|
"yandex.kz",
|
||||||
"www.yandex.com",
|
"www.yandex.com",
|
||||||
} {
|
} {
|
||||||
res, err := ss.CheckHost(host, dns.TypeA)
|
var res filtering.Result
|
||||||
|
res, err = ss.CheckHost(host, testQType)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, res.IsFiltered)
|
assert.True(t, res.IsFiltered)
|
||||||
@@ -69,12 +71,14 @@ func TestCheckHostSafeSearchYandex(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckHostSafeSearchGoogle(t *testing.T) {
|
func TestDefault_CheckHost_google(t *testing.T) {
|
||||||
resolver := &aghtest.TestResolver{}
|
resolver := &aghtest.TestResolver{}
|
||||||
ip, _ := resolver.HostToIPs("forcesafesearch.google.com")
|
ip, _ := resolver.HostToIPs("forcesafesearch.google.com")
|
||||||
|
|
||||||
ss := newForTest(t, defaultSafeSearchConf)
|
conf := testConf
|
||||||
ss.resolver = resolver
|
conf.CustomResolver = resolver
|
||||||
|
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Check host for each domain.
|
// Check host for each domain.
|
||||||
for _, host := range []string{
|
for _, host := range []string{
|
||||||
@@ -87,7 +91,8 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
|
|||||||
"www.google.je",
|
"www.google.je",
|
||||||
} {
|
} {
|
||||||
t.Run(host, func(t *testing.T) {
|
t.Run(host, func(t *testing.T) {
|
||||||
res, err := ss.CheckHost(host, dns.TypeA)
|
var res filtering.Result
|
||||||
|
res, err = ss.CheckHost(host, testQType)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, res.IsFiltered)
|
assert.True(t, res.IsFiltered)
|
||||||
@@ -100,103 +105,35 @@ func TestCheckHostSafeSearchGoogle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSafeSearchCacheYandex(t *testing.T) {
|
func TestDefault_Update(t *testing.T) {
|
||||||
const domain = "yandex.ru"
|
conf := testConf
|
||||||
|
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
||||||
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
|
|
||||||
|
|
||||||
// Check host with disabled safesearch.
|
|
||||||
res, err := ss.CheckHost(domain, dns.TypeA)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.False(t, res.IsFiltered)
|
res, err := ss.CheckHost("www.yandex.com", testQType)
|
||||||
assert.Empty(t, res.Rules)
|
|
||||||
|
|
||||||
ss = newForTest(t, defaultSafeSearchConf)
|
|
||||||
res, err = ss.CheckHost(domain, dns.TypeA)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// For yandex we already know valid IP.
|
assert.True(t, res.IsFiltered)
|
||||||
require.Len(t, res.Rules, 1)
|
|
||||||
|
|
||||||
assert.Equal(t, res.Rules[0].IP, yandexIP)
|
err = ss.Update(filtering.SafeSearchConfig{
|
||||||
|
Enabled: true,
|
||||||
// Check cache.
|
Google: false,
|
||||||
cachedValue, isFound := ss.getCachedResult(domain)
|
|
||||||
require.True(t, isFound)
|
|
||||||
require.Len(t, cachedValue.Rules, 1)
|
|
||||||
|
|
||||||
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSafeSearchCacheGoogle(t *testing.T) {
|
|
||||||
const domain = "www.google.ru"
|
|
||||||
|
|
||||||
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
|
|
||||||
|
|
||||||
res, err := ss.CheckHost(domain, dns.TypeA)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.False(t, res.IsFiltered)
|
|
||||||
assert.Empty(t, res.Rules)
|
|
||||||
|
|
||||||
resolver := &aghtest.TestResolver{}
|
|
||||||
ss = newForTest(t, defaultSafeSearchConf)
|
|
||||||
ss.resolver = resolver
|
|
||||||
|
|
||||||
// Lookup for safesearch domain.
|
|
||||||
rewrite := ss.SearchHost(domain, dns.TypeA)
|
|
||||||
|
|
||||||
ips, err := resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var foundIP net.IP
|
|
||||||
for _, ip := range ips {
|
|
||||||
if ip.To4() != nil {
|
|
||||||
foundIP = ip
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err = ss.CheckHost(domain, dns.TypeA)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, res.Rules, 1)
|
|
||||||
|
|
||||||
assert.True(t, res.Rules[0].IP.Equal(foundIP))
|
|
||||||
|
|
||||||
// Check cache.
|
|
||||||
cachedValue, isFound := ss.getCachedResult(domain)
|
|
||||||
require.True(t, isFound)
|
|
||||||
require.Len(t, cachedValue.Rules, 1)
|
|
||||||
|
|
||||||
assert.True(t, cachedValue.Rules[0].IP.Equal(foundIP))
|
|
||||||
}
|
|
||||||
|
|
||||||
const googleHost = "www.google.com"
|
|
||||||
|
|
||||||
var dnsRewriteSink *rules.DNSRewrite
|
|
||||||
|
|
||||||
func BenchmarkSafeSearch(b *testing.B) {
|
|
||||||
ss := newForTest(b, defaultSafeSearchConf)
|
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
|
||||||
dnsRewriteSink = ss.SearchHost(googleHost, dns.TypeA)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(b, "forcesafesearch.google.com", dnsRewriteSink.NewCNAME)
|
|
||||||
}
|
|
||||||
|
|
||||||
var dnsRewriteParallelSink *rules.DNSRewrite
|
|
||||||
|
|
||||||
func BenchmarkSafeSearch_parallel(b *testing.B) {
|
|
||||||
ss := newForTest(b, defaultSafeSearchConf)
|
|
||||||
|
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
|
||||||
for pb.Next() {
|
|
||||||
dnsRewriteParallelSink = ss.SearchHost(googleHost, dns.TypeA)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(b, "forcesafesearch.google.com", dnsRewriteParallelSink.NewCNAME)
|
res, err = ss.CheckHost("www.yandex.com", testQType)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.False(t, res.IsFiltered)
|
||||||
|
|
||||||
|
err = ss.Update(filtering.SafeSearchConfig{
|
||||||
|
Enabled: false,
|
||||||
|
Google: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err = ss.CheckHost("www.yandex.com", testQType)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.False(t, res.IsFiltered)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,11 +50,19 @@ func (d *DNSFilter) handleSafeSearchSettings(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conf := *req
|
||||||
|
err = d.safeSearch.Update(conf)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "updating: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func() {
|
func() {
|
||||||
d.confLock.Lock()
|
d.confLock.Lock()
|
||||||
defer d.confLock.Unlock()
|
defer d.confLock.Unlock()
|
||||||
|
|
||||||
d.Config.SafeSearchConf = *req
|
d.Config.SafeSearchConf = conf
|
||||||
}()
|
}()
|
||||||
|
|
||||||
d.Config.ConfigModified()
|
d.Config.ConfigModified()
|
||||||
|
|||||||
@@ -311,6 +311,14 @@ var blockedServices = []blockedService{{
|
|||||||
"||warp.plus^",
|
"||warp.plus^",
|
||||||
"||workers.dev^",
|
"||workers.dev^",
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
ID: "crunchyroll",
|
||||||
|
Name: "Crunchyroll",
|
||||||
|
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M 25 3 C 12.85 3 3 12.85 3 25 C 3 40.188 13.387672 44.538609 20.388672 45.974609 C 20.427672 45.982609 20.465953 45.986328 20.501953 45.986328 C 21.006953 45.986328 21.206312 45.25525 20.695312 45.03125 C 13.285312 41.79025 8.0301562 34.327141 9.1601562 25.494141 C 10.256156 16.920141 17.244938 10.069141 25.835938 9.1191406 C 26.564937 9.0381406 27.287 9 28 9 C 35.541 9 42.044422 13.395672 45.107422 19.763672 C 45.206422 19.968672 45.382594 20.058594 45.558594 20.058594 C 45.853594 20.058594 46.144828 19.8075 46.048828 19.4375 C 44.302828 12.7105 39 3 25 3 z M 29 14 C 20.481 14 13.619625 21.101031 14.015625 29.707031 C 14.366625 37.346031 20.653016 43.631422 28.291016 43.982422 C 28.528016 43.994422 28.766 44 29 44 C 37.285 44 44 37.285 44 29 C 44 27.819 43.860563 26.670359 43.601562 25.568359 C 43.542563 25.319359 43.332234 25.183594 43.115234 25.183594 C 42.961234 25.183594 42.806266 25.251484 42.697266 25.396484 C 41.512266 26.976484 39.627 28 37.5 28 C 37.397 28 37.293453 27.997188 37.189453 27.992188 C 34.031453 27.845188 31.348203 25.317875 31.033203 22.171875 C 30.763203 19.477875 32.142297 17.082328 34.279297 15.861328 C 34.656297 15.646328 34.62475 15.100266 34.21875 14.947266 C 32.59375 14.340266 30.838 14 29 14 z M 44.296875 26.595703 L 44.300781 26.595703 L 44.296875 26.595703 z\"/></svg>"),
|
||||||
|
Rules: []string{
|
||||||
|
"||crunchyroll.com^",
|
||||||
|
"||gccrunchyroll.com^",
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
ID: "dailymotion",
|
ID: "dailymotion",
|
||||||
Name: "Dailymotion",
|
Name: "Dailymotion",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
@@ -379,9 +380,9 @@ func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error)
|
|||||||
// TODO(a.garipov): Support header Forwarded from RFC 7329.
|
// TODO(a.garipov): Support header Forwarded from RFC 7329.
|
||||||
func realIP(r *http.Request) (ip net.IP, err error) {
|
func realIP(r *http.Request) (ip net.IP, err error) {
|
||||||
proxyHeaders := []string{
|
proxyHeaders := []string{
|
||||||
"CF-Connecting-IP",
|
httphdr.CFConnectingIP,
|
||||||
"True-Client-IP",
|
httphdr.TrueClientIP,
|
||||||
"X-Real-IP",
|
httphdr.XRealIP,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, h := range proxyHeaders {
|
for _, h := range proxyHeaders {
|
||||||
@@ -394,7 +395,7 @@ func realIP(r *http.Request) (ip net.IP, err error) {
|
|||||||
|
|
||||||
// If none of the above yielded any results, get the leftmost IP address
|
// If none of the above yielded any results, get the leftmost IP address
|
||||||
// from the X-Forwarded-For header.
|
// from the X-Forwarded-For header.
|
||||||
s := r.Header.Get("X-Forwarded-For")
|
s := r.Header.Get(httphdr.XForwardedFor)
|
||||||
ipStrs := strings.SplitN(s, ", ", 2)
|
ipStrs := strings.SplitN(s, ", ", 2)
|
||||||
ip = net.ParseIP(ipStrs[0])
|
ip = net.ParseIP(ipStrs[0])
|
||||||
if ip != nil {
|
if ip != nil {
|
||||||
@@ -411,6 +412,21 @@ func realIP(r *http.Request) (ip net.IP, err error) {
|
|||||||
return net.ParseIP(ipStr), nil
|
return net.ParseIP(ipStr), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeErrorWithIP is like [aghhttp.Error], but includes the remote IP address
|
||||||
|
// when it writes to the log.
|
||||||
|
func writeErrorWithIP(
|
||||||
|
r *http.Request,
|
||||||
|
w http.ResponseWriter,
|
||||||
|
code int,
|
||||||
|
remoteIP string,
|
||||||
|
format string,
|
||||||
|
args ...any,
|
||||||
|
) {
|
||||||
|
text := fmt.Sprintf(format, args...)
|
||||||
|
log.Error("%s %s %s: from ip %s: %s", r.Method, r.Host, r.URL, remoteIP, text)
|
||||||
|
http.Error(w, text, code)
|
||||||
|
}
|
||||||
|
|
||||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
req := loginJSON{}
|
req := loginJSON{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
@@ -420,31 +436,45 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var remoteAddr string
|
var remoteIP string
|
||||||
// realIP cannot be used here without taking TrustedProxies into account due
|
// realIP cannot be used here without taking TrustedProxies into account due
|
||||||
// to security issues.
|
// to security issues.
|
||||||
//
|
//
|
||||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Use realIP when the issue will be fixed.
|
// TODO(e.burkov): Use realIP when the issue will be fixed.
|
||||||
if remoteAddr, err = netutil.SplitHost(r.RemoteAddr); err != nil {
|
if remoteIP, err = netutil.SplitHost(r.RemoteAddr); err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "auth: getting remote address: %s", err)
|
writeErrorWithIP(
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
r.RemoteAddr,
|
||||||
|
"auth: getting remote address: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if rateLimiter := Context.auth.raleLimiter; rateLimiter != nil {
|
if rateLimiter := Context.auth.raleLimiter; rateLimiter != nil {
|
||||||
if left := rateLimiter.check(remoteAddr); left > 0 {
|
if left := rateLimiter.check(remoteIP); left > 0 {
|
||||||
w.Header().Set("Retry-After", strconv.Itoa(int(left.Seconds())))
|
w.Header().Set(httphdr.RetryAfter, strconv.Itoa(int(left.Seconds())))
|
||||||
aghhttp.Error(r, w, http.StatusTooManyRequests, "auth: blocked for %s", left)
|
writeErrorWithIP(
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusTooManyRequests,
|
||||||
|
remoteIP,
|
||||||
|
"auth: blocked for %s",
|
||||||
|
left,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie, err := Context.auth.newCookie(req, remoteAddr)
|
cookie, err := Context.auth.newCookie(req, remoteIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusForbidden, "%s", err)
|
writeErrorWithIP(r, w, http.StatusForbidden, remoteIP, "%s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -452,10 +482,7 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Use realIP here, since this IP address is only used for logging.
|
// Use realIP here, since this IP address is only used for logging.
|
||||||
ip, err := realIP(r)
|
ip, err := realIP(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("auth: getting real ip from request: %s", err)
|
log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
|
||||||
} else if ip == nil {
|
|
||||||
// Technically shouldn't happen.
|
|
||||||
log.Error("auth: unknown ip")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
|
log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
|
||||||
@@ -463,9 +490,9 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
|
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
h.Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
|
h.Set(httphdr.CacheControl, "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||||
h.Set("Pragma", "no-cache")
|
h.Set(httphdr.Pragma, "no-cache")
|
||||||
h.Set("Expires", "0")
|
h.Set(httphdr.Expires, "0")
|
||||||
|
|
||||||
aghhttp.OK(w)
|
aghhttp.OK(w)
|
||||||
}
|
}
|
||||||
@@ -476,7 +503,7 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
|
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
|
||||||
// The user is already logged out.
|
// The user is already logged out.
|
||||||
respHdr.Set("Location", "/login.html")
|
respHdr.Set(httphdr.Location, "/login.html")
|
||||||
w.WriteHeader(http.StatusFound)
|
w.WriteHeader(http.StatusFound)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -494,8 +521,8 @@ func handleLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
respHdr.Set("Location", "/login.html")
|
respHdr.Set(httphdr.Location, "/login.html")
|
||||||
respHdr.Set("Set-Cookie", c.String())
|
respHdr.Set(httphdr.SetCookie, c.String())
|
||||||
w.WriteHeader(http.StatusFound)
|
w.WriteHeader(http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,8 +570,7 @@ func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
|
|||||||
log.Debug("auth: redirected to login page by GL-Inet submodule")
|
log.Debug("auth: redirected to login page by GL-Inet submodule")
|
||||||
} else {
|
} else {
|
||||||
log.Debug("auth: redirected to login page")
|
log.Debug("auth: redirected to login page")
|
||||||
w.Header().Set("Location", "/login.html")
|
http.Redirect(w, r, "login.html", http.StatusFound)
|
||||||
w.WriteHeader(http.StatusFound)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Debug("auth: responded with forbidden to %s %s", r.Method, p)
|
log.Debug("auth: responded with forbidden to %s %s", r.Method, p)
|
||||||
@@ -569,8 +595,7 @@ func optionalAuth(
|
|||||||
// Redirect to the dashboard if already authenticated.
|
// Redirect to the dashboard if already authenticated.
|
||||||
res := Context.auth.checkSession(cookie.Value)
|
res := Context.auth.checkSession(cookie.Value)
|
||||||
if res == checkSessionOK {
|
if res == checkSessionOK {
|
||||||
w.Header().Set("Location", "/")
|
http.Redirect(w, r, "", http.StatusFound)
|
||||||
w.WriteHeader(http.StatusFound)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -135,11 +136,11 @@ func TestAuthHTTP(t *testing.T) {
|
|||||||
handlerCalled = false
|
handlerCalled = false
|
||||||
handler2(&w, &r)
|
handler2(&w, &r)
|
||||||
assert.Equal(t, http.StatusFound, w.statusCode)
|
assert.Equal(t, http.StatusFound, w.statusCode)
|
||||||
assert.NotEmpty(t, w.hdr.Get("Location"))
|
assert.NotEmpty(t, w.hdr.Get(httphdr.Location))
|
||||||
assert.False(t, handlerCalled)
|
assert.False(t, handlerCalled)
|
||||||
|
|
||||||
// go to login page
|
// go to login page
|
||||||
loginURL := w.hdr.Get("Location")
|
loginURL := w.hdr.Get(httphdr.Location)
|
||||||
r.URL = &url.URL{Path: loginURL}
|
r.URL = &url.URL{Path: loginURL}
|
||||||
handlerCalled = false
|
handlerCalled = false
|
||||||
handler2(&w, &r)
|
handler2(&w, &r)
|
||||||
@@ -153,13 +154,13 @@ func TestAuthHTTP(t *testing.T) {
|
|||||||
// get /
|
// get /
|
||||||
handler2 = optionalAuth(handler)
|
handler2 = optionalAuth(handler)
|
||||||
w.hdr = make(http.Header)
|
w.hdr = make(http.Header)
|
||||||
r.Header.Set("Cookie", cookie.String())
|
r.Header.Set(httphdr.Cookie, cookie.String())
|
||||||
r.URL = &url.URL{Path: "/"}
|
r.URL = &url.URL{Path: "/"}
|
||||||
handlerCalled = false
|
handlerCalled = false
|
||||||
handler2(&w, &r)
|
handler2(&w, &r)
|
||||||
assert.True(t, handlerCalled)
|
assert.True(t, handlerCalled)
|
||||||
|
|
||||||
r.Header.Del("Cookie")
|
r.Header.Del(httphdr.Cookie)
|
||||||
|
|
||||||
// get / with basic auth
|
// get / with basic auth
|
||||||
handler2 = optionalAuth(handler)
|
handler2 = optionalAuth(handler)
|
||||||
@@ -169,28 +170,28 @@ func TestAuthHTTP(t *testing.T) {
|
|||||||
handlerCalled = false
|
handlerCalled = false
|
||||||
handler2(&w, &r)
|
handler2(&w, &r)
|
||||||
assert.True(t, handlerCalled)
|
assert.True(t, handlerCalled)
|
||||||
r.Header.Del("Authorization")
|
r.Header.Del(httphdr.Authorization)
|
||||||
|
|
||||||
// get login page with a valid cookie - we're redirected to /
|
// get login page with a valid cookie - we're redirected to /
|
||||||
handler2 = optionalAuth(handler)
|
handler2 = optionalAuth(handler)
|
||||||
w.hdr = make(http.Header)
|
w.hdr = make(http.Header)
|
||||||
r.Header.Set("Cookie", cookie.String())
|
r.Header.Set(httphdr.Cookie, cookie.String())
|
||||||
r.URL = &url.URL{Path: loginURL}
|
r.URL = &url.URL{Path: loginURL}
|
||||||
handlerCalled = false
|
handlerCalled = false
|
||||||
handler2(&w, &r)
|
handler2(&w, &r)
|
||||||
assert.NotEmpty(t, w.hdr.Get("Location"))
|
assert.NotEmpty(t, w.hdr.Get(httphdr.Location))
|
||||||
assert.False(t, handlerCalled)
|
assert.False(t, handlerCalled)
|
||||||
r.Header.Del("Cookie")
|
r.Header.Del(httphdr.Cookie)
|
||||||
|
|
||||||
// get login page with an invalid cookie
|
// get login page with an invalid cookie
|
||||||
handler2 = optionalAuth(handler)
|
handler2 = optionalAuth(handler)
|
||||||
w.hdr = make(http.Header)
|
w.hdr = make(http.Header)
|
||||||
r.Header.Set("Cookie", "bad")
|
r.Header.Set(httphdr.Cookie, "bad")
|
||||||
r.URL = &url.URL{Path: loginURL}
|
r.URL = &url.URL{Path: loginURL}
|
||||||
handlerCalled = false
|
handlerCalled = false
|
||||||
handler2(&w, &r)
|
handler2(&w, &r)
|
||||||
assert.True(t, handlerCalled)
|
assert.True(t, handlerCalled)
|
||||||
r.Header.Del("Cookie")
|
r.Header.Del(httphdr.Cookie)
|
||||||
|
|
||||||
Context.auth.Close()
|
Context.auth.Close()
|
||||||
}
|
}
|
||||||
@@ -213,7 +214,7 @@ func TestRealIP(t *testing.T) {
|
|||||||
}, {
|
}, {
|
||||||
name: "success_proxy",
|
name: "success_proxy",
|
||||||
header: http.Header{
|
header: http.Header{
|
||||||
textproto.CanonicalMIMEHeaderKey("X-Real-IP"): []string{"1.2.3.5"},
|
textproto.CanonicalMIMEHeaderKey(httphdr.XRealIP): []string{"1.2.3.5"},
|
||||||
},
|
},
|
||||||
remoteAddr: remoteAddr,
|
remoteAddr: remoteAddr,
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
@@ -221,7 +222,7 @@ func TestRealIP(t *testing.T) {
|
|||||||
}, {
|
}, {
|
||||||
name: "success_proxy_multiple",
|
name: "success_proxy_multiple",
|
||||||
header: http.Header{
|
header: http.Header{
|
||||||
textproto.CanonicalMIMEHeaderKey("X-Forwarded-For"): []string{
|
textproto.CanonicalMIMEHeaderKey(httphdr.XForwardedFor): []string{
|
||||||
"1.2.3.6, 1.2.3.5",
|
"1.2.3.6, 1.2.3.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/josharian/native"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GLMode - enable GL-Inet compatibility mode
|
// GLMode - enable GL-Inet compatibility mode
|
||||||
@@ -102,7 +102,7 @@ func glGetTokenDate(file string) uint32 {
|
|||||||
|
|
||||||
buf := bytes.NewBuffer(bs)
|
buf := bytes.NewBuffer(bs)
|
||||||
|
|
||||||
err = binary.Read(buf, aghos.NativeEndian, &dateToken)
|
err = binary.Read(buf, native.Endian, &dateToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("decoding token: %s", err)
|
log.Error("decoding token: %s", err)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/josharian/native"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -19,13 +19,13 @@ func TestAuthGL(t *testing.T) {
|
|||||||
glFilePrefix = dir + "/gl_token_"
|
glFilePrefix = dir + "/gl_token_"
|
||||||
|
|
||||||
data := make([]byte, 4)
|
data := make([]byte, 4)
|
||||||
aghos.NativeEndian.PutUint32(data, 1)
|
native.Endian.PutUint32(data, 1)
|
||||||
|
|
||||||
require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
|
require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
|
||||||
assert.False(t, glCheckToken("test"))
|
assert.False(t, glCheckToken("test"))
|
||||||
|
|
||||||
data = make([]byte, 4)
|
data = make([]byte, 4)
|
||||||
aghos.NativeEndian.PutUint32(data, uint32(time.Now().UTC().Unix()+60))
|
native.Endian.PutUint32(data, uint32(time.Now().UTC().Unix()+60))
|
||||||
|
|
||||||
require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
|
require.NoError(t, os.WriteFile(glFilePrefix+"test", data, 0o644))
|
||||||
r, _ := http.NewRequest(http.MethodGet, "http://localhost/", nil)
|
r, _ := http.NewRequest(http.MethodGet, "http://localhost/", nil)
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package home
|
|||||||
import (
|
import (
|
||||||
"encoding"
|
"encoding"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,6 +33,8 @@ type Client struct {
|
|||||||
SafeBrowsingEnabled bool
|
SafeBrowsingEnabled bool
|
||||||
ParentalEnabled bool
|
ParentalEnabled bool
|
||||||
UseOwnBlockedServices bool
|
UseOwnBlockedServices bool
|
||||||
|
IgnoreQueryLog bool
|
||||||
|
IgnoreStatistics bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// closeUpstreams closes the client-specific upstream config of c if any.
|
// closeUpstreams closes the client-specific upstream config of c if any.
|
||||||
@@ -45,6 +49,23 @@ func (c *Client) closeUpstreams() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setSafeSearch initializes and sets the safe search filter for this client.
|
||||||
|
func (c *Client) setSafeSearch(
|
||||||
|
conf filtering.SafeSearchConfig,
|
||||||
|
cacheSize uint,
|
||||||
|
cacheTTL time.Duration,
|
||||||
|
) (err error) {
|
||||||
|
ss, err := safesearch.NewDefault(conf, fmt.Sprintf("client %q", c.Name), cacheSize, cacheTTL)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SafeSearch = ss
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// clientSource represents the source from which the information about the
|
// clientSource represents the source from which the information about the
|
||||||
// client has been obtained.
|
// client has been obtained.
|
||||||
type clientSource uint
|
type clientSource uint
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
@@ -52,9 +51,17 @@ type clientsContainer struct {
|
|||||||
// lock protects all fields.
|
// lock protects all fields.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Use a pointer and describe which fields are protected in
|
// TODO(a.garipov): Use a pointer and describe which fields are protected in
|
||||||
// more detail.
|
// more detail. Use sync.RWMutex.
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
|
|
||||||
|
// safeSearchCacheSize is the size of the safe search cache to use for
|
||||||
|
// persistent clients.
|
||||||
|
safeSearchCacheSize uint
|
||||||
|
|
||||||
|
// safeSearchCacheTTL is the TTL of the safe search cache to use for
|
||||||
|
// persistent clients.
|
||||||
|
safeSearchCacheTTL time.Duration
|
||||||
|
|
||||||
// testing is a flag that disables some features for internal tests.
|
// testing is a flag that disables some features for internal tests.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Awful. Remove.
|
// TODO(a.garipov): Awful. Remove.
|
||||||
@@ -74,6 +81,7 @@ func (clients *clientsContainer) Init(
|
|||||||
if clients.list != nil {
|
if clients.list != nil {
|
||||||
log.Fatal("clients.list != nil")
|
log.Fatal("clients.list != nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
clients.list = make(map[string]*Client)
|
clients.list = make(map[string]*Client)
|
||||||
clients.idIndex = make(map[string]*Client)
|
clients.idIndex = make(map[string]*Client)
|
||||||
clients.ipToRC = map[netip.Addr]*RuntimeClient{}
|
clients.ipToRC = map[netip.Addr]*RuntimeClient{}
|
||||||
@@ -85,6 +93,9 @@ func (clients *clientsContainer) Init(
|
|||||||
clients.arpdb = arpdb
|
clients.arpdb = arpdb
|
||||||
clients.addFromConfig(objects, filteringConf)
|
clients.addFromConfig(objects, filteringConf)
|
||||||
|
|
||||||
|
clients.safeSearchCacheSize = filteringConf.SafeSearchCacheSize
|
||||||
|
clients.safeSearchCacheTTL = time.Minute * time.Duration(filteringConf.CacheTime)
|
||||||
|
|
||||||
if clients.testing {
|
if clients.testing {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -148,6 +159,9 @@ type clientObject struct {
|
|||||||
ParentalEnabled bool `yaml:"parental_enabled"`
|
ParentalEnabled bool `yaml:"parental_enabled"`
|
||||||
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
|
||||||
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
|
UseGlobalBlockedServices bool `yaml:"use_global_blocked_services"`
|
||||||
|
|
||||||
|
IgnoreQueryLog bool `yaml:"ignore_querylog"`
|
||||||
|
IgnoreStatistics bool `yaml:"ignore_statistics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// addFromConfig initializes the clients container with objects from the
|
// addFromConfig initializes the clients container with objects from the
|
||||||
@@ -166,23 +180,23 @@ func (clients *clientsContainer) addFromConfig(objects []*clientObject, filterin
|
|||||||
safeSearchConf: o.SafeSearchConf,
|
safeSearchConf: o.SafeSearchConf,
|
||||||
SafeBrowsingEnabled: o.SafeBrowsingEnabled,
|
SafeBrowsingEnabled: o.SafeBrowsingEnabled,
|
||||||
UseOwnBlockedServices: !o.UseGlobalBlockedServices,
|
UseOwnBlockedServices: !o.UseGlobalBlockedServices,
|
||||||
|
IgnoreQueryLog: o.IgnoreQueryLog,
|
||||||
|
IgnoreStatistics: o.IgnoreStatistics,
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.SafeSearchConf.Enabled {
|
if o.SafeSearchConf.Enabled {
|
||||||
o.SafeSearchConf.CustomResolver = safeSearchResolver{}
|
o.SafeSearchConf.CustomResolver = safeSearchResolver{}
|
||||||
|
|
||||||
ss, err := safesearch.NewDefaultSafeSearch(
|
err := cli.setSafeSearch(
|
||||||
o.SafeSearchConf,
|
o.SafeSearchConf,
|
||||||
filteringConf.SafeSearchCacheSize,
|
filteringConf.SafeSearchCacheSize,
|
||||||
time.Minute*time.Duration(filteringConf.CacheTime),
|
time.Minute*time.Duration(filteringConf.CacheTime),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("clients: init client safesearch %s: %s", cli.Name, err)
|
log.Error("clients: init client safesearch %q: %s", cli.Name, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.SafeSearch = ss
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range o.BlockedServices {
|
for _, s := range o.BlockedServices {
|
||||||
@@ -232,6 +246,8 @@ func (clients *clientsContainer) forConfig() (objs []*clientObject) {
|
|||||||
SafeSearchConf: cli.safeSearchConf,
|
SafeSearchConf: cli.safeSearchConf,
|
||||||
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
|
SafeBrowsingEnabled: cli.SafeBrowsingEnabled,
|
||||||
UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
|
UseGlobalBlockedServices: !cli.UseOwnBlockedServices,
|
||||||
|
IgnoreQueryLog: cli.IgnoreQueryLog,
|
||||||
|
IgnoreStatistics: cli.IgnoreStatistics,
|
||||||
}
|
}
|
||||||
|
|
||||||
objs = append(objs, o)
|
objs = append(objs, o)
|
||||||
@@ -343,7 +359,8 @@ func (clients *clientsContainer) clientOrArtificial(
|
|||||||
client, ok := clients.Find(id)
|
client, ok := clients.Find(id)
|
||||||
if ok {
|
if ok {
|
||||||
return &querylog.Client{
|
return &querylog.Client{
|
||||||
Name: client.Name,
|
Name: client.Name,
|
||||||
|
IgnoreQueryLog: client.IgnoreQueryLog,
|
||||||
}, false
|
}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,6 +395,20 @@ func (clients *clientsContainer) Find(id string) (c *Client, ok bool) {
|
|||||||
return c, true
|
return c, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shouldCountClient is a wrapper around Find to make it a valid client
|
||||||
|
// information finder for the statistics. If no information about the client
|
||||||
|
// is found, it returns true.
|
||||||
|
func (clients *clientsContainer) shouldCountClient(ids []string) (y bool) {
|
||||||
|
for _, id := range ids {
|
||||||
|
client, ok := clients.Find(id)
|
||||||
|
if ok {
|
||||||
|
return !client.IgnoreStatistics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// findUpstreams returns upstreams configured for the client, identified either
|
// findUpstreams returns upstreams configured for the client, identified either
|
||||||
// by its IP address or its ClientID. upsConf is nil if the client isn't found
|
// by its IP address or its ClientID. upsConf is nil if the client isn't found
|
||||||
// or if the client has no custom upstreams.
|
// or if the client has no custom upstreams.
|
||||||
|
|||||||
@@ -9,17 +9,27 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestClients(t *testing.T) {
|
// newClientsContainer is a helper that creates a new clients container for
|
||||||
clients := clientsContainer{}
|
// tests.
|
||||||
clients.testing = true
|
func newClientsContainer() (c *clientsContainer) {
|
||||||
|
c = &clientsContainer{
|
||||||
|
testing: true,
|
||||||
|
}
|
||||||
|
|
||||||
clients.Init(nil, nil, nil, nil, nil)
|
c.Init(nil, nil, nil, nil, &filtering.Config{})
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClients(t *testing.T) {
|
||||||
|
clients := newClientsContainer()
|
||||||
|
|
||||||
t.Run("add_success", func(t *testing.T) {
|
t.Run("add_success", func(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
@@ -198,10 +208,7 @@ func TestClients(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClientsWHOIS(t *testing.T) {
|
func TestClientsWHOIS(t *testing.T) {
|
||||||
clients := clientsContainer{
|
clients := newClientsContainer()
|
||||||
testing: true,
|
|
||||||
}
|
|
||||||
clients.Init(nil, nil, nil, nil, nil)
|
|
||||||
whois := &RuntimeClientWHOISInfo{
|
whois := &RuntimeClientWHOISInfo{
|
||||||
Country: "AU",
|
Country: "AU",
|
||||||
Orgname: "Example Org",
|
Orgname: "Example Org",
|
||||||
@@ -247,10 +254,7 @@ func TestClientsWHOIS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClientsAddExisting(t *testing.T) {
|
func TestClientsAddExisting(t *testing.T) {
|
||||||
clients := clientsContainer{
|
clients := newClientsContainer()
|
||||||
testing: true,
|
|
||||||
}
|
|
||||||
clients.Init(nil, nil, nil, nil, nil)
|
|
||||||
|
|
||||||
t.Run("simple", func(t *testing.T) {
|
t.Run("simple", func(t *testing.T) {
|
||||||
ip := netip.MustParseAddr("1.1.1.1")
|
ip := netip.MustParseAddr("1.1.1.1")
|
||||||
@@ -325,10 +329,7 @@ func TestClientsAddExisting(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClientsCustomUpstream(t *testing.T) {
|
func TestClientsCustomUpstream(t *testing.T) {
|
||||||
clients := clientsContainer{
|
clients := newClientsContainer()
|
||||||
testing: true,
|
|
||||||
}
|
|
||||||
clients.Init(nil, nil, nil, nil, nil)
|
|
||||||
|
|
||||||
// Add client with upstreams.
|
// Add client with upstreams.
|
||||||
ok, err := clients.Add(&Client{
|
ok, err := clients.Add(&Client{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
)
|
)
|
||||||
@@ -44,13 +45,16 @@ type clientJSON struct {
|
|||||||
SafeSearchEnabled bool `json:"safesearch_enabled"`
|
SafeSearchEnabled bool `json:"safesearch_enabled"`
|
||||||
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
|
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
|
||||||
UseGlobalSettings bool `json:"use_global_settings"`
|
UseGlobalSettings bool `json:"use_global_settings"`
|
||||||
|
|
||||||
|
IgnoreQueryLog aghalg.NullBool `json:"ignore_querylog"`
|
||||||
|
IgnoreStatistics aghalg.NullBool `json:"ignore_statistics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type runtimeClientJSON struct {
|
type runtimeClientJSON struct {
|
||||||
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info"`
|
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info"`
|
||||||
|
|
||||||
Name string `json:"name"`
|
|
||||||
IP netip.Addr `json:"ip"`
|
IP netip.Addr `json:"ip"`
|
||||||
|
Name string `json:"name"`
|
||||||
Source clientSource `json:"source"`
|
Source clientSource `json:"source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,14 +94,16 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
|
|||||||
}
|
}
|
||||||
|
|
||||||
// jsonToClient converts JSON object to Client object.
|
// jsonToClient converts JSON object to Client object.
|
||||||
func jsonToClient(cj clientJSON) (c *Client) {
|
func (clients *clientsContainer) jsonToClient(cj clientJSON, prev *Client) (c *Client, err error) {
|
||||||
var safeSearchConf filtering.SafeSearchConfig
|
var safeSearchConf filtering.SafeSearchConfig
|
||||||
if cj.SafeSearchConf != nil {
|
if cj.SafeSearchConf != nil {
|
||||||
safeSearchConf = *cj.SafeSearchConf
|
safeSearchConf = *cj.SafeSearchConf
|
||||||
} else {
|
} else {
|
||||||
// TODO(d.kolyshev): Remove after cleaning the deprecated
|
// TODO(d.kolyshev): Remove after cleaning the deprecated
|
||||||
// [clientJSON.SafeSearchEnabled] field.
|
// [clientJSON.SafeSearchEnabled] field.
|
||||||
safeSearchConf = filtering.SafeSearchConfig{Enabled: cj.SafeSearchEnabled}
|
safeSearchConf = filtering.SafeSearchConfig{
|
||||||
|
Enabled: cj.SafeSearchEnabled,
|
||||||
|
}
|
||||||
|
|
||||||
// Set default service flags for enabled safesearch.
|
// Set default service flags for enabled safesearch.
|
||||||
if safeSearchConf.Enabled {
|
if safeSearchConf.Enabled {
|
||||||
@@ -110,20 +116,47 @@ func jsonToClient(cj clientJSON) (c *Client) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{
|
c = &Client{
|
||||||
Name: cj.Name,
|
safeSearchConf: safeSearchConf,
|
||||||
IDs: cj.IDs,
|
|
||||||
Tags: cj.Tags,
|
Name: cj.Name,
|
||||||
|
|
||||||
|
IDs: cj.IDs,
|
||||||
|
Tags: cj.Tags,
|
||||||
|
BlockedServices: cj.BlockedServices,
|
||||||
|
Upstreams: cj.Upstreams,
|
||||||
|
|
||||||
UseOwnSettings: !cj.UseGlobalSettings,
|
UseOwnSettings: !cj.UseGlobalSettings,
|
||||||
FilteringEnabled: cj.FilteringEnabled,
|
FilteringEnabled: cj.FilteringEnabled,
|
||||||
ParentalEnabled: cj.ParentalEnabled,
|
ParentalEnabled: cj.ParentalEnabled,
|
||||||
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
|
SafeBrowsingEnabled: cj.SafeBrowsingEnabled,
|
||||||
safeSearchConf: safeSearchConf,
|
|
||||||
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
|
UseOwnBlockedServices: !cj.UseGlobalBlockedServices,
|
||||||
BlockedServices: cj.BlockedServices,
|
|
||||||
|
|
||||||
Upstreams: cj.Upstreams,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cj.IgnoreQueryLog != aghalg.NBNull {
|
||||||
|
c.IgnoreQueryLog = cj.IgnoreQueryLog == aghalg.NBTrue
|
||||||
|
} else if prev != nil {
|
||||||
|
c.IgnoreQueryLog = prev.IgnoreQueryLog
|
||||||
|
}
|
||||||
|
|
||||||
|
if cj.IgnoreStatistics != aghalg.NBNull {
|
||||||
|
c.IgnoreStatistics = cj.IgnoreStatistics == aghalg.NBTrue
|
||||||
|
} else if prev != nil {
|
||||||
|
c.IgnoreStatistics = prev.IgnoreStatistics
|
||||||
|
}
|
||||||
|
|
||||||
|
if safeSearchConf.Enabled {
|
||||||
|
err = c.setSafeSearch(
|
||||||
|
safeSearchConf,
|
||||||
|
clients.safeSearchCacheSize,
|
||||||
|
clients.safeSearchCacheTTL,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating safesearch for client %q: %w", c.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// clientToJSON converts Client object to JSON.
|
// clientToJSON converts Client object to JSON.
|
||||||
@@ -148,6 +181,9 @@ func clientToJSON(c *Client) (cj *clientJSON) {
|
|||||||
BlockedServices: c.BlockedServices,
|
BlockedServices: c.BlockedServices,
|
||||||
|
|
||||||
Upstreams: c.Upstreams,
|
Upstreams: c.Upstreams,
|
||||||
|
|
||||||
|
IgnoreQueryLog: aghalg.BoolToNullBool(c.IgnoreQueryLog),
|
||||||
|
IgnoreStatistics: aghalg.BoolToNullBool(c.IgnoreStatistics),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +197,13 @@ func (clients *clientsContainer) handleAddClient(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c := jsonToClient(cj)
|
c, err := clients.jsonToClient(cj, nil)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ok, err := clients.Add(c)
|
ok, err := clients.Add(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
@@ -209,6 +251,8 @@ type updateJSON struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleUpdateClient is the handler for POST /control/clients/update HTTP API.
|
// handleUpdateClient is the handler for POST /control/clients/update HTTP API.
|
||||||
|
//
|
||||||
|
// TODO(s.chzhen): Accept updated parameters instead of whole structure.
|
||||||
func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) {
|
func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||||
dj := updateJSON{}
|
dj := updateJSON{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&dj)
|
err := json.NewDecoder(r.Body).Decode(&dj)
|
||||||
@@ -224,7 +268,27 @@ func (clients *clientsContainer) handleUpdateClient(w http.ResponseWriter, r *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c := jsonToClient(dj.Data)
|
var prev *Client
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
func() {
|
||||||
|
clients.lock.Lock()
|
||||||
|
defer clients.lock.Unlock()
|
||||||
|
|
||||||
|
prev, ok = clients.list[dj.Name]
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "client not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := clients.jsonToClient(dj.Data, prev)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = clients.Update(dj.Name, c)
|
err = clients.Update(dj.Name, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
|
|||||||
@@ -296,12 +296,26 @@ var config = &configuration{
|
|||||||
MaxGoroutines: 300,
|
MaxGoroutines: 300,
|
||||||
},
|
},
|
||||||
DnsfilterConf: &filtering.Config{
|
DnsfilterConf: &filtering.Config{
|
||||||
SafeBrowsingCacheSize: 1 * 1024 * 1024,
|
|
||||||
SafeSearchCacheSize: 1 * 1024 * 1024,
|
|
||||||
ParentalCacheSize: 1 * 1024 * 1024,
|
|
||||||
CacheTime: 30,
|
|
||||||
FilteringEnabled: true,
|
FilteringEnabled: true,
|
||||||
FiltersUpdateIntervalHours: 24,
|
FiltersUpdateIntervalHours: 24,
|
||||||
|
|
||||||
|
ParentalEnabled: false,
|
||||||
|
SafeBrowsingEnabled: false,
|
||||||
|
|
||||||
|
SafeBrowsingCacheSize: 1 * 1024 * 1024,
|
||||||
|
SafeSearchCacheSize: 1 * 1024 * 1024,
|
||||||
|
ParentalCacheSize: 1 * 1024 * 1024,
|
||||||
|
CacheTime: 30,
|
||||||
|
|
||||||
|
SafeSearchConf: filtering.SafeSearchConfig{
|
||||||
|
Enabled: false,
|
||||||
|
Bing: true,
|
||||||
|
DuckDuckGo: true,
|
||||||
|
Google: true,
|
||||||
|
Pixabay: true,
|
||||||
|
Yandex: true,
|
||||||
|
YouTube: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
|
UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
|
||||||
UsePrivateRDNS: true,
|
UsePrivateRDNS: true,
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||||
"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"
|
||||||
|
"github.com/AdguardTeam/golibs/mathutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/NYTimes/gziphandler"
|
"github.com/NYTimes/gziphandler"
|
||||||
)
|
)
|
||||||
@@ -97,14 +99,17 @@ func collectDNSAddresses() (addrs []string, err error) {
|
|||||||
|
|
||||||
// statusResponse is a response for /control/status endpoint.
|
// statusResponse is a response for /control/status endpoint.
|
||||||
type statusResponse struct {
|
type statusResponse struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
DNSAddrs []string `json:"dns_addresses"`
|
DNSAddrs []string `json:"dns_addresses"`
|
||||||
DNSPort int `json:"dns_port"`
|
DNSPort int `json:"dns_port"`
|
||||||
HTTPPort int `json:"http_port"`
|
HTTPPort int `json:"http_port"`
|
||||||
IsProtectionEnabled bool `json:"protection_enabled"`
|
|
||||||
// ProtectionDisabledDuration is a pause duration in milliseconds.
|
// ProtectionDisabledDuration is the duration of the protection pause in
|
||||||
|
// milliseconds.
|
||||||
ProtectionDisabledDuration int64 `json:"protection_disabled_duration"`
|
ProtectionDisabledDuration int64 `json:"protection_disabled_duration"`
|
||||||
|
|
||||||
|
ProtectionEnabled bool `json:"protection_enabled"`
|
||||||
// TODO(e.burkov): Inspect if front-end doesn't requires this field as
|
// TODO(e.burkov): Inspect if front-end doesn't requires this field as
|
||||||
// openapi.yaml declares.
|
// openapi.yaml declares.
|
||||||
IsDHCPAvailable bool `json:"dhcp_available"`
|
IsDHCPAvailable bool `json:"dhcp_available"`
|
||||||
@@ -121,12 +126,15 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isProtectionEnabled := false
|
var (
|
||||||
var c *dnsforward.FilteringConfig
|
fltConf *dnsforward.FilteringConfig
|
||||||
|
protectionDisabledUntil *time.Time
|
||||||
|
protectionEnabled bool
|
||||||
|
)
|
||||||
if Context.dnsServer != nil {
|
if Context.dnsServer != nil {
|
||||||
c = &dnsforward.FilteringConfig{}
|
fltConf = &dnsforward.FilteringConfig{}
|
||||||
Context.dnsServer.WriteDiskConfig(c)
|
Context.dnsServer.WriteDiskConfig(fltConf)
|
||||||
isProtectionEnabled = Context.dnsServer.UpdatedProtectionStatus()
|
protectionEnabled, protectionDisabledUntil = Context.dnsServer.UpdatedProtectionStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp statusResponse
|
var resp statusResponse
|
||||||
@@ -134,20 +142,26 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
config.RLock()
|
config.RLock()
|
||||||
defer config.RUnlock()
|
defer config.RUnlock()
|
||||||
|
|
||||||
var pauseDuration int64
|
var protectionDisabledDuration int64
|
||||||
if until := config.DNS.ProtectionDisabledUntil; until != nil {
|
if protectionDisabledUntil != nil {
|
||||||
pauseDuration = time.Until(*until).Milliseconds()
|
// Make sure that we don't send negative numbers to the frontend,
|
||||||
|
// since enough time might have passed to make the difference less
|
||||||
|
// than zero.
|
||||||
|
protectionDisabledDuration = mathutil.Max(
|
||||||
|
0,
|
||||||
|
time.Until(*protectionDisabledUntil).Milliseconds(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = statusResponse{
|
resp = statusResponse{
|
||||||
Version: version.Version(),
|
Version: version.Version(),
|
||||||
|
Language: config.Language,
|
||||||
DNSAddrs: dnsAddrs,
|
DNSAddrs: dnsAddrs,
|
||||||
DNSPort: config.DNS.Port,
|
DNSPort: config.DNS.Port,
|
||||||
HTTPPort: config.BindPort,
|
HTTPPort: config.BindPort,
|
||||||
Language: config.Language,
|
ProtectionDisabledDuration: protectionDisabledDuration,
|
||||||
|
ProtectionEnabled: protectionEnabled,
|
||||||
IsRunning: isRunning(),
|
IsRunning: isRunning(),
|
||||||
ProtectionDisabledDuration: pauseDuration,
|
|
||||||
IsProtectionEnabled: isProtectionEnabled,
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -229,7 +243,7 @@ func modifiesData(m string) (ok bool) {
|
|||||||
func ensureContentType(w http.ResponseWriter, r *http.Request) (ok bool) {
|
func ensureContentType(w http.ResponseWriter, r *http.Request) (ok bool) {
|
||||||
const statusUnsup = http.StatusUnsupportedMediaType
|
const statusUnsup = http.StatusUnsupportedMediaType
|
||||||
|
|
||||||
cType := r.Header.Get(aghhttp.HdrNameContentType)
|
cType := r.Header.Get(httphdr.ContentType)
|
||||||
if r.ContentLength == 0 {
|
if r.ContentLength == 0 {
|
||||||
if cType == "" {
|
if cType == "" {
|
||||||
return true
|
return true
|
||||||
@@ -318,13 +332,17 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var serveHTTP3 bool
|
var (
|
||||||
var portHTTPS int
|
forceHTTPS bool
|
||||||
|
serveHTTP3 bool
|
||||||
|
portHTTPS int
|
||||||
|
)
|
||||||
func() {
|
func() {
|
||||||
config.RLock()
|
config.RLock()
|
||||||
defer config.RUnlock()
|
defer config.RUnlock()
|
||||||
|
|
||||||
serveHTTP3, portHTTPS = config.DNS.ServeHTTP3, config.TLS.PortHTTPS
|
serveHTTP3, portHTTPS = config.DNS.ServeHTTP3, config.TLS.PortHTTPS
|
||||||
|
forceHTTPS = config.TLS.ForceHTTPS && config.TLS.Enabled && config.TLS.PortHTTPS != 0
|
||||||
}()
|
}()
|
||||||
|
|
||||||
respHdr := w.Header()
|
respHdr := w.Header()
|
||||||
@@ -337,13 +355,13 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
|
|||||||
// default is 24 hours.
|
// default is 24 hours.
|
||||||
if serveHTTP3 {
|
if serveHTTP3 {
|
||||||
altSvc := fmt.Sprintf(`h3=":%d"`, portHTTPS)
|
altSvc := fmt.Sprintf(`h3=":%d"`, portHTTPS)
|
||||||
respHdr.Set(aghhttp.HdrNameAltSvc, altSvc)
|
respHdr.Set(httphdr.AltSvc, altSvc)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.TLS == nil && web.forceHTTPS {
|
if r.TLS == nil && forceHTTPS {
|
||||||
hostPort := host
|
hostPort := host
|
||||||
if port := web.conf.PortHTTPS; port != defaultPortHTTPS {
|
if portHTTPS != defaultPortHTTPS {
|
||||||
hostPort = netutil.JoinHostPort(host, port)
|
hostPort = netutil.JoinHostPort(host, portHTTPS)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpsURL := &url.URL{
|
httpsURL := &url.URL{
|
||||||
@@ -367,8 +385,8 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
|
|||||||
Host: r.Host,
|
Host: r.Host,
|
||||||
}
|
}
|
||||||
|
|
||||||
respHdr.Set(aghhttp.HdrNameAccessControlAllowOrigin, originURL.String())
|
respHdr.Set(httphdr.AccessControlAllowOrigin, originURL.String())
|
||||||
respHdr.Set(aghhttp.HdrNameVary, aghhttp.HdrNameOrigin)
|
respHdr.Set(httphdr.Vary, httphdr.Origin)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -381,7 +399,7 @@ func postInstall(handler func(http.ResponseWriter, *http.Request)) func(http.Res
|
|||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
if Context.firstRun && !strings.HasPrefix(path, "/install.") &&
|
if Context.firstRun && !strings.HasPrefix(path, "/install.") &&
|
||||||
!strings.HasPrefix(path, "/assets/") {
|
!strings.HasPrefix(path, "/assets/") {
|
||||||
http.Redirect(w, r, "/install.html", http.StatusFound)
|
http.Redirect(w, r, "install.html", http.StatusFound)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ type getAddrsResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleInstallGetAddresses is the handler for /install/get_addresses endpoint.
|
// handleInstallGetAddresses is the handler for /install/get_addresses endpoint.
|
||||||
func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
|
func (web *webAPI) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) {
|
||||||
data := getAddrsResponse{
|
data := getAddrsResponse{
|
||||||
Version: version.Version(),
|
Version: version.Version(),
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ func (req *checkConfReq) validateDNS(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleInstallCheckConfig handles the /check_config endpoint.
|
// handleInstallCheckConfig handles the /check_config endpoint.
|
||||||
func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
|
func (web *webAPI) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
req := &checkConfReq{}
|
req := &checkConfReq{}
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(req)
|
err := json.NewDecoder(r.Body).Decode(req)
|
||||||
@@ -375,7 +375,7 @@ func shutdownSrv3(srv *http3.Server) {
|
|||||||
const PasswordMinRunes = 8
|
const PasswordMinRunes = 8
|
||||||
|
|
||||||
// Apply new configuration, start DNS server, restart Web server
|
// Apply new configuration, start DNS server, restart Web server
|
||||||
func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
||||||
req, restartHTTP, err := decodeApplyConfigReq(r.Body)
|
req, restartHTTP, err := decodeApplyConfigReq(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
@@ -503,7 +503,7 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e
|
|||||||
return req, restartHTTP, err
|
return req, restartHTTP, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) registerInstallHandlers() {
|
func (web *webAPI) registerInstallHandlers() {
|
||||||
Context.mux.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(web.handleInstallGetAddresses)))
|
Context.mux.HandleFunc("/control/install/get_addresses", preInstall(ensureGET(web.handleInstallGetAddresses)))
|
||||||
Context.mux.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig)))
|
Context.mux.HandleFunc("/control/install/check_config", preInstall(ensurePOST(web.handleInstallCheckConfig)))
|
||||||
Context.mux.HandleFunc("/control/install/configure", preInstall(ensurePOST(web.handleInstallConfigure)))
|
Context.mux.HandleFunc("/control/install/configure", preInstall(ensurePOST(web.handleInstallConfigure)))
|
||||||
|
|||||||
@@ -51,11 +51,12 @@ func initDNS() (err error) {
|
|||||||
anonymizer := config.anonymizer()
|
anonymizer := config.anonymizer()
|
||||||
|
|
||||||
statsConf := stats.Config{
|
statsConf := stats.Config{
|
||||||
Filename: filepath.Join(baseDir, "stats.db"),
|
Filename: filepath.Join(baseDir, "stats.db"),
|
||||||
Limit: config.Stats.Interval.Duration,
|
Limit: config.Stats.Interval.Duration,
|
||||||
ConfigModified: onConfigModified,
|
ConfigModified: onConfigModified,
|
||||||
HTTPRegister: httpRegister,
|
HTTPRegister: httpRegister,
|
||||||
Enabled: config.Stats.Enabled,
|
Enabled: config.Stats.Enabled,
|
||||||
|
ShouldCountClient: Context.clients.shouldCountClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
set, err := aghnet.NewDomainNameSet(config.Stats.Ignored)
|
set, err := aghnet.NewDomainNameSet(config.Stats.Ignored)
|
||||||
@@ -545,6 +546,8 @@ var _ filtering.Resolver = safeSearchResolver{}
|
|||||||
|
|
||||||
// LookupIP implements [filtering.Resolver] interface for safeSearchResolver.
|
// LookupIP implements [filtering.Resolver] interface for safeSearchResolver.
|
||||||
// It returns the slice of net.IP with IPv4 and IPv6 instances.
|
// It returns the slice of net.IP with IPv4 and IPv6 instances.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Support network.
|
||||||
func (r safeSearchResolver) LookupIP(_ context.Context, _, host string) (ips []net.IP, err error) {
|
func (r safeSearchResolver) LookupIP(_ context.Context, _, host string) (ips []net.IP, err error) {
|
||||||
addrs, err := Context.dnsServer.Resolve(host)
|
addrs, err := Context.dnsServer.Resolve(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -59,7 +58,7 @@ type homeContext struct {
|
|||||||
dhcpServer dhcpd.Interface // DHCP module
|
dhcpServer dhcpd.Interface // DHCP module
|
||||||
auth *Auth // HTTP authentication module
|
auth *Auth // HTTP authentication module
|
||||||
filters *filtering.DNSFilter // DNS filtering module
|
filters *filtering.DNSFilter // DNS filtering module
|
||||||
web *Web // Web (HTTP, HTTPS) module
|
web *webAPI // Web (HTTP, HTTPS) module
|
||||||
tls *tlsManager // TLS module
|
tls *tlsManager // TLS module
|
||||||
|
|
||||||
// etcHosts contains IP-hostname mappings taken from the OS-specific hosts
|
// etcHosts contains IP-hostname mappings taken from the OS-specific hosts
|
||||||
@@ -297,8 +296,9 @@ func setupConfig(opts options) (err error) {
|
|||||||
config.DNS.DnsfilterConf.HTTPClient = Context.client
|
config.DNS.DnsfilterConf.HTTPClient = Context.client
|
||||||
|
|
||||||
config.DNS.DnsfilterConf.SafeSearchConf.CustomResolver = safeSearchResolver{}
|
config.DNS.DnsfilterConf.SafeSearchConf.CustomResolver = safeSearchResolver{}
|
||||||
config.DNS.DnsfilterConf.SafeSearch, err = safesearch.NewDefaultSafeSearch(
|
config.DNS.DnsfilterConf.SafeSearch, err = safesearch.NewDefault(
|
||||||
config.DNS.DnsfilterConf.SafeSearchConf,
|
config.DNS.DnsfilterConf.SafeSearchConf,
|
||||||
|
"default",
|
||||||
config.DNS.DnsfilterConf.SafeSearchCacheSize,
|
config.DNS.DnsfilterConf.SafeSearchCacheSize,
|
||||||
time.Minute*time.Duration(config.DNS.DnsfilterConf.CacheTime),
|
time.Minute*time.Duration(config.DNS.DnsfilterConf.CacheTime),
|
||||||
)
|
)
|
||||||
@@ -387,7 +387,7 @@ func checkPorts() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) {
|
func initWeb(opts options, clientBuildFS fs.FS) (web *webAPI, err error) {
|
||||||
var clientFS fs.FS
|
var clientFS fs.FS
|
||||||
if opts.localFrontend {
|
if opts.localFrontend {
|
||||||
log.Info("warning: using local frontend files")
|
log.Info("warning: using local frontend files")
|
||||||
@@ -414,7 +414,7 @@ func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) {
|
|||||||
serveHTTP3: config.DNS.ServeHTTP3,
|
serveHTTP3: config.DNS.ServeHTTP3,
|
||||||
}
|
}
|
||||||
|
|
||||||
web = newWeb(&webConf)
|
web = newWebAPI(&webConf)
|
||||||
if web == nil {
|
if web == nil {
|
||||||
return nil, fmt.Errorf("initializing web: %w", err)
|
return nil, fmt.Errorf("initializing web: %w", err)
|
||||||
}
|
}
|
||||||
@@ -469,26 +469,8 @@ func run(opts options, clientBuildFS fs.FS) {
|
|||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
if config.DebugPProf {
|
if config.DebugPProf {
|
||||||
mux := http.NewServeMux()
|
// TODO(a.garipov): Make the address configurable.
|
||||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
startPprof("localhost:6060")
|
||||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
|
||||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
|
||||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
|
||||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
|
||||||
|
|
||||||
// See profileSupportsDelta in src/net/http/pprof/pprof.go.
|
|
||||||
mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
|
|
||||||
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
|
|
||||||
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
|
|
||||||
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
|
|
||||||
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
|
|
||||||
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
log.Info("pprof: listening on localhost:6060")
|
|
||||||
lerr := http.ListenAndServe("localhost:6060", mux)
|
|
||||||
log.Error("Error while running the pprof server: %s", lerr)
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,7 +533,7 @@ func run(opts options, clientBuildFS fs.FS) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.web.Start()
|
Context.web.start()
|
||||||
|
|
||||||
// wait indefinitely for other go-routines to complete their job
|
// wait indefinitely for other go-routines to complete their job
|
||||||
select {}
|
select {}
|
||||||
@@ -731,7 +713,7 @@ func cleanup(ctx context.Context) {
|
|||||||
log.Info("stopping AdGuard Home")
|
log.Info("stopping AdGuard Home")
|
||||||
|
|
||||||
if Context.web != nil {
|
if Context.web != nil {
|
||||||
Context.web.Close(ctx)
|
Context.web.close(ctx)
|
||||||
Context.web = nil
|
Context.web = nil
|
||||||
}
|
}
|
||||||
if Context.auth != nil {
|
if Context.auth != nil {
|
||||||
@@ -869,8 +851,10 @@ func detectFirstRun() bool {
|
|||||||
// Connect to a remote server resolving hostname using our own DNS server.
|
// Connect to a remote server resolving hostname using our own DNS server.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): This messy logic should be decomposed and clarified.
|
// TODO(e.burkov): This messy logic should be decomposed and clarified.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Support network.
|
||||||
func customDialContext(ctx context.Context, network, addr string) (conn net.Conn, err error) {
|
func customDialContext(ctx context.Context, network, addr string) (conn net.Conn, err error) {
|
||||||
log.Tracef("network:%v addr:%v", network, addr)
|
log.Debug("home: customdial: dialing addr %q for network %s", addr, network)
|
||||||
|
|
||||||
host, port, err := net.SplitHostPort(addr)
|
host, port, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"howett.net/plist"
|
"howett.net/plist"
|
||||||
@@ -170,7 +171,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/xml")
|
w.Header().Set(httphdr.ContentType, "application/xml")
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dohContDisp = `attachment; filename=doh.mobileconfig`
|
dohContDisp = `attachment; filename=doh.mobileconfig`
|
||||||
@@ -182,7 +183,7 @@ func handleMobileConfig(w http.ResponseWriter, r *http.Request, dnsp string) {
|
|||||||
contDisp = dotContDisp
|
contDisp = dotContDisp
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Disposition", contDisp)
|
w.Header().Set(httphdr.ContentDisposition, contDisp)
|
||||||
|
|
||||||
_, _ = w.Write(mobileconfig)
|
_, _ = w.Write(mobileconfig)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,13 +249,15 @@ var cmdLineOpts = []cmdLineOpt{{
|
|||||||
updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil },
|
updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil },
|
||||||
effect: func(_ options, _ string) (f effect, err error) {
|
effect: func(_ options, _ string) (f effect, err error) {
|
||||||
log.Info(
|
log.Info(
|
||||||
"warning: --no-etc-hosts flag is deprecated and will be removed in the future versions",
|
"warning: --no-etc-hosts flag is deprecated " +
|
||||||
|
"and will be removed in the future versions; " +
|
||||||
|
"set clients.runtime_sources.hosts in the configuration file to false instead",
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
},
|
},
|
||||||
serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts },
|
serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts },
|
||||||
description: "Deprecated. Do not use the OS-provided hosts.",
|
description: "Deprecated: use clients.runtime_sources.hosts instead. Do not use the OS-provided hosts.",
|
||||||
longName: "no-etc-hosts",
|
longName: "no-etc-hosts",
|
||||||
shortName: "",
|
shortName: "",
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
39
internal/home/pprof.go
Normal file
39
internal/home/pprof.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package home
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/pprof"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// startPprof launches the debug and profiling server on addr.
|
||||||
|
func startPprof(addr string) {
|
||||||
|
runtime.SetBlockProfileRate(1)
|
||||||
|
runtime.SetMutexProfileFraction(1)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||||
|
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||||
|
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||||
|
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||||
|
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||||
|
|
||||||
|
// See profileSupportsDelta in src/net/http/pprof/pprof.go.
|
||||||
|
mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
|
||||||
|
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
|
||||||
|
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
|
||||||
|
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
|
||||||
|
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
|
||||||
|
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer log.OnPanic("pprof server")
|
||||||
|
|
||||||
|
log.Info("pprof: listening on %q", addr)
|
||||||
|
err := http.ListenAndServe(addr, mux)
|
||||||
|
log.Info("pprof server errors: %v", err)
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -108,7 +108,7 @@ func (m *tlsManager) start() {
|
|||||||
// The background context is used because the TLSConfigChanged wraps context
|
// The background context is used because the TLSConfigChanged wraps context
|
||||||
// with timeout on its own and shuts down the server, which handles current
|
// with timeout on its own and shuts down the server, which handles current
|
||||||
// request.
|
// request.
|
||||||
Context.web.TLSConfigChanged(context.Background(), tlsConf)
|
Context.web.tlsConfigChanged(context.Background(), tlsConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reload updates the configuration and restarts t.
|
// reload updates the configuration and restarts t.
|
||||||
@@ -156,7 +156,7 @@ func (m *tlsManager) reload() {
|
|||||||
// The background context is used because the TLSConfigChanged wraps context
|
// The background context is used because the TLSConfigChanged wraps context
|
||||||
// with timeout on its own and shuts down the server, which handles current
|
// with timeout on its own and shuts down the server, which handles current
|
||||||
// request.
|
// request.
|
||||||
Context.web.TLSConfigChanged(context.Background(), tlsConf)
|
Context.web.tlsConfigChanged(context.Background(), tlsConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadTLSConf loads and validates the TLS configuration. The returned error is
|
// loadTLSConf loads and validates the TLS configuration. The returned error is
|
||||||
@@ -454,7 +454,7 @@ func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request)
|
|||||||
// same reason.
|
// same reason.
|
||||||
if restartHTTPS {
|
if restartHTTPS {
|
||||||
go func() {
|
go func() {
|
||||||
Context.web.TLSConfigChanged(context.Background(), req.tlsConfigSettings)
|
Context.web.tlsConfigChanged(context.Background(), req.tlsConfigSettings)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,8 @@ const (
|
|||||||
type webConfig struct {
|
type webConfig struct {
|
||||||
clientFS fs.FS
|
clientFS fs.FS
|
||||||
|
|
||||||
BindHost netip.Addr
|
BindHost netip.Addr
|
||||||
BindPort int
|
BindPort int
|
||||||
PortHTTPS int
|
|
||||||
|
|
||||||
// ReadTimeout is an option to pass to http.Server for setting an
|
// ReadTimeout is an option to pass to http.Server for setting an
|
||||||
// appropriate field.
|
// appropriate field.
|
||||||
@@ -72,8 +71,8 @@ type httpsServer struct {
|
|||||||
enabled bool
|
enabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web is the web UI and API server.
|
// webAPI is the web UI and API server.
|
||||||
type Web struct {
|
type webAPI struct {
|
||||||
conf *webConfig
|
conf *webConfig
|
||||||
|
|
||||||
// TODO(a.garipov): Refactor all these servers.
|
// TODO(a.garipov): Refactor all these servers.
|
||||||
@@ -82,15 +81,13 @@ type Web struct {
|
|||||||
// httpsServer is the server that handles HTTPS traffic. If it is not nil,
|
// httpsServer is the server that handles HTTPS traffic. If it is not nil,
|
||||||
// [Web.http3Server] must also not be nil.
|
// [Web.http3Server] must also not be nil.
|
||||||
httpsServer httpsServer
|
httpsServer httpsServer
|
||||||
|
|
||||||
forceHTTPS bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newWeb creates a new instance of the web UI and API server.
|
// newWebAPI creates a new instance of the web UI and API server.
|
||||||
func newWeb(conf *webConfig) (w *Web) {
|
func newWebAPI(conf *webConfig) (w *webAPI) {
|
||||||
log.Info("web: initializing")
|
log.Info("web: initializing")
|
||||||
|
|
||||||
w = &Web{
|
w = &webAPI{
|
||||||
conf: conf,
|
conf: conf,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,12 +122,10 @@ func webCheckPortAvailable(port int) (ok bool) {
|
|||||||
return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
|
return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
|
// tlsConfigChanged updates the TLS configuration and restarts the HTTPS server
|
||||||
// if necessary.
|
// if necessary.
|
||||||
func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
|
func (web *webAPI) tlsConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
|
||||||
log.Debug("web: applying new tls configuration")
|
log.Debug("web: applying new tls configuration")
|
||||||
web.conf.PortHTTPS = tlsConf.PortHTTPS
|
|
||||||
web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0)
|
|
||||||
|
|
||||||
enabled := tlsConf.Enabled &&
|
enabled := tlsConf.Enabled &&
|
||||||
tlsConf.PortHTTPS != 0 &&
|
tlsConf.PortHTTPS != 0 &&
|
||||||
@@ -161,8 +156,8 @@ func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings)
|
|||||||
web.httpsServer.cond.L.Unlock()
|
web.httpsServer.cond.L.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start - start serving HTTP requests
|
// start - start serving HTTP requests
|
||||||
func (web *Web) Start() {
|
func (web *webAPI) start() {
|
||||||
log.Println("AdGuard Home is available at the following addresses:")
|
log.Println("AdGuard Home is available at the following addresses:")
|
||||||
|
|
||||||
// for https, we have a separate goroutine loop
|
// for https, we have a separate goroutine loop
|
||||||
@@ -203,8 +198,8 @@ func (web *Web) Start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close gracefully shuts down the HTTP servers.
|
// close gracefully shuts down the HTTP servers.
|
||||||
func (web *Web) Close(ctx context.Context) {
|
func (web *webAPI) close(ctx context.Context) {
|
||||||
log.Info("stopping http server...")
|
log.Info("stopping http server...")
|
||||||
|
|
||||||
web.httpsServer.cond.L.Lock()
|
web.httpsServer.cond.L.Lock()
|
||||||
@@ -222,7 +217,7 @@ func (web *Web) Close(ctx context.Context) {
|
|||||||
log.Info("stopped http server")
|
log.Info("stopped http server")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) tlsServerLoop() {
|
func (web *webAPI) tlsServerLoop() {
|
||||||
for {
|
for {
|
||||||
web.httpsServer.cond.L.Lock()
|
web.httpsServer.cond.L.Lock()
|
||||||
if web.httpsServer.inShutdown {
|
if web.httpsServer.inShutdown {
|
||||||
@@ -241,7 +236,15 @@ func (web *Web) tlsServerLoop() {
|
|||||||
|
|
||||||
web.httpsServer.cond.L.Unlock()
|
web.httpsServer.cond.L.Unlock()
|
||||||
|
|
||||||
addr := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS)
|
var portHTTPS int
|
||||||
|
func() {
|
||||||
|
config.RLock()
|
||||||
|
defer config.RUnlock()
|
||||||
|
|
||||||
|
portHTTPS = config.TLS.PortHTTPS
|
||||||
|
}()
|
||||||
|
|
||||||
|
addr := netutil.JoinHostPort(web.conf.BindHost.String(), portHTTPS)
|
||||||
web.httpsServer.server = &http.Server{
|
web.httpsServer.server = &http.Server{
|
||||||
ErrorLog: log.StdLog("web: https", log.DEBUG),
|
ErrorLog: log.StdLog("web: https", log.DEBUG),
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
@@ -272,7 +275,7 @@ func (web *Web) tlsServerLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) mustStartHTTP3(address string) {
|
func (web *webAPI) mustStartHTTP3(address string) {
|
||||||
defer log.OnPanic("web: http3")
|
defer log.OnPanic("web: http3")
|
||||||
|
|
||||||
web.httpsServer.server3 = &http3.Server{
|
web.httpsServer.server3 = &http3.Server{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,8 +100,8 @@ func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
|
|||||||
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
|
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
|
||||||
// TODO(a.garipov): Put some of these to a middleware.
|
// TODO(a.garipov): Put some of these to a middleware.
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
h.Set(aghhttp.HdrNameContentType, aghhttp.HdrValApplicationJSON)
|
h.Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)
|
||||||
h.Set(aghhttp.HdrNameServer, aghhttp.UserAgent())
|
h.Set(httphdr.Server, aghhttp.UserAgent())
|
||||||
|
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
package websvc
|
package websvc
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
|
)
|
||||||
|
|
||||||
// Middlewares
|
// Middlewares
|
||||||
|
|
||||||
// jsonMw sets the content type of the response to application/json.
|
// jsonMw sets the content type of the response to application/json.
|
||||||
func jsonMw(h http.Handler) (wrapped http.HandlerFunc) {
|
func jsonMw(h http.Handler) (wrapped http.HandlerFunc) {
|
||||||
f := func(w http.ResponseWriter, r *http.Request) {
|
f := func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set(httphdr.ContentType, aghhttp.HdrValApplicationJSON)
|
||||||
|
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type Client struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
DisallowedRule string `json:"disallowed_rule"`
|
DisallowedRule string `json:"disallowed_rule"`
|
||||||
Disallowed bool `json:"disallowed"`
|
Disallowed bool `json:"disallowed"`
|
||||||
|
IgnoreQueryLog bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientWHOIS is the filtered WHOIS data for the client.
|
// ClientWHOIS is the filtered WHOIS data for the client.
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ func (e *logEntry) addResponse(resp *dns.Msg, isOrig bool) {
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
if isOrig {
|
if isOrig {
|
||||||
e.Answer, err = resp.Pack()
|
|
||||||
err = errors.Annotate(err, "packing answer: %w")
|
|
||||||
} else {
|
|
||||||
e.OrigAnswer, err = resp.Pack()
|
e.OrigAnswer, err = resp.Pack()
|
||||||
err = errors.Annotate(err, "packing orig answer: %w")
|
err = errors.Annotate(err, "packing orig answer: %w")
|
||||||
|
} else {
|
||||||
|
e.Answer, err = resp.Pack()
|
||||||
|
err = errors.Annotate(err, "packing answer: %w")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("querylog: %s", err)
|
log.Error("querylog: %s", err)
|
||||||
|
|||||||
@@ -247,10 +247,19 @@ func (l *queryLog) Add(params *AddParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ShouldLog returns true if request for the host should be logged.
|
// ShouldLog returns true if request for the host should be logged.
|
||||||
func (l *queryLog) ShouldLog(host string, _, _ uint16) bool {
|
func (l *queryLog) ShouldLog(host string, _, _ uint16, ids []string) bool {
|
||||||
l.confMu.RLock()
|
l.confMu.RLock()
|
||||||
defer l.confMu.RUnlock()
|
defer l.confMu.RUnlock()
|
||||||
|
|
||||||
|
c, err := l.findClient(ids)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("querylog: finding client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c != nil && c.IgnoreQueryLog {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return !l.isIgnored(host)
|
return !l.isIgnored(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,36 +258,52 @@ func TestQueryLogShouldLog(t *testing.T) {
|
|||||||
)
|
)
|
||||||
set := stringutil.NewSet(ignored1, ignored2)
|
set := stringutil.NewSet(ignored1, ignored2)
|
||||||
|
|
||||||
|
findClient := func(ids []string) (c *Client, err error) {
|
||||||
|
log := ids[0] == "no_log"
|
||||||
|
|
||||||
|
return &Client{IgnoreQueryLog: log}, nil
|
||||||
|
}
|
||||||
|
|
||||||
l, err := newQueryLog(Config{
|
l, err := newQueryLog(Config{
|
||||||
Ignored: set,
|
Ignored: set,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RotationIvl: timeutil.Day,
|
RotationIvl: timeutil.Day,
|
||||||
MemSize: 100,
|
MemSize: 100,
|
||||||
BaseDir: t.TempDir(),
|
BaseDir: t.TempDir(),
|
||||||
|
FindClient: findClient,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
host string
|
host string
|
||||||
|
ids []string
|
||||||
wantLog bool
|
wantLog bool
|
||||||
}{{
|
}{{
|
||||||
name: "log",
|
name: "log",
|
||||||
host: "example.com",
|
host: "example.com",
|
||||||
|
ids: []string{"whatever"},
|
||||||
wantLog: true,
|
wantLog: true,
|
||||||
}, {
|
}, {
|
||||||
name: "no_log_ignored_1",
|
name: "no_log_ignored_1",
|
||||||
host: ignored1,
|
host: ignored1,
|
||||||
|
ids: []string{"whatever"},
|
||||||
wantLog: false,
|
wantLog: false,
|
||||||
}, {
|
}, {
|
||||||
name: "no_log_ignored_2",
|
name: "no_log_ignored_2",
|
||||||
host: ignored2,
|
host: ignored2,
|
||||||
|
ids: []string{"whatever"},
|
||||||
|
wantLog: false,
|
||||||
|
}, {
|
||||||
|
name: "no_log_client_ignore",
|
||||||
|
host: "example.com",
|
||||||
|
ids: []string{"no_log"},
|
||||||
wantLog: false,
|
wantLog: false,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
res := l.ShouldLog(tc.host, dns.TypeA, dns.ClassINET)
|
res := l.ShouldLog(tc.host, dns.TypeA, dns.ClassINET, tc.ids)
|
||||||
|
|
||||||
assert.Equal(t, tc.wantLog, res)
|
assert.Equal(t, tc.wantLog, res)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ type QueryLog interface {
|
|||||||
WriteDiskConfig(c *Config)
|
WriteDiskConfig(c *Config)
|
||||||
|
|
||||||
// ShouldLog returns true if request for the host should be logged.
|
// ShouldLog returns true if request for the host should be logged.
|
||||||
ShouldLog(host string, qType, qClass uint16) bool
|
ShouldLog(host string, qType, qClass uint16, ids []string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config is the query log configuration structure.
|
// Config is the query log configuration structure.
|
||||||
|
|||||||
@@ -288,6 +288,10 @@ func (l *queryLog) readNextEntry(
|
|||||||
// Go on and try to match anyway.
|
// Go on and try to match anyway.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.client != nil && e.client.IgnoreQueryLog {
|
||||||
|
return nil, ts, nil
|
||||||
|
}
|
||||||
|
|
||||||
ts = e.Time.UnixNano()
|
ts = e.Time.UnixNano()
|
||||||
if !params.match(e) {
|
if !params.match(e) {
|
||||||
return nil, ts, nil
|
return nil, ts, nil
|
||||||
|
|||||||
@@ -24,18 +24,19 @@ func TestHandleStatsConfig(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
conf := Config{
|
conf := Config{
|
||||||
UnitID: func() (id uint32) { return 0 },
|
UnitID: func() (id uint32) { return 0 },
|
||||||
ConfigModified: func() {},
|
ConfigModified: func() {},
|
||||||
Filename: filepath.Join(t.TempDir(), "stats.db"),
|
ShouldCountClient: func([]string) bool { return true },
|
||||||
Limit: time.Hour * 24,
|
Filename: filepath.Join(t.TempDir(), "stats.db"),
|
||||||
Enabled: true,
|
Limit: time.Hour * 24,
|
||||||
|
Enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
wantErr string
|
||||||
body getConfigResp
|
body getConfigResp
|
||||||
wantCode int
|
wantCode int
|
||||||
wantErr string
|
|
||||||
}{{
|
}{{
|
||||||
name: "set_ivl_1_minIvl",
|
name: "set_ivl_1_minIvl",
|
||||||
body: getConfigResp{
|
body: getConfigResp{
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ type Config struct {
|
|||||||
// interface.
|
// interface.
|
||||||
ConfigModified func()
|
ConfigModified func()
|
||||||
|
|
||||||
|
// ShouldCountClient returns client's ignore setting.
|
||||||
|
ShouldCountClient func([]string) bool
|
||||||
|
|
||||||
// HTTPRegister is the function that registers handlers for the stats
|
// HTTPRegister is the function that registers handlers for the stats
|
||||||
// endpoints.
|
// endpoints.
|
||||||
HTTPRegister aghhttp.RegisterFunc
|
HTTPRegister aghhttp.RegisterFunc
|
||||||
@@ -87,7 +90,7 @@ type Interface interface {
|
|||||||
WriteDiskConfig(dc *Config)
|
WriteDiskConfig(dc *Config)
|
||||||
|
|
||||||
// ShouldCount returns true if request for the host should be counted.
|
// ShouldCount returns true if request for the host should be counted.
|
||||||
ShouldCount(host string, qType, qClass uint16) bool
|
ShouldCount(host string, qType, qClass uint16, ids []string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatsCtx collects the statistics and flushes it to the database. Its default
|
// StatsCtx collects the statistics and flushes it to the database. Its default
|
||||||
@@ -118,6 +121,9 @@ type StatsCtx struct {
|
|||||||
// ignored is the list of host names, which should not be counted.
|
// ignored is the list of host names, which should not be counted.
|
||||||
ignored *stringutil.Set
|
ignored *stringutil.Set
|
||||||
|
|
||||||
|
// shouldCountClient returns client's ignore setting.
|
||||||
|
shouldCountClient func([]string) bool
|
||||||
|
|
||||||
// filename is the name of database file.
|
// filename is the name of database file.
|
||||||
filename string
|
filename string
|
||||||
|
|
||||||
@@ -138,16 +144,21 @@ func New(conf Config) (s *StatsCtx, err error) {
|
|||||||
return nil, fmt.Errorf("unsupported interval: %w", err)
|
return nil, fmt.Errorf("unsupported interval: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if conf.ShouldCountClient == nil {
|
||||||
|
return nil, errors.Error("should count client is unspecified")
|
||||||
|
}
|
||||||
|
|
||||||
s = &StatsCtx{
|
s = &StatsCtx{
|
||||||
currMu: &sync.RWMutex{},
|
currMu: &sync.RWMutex{},
|
||||||
httpRegister: conf.HTTPRegister,
|
httpRegister: conf.HTTPRegister,
|
||||||
configModified: conf.ConfigModified,
|
configModified: conf.ConfigModified,
|
||||||
filename: conf.Filename,
|
filename: conf.Filename,
|
||||||
|
|
||||||
confMu: &sync.RWMutex{},
|
confMu: &sync.RWMutex{},
|
||||||
ignored: conf.Ignored,
|
ignored: conf.Ignored,
|
||||||
limit: conf.Limit,
|
shouldCountClient: conf.ShouldCountClient,
|
||||||
enabled: conf.Enabled,
|
limit: conf.Limit,
|
||||||
|
enabled: conf.Enabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.unitIDGen = newUnitID; conf.UnitID != nil {
|
if s.unitIDGen = newUnitID; conf.UnitID != nil {
|
||||||
@@ -577,10 +588,14 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, firstID uint32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ShouldCount returns true if request for the host should be counted.
|
// ShouldCount returns true if request for the host should be counted.
|
||||||
func (s *StatsCtx) ShouldCount(host string, _, _ uint16) bool {
|
func (s *StatsCtx) ShouldCount(host string, _, _ uint16, ids []string) bool {
|
||||||
s.confMu.RLock()
|
s.confMu.RLock()
|
||||||
defer s.confMu.RUnlock()
|
defer s.confMu.RUnlock()
|
||||||
|
|
||||||
|
if !s.shouldCountClient(ids) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return !s.isIgnored(host)
|
return !s.isIgnored(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user