Compare commits

...

28 Commits

Author SHA1 Message Date
Eugene Burkov
24a62d0638 all: imp script, code 2023-11-09 13:02:05 +03:00
Eugene Burkov
f81a94eb94 all: bump go in ci 2023-11-09 12:38:10 +03:00
Eugene Burkov
ca898fe74e dnsforward: imp code, rm wg 2023-11-09 12:26:44 +03:00
Eugene Burkov
366ec81621 dnsforward: fix options 2023-11-08 18:46:02 +03:00
Eugene Burkov
f9ee511094 dnsforward: add tests, todo 2023-11-08 18:33:40 +03:00
Eugene Burkov
deedc490e1 dnsforward: fix upstream check endpoint 2023-11-08 17:51:34 +03:00
Ainar Garipov
f8fe9bfc8b Pull request 2061: 6398-readme-patch
Updates #6398.

* commit '6cff5865d27d7ac9e45db014fe09b9bb2bd7a997':
  Update README.md
2023-11-08 14:39:42 +03:00
TimTheBig
6cff5865d2 Update README.md 2023-11-07 20:23:18 -05:00
Ildar Kamalov
cbcc17a58b Pull request 2055: ADG-7651 fix dashboard client cell ellipsis
Merge in DNS/adguard-home from ADG-7651 to master

Updates #6338.

Squashed commit of the following:

commit 21acb7a02ef39de478d894dfedaba5f2e32eff93
Merge: 1c0ba2c78 6a3906aa9
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Nov 3 17:12:55 2023 +0300

    Merge branch 'master' into ADG-7651

commit 1c0ba2c789289035aab236c1df5560185e80ef46
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Nov 2 10:06:48 2023 +0300

    changelog

commit 14d8a2c261d11eb65a540f6e45ed52cedcf43fd8
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Oct 31 18:04:18 2023 +0300

    fix changelog

commit bfc3f455380583592bfbc676a150c55276c201eb
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Oct 31 18:03:24 2023 +0300

    changelog

commit 64cc673449ac20d015cda4c93129de60c1866aab
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Oct 31 18:00:50 2023 +0300

    ADG-7651 fix dashboard client cell ellipsis
2023-11-03 17:24:30 +03:00
Ildar Kamalov
6a3906aa95 Pull request 2056: ADG-7673 fix filters list height
Merge in DNS/adguard-home from ADG-7673 to master

Updates #6358.

Squashed commit of the following:

commit c7fc33de1e8f32c526e3820e022556227a3e2d3c
Merge: 82368de34 ffdebc7b2
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Nov 3 16:52:26 2023 +0300

    Merge branch 'master' into ADG-7673

commit 82368de34afbba5ca875f7402f53e84686bf8d5d
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Nov 2 10:07:55 2023 +0300

    changelog

commit e8514638fb87b56a5598faec049afe105dcbed25
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Oct 31 18:31:35 2023 +0300

    ADG-7673 fix filters list height
2023-11-03 17:11:22 +03:00
Ildar Kamalov
ffdebc7b2d Pull request 2058: ADG-7652 fix table titles scroll issue
Merge in DNS/adguard-home from ADG-7652 to master

Updates #6337.

Squashed commit of the following:

commit 3b10beacb3d2f44152c73412ec6e2ae1e77dafc3
Merge: 241c5ea18 f3817e441
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Nov 3 16:29:48 2023 +0300

    Merge branch 'master' into ADG-7652

commit 241c5ea18ab7ae0b4d945768a9dc5fc60309d0d6
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Nov 2 10:12:23 2023 +0300

    changelog

commit 18afa19abf3a4fa08549b802e97e554199143ee7
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Nov 1 11:31:11 2023 +0300

    ADG-7652 fix table titles scroll issue
2023-11-03 16:47:30 +03:00
Stanislav Chzhen
f3817e4411 Pull request 2053: 6357-auth-log-remote-ip
Updates #6357.

Squashed commit of the following:

commit 0d375446204d126d3fc20db0a0718e849112450b
Merge: 61858bdec 52713a260
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Nov 3 14:47:10 2023 +0300

    Merge branch 'master' into 6357-auth-log-remote-ip

commit 61858bdec27f9efb35c6fa5306ace1c0053300ca
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Nov 3 14:44:58 2023 +0300

    all: upd chlog

commit 1eef67261ff1e4eb667e11a58a5fe1f9b1dbdd7c
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Nov 2 19:20:41 2023 +0300

    home: imp code

commit 2956aed9054309ab15dc9e61bcae59b76ccd5930
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Nov 2 16:10:07 2023 +0300

    home: imp docs

commit ca0f53d7c28d17287d80c0c5d1d76b21506acb64
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Oct 31 15:08:37 2023 +0300

    home: imp code

commit 6b11b461180f1ee7528ffbaf37d5e76a1a7f208a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Oct 27 19:45:55 2023 +0300

    home: auth log remote ip
2023-11-03 16:07:15 +03:00
Ainar Garipov
52713a2600 Pull request 2057: dnsforward: imp clientid log
Updates #6371.

Squashed commit of the following:

commit 0461266a5420c985af6bb0b36a42af54bc62e075
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 31 21:38:20 2023 +0300

    dnsforward: imp clientid log
2023-11-01 13:57:59 +03:00
Stanislav Chzhen
62ec0d5adc Pull request 2052: 4977-multiple-domain-specific-upstreams
Updates #4977.

Squashed commit of the following:

commit da28c1b508b1aa4838d753fbb5fcac64a5fcebb9
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Oct 27 17:24:38 2023 +0300

    all: fix typo

commit d6bca6b252c9bd264737c93072869499afa24864
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Oct 27 14:44:20 2023 +0300

    all: add todo

commit 30875515942c58881305aa963220d57d31e0e67d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Oct 25 20:00:17 2023 +0300

    all: imp docs

commit 04003c342fcf82aeb671938fb89592fd6baff16d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Oct 25 16:59:14 2023 +0300

    all: multiple domain specific upstreams
2023-10-27 20:18:29 +03:00
Dimitry Kolyshev
2a56c78f26 Pull request: all: upd dep
Merge in DNS/adguard-home from 6204-dnsrewrite-important to master

Squashed commit of the following:

commit 379a1c4f3ec3784bc88a3250623e9f4852ef4cb2
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Oct 25 14:09:00 2023 +0300

    all: upd dep

commit 40485f9b791a22218bae76b0aa6cb8d87173ccb2
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Oct 25 14:07:58 2023 +0300

    all: upd dep

commit 8c09fe9d937be2c603c9632907edbcde60dd6680
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Oct 25 14:04:25 2023 +0300

    docs: changelog

commit 33451738f5dd4b16bbba73bfe1cb495cb976abf9
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Oct 25 14:01:27 2023 +0300

    docs: changelog

commit cc94b4232c2990234d0bdfc510af58aef10d61a5
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Oct 25 14:00:58 2023 +0300

    docs: changelog

commit adea5117cb0ca820556a8f00aafb8fd56f471e24
Merge: b90197e87 c0588146e
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Oct 25 13:58:13 2023 +0300

    Merge remote-tracking branch 'origin/master' into 6204-dnsrewrite-important

commit b90197e871e238404a27f43f51b12248a8cff79e
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Oct 25 11:21:05 2023 +0300

    all: upd dep
2023-10-25 14:36:54 +03:00
Ildar Kamalov
c0588146e7 Pull request: 6329 fix dark mode rewrite background
Updates #6329

Squashed commit of the following:

commit 4634d78971302cb757c20e007b9110e6f770017f
Merge: 681b7f6a2 f6e34adee
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Oct 25 10:41:02 2023 +0300

    Merge branch 'master' into ADG-7627

commit 681b7f6a272686c93e6c3b54a430e2394d27488d
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 20 10:40:03 2023 +0300

    changelog

commit 1b00a8f770f42140de0be0eaf8e639cfdb253ad5
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 20 10:31:53 2023 +0300

    fix

commit ec6f9a55a2e8add51fcf9d47508c670088492db9
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 20 10:30:08 2023 +0300

    ADG-7627 fix rewrite background
2023-10-25 13:41:17 +03:00
Ainar Garipov
f6e34adee7 Pull request 2050: all: upd quic-go
Updates #6335.

Squashed commit of the following:

commit b76b55862c9d8bec4dcc648f813c9f78c6475025
Merge: dda2c9f7f e3cc3b064
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 24 19:43:03 2023 +0300

    Merge branch 'master' into 6335-try-ecn-fix

commit dda2c9f7f5dbde7a5edb925654c65be029c61199
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 24 19:32:32 2023 +0300

    all: upd quic-go
2023-10-24 19:53:59 +03:00
Stanislav Chzhen
e3cc3b0642 Pull request 2043: AG-26544-ipset-persistent-entries
Squashed commit of the following:

commit e5daef40330daf97cfd259006586fcc0196fc8e1
Merge: 7c6e63a39 cd09ba63b
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Oct 24 14:06:13 2023 +0300

    Merge branch 'master' into AG-26544-ipset-persistent-entries

commit 7c6e63a393a05ae9e6007af1ae539b3c70b49fda
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Oct 23 16:28:34 2023 +0300

    ipset: imp docs

commit cfb5d8a6573e33ed466a3767290da84e6db96167
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Oct 20 18:09:01 2023 +0300

    ipset: imp code

commit 4ef03c9e0066ddb10f11c653338699f8001ae0de
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Oct 18 20:17:16 2023 +0300

    ipset: imp docs

commit 544982b5d7d333d2575da655ebcf15b941fd74d0
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Oct 16 19:05:43 2023 +0300

    ipset: add persistent entries
2023-10-24 14:17:14 +03:00
Stanislav Chzhen
cd09ba63b6 Pull request 2048: AG-26594-fix-filtering-race
Squashed commit of the following:

commit 9b5b035aa3edfe20cbc26772b8a5c76d81288116
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Oct 20 13:00:29 2023 +0300

    filtering: imp code

commit 406f4015d80d8b11fbd0aeacfabe686931bbe3fb
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Oct 19 15:04:13 2023 +0300

    filtering: fix race
2023-10-20 15:45:57 +03:00
Ainar Garipov
1d1de1bfb5 Pull request 2047: Upd chlog deps
Squashed commit of the following:

commit 3485c5bd6e0e93049723bd38a8e35cf99a61f653
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Wed Oct 18 19:45:19 2023 +0300

    all: upd chlog, deps
2023-10-18 19:54:42 +03:00
Ainar Garipov
763bbb5e6b Pull request 2046: fix-chlog
Squashed commit of the following:

commit 248ecfb16d5aaaa7b18b5210181ef433035b3c39
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 17 18:35:14 2023 +0300

    all: fix chlog
2023-10-17 18:42:25 +03:00
Ainar Garipov
9e9d0206b8 Pull request 2045: upd-all
Squashed commit of the following:

commit 7305330b1cde1bb0eed5eb97fdb83bbb2b3a475c
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 17 17:26:44 2023 +0300

    all: upd i18n, svcs, tools, trackers
2023-10-17 17:44:34 +03:00
Ainar Garipov
76899cc31e Pull request 2044: 6301-upd-dnsproxy-quic-go
Updates #6301.

Squashed commit of the following:

commit 1199ea26d5c1d1066070268a97a3c89ba0634515
Merge: 7218fdc9e f514f365a
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 17 16:54:36 2023 +0300

    Merge branch 'master' into 6301-upd-dnsproxy-quic-go

commit 7218fdc9e17749746c6b981f56f24fd2169cef73
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 17 16:23:24 2023 +0300

    all: upd dnsproxy, quic-go
2023-10-17 17:08:27 +03:00
Ildar Kamalov
f514f365ab Pull request: 6180 fix dashboard tables scroll
Updates #6180

Squashed commit of the following:

commit eee7fa39d4e1d366086a541216a1b00051c3af8a
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Oct 17 11:47:41 2023 +0300

    fix scroll

commit 0a86a1a5c3d56902ad8de92a34b28d3e462a00c3
Merge: 07f4550ed 413f48481
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Oct 17 10:27:29 2023 +0300

    Merge branch 'master' into ADG-7473-1

commit 07f4550ed0fbb400f4d99a3e9fd60acf8b9f2e2e
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 17:41:47 2023 +0300

    changelog

commit 28a1a7360344475b1d1d46b7d9ef14b292d82746
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 16:13:59 2023 +0300

    overflow

commit aa38288326bcfa374245416d270bd57bc217cabd
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 16:09:05 2023 +0300

    firefox scroll

commit ac98653f2daf57d23f877a3a3f0c8cb9d3b0f536
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 16:06:49 2023 +0300

    constants

commit a3cdd0acd454934af8c1999d87ecb736dbdca0b9
Merge: 0cd848115 506d71310
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 16:03:16 2023 +0300

    Merge branch 'master' into ADG-7473-1

commit 0cd8481150d6b3000bc8796bdff963c7a07a2e7d
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 12 11:13:14 2023 +0300

    changelog

commit ed678070e7b1e6a144ebd7fefe99137a1529c025
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 12 11:00:23 2023 +0300

    fix rows

commit 5e4f35c874de15fc51a312e0ab2ba19018537194
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 12 10:58:58 2023 +0300

    ADG-7473 fix dashboard tables scroll
2023-10-17 16:52:28 +03:00
Ildar Kamalov
413f484810 Pull request: 684 move block/unblock button to the tooltip menu
Updates #684

Squashed commit of the following:

commit 2c5ac44d3edb55d0d3be169fb0bbfb336920964d
Merge: 0fce61dfa 733d6c1fc
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Oct 16 10:08:58 2023 +0300

    Merge branch 'master' into ADG-7200

commit 0fce61dfad31951c3d5a484a14383c58964b47f0
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 16:38:13 2023 +0300

    fix dashboard clients table block/unblock button

commit ef5d72fd7cac81b70503c8813cf6e96952ec3b65
Merge: f49281b88 506d71310
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 16:16:21 2023 +0300

    Merge branch 'master' into ADG-7200

commit f49281b8818728ab298d769b8164919ea415d6c9
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 12 11:25:05 2023 +0300

    fix disabled

commit 0031861174108214e9741852f6e00c5873c3b20f
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 12 11:20:50 2023 +0300

    fix icon color

commit 2916937b0d6a0d126bd48e59f41a6fcbc4622ea1
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 12 11:15:55 2023 +0300

    changelog

commit 34493c974fb9304267e9149360c802d9364dd7de
Merge: d3267bbe9 d3fabdda4
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 12 11:13:43 2023 +0300

    Merge branch 'master' into ADG-7200

commit d3267bbe9477d6db0783534cb4ab4f7afc4d63b0
Merge: 58a6c766f 6a3661562
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Oct 11 16:46:39 2023 +0300

    Merge branch 'master' into ADG-7200

commit 58a6c766f3b783f0e2fdc36d40889042f3c74f2f
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Oct 11 16:46:13 2023 +0300

    ADG-7200 move block/unblock button to the tooltip menu
2023-10-16 18:00:28 +03:00
Ildar Kamalov
733d6c1fca 6296 fix charts and custom retention labels
Updates #6296

Squashed commit of the following:

commit e6a36f107b7bdd17fe4141b035fe5c1f6edd5c6c
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 17:42:41 2023 +0300

    changelog

commit 96f11f2090d2aff600f6dcd6ca601e25cd857530
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 16:01:27 2023 +0300

    changelog

commit 136fb14f61dcb2bdec3bc9b8943a05544752a606
Author: Ildar Kamalov <ik@adguard.com>
Date:   Fri Oct 13 15:20:14 2023 +0300

    ADG-7598 fix charts and custom retention labels
2023-10-16 10:06:37 +03:00
Ainar Garipov
506d71310c Pull request 2040: 6301-rollback-quic-go
Updates #6301.

Squashed commit of the following:

commit e1a1b039323fa932d26039a157b0f959bafa6f2b
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Oct 12 21:41:14 2023 +0300

    all: rollback quic-go
2023-10-12 21:50:01 +03:00
Stanislav Chzhen
bb652cd9a1 Pull request 2039: 6304-fix-ring-buffer
Updates #6304.

Squashed commit of the following:

commit 7382630404cf18ff45e54234134c192161f5d94a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Oct 12 19:37:01 2023 +0300

    all: upd chlog

commit ff9a82a6872f3bd93fe89956c5441429c189bce7
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Oct 12 19:27:35 2023 +0300

    querylog: fix ring buffer
2023-10-12 19:48:12 +03:00
86 changed files with 1704 additions and 1039 deletions

View File

@@ -1,7 +1,7 @@
'name': 'build'
'env':
'GO_VERSION': '1.20.10'
'GO_VERSION': '1.20.11'
'NODE_VERSION': '16'
'on':

View File

@@ -1,7 +1,7 @@
'name': 'lint'
'env':
'GO_VERSION': '1.20.10'
'GO_VERSION': '1.20.11'
'on':
'push':

View File

@@ -14,21 +14,76 @@ and this project adheres to
<!--
## [v0.108.0] - TBA
## [v0.107.40] - 2023-10-25 (APPROX.)
## [v0.107.41] - 2023-11-01 (APPROX.)
See also the [v0.107.40 GitHub milestone][ms-v0.107.40].
See also the [v0.107.41 GitHub milestone][ms-v0.107.41].
[ms-v0.107.40]: https://github.com/AdguardTeam/AdGuardHome/milestone/75?closed=1
[ms-v0.107.41]: https://github.com/AdguardTeam/AdGuardHome/milestone/76?closed=1
NOTE: Add new changes BELOW THIS COMMENT.
-->
### Added
- Ability to specify multiple domain specific upstreams per line, e.g.
`[/domain1/../domain2/]upstream1 upstream2 .. upstreamN` ([#4977]).
### Changed
- The height of ready-to-use filter lists has been increased ([#6358]).
- Improved authentication failure logging ([#6357]).
### Fixed
- Redundant shortening long client names in the Top Clients table ([#6338]).
- Scrolling column headers in the tables ([#6337]).
- `$important,dnsrewrite` rules do not take precedence over allowlist rules
([#6204]).
- Dark mode DNS rewrite background ([#6329]).
- Issues with QUIC and HTTP/3 upstreams on Linux ([#6335]).
[#4977]: https://github.com/AdguardTeam/AdGuardHome/issues/4977
[#6204]: https://github.com/AdguardTeam/AdGuardHome/issues/6204
[#6329]: https://github.com/AdguardTeam/AdGuardHome/issues/6329
[#6335]: https://github.com/AdguardTeam/AdGuardHome/issues/6335
[#6337]: https://github.com/AdguardTeam/AdGuardHome/issues/6337
[#6338]: https://github.com/AdguardTeam/AdGuardHome/issues/6338
[#6357]: https://github.com/AdguardTeam/AdGuardHome/issues/6357
[#6358]: https://github.com/AdguardTeam/AdGuardHome/issues/6358
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
## [v0.107.40] - 2023-10-18
See also the [v0.107.40 GitHub milestone][ms-v0.107.40].
### Changed
- *Block* and *Unblock* buttons of the query log moved to the tooltip menu
([#684]).
### Fixed
- Dashboard tables scroll issue ([#6180]).
- The time shown in the statistics is one hour less than the current time
([#6296]).
- Issues with QUIC and HTTP/3 upstreams on FreeBSD ([#6301]).
- Panic on clearing the query log ([#6304]).
[#684]: https://github.com/AdguardTeam/AdGuardHome/issues/684
[#6180]: https://github.com/AdguardTeam/AdGuardHome/issues/6180
[#6296]: https://github.com/AdguardTeam/AdGuardHome/issues/6296
[#6301]: https://github.com/AdguardTeam/AdGuardHome/issues/6301
[#6304]: https://github.com/AdguardTeam/AdGuardHome/issues/6304
[ms-v0.107.40]: https://github.com/AdguardTeam/AdGuardHome/milestone/75?closed=1
## [v0.107.39] - 2023-10-11
See also the [v0.107.39 GitHub milestone][ms-v0.107.39].
@@ -66,9 +121,8 @@ See also the [v0.107.39 GitHub milestone][ms-v0.107.39].
[#6233]: https://github.com/AdguardTeam/AdGuardHome/issues/6233
[#6280]: https://github.com/AdguardTeam/AdGuardHome/issues/6280
[go-1.20.9]: https://groups.google.com/g/golang-announce/c/XBa1oHDevAo/m/desYyx3qAgAJ
[go-1.20.10]: https://groups.google.com/g/golang-announce/c/iNNxDTCjZvo/m/UDd7VKQuAAAJ
[go-1.20.10]: https://groups.google.com/g/golang-announce/c/iNNxDTCjZvo/m/UDd7VKQuAAAJ
[go-1.20.9]: https://groups.google.com/g/golang-announce/c/XBa1oHDevAo/m/desYyx3qAgAJ
[ms-v0.107.39]: https://github.com/AdguardTeam/AdGuardHome/milestone/74?closed=1
@@ -2531,11 +2585,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
<!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.40...HEAD
[v0.107.40]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.39...v0.107.40
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.41...HEAD
[v0.107.41]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.40...v0.107.41
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.39...HEAD
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.40...HEAD
[v0.107.40]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.39...v0.107.40
[v0.107.39]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.38...v0.107.39
[v0.107.38]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.37...v0.107.38
[v0.107.37]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.36...v0.107.37

View File

@@ -201,7 +201,7 @@ opinion, this cannot be legitimately counted as a Pi-Hole's feature.
| Cross-platform | ✅ | ❌ (not natively, only via Docker) |
| Running as a DNS-over-HTTPS or DNS-over-TLS server | ✅ | ❌ (requires additional software) |
| Blocking phishing and malware domains | ✅ | ❌ (requires non-default blocklists) |
| Parental control (blocking adult domains) | ✅ | ❌ |
| Parental control (blocking adult domains) | ✅ | ❌ (requires non-default blocklists) |
| Force Safe search on search engines | ✅ | ❌ |
| Per-client (device) configuration | ✅ | ✅ |
| Access settings (choose who can use AGH DNS) | ✅ | ❌ |

View File

@@ -7,7 +7,7 @@
# Make sure to sync any changes with the branch overrides below.
'variables':
'channel': 'edge'
'dockerGo': 'adguard/golang-ubuntu:7.4'
'dockerGo': 'adguard/golang-ubuntu:7.5'
'stages':
- 'Build frontend':

View File

@@ -10,7 +10,7 @@
# Make sure to sync any changes with the branch overrides below.
'variables':
'channel': 'edge'
'dockerGo': 'adguard/golang-ubuntu:7.4'
'dockerGo': 'adguard/golang-ubuntu:7.5'
'snapcraftChannel': 'edge'
'stages':

View File

@@ -5,7 +5,7 @@
'key': 'AHBRTSPECS'
'name': 'AdGuard Home - Build and run tests'
'variables':
'dockerGo': 'adguard/golang-ubuntu:7.4'
'dockerGo': 'adguard/golang-ubuntu:7.5'
'stages':
- 'Tests':

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Заблакаваныя шкодныя і фішынгавыя сайты",
"stats_adult": "Заблакаваныя «дарослыя» сайты",
"stats_query_domain": "Часта запытаныя дамены",
"for_last_24_hours": "за 24 гадзіны",
"for_last_hours": "за апошнюю {{count}} гадзіну",
"for_last_hours_plural": "за апошнія {{count}} гадзін",
"for_last_days": "за апошні {{count}} дзень",
"for_last_days_plural": "за апошнія {{count}} дзён",
"stats_disabled": "Статыстыка была адключаная. Вы можаце ўключыць яго <0>на старонцы налад </0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Няма дадзеных аб upstream серверах",
"number_of_dns_query_days": "Колькасць DNS-запытаў за апошні {{count}} дзень",
"number_of_dns_query_days_plural": "Колькасць DNS запытаў, апрацаваных за апошнія {{count}} дзён",
"number_of_dns_query_24_hours": "Колькасць DNS-запытаў за 24 гадзіны",
"number_of_dns_query_hours": "Колькасць DNS-запытаў, апрацаваных за апошнюю {{count}} гадзіну",
"number_of_dns_query_hours_plural": "Колькасць DNS-запытаў, апрацаваных за апошнія {{count}} гадзін",
"number_of_dns_query_blocked_24_hours": "Колькасць DNS-запытаў, заблакаваных фільтрамі і блок-спісамі",
"number_of_dns_query_blocked_24_hours_by_sec": "Колькасць DNS-запытаў, заблакаваных модулем Антыфішынгу AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Колькасць заблакаваных «сайтаў для дарослых»",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Blokovaný malware/podvody",
"stats_adult": "Blokované stránky pro dospělé",
"stats_query_domain": "Nejčastěji dotazované domény",
"for_last_24_hours": "za posledních 24 hodin",
"for_last_hours": "za poslední {{count}} hodinu",
"for_last_hours_plural": "za posledních {{count}} hodin",
"for_last_days": "za posledních {{count}} dní",
"for_last_days_plural": "za posledních {{count}} dní",
"stats_disabled": "Statistiky byly vypnuty. Můžete je zapnout ze <0>stránky nastavení</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Nebyla nalezena žádná data odchozích připojení",
"number_of_dns_query_days": "Počet DNS dotazů zpracovaných za posledních {{count}} den",
"number_of_dns_query_days_plural": "Počet DNS dotazů zpracovaných za posledních {{count}} dní",
"number_of_dns_query_24_hours": "Počet DNS dotazů zpracovaných za posledních 24 hodin",
"number_of_dns_query_hours": "Počet DNS dotazů zpracovaných za poslední {{count}} hodinu",
"number_of_dns_query_hours_plural": "Počet DNS dotazů zpracovaných za posledních {{count}} hodin",
"number_of_dns_query_blocked_24_hours": "Počet požadavků DNS zablokovaných filtrem reklam a seznamy blokování hostitelů",
"number_of_dns_query_blocked_24_hours_by_sec": "Počet požadavků DNS zablokovaných AdGuard modulem Bezpečné prohlížení",
"number_of_dns_query_blocked_24_hours_adult": "Počet zablokovaných stránek pro dospělé",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Blokeret malware/phishing",
"stats_adult": "Blokerede voksne websteder",
"stats_query_domain": "Mest forespurgte domæner",
"for_last_24_hours": "de seneste 24 timer",
"for_last_hours": "den seneste {{count}} time",
"for_last_hours_plural": "de seneste {{count}} timer",
"for_last_days": "den seneste {{count}} dag",
"for_last_days_plural": "de seneste {{count}} dage",
"stats_disabled": "Statistikker er deaktiveret. De kan aktiveres via <0>indstillingssiden</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Ingen upstreams-data fundet",
"number_of_dns_query_days": "Antallet af DNS-forespørgsler behandlet den seneste {{count}} dag",
"number_of_dns_query_days_plural": "Antallet af DNS-forespørgsler behandlet de seneste {{count}} dage",
"number_of_dns_query_24_hours": "Antallet af DNS-forespørgsler behandlet de seneste 24 timer",
"number_of_dns_query_hours": "Antallet af DNS-forespørgsler behandlet den seneste {{count}} time",
"number_of_dns_query_hours_plural": "Antallet af DNS-forespørgsler behandlet de seneste {{count}} timer",
"number_of_dns_query_blocked_24_hours": "Antallet af DNS-forespørgsler blokeret af adblockfiltre og værtssortlister",
"number_of_dns_query_blocked_24_hours_by_sec": "Antallet af DNS-forespørgsler blokeret af AdGuards browsingsikkerhedsmodul",
"number_of_dns_query_blocked_24_hours_adult": "Antallet af blokerede voksenwebsteder",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Gesperrte Schädliche/Phishing-Websites",
"stats_adult": "Gesperrte jugendgefährdende Websites",
"stats_query_domain": "Am häufigsten angefragte Domains",
"for_last_24_hours": "für die letzten 24 Stunden",
"for_last_hours": "in die letzte {{count}} Stunde",
"for_last_hours_plural": "in die letzten {{count}} Stunden",
"for_last_days": "am letzten {{count}} Tag",
"for_last_days_plural": "in den letzten {{count}} Tage",
"stats_disabled": "Die Statistik wurde deaktiviert. Sie können diese in den <0>Einstellungen</0> erneut aktivieren.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Keine Upstream-Daten gefunden",
"number_of_dns_query_days": "Anzahl der in den letzten {{count}} Tagen verarbeiteten DNS-Anfragen",
"number_of_dns_query_days_plural": "Anzahl der DNS-Abfragen, die in den letzten {{count}} Tagen verarbeitet wurden",
"number_of_dns_query_24_hours": "Anzahl der in den letzten 24 Stunden durchgeführten DNS-Anfragen",
"number_of_dns_query_hours": "Die Anzahl der DNS-Anfragen, die in der letzten {{count}} Stunde verarbeitet wurden",
"number_of_dns_query_hours_plural": "Die Anzahl der DNS-Anfragen, die in den letzten {{count}} Stunden verarbeitet wurden",
"number_of_dns_query_blocked_24_hours": "Anzahl der durch Werbefilter und Host-Sperrlisten abgelehnte DNS-Anfragen",
"number_of_dns_query_blocked_24_hours_by_sec": "Anzahl der durch das AdGuard-Modul „Internetsicherheit“ gesperrten DNS-Anfragen",
"number_of_dns_query_blocked_24_hours_adult": "Anzahl der gesperrten Websites mit jugendgefährdenden Inhalten",

View File

@@ -1,6 +1,7 @@
{
"client_settings": "Client settings",
"example_upstream_reserved": "an upstream <0>for specific domains</0>;",
"example_multiple_upstreams_reserved": "multiple upstreams <0>for specific domains</0>;",
"example_upstream_comment": "a comment.",
"upstream_parallel": "Use parallel queries to speed up resolving by querying all upstream servers simultaneously.",
"parallel_requests": "Parallel requests",
@@ -119,7 +120,8 @@
"stats_malware_phishing": "Blocked malware/phishing",
"stats_adult": "Blocked adult websites",
"stats_query_domain": "Top queried domains",
"for_last_24_hours": "for the last 24 hours",
"for_last_hours": "for the last {{count}} hour",
"for_last_hours_plural": "for the last {{count}} hours",
"for_last_days": "for the last {{count}} day",
"for_last_days_plural": "for the last {{count}} days",
"stats_disabled": "The statistics have been disabled. You can turn it on from the <0>settings page</0>.",
@@ -134,7 +136,8 @@
"no_upstreams_data_found": "No upstreams data found",
"number_of_dns_query_days": "The number of DNS queries processed for the last {{count}} day",
"number_of_dns_query_days_plural": "The number of DNS queries processed for the last {{count}} days",
"number_of_dns_query_24_hours": "The number of DNS queries processed for the last 24 hours",
"number_of_dns_query_hours": "The number of DNS queries processed for the last {{count}} hour",
"number_of_dns_query_hours_plural": "The number of DNS queries processed for the last {{count}} hours",
"number_of_dns_query_blocked_24_hours": "The number of DNS requests blocked by adblock filters and hosts blocklists",
"number_of_dns_query_blocked_24_hours_by_sec": "The number of DNS requests blocked by the AdGuard browsing security module",
"number_of_dns_query_blocked_24_hours_adult": "The number of adult websites blocked",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Malware/phishing bloqueado",
"stats_adult": "Sitios web para adultos bloqueado",
"stats_query_domain": "Dominios más consultados",
"for_last_24_hours": "en las últimas 24 horas",
"for_last_hours": "de la última {{count}} hora",
"for_last_hours_plural": "de las últimas {{count}} horas",
"for_last_days": "durante los últimos {{count}} días",
"for_last_days_plural": "durante los últimos {{count}} días",
"stats_disabled": "Las estadísticas se han deshabilitado. Puedes habilitarlas desde la <0>página de configuración</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "No se han encontrado datos de upstreams",
"number_of_dns_query_days": "Número de consultas DNS procesadas durante el último {{count}} día",
"number_of_dns_query_days_plural": "Número de consultas DNS procesadas durante los últimos {{count}} días",
"number_of_dns_query_24_hours": "Número de consultas DNS procesadas durante las últimas 24 horas",
"number_of_dns_query_hours": "Número de consultas DNS procesadas durante la última {{count}} hora",
"number_of_dns_query_hours_plural": "Número de consultas DNS procesadas durante las últimas {{count}} horas",
"number_of_dns_query_blocked_24_hours": "Número de peticiones DNS bloqueadas por los filtros y listas de bloqueo de hosts",
"number_of_dns_query_blocked_24_hours_by_sec": "Número de peticiones DNS bloqueadas por el módulo de seguridad de navegación de AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Número de sitios web para adultos bloqueado",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Estetyt haittaohjelmat/tietojenkalastelut",
"stats_adult": "Estetyt aikuisille tarkoitetut sivustot",
"stats_query_domain": "Kysytyimmät verkkotunnukset",
"for_last_24_hours": "viimeisten 24 tunnin ajalta",
"for_last_hours": "viimeisen {{count}} tunnin ajalta",
"for_last_hours_plural": "viimeisen {{count}} tunnin ajalta",
"for_last_days": "viimeisten {{count}} päivän ajalta",
"for_last_days_plural": "viimeisten {{count}} päivän ajalta",
"stats_disabled": "Tilastointi ei ole käytössä. Voit ottaa sen käyttöön <0>asetuksista</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Ylävirtatietoja ei löytynyt",
"number_of_dns_query_days": "Käsiteltyjen DNS-pyyntöjen määrä viimeisten {{count}} päivän ajalta",
"number_of_dns_query_days_plural": "Käsiteltyjen DNS-pyyntöjen määrä viimeisten {{count}} päivän ajalta",
"number_of_dns_query_24_hours": "Käsiteltyjen DNS-pyyntöjen määrä viimeisten 24 tunnin ajalta",
"number_of_dns_query_hours": "Viimeisen {{count}} tunnin aikana käsiteltyjen DNS-kyselyiden määrä",
"number_of_dns_query_hours_plural": "Viimeisen {{count}} tunnin aikana käsiteltyjen DNS-kyselyiden määrä",
"number_of_dns_query_blocked_24_hours": "Mainoseston suodattimien ja hosts-estolistojen estämien DNS-pyyntöjen määrä",
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuardin Turvallinen selaus -moduulin estämien DNS-pyyntöjen määrä",
"number_of_dns_query_blocked_24_hours_adult": "Estettyjen aikuisille tarkoitettujen sivustojen määrä",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Tentative de malware/hameçonnage bloquée",
"stats_adult": "Sites à contenu adulte bloqués",
"stats_query_domain": "Domaines les plus recherchés",
"for_last_24_hours": "pendant les dernières 24 heures",
"for_last_hours": "pendant la dernière {{count}} heure",
"for_last_hours_plural": "pendant les dernières {{count}} heures",
"for_last_days": "pour les {{count}} derniers jours",
"for_last_days_plural": "pour les {{count}} derniers jours",
"stats_disabled": "Les statistiques ont été désactivées. Vous pouvez l'activer à partir de la <0>page des paramètres</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Aucune donnée en amont trouvée",
"number_of_dns_query_days": "Le nombre de requêtes DNS traitées pour les {{count}} derniers jours",
"number_of_dns_query_days_plural": "Le nombre de requêtes DNS traitées ces {{count}} derniers jours",
"number_of_dns_query_24_hours": "Le nombre de requêtes DNS traitées au cours des 24 dernières heures",
"number_of_dns_query_hours": "Le nombre de requêtes DNS traitées pendant la dernière {{count}} heure",
"number_of_dns_query_hours_plural": "Le nombre de requêtes DNS traitées pendant les dernières {{count}} heures",
"number_of_dns_query_blocked_24_hours": "Le nombre de requêtes DNS bloquées par les filtres adblock et les listes de blocage des hôtes",
"number_of_dns_query_blocked_24_hours_by_sec": "Le nombre de requêtes DNS bloquées par le module Sécurité de navigation d'AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Le nombre de sites à contenu adulte bloqués",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Blokiran zločudni program/krađe identiteta",
"stats_adult": "Blokirane web stranice za odrasle",
"stats_query_domain": "Top tražene domene",
"for_last_24_hours": "u zadnja 24 sata",
"for_last_hours": "za posljednji {{count}} sat",
"for_last_hours_plural": "za posljednjih {{count}} sati",
"for_last_days": "zadnjih {{count}} dana",
"for_last_days_plural": "zadnjih {{count}} dana",
"stats_disabled": "Statistika je onemogućena. Možete ga uključiti sa <0>stranice s postavkama</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Nema podataka o upstream poslužiteljima",
"number_of_dns_query_days": "Broj DNS upita obrađenih u posljednja {{count}} dan",
"number_of_dns_query_days_plural": "Broj DNS upita obrađenih u posljednja {{count}} dana",
"number_of_dns_query_24_hours": "Broj DNS upita obrađenih u posljednja 24 sata",
"number_of_dns_query_hours": "Broj DNS upita obrađenih za posljednji {{count}} sat",
"number_of_dns_query_hours_plural": "Broj DNS upita obrađenih za posljednjih {{count}} sati",
"number_of_dns_query_blocked_24_hours": "Broj DNS zahtjeva koji blokiraju filtri za blokiranje oglasa i popisi blokova hostova",
"number_of_dns_query_blocked_24_hours_by_sec": "Broj DNS zahtjeva koje je blokirao modul AdGuard zaštita pregledavanja",
"number_of_dns_query_blocked_24_hours_adult": "Broj blokiranih stranica s sadržajem za odrasle",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Blokkolt kártevő/adathalászat",
"stats_adult": "Blokkolt felnőtt tartalom",
"stats_query_domain": "Leglátogatottabb domainek",
"for_last_24_hours": "az utóbbi 24 órában",
"for_last_hours": "az utolsó {{count}} órában",
"for_last_hours_plural": "az utolsó {{count}} órában",
"for_last_days": "az utóbbi {{count}} napban",
"for_last_days_plural": "az utóbbi {{count}} napban",
"stats_disabled": "Ezek a statisztikák ki lettek kapcsolva. Be tudja kapcsolni őket a <0>beállítások oldalon</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Nem található upstream szerver adat",
"number_of_dns_query_days": "Lekérdezések száma az utolsó {{count}} napban",
"number_of_dns_query_days_plural": "Feldolgozott DNS lekérdezések száma az utolsó {{count}} napban",
"number_of_dns_query_24_hours": "Az elmúlt 24 órában feldolgozott DNS lekérdezések száma",
"number_of_dns_query_hours": "Feldolgozott DNS lekérdezések száma az utolsó {{count}} órában",
"number_of_dns_query_hours_plural": "Feldolgozott DNS lekérdezések száma az utolsó {{count}} órában",
"number_of_dns_query_blocked_24_hours": "A hirdetésblokkoló szűrők és a hosztfájlok által letiltott DNS kérések száma",
"number_of_dns_query_blocked_24_hours_by_sec": "Az AdGuard böngészési biztonság modulja által letiltott DNS kérések száma",
"number_of_dns_query_blocked_24_hours_adult": "Blokkolt felnőtt tartalmak száma",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Malware/phishing diblokir",
"stats_adult": "Situs dewasa diblokir",
"stats_query_domain": "Kueri domain teratas",
"for_last_24_hours": "untuk 24 jam terakhir",
"for_last_hours": "selama {{count}} jam terakhir",
"for_last_hours_plural": "selama {{count}} jam terakhir",
"for_last_days": "untuk {{count}} hari terakhir",
"for_last_days_plural": "selama {{count}} hari terakhir",
"stats_disabled": "Statistik telah dinonaktifkan. Anda dapat mengaktifkannya dari <0>halaman setelan</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Tidak ada data server upstream yang ditemukan",
"number_of_dns_query_days": "Jumlah kueri DNS diproses selama {{value}} hari terakhir",
"number_of_dns_query_days_plural": "Jumlah kueri DNS yang diproses selama {{count}} hari terakhir",
"number_of_dns_query_24_hours": "Jumlah kueri DNS diproses selama 24 jam terakhir",
"number_of_dns_query_hours": "Jumlah kueri DNS diproses selama {{{count}} jam terakhir",
"number_of_dns_query_hours_plural": "Jumlah kueri DNS diproses selama {{count}} jam terakhir",
"number_of_dns_query_blocked_24_hours": "Julah DNS diblokir oleh penyaring adblock dan daftar blokir hosts",
"number_of_dns_query_blocked_24_hours_by_sec": "Jumlah perminataan DNS diblokir oleh modul Kemanan Penjelajahan AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Jumlah website dewasa diblokir",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Malware/phishing bloccati",
"stats_adult": "Siti per adulti bloccati",
"stats_query_domain": "Domini maggiormente richiesti",
"for_last_24_hours": "nelle ultime 24 ore",
"for_last_hours": "per l'ultima {{count}} ora",
"for_last_hours_plural": "per le ultime {{count}} ore",
"for_last_days": "per gli ultimi {{count}} giorni",
"for_last_days_plural": "per gli ultimi {{count}} giorni",
"stats_disabled": "Le statistiche sono state disattivate. Puoi attivarle dalla <0>pagina delle impostazioni</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Nessun dato upstream trovato",
"number_of_dns_query_days": "Numero di richieste DNS elaborate negli ultimi {{count}} giorni",
"number_of_dns_query_days_plural": "Numero di richieste DNS elaborate negli ultimi {{count}} giorni",
"number_of_dns_query_24_hours": "Numero di richieste DNS elaborate nelle ultime 24 ore",
"number_of_dns_query_hours": "Numero di richieste DNS processate nell'ultima {{count}} ora",
"number_of_dns_query_hours_plural": "Numero di richieste DNS processate nelle ultime {{count}} ore",
"number_of_dns_query_blocked_24_hours": "Numero di richieste DNS bloccate dai filtri per annunci e dagli elenchi di blocco host",
"number_of_dns_query_blocked_24_hours_by_sec": "Numero di richieste DNS bloccate dal modulo sicurezza di navigazione di AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Numero di siti web per adulti bloccati",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "ブロックされたマルウェア/フィッシング",
"stats_adult": "ブロックされたアダルトウェブサイト",
"stats_query_domain": "最も問合せされたドメイン",
"for_last_24_hours": "過去24時間以内",
"for_last_hours": "過去{{count}}時間",
"for_last_hours_plural": "過去{{count}}時間",
"for_last_days": "過去{{count}}日間以内",
"for_last_days_plural": "過去{{count}}日間以内",
"stats_disabled": "統計は無効化されています。<0>設定ページ</0>でオンにすることができます。",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "アップストリームのデータが見つかりません",
"number_of_dns_query_days": "過去{{count}}日間に処理されたDNSクエリの数",
"number_of_dns_query_days_plural": "過去{{count}}日間に処理されたDNSクエリの数",
"number_of_dns_query_24_hours": "過去24時間に処理されたDNSクエリの数",
"number_of_dns_query_hours": "過去{{count}}時間に処理されたDNSクエリの数",
"number_of_dns_query_hours_plural": "過去{{count}}時間に処理されたDNSクエリの数",
"number_of_dns_query_blocked_24_hours": "広告ブロックフィルタとhostsブロックリストによってブロックされたDNSリクエストの数",
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuardブラウジングセキュリティモジュールによってブロックされたDNSリクエストの数",
"number_of_dns_query_blocked_24_hours_adult": "ブロックされたアダルトウェブサイトの数",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "차단된 멀웨어/피싱",
"stats_adult": "차단된 성인 웹사이트",
"stats_query_domain": "쿼리 도메인",
"for_last_24_hours": "지난 24시간 동안",
"for_last_hours": "마지막 {{count}} 시간",
"for_last_hours_plural": "마지막 {{count}} 시간의 기록",
"for_last_days": "마지막 {{count}} 일",
"for_last_days_plural": "마지막 {{count}} 일의 기록",
"stats_disabled": "통계 기능이 꺼졌습니다. <0>설정 페이지</0>에서 켤 수 있습니다.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "업스트림 데이터 없음",
"number_of_dns_query_days": "최근 {{count}}일 동안 처리된 DNS 쿼리의 수",
"number_of_dns_query_days_plural": "최근 {{count}}일 동안 처리된 DNS 쿼리의 수",
"number_of_dns_query_24_hours": "최근 24시간 동안 처리된 DNS 쿼리의 수",
"number_of_dns_query_hours": "최근 {{count}}시간 동안 처리된 DNS 쿼리의 수",
"number_of_dns_query_hours_plural": "최근 {{count}}시간 동안 처리된 DNS 쿼리의 수",
"number_of_dns_query_blocked_24_hours": "광고 차단 필터 및 호스트 차단 목록에 의해 차단된 DNS 요청 수",
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuard 브라우징 보안 모듈에 의해 차단된 DNS 요청 수",
"number_of_dns_query_blocked_24_hours_adult": "차단된 성인 웹 사이트의 수",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Geblokkeerde malware/phishing",
"stats_adult": "Geblokkeerde 18+ websites",
"stats_query_domain": "Meest bezochte domeinen",
"for_last_24_hours": "van de laatste 24-uur",
"for_last_hours": "voor het afgelopen {{count}} uur",
"for_last_hours_plural": "voor de afgelopen {{count}} uren",
"for_last_days": "sinds de laatste {{count}} dagen",
"for_last_days_plural": "sinds de laatste {{count}} dagen",
"stats_disabled": "Statistieken zijn uitgeschakeld. Je kunt ze inschakelen op de <0>instellingen pagina</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Geen upstreams-gegevens gevonden",
"number_of_dns_query_days": "Aantal verwerkte DNS aanvragen van de laatste {{count}} dag",
"number_of_dns_query_days_plural": "Aantal verwerkte DNS aanvragen van de laatste {{count}} dagen",
"number_of_dns_query_24_hours": "Aantal verwerkte DNS aanvragen van de laatste 24 uur",
"number_of_dns_query_hours": "Het aantal DNS-verzoeken dat het afgelopen {{count}} uur is verwerkt",
"number_of_dns_query_hours_plural": "Het aantal DNS-verzoeken dat de afgelopen {{count}} uren is verwerkt",
"number_of_dns_query_blocked_24_hours": "Aantal geblokkeerde DNS aanvragen door advertentie blokkering en hosts blokkeerlijsten",
"number_of_dns_query_blocked_24_hours_by_sec": "Aantal geblokkeerde DNS aanvragen door AdGuard browsing beveiligingsmodule",
"number_of_dns_query_blocked_24_hours_adult": "Aantal geblokkeerde 18+ websites",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Zablokowane złośliwe oprogramowanie/phishing",
"stats_adult": "Zablokowane witryny dla dorosłych",
"stats_query_domain": "Najczęściej wyszukiwane domeny",
"for_last_24_hours": "przez ostatnie 24 godziny",
"for_last_hours": "w ciągu ostatniej {{count}} godziny",
"for_last_hours_plural": "w ciągu ostatnich {{count}} godzin",
"for_last_days": "za ostatni dzień {{count}}",
"for_last_days_plural": "z ostatnich {{count}} dni",
"stats_disabled": "Statystyki zostały wyłączone. Można je włączyć na <0>stronie ustawień</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Brak danych dotyczących serwerów nadrzędnych",
"number_of_dns_query_days": "Liczba przetworzonych zapytań DNS w ciągu ostatnich {{count}} dni",
"number_of_dns_query_days_plural": "Liczba przetworzonych zapytań DNS w ciągu ostatnich {{count}} dni",
"number_of_dns_query_24_hours": "Liczba zapytań DNS przetworzonych w ciągu ostatnich 24 godzin",
"number_of_dns_query_hours": "Liczba przetworzonych zapytań DNS w ciągu ostatniej {{count}} godziny",
"number_of_dns_query_hours_plural": "Liczba przetworzonych zapytań DNS w ciągu ostatnich {{count}} godzin",
"number_of_dns_query_blocked_24_hours": "Liczba żądań DNS zablokowanych przez filtry blokowania reklam i listy zablokowanych hostów",
"number_of_dns_query_blocked_24_hours_by_sec": "Liczba żądań DNS zablokowanych przez moduł Bezpiecznego przeglądania AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Liczba zablokowanych witryn dla dorosłych",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Bloqueado malware/phishing",
"stats_adult": "Bloqueado sites adultos",
"stats_query_domain": "Principais domínios consultados",
"for_last_24_hours": "nas últimas 24 horas",
"for_last_hours": "na última {{count}} hora",
"for_last_hours_plural": "nas últimas {{count}} horas",
"for_last_days": "nos últimos {{count}} dias",
"for_last_days_plural": "nos últimos {{count}} dias",
"stats_disabled": "As estatísticas foram desativadas. Você pode ligá-las através da <0>página de configurações</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Nenhum dado de servidor DNS primário encontrado",
"number_of_dns_query_days": "O número de consultas DNS processadas nos últimos {{count}} dias",
"number_of_dns_query_days_plural": "Número de consultas DNS processadas nos últimos {{count}} dias",
"number_of_dns_query_24_hours": "O número de consultas DNS processadas nas últimas 24 horas",
"number_of_dns_query_hours": "Número de consultas DNS processadas durante a última {{count}} hora",
"number_of_dns_query_hours_plural": "Número de consultas DNS processadas durante as últimas {{count}} horas",
"number_of_dns_query_blocked_24_hours": "Várias solicitações DNS bloqueadas por filtros de bloqueio de anúncios e listas de bloqueio de hosts",
"number_of_dns_query_blocked_24_hours_by_sec": "Várias solicitações de DNS bloqueadas pelo módulo de segurança da navegação do AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "O número de sites adultos bloqueados",
@@ -689,7 +691,7 @@
"log_and_stats_section_label": "Registro de consultas e estatísticas",
"ignore_query_log": "Ignorar este cliente no registo de consultas",
"ignore_statistics": "Ignorar este cliente nas estatísticas",
"schedule_services": "Pausa o bloqueio de serviço",
"schedule_services": "Pausar bloqueio de serviço",
"schedule_services_desc": "Configura o agendamento de pausa do filtro de bloqueio de serviço",
"schedule_services_desc_client": "Configura o agendamento de pausa do filtro de bloqueio de serviço para este cliente",
"schedule_desc": "Define períodos de inatividade para serviços bloqueados",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Malware/phishing bloqueados",
"stats_adult": "Sítios adultos bloqueados",
"stats_query_domain": "Principais domínios consultados",
"for_last_24_hours": "nas últimas 24 horas",
"for_last_hours": "na última {{count}} hora",
"for_last_hours_plural": "nas últimas {{count}} horas",
"for_last_days": "nos últimos {{count}} dias",
"for_last_days_plural": "nos últimos {{count}} dias",
"stats_disabled": "As estatísticas foram desativadas. Você pode ligá-las através da <0>página de definições</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Nenhum dado de servidor DNS primário encontrado",
"number_of_dns_query_days": "Número de consultas DNS processadas durante los últimos {{count}} días",
"number_of_dns_query_days_plural": "Número de consultas DNS processadas durante os últimos {{count}} dias",
"number_of_dns_query_24_hours": "O número de consultas DNS processadas nas últimas 24 horas",
"number_of_dns_query_hours": "Número de consultas DNS processadas durante a última {{count}} hora",
"number_of_dns_query_hours_plural": "Número de consultas DNS processadas durante as últimas {{count}} horas",
"number_of_dns_query_blocked_24_hours": "Várias solicitações DNS bloqueadas por filtros de bloqueio de anúncios e listas de bloqueio de hosts",
"number_of_dns_query_blocked_24_hours_by_sec": "Várias solicitações de DNS bloqueadas pelo módulo de segurança da navegação do AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "O número de sítios adultos bloqueados",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Malware/phishing blocate",
"stats_adult": "Site-uri cu conținut adult blocate",
"stats_query_domain": "Domeniile cele mai căutate",
"for_last_24_hours": "în ultimele 24 ore",
"for_last_hours": "în ultima {{count}} oră",
"for_last_hours_plural": "în ultimele {{count}} ore",
"for_last_days": "în ultima {{count}} zi",
"for_last_days_plural": "pentru ultimele {{count}} zile",
"stats_disabled": "Statisticile au fost dezactivate. Puteți să le porniți din <0>pagina de setări</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Nu există date despre serverele din amonte",
"number_of_dns_query_days": "Numărul de interogări DNS procesate în ultima {{count}} zi",
"number_of_dns_query_days_plural": "Numărul de interogări DNS procesate în ultimele {{count}} zile",
"number_of_dns_query_24_hours": "Numărul de interogări DNS procesate în ultimele 24 de ore",
"number_of_dns_query_hours": "Numărul de interogări DNS procesate în ultima {{count}} oră",
"number_of_dns_query_hours_plural": "Numărul de interogări DNS procesate în ultimele {{count}} ore",
"number_of_dns_query_blocked_24_hours": "Numărul de interogări DNS blocate de filtrele adblock și lista de blocări din hosts",
"number_of_dns_query_blocked_24_hours_by_sec": "Numărul de interogări DNS blocate de modulul de securitate de navigare AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Numărul de site-uri pentru adulți blocate",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Заблокированные вредоносные и фишинговые сайты",
"stats_adult": "Заблокированные «взрослые» сайты",
"stats_query_domain": "Часто запрашиваемые домены",
"for_last_24_hours": "за 24 часа",
"for_last_hours": "за последний {{count}} час",
"for_last_hours_plural": "за последние {{count}} часов",
"for_last_days": "за последний {{count}} день",
"for_last_days_plural": "за последние {{count}} дней",
"stats_disabled": "Статистика отключена. Вы можете включить её на <0>странице настроек</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Нет данных об upstream-серверах",
"number_of_dns_query_days": "Количество DNS-запросов за последний {{count}} день",
"number_of_dns_query_days_plural": "Количество DNS запросов, обработанных за последние {{count}} дней",
"number_of_dns_query_24_hours": "Количество DNS-запросов за последние 24 часа",
"number_of_dns_query_hours": "Количество DNS-запросов, обработанных за последний {{count}} час",
"number_of_dns_query_hours_plural": "Количество DNS-запросов, обработанных за последние {{count}} часов",
"number_of_dns_query_blocked_24_hours": "Количество DNS-запросов, заблокированных фильтрами и блок-списками",
"number_of_dns_query_blocked_24_hours_by_sec": "Количество DNS-запросов, заблокированных модулем Антифишинга AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Количество заблокированных «сайтов для взрослых»",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Blokovaný škodlivý kód/pokus o podvod",
"stats_adult": "Blokovaná stránka pre dospelých",
"stats_query_domain": "Najčastejšie dopytované domény",
"for_last_24_hours": "za posledných 24 hodín",
"for_last_hours": "za poslednú {{count}} hodinu",
"for_last_hours_plural": "za posledné {{count}} hodiny|za posledných {{count}} hodín",
"for_last_days": "za posledný {{count}} deň",
"for_last_days_plural": "za posledných {{count}} dní",
"stats_disabled": "Štatistiky boli vypnuté. Môžete ich zapnúť na <0>stránke nastavení</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Nenašli sa žiadne údaje o upstream serveroch",
"number_of_dns_query_days": "Počet DNS dopytov spracovaných za posledný {{count}} deň",
"number_of_dns_query_days_plural": "Počet DNS dopytov spracovaných za posledných {{count}} dní",
"number_of_dns_query_24_hours": "Počet DNS dopytov spracovaných za posledných 24 hodín",
"number_of_dns_query_hours": "Počet DNS dopytov spracovaných za poslednú {{count}} hodinu",
"number_of_dns_query_hours_plural": "Počet DNS dopytov spracovaných za posledné {{count}} hodiny)|Počet DNS dopytov spracovaných za posledných {{count}} hodín",
"number_of_dns_query_blocked_24_hours": "Počet DNS dopytov zablokovaných filtrami reklamy a zoznamami blokovaných hostov",
"number_of_dns_query_blocked_24_hours_by_sec": "Počet DNS dopytov zablokovaných AdGuard modulom Bezpečné prehliadanie",
"number_of_dns_query_blocked_24_hours_adult": "Počet zablokovaných stránok pre dospelých",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Onemogočeno zlonamernih programov/lažnih predstavljanj",
"stats_adult": "Onemogočeno spletnih strani za odrasle",
"stats_query_domain": "Najbolj poizvedovane domene",
"for_last_24_hours": "v zadnjih 24 urah",
"for_last_hours": "za zadnjo {{count}} uro",
"for_last_hours_plural": "za zadnjih {{count}} ur",
"for_last_days": "zadnjega {{count}} dne",
"for_last_days_plural": "zadnjih {{count}} dni",
"stats_disabled": "Statistika je onemogočena. Vklopite ga lahko na <0>strani z nastavitvami</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Ni podatkov o gorvodnih strežnikih",
"number_of_dns_query_days": "Število obdelanih poizvedb DNS v zadnjem {{count}} dnevu",
"number_of_dns_query_days_plural": "Število obdelanih poizvedb DNS v zadnjih {{count}} dneh",
"number_of_dns_query_24_hours": "Število obdelanih poizvedb DNS v zadnjih 24 urah",
"number_of_dns_query_hours": "Število poizvedb DNS, obdelanih v zadnji {{count}} uri",
"number_of_dns_query_hours_plural": "Število poizvedb DNS, obdelanih v zadnjih {{count}} urah",
"number_of_dns_query_blocked_24_hours": "Število zahtev DNS, ki so jih onemogočili filtri za zaviranje oglasov in seznami nedovoljenih, gostiteljev",
"number_of_dns_query_blocked_24_hours_by_sec": "Število zahtev DNS, ki jih je blokiral AdGuard zaščitni modul brskanja",
"number_of_dns_query_blocked_24_hours_adult": "Število onemogočenih spletnih strani za odrasle",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Blokiraj štetan softver i fišing",
"stats_adult": "Blokiraj sajtove za odrasle",
"stats_query_domain": "Najčešće unošeni domeni",
"for_last_24_hours": "u poslednja 24 časa",
"for_last_hours": "u poslednjih {{count}} sat",
"for_last_hours_plural": "u poslednjih {{count}} sati",
"for_last_days": "u poslednjih {{count}} dana",
"for_last_days_plural": "u poslednjih {{count}} dana",
"stats_disabled": "Statistika je isključena. Možete ga uključiti sa stranice <0>sa postavkama</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Nema podataka o upstream serverima",
"number_of_dns_query_days": "Broj obrađenih DNS unosa u poslednjih {{count}} dan",
"number_of_dns_query_days_plural": "Broj obrađenih DNS unosa u poslednjih {{count}} dana",
"number_of_dns_query_24_hours": "Broj obrađenih DNS unosa u poslednja 24 časa",
"number_of_dns_query_hours": "Broj obrađenih DNS unosa u poslednji {{count}} sat",
"number_of_dns_query_hours_plural": "Broj obrađenih DNS unosa u poslednjih {{count}} sati",
"number_of_dns_query_blocked_24_hours": "Broj DNS zahteva blokiranih od filtera blokatora reklama i blok liste hostova",
"number_of_dns_query_blocked_24_hours_by_sec": "Broj DNS zahteva blokiranih od AdGuard-ovog podprograma za bezbedno pregledanje",
"number_of_dns_query_blocked_24_hours_adult": "Broj blokiranih sajtova za odrasle",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Blockerad skadekod/phishing",
"stats_adult": "Blockerade vuxensajter",
"stats_query_domain": "Mest eftersökta domäner",
"for_last_24_hours": "under de senaste 24 timmarna",
"for_last_hours": "r den senaste {{count}} timme",
"for_last_hours_plural": "för de senaste {{count}} timmar",
"for_last_days": "för den senaste {{count}} dagen",
"for_last_days_plural": "för de senaste {{count}} dagarna",
"stats_disabled": "Statistiken har inaktiverats. Du kan aktivera det från <0>inställningssidan</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Inga uppströmsdata hittades",
"number_of_dns_query_days": "Antalet DNS-förfrågningar som utfördes under senaste {{count}} dagen",
"number_of_dns_query_days_plural": "Ett antal DNS förfrågningar utfördes under de senaste {{count}} dagarna",
"number_of_dns_query_24_hours": "Antalet DNS-förfrågningar som utfördes under de senaste 24 timmarna",
"number_of_dns_query_hours": "Ett antal DNS förfrågningar utfördes r den senaste {{count}} timme",
"number_of_dns_query_hours_plural": "Ett antal DNS förfrågningar utfördes för den senaste {{count}} timmar",
"number_of_dns_query_blocked_24_hours": "Antalet DNS-förfrågningar som blockerades av annonsfilter och värdens blockeringsklistor",
"number_of_dns_query_blocked_24_hours_by_sec": "Antalet DNS-förfrågningar som blockerades av AdGuards modul för surfsäkerhet",
"number_of_dns_query_blocked_24_hours_adult": "Antalet vuxensajter som blockerats",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Engellenen kötü amaçlı yazılım ve kimlik avı",
"stats_adult": "Engellenen yetişkin içerikli siteler",
"stats_query_domain": "Başlıca sorgulanan alan adları",
"for_last_24_hours": "son 24 saat içindekiler",
"for_last_hours": "son {{count}} saat için",
"for_last_hours_plural": "son {{count}} saat için",
"for_last_days": "son {{count}} gün boyunca",
"for_last_days_plural": "son {{count}} gün boyunca",
"stats_disabled": "İstatistikler devre dışı bırakıldı. Bunu, <0>ayarlar sayfasından</0> etkinleştirebilirsiniz.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Üst kaynak verisi bulunamadı",
"number_of_dns_query_days": "Son {{count}} gün boyunca işlenen DNS sorgularının sayısı",
"number_of_dns_query_days_plural": "Son {{count}} gün boyunca işlenen DNS sorgularının sayısı",
"number_of_dns_query_24_hours": "Son 24 saat içinde işlenen DNS sorgularının sayısı",
"number_of_dns_query_hours": "Son {{count}} saat için işlenen DNS sorgularının sayısı",
"number_of_dns_query_hours_plural": "Son {{count}} saatiçin işlenen DNS sorgularının sayısı",
"number_of_dns_query_blocked_24_hours": "Reklam engelleme filtreleri ve hosts engel listeleri tarafından engellenen DNS isteklerinin sayısı",
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuard gezinti koruması modülü tarafından engellenen DNS isteklerinin sayısı",
"number_of_dns_query_blocked_24_hours_adult": "Engellenen yetişkin içerikli sitelerin sayısı",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Заблоковано зловмисних/шахрайських програм",
"stats_adult": "Заблоковано вебсайтів для дорослих",
"stats_query_domain": "Найчастіші запити доменів",
"for_last_24_hours": "за останні 24 години",
"for_last_hours": "за останню {{count}} годину",
"for_last_hours_plural": "за останні {{count}} годин",
"for_last_days": "за останній {{count}} день",
"for_last_days_plural": "за останні {{count}} днів",
"stats_disabled": "Статистику вимкнено. Ви можете увімкнути її на <0>сторінці налаштувань</0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Немає даних про upstream-сервери",
"number_of_dns_query_days": "Кількість DNS-запитів, оброблених за останні {{count}} дні",
"number_of_dns_query_days_plural": "Кількість DNS-запитів, оброблених за останні {{count}} днів",
"number_of_dns_query_24_hours": "Кількість DNS-запитів, оброблених за останні 24 години",
"number_of_dns_query_hours": "Кількість DNS-запитів, оброблених за останню {{count}} годину",
"number_of_dns_query_hours_plural": "Кількість DNS-запитів, оброблених за останні {{count}} годин",
"number_of_dns_query_blocked_24_hours": "Кількість DNS-запитів, заблокованих фільтрами і списками блокування hosts",
"number_of_dns_query_blocked_24_hours_by_sec": "Кількість DNS-запитів, заблокованих модулем «Безпека перегляду» AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Кількість заблокованих вебсайтів для дорослих",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "Mã độc/lừa đảo đã chặn",
"stats_adult": "Website người lớn đã chặn",
"stats_query_domain": "Tên miền truy vấn nhiều",
"for_last_24_hours": "trong 24 giờ qua",
"for_last_hours": "trong {{count}} giờ qua",
"for_last_hours_plural": "trong {{count}} giờ qua",
"for_last_days": "trong {{count}} ngày qua",
"for_last_days_plural": "trong {{count}} ngày qua",
"stats_disabled": "Số liệu thống kê đã bị vô hiệu hóa. Bạn có thể bật nó từ <0> trang cài đặt </0>.",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "Không tìm thấy dữ liệu máy chủ ngược dòng",
"number_of_dns_query_days": "Một số truy vấn DNS được xử lý trong {{count}} ngày qua",
"number_of_dns_query_days_plural": "Một số truy vấn DNS được xử lý trong {{count}} ngày qua",
"number_of_dns_query_24_hours": "Số yêu cầu DNS đã xử lý trong 24 giờ qua",
"number_of_dns_query_hours": "Một số truy vấn DNS được xử lý trong {{count}} giờ qua",
"number_of_dns_query_hours_plural": "Một số truy vấn DNS được xử lý trong {{count}} giờ qua",
"number_of_dns_query_blocked_24_hours": "Số yêu cầu DNS bị chặn bởi bộ lọc quảng cáo và danh sách chặn host",
"number_of_dns_query_blocked_24_hours_by_sec": "Số yêu cầu DNS bị chặn bởi chế độ bảo vệ duyệt web AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Số trang web người lớn đã chặn",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "被拦截的恶意/钓鱼网站",
"stats_adult": "被拦截的成人网站",
"stats_query_domain": "请求域名排行",
"for_last_24_hours": "在过去 24 小时",
"for_last_hours": "最近 {{count}} 小时",
"for_last_hours_plural": "最近 {{count}} 小时",
"for_last_days": "最近 {{count}} 天",
"for_last_days_plural": "最近 {{count}} 天",
"stats_disabled": "已禁用统计数据。您可以从<0>设置页面</0>打开它。",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "未找到上游服务器数据",
"number_of_dns_query_days": "过去 {{count}} 天内处理的 DNS 查询总数",
"number_of_dns_query_days_plural": "在过去的 {{count}} 天内处理了多少个 DNS 查询",
"number_of_dns_query_24_hours": "过去 24 小时内处理的 DNS 请求总数",
"number_of_dns_query_hours": "最近 {{count}} 小时内处理的 DNS 查询次数",
"number_of_dns_query_hours_plural": "最近 {{count}} 小时内处理的 DNS 查询次数",
"number_of_dns_query_blocked_24_hours": "被广告过滤器和 Hosts 黑名单阻止的 DNS 请求总数",
"number_of_dns_query_blocked_24_hours_by_sec": "被 AdGuard 安全浏览模块阻止的 DNS 请求总数",
"number_of_dns_query_blocked_24_hours_adult": "被阻止的成人网站总数",

View File

@@ -119,7 +119,8 @@
"stats_malware_phishing": "已封鎖的惡意軟體/網路釣魚",
"stats_adult": "已封鎖的成人網站",
"stats_query_domain": "熱門已查詢的網域",
"for_last_24_hours": "在最近的 24 小時內",
"for_last_hours": "在過去的 {{count}} 小時內",
"for_last_hours_plural": "在過去的 {{count}} 小時內",
"for_last_days": "在最近的 {{count}} 日內",
"for_last_days_plural": "在最近的 {{count}} 日內",
"stats_disabled": "該統計資料已被禁用。您可從<0>設定頁面</0>中打開它。",
@@ -134,7 +135,8 @@
"no_upstreams_data_found": "找不到上游伺服器資料",
"number_of_dns_query_days": "在最近的 {{count}} 日內已處理的 DNS 查詢之數量",
"number_of_dns_query_days_plural": "在最近的 {{count}} 日內已處理的 DNS 查詢之數量",
"number_of_dns_query_24_hours": "在最近的 24 小時內處理的 DNS 查詢之數量",
"number_of_dns_query_hours": "過去 {{count}} 小時內處理的 DNS 查詢次數",
"number_of_dns_query_hours_plural": "過去 {{count}} 小時內處理的 DNS 查詢次數",
"number_of_dns_query_blocked_24_hours": "被廣告封鎖過濾器和主機封鎖清單阻擋的 DNS 請求之數量",
"number_of_dns_query_blocked_24_hours_by_sec": "被 AdGuard 瀏覽安全模組封鎖的 DNS 請求之數量",
"number_of_dns_query_blocked_24_hours_adult": "已封鎖的成人網站之數量",

View File

@@ -118,6 +118,11 @@ body {
overflow-y: auto;
}
.modal-body--filters {
max-height: 600px;
overflow-y: auto;
}
.modal-body__item:not(:first-child) {
padding-top: 1.5rem;
}
@@ -134,15 +139,6 @@ body {
cursor: not-allowed;
}
.button-action {
visibility: hidden;
}
.logs__row:hover .button-action,
.button-action--active {
visibility: visible;
}
.ReactModal__Body--open {
overflow: hidden;
}

View File

@@ -8,7 +8,7 @@ import Cell from '../ui/Cell';
import DomainCell from './DomainCell';
import { getPercent } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
const CountCell = (totalBlocked) => function cell(row) {
const { value } = row;
@@ -62,8 +62,8 @@ const BlockedDomains = ({
]}
showPagination={false}
noDataText={t('no_domains_found')}
minRows={6}
defaultPageSize={100}
minRows={TABLES_MIN_ROWS}
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
className="-highlight card-table-overflow--limited stats__table"
/>
</Card>

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import { Trans, useTranslation } from 'react-i18next';
@@ -9,10 +9,16 @@ import Card from '../ui/Card';
import Cell from '../ui/Cell';
import { getPercent, sortIp } from '../../helpers/helpers';
import { BLOCK_ACTIONS, STATUS_COLORS } from '../../helpers/constants';
import {
BLOCK_ACTIONS,
DASHBOARD_TABLES_DEFAULT_PAGE_SIZE,
STATUS_COLORS,
TABLES_MIN_ROWS,
} from '../../helpers/constants';
import { toggleClientBlock } from '../../actions/access';
import { renderFormattedClientCell } from '../../helpers/renderFormattedClientCell';
import { getStats } from '../../actions/stats';
import IconTooltip from '../Logs/Cells/IconTooltip';
const getClientsPercentColor = (percent) => {
if (percent > 50) {
@@ -40,9 +46,7 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
const processingSet = useSelector((state) => state.access.processingSet);
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
const buttonClass = classNames('button-action button-action--main', {
'button-action--unblock': disallowed,
});
const [isOptionsOpened, setOptionsOpened] = useState(false);
const toggleClientStatus = async (ip, disallowed, disallowed_rule) => {
let confirmMessage;
@@ -62,23 +66,49 @@ const renderBlockingButton = (ip, disallowed, disallowed_rule) => {
}
};
const onClick = () => toggleClientStatus(ip, disallowed, disallowed_rule);
const onClick = () => {
toggleClientStatus(ip, disallowed, disallowed_rule);
setOptionsOpened(false);
};
const text = disallowed ? BLOCK_ACTIONS.UNBLOCK : BLOCK_ACTIONS.BLOCK;
const lastRuleInAllowlist = !disallowed && allowedСlients === disallowed_rule;
const disabled = processingSet || lastRuleInAllowlist;
return (
<div className="table__action pl-4">
<div className="table__action">
<button
type="button"
className={buttonClass}
onClick={onClick}
disabled={disabled}
title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''}
className="btn btn-icon btn-sm px-0"
onClick={() => setOptionsOpened(true)}
>
<Trans>{text}</Trans>
<svg className="icon24 icon--lightgray button-action__icon">
<use xlinkHref="#bullets" />
</svg>
</button>
{isOptionsOpened && (
<IconTooltip
className="icon24"
tooltipClass="button-action--arrow-option-container"
xlinkHref="bullets"
triggerClass="btn btn-icon btn-sm px-0 button-action__hidden-trigger"
content={(
<button
className={classNames('button-action--arrow-option px-4 py-1', disallowed ? 'bg--green' : 'bg--danger')}
onClick={onClick}
disabled={disabled}
title={lastRuleInAllowlist ? t('last_rule_in_allowlist', { disallowed_rule }) : ''}
>
<Trans>{text}</Trans>
</button>
)}
placement="bottom-end"
trigger="click"
onVisibilityChange={setOptionsOpened}
defaultTooltipShown={true}
delayHide={0}
/>
)}
</div>
);
};
@@ -134,8 +164,8 @@ const Clients = ({
]}
showPagination={false}
noDataText={t('no_clients_found')}
minRows={6}
defaultPageSize={100}
minRows={TABLES_MIN_ROWS}
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
className="-highlight card-table-overflow--limited clients__table"
getTrProps={(_state, rowInfo) => {
if (!rowInfo) {

View File

@@ -4,9 +4,9 @@ import { Trans, useTranslation } from 'react-i18next';
import round from 'lodash/round';
import { shallowEqual, useSelector } from 'react-redux';
import Card from '../ui/Card';
import { formatNumber } from '../../helpers/helpers';
import { formatNumber, msToDays, msToHours } from '../../helpers/helpers';
import LogsSearchLink from '../ui/LogsSearchLink';
import { RESPONSE_FILTER, DAY } from '../../helpers/constants';
import { RESPONSE_FILTER, TIME_UNITS } from '../../helpers/constants';
import Tooltip from '../ui/Tooltip';
const Row = ({
@@ -52,14 +52,19 @@ const Counters = ({ refreshButton, subtitle }) => {
numReplacedParental,
numReplacedSafesearch,
avgProcessingTime,
timeUnits,
} = useSelector((state) => state.stats, shallowEqual);
const { t } = useTranslation();
const days = interval / DAY;
const dnsQueryTooltip = timeUnits === TIME_UNITS.HOURS
? t('number_of_dns_query_hours', { count: msToHours(interval) })
: t('number_of_dns_query_days', { count: msToDays(interval) });
const rows = [
{
label: 'dns_query',
count: numDnsQueries,
tooltipTitle: days === 1 ? 'number_of_dns_query_24_hours' : t('number_of_dns_query_days', { count: days }),
tooltipTitle: dnsQueryTooltip,
response_status: RESPONSE_FILTER.ALL.QUERY,
},
{

View File

@@ -28,11 +28,14 @@
border-bottom: 6px solid #585965;
}
@media (max-width: 1279.98px) {
.table__action {
position: absolute;
right: 0;
}
.table__action {
position: relative;
margin-left: auto;
}
.table__action .btn-icon {
outline: 0;
box-shadow: none;
}
.page-title--dashboard {

View File

@@ -7,7 +7,7 @@ import Card from '../ui/Card';
import Cell from '../ui/Cell';
import DomainCell from './DomainCell';
import { STATUS_COLORS } from '../../helpers/constants';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
import { getPercent } from '../../helpers/helpers';
const getQueriedPercentColor = (percent) => {
@@ -58,8 +58,8 @@ const QueriedDomains = ({
]}
showPagination={false}
noDataText={t('no_domains_found')}
minRows={6}
defaultPageSize={100}
minRows={TABLES_MIN_ROWS}
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
className="-highlight card-table-overflow--limited stats__table"
/>
</Card>

View File

@@ -6,6 +6,7 @@ import { withTranslation, Trans } from 'react-i18next';
import Card from '../ui/Card';
import DomainCell from './DomainCell';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, TABLES_MIN_ROWS } from '../../helpers/constants';
const TimeCell = ({ value }) => {
if (!value) {
@@ -62,8 +63,8 @@ const UpstreamAvgTime = ({
]}
showPagination={false}
noDataText={t('no_upstreams_data_found')}
minRows={6}
defaultPageSize={100}
minRows={TABLES_MIN_ROWS}
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
className="-highlight card-table-overflow--limited stats__table"
/>
</Card>

View File

@@ -8,7 +8,7 @@ import Cell from '../ui/Cell';
import DomainCell from './DomainCell';
import { getPercent } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
import { DASHBOARD_TABLES_DEFAULT_PAGE_SIZE, STATUS_COLORS, TABLES_MIN_ROWS } from '../../helpers/constants';
const CountCell = (totalBlocked) => (
function cell(row) {
@@ -64,8 +64,8 @@ const UpstreamResponses = ({
]}
showPagination={false}
noDataText={t('no_upstreams_data_found')}
minRows={6}
defaultPageSize={100}
minRows={TABLES_MIN_ROWS}
defaultPageSize={DASHBOARD_TABLES_DEFAULT_PAGE_SIZE}
className="-highlight card-table-overflow--limited stats__table"
/>
</Card>

View File

@@ -9,7 +9,12 @@ import Counters from './Counters';
import Clients from './Clients';
import QueriedDomains from './QueriedDomains';
import BlockedDomains from './BlockedDomains';
import { DISABLE_PROTECTION_TIMINGS, ONE_SECOND_IN_MS, SETTINGS_URLS } from '../../helpers/constants';
import {
DISABLE_PROTECTION_TIMINGS,
ONE_SECOND_IN_MS,
SETTINGS_URLS,
TIME_UNITS,
} from '../../helpers/constants';
import {
msToSeconds,
msToMinutes,
@@ -46,15 +51,12 @@ const Dashboard = ({
getAllStats();
}, []);
const getSubtitle = () => {
const ONE_DAY = 1;
const intervalInDays = msToDays(stats.interval);
if (intervalInDays < ONE_DAY) {
if (!stats.enabled) {
return t('stats_disabled_short');
}
return intervalInDays === ONE_DAY
? t('for_last_24_hours')
return stats.timeUnits === TIME_UNITS.HOURS
? t('for_last_hours', { count: msToHours(stats.interval) })
: t('for_last_days', { count: msToDays(stats.interval) });
};

View File

@@ -28,7 +28,7 @@ const renderIcons = (iconsData) => iconsData.map(({
}) => <a key={iconName} href={href} target="_blank" rel="noopener noreferrer"
className={classNames('d-flex align-items-center', className)}
>
<svg className="nav-icon nav-icon--gray">
<svg className="icon icon--15 mr-1 icon--gray">
<use xlinkHref={`#${iconName}`} />
</svg>
</a>);
@@ -110,7 +110,7 @@ const Form = (props) => {
const openAddFiltersModal = () => openModal(MODAL_TYPE.ADD_FILTERS);
return <form onSubmit={handleSubmit}>
<div className="modal-body modal-body--medium">
<div className="modal-body modal-body--filters">
{modalType === MODAL_TYPE.SELECT_MODAL_TYPE
&& <div className="d-flex justify-content-around">
<button onClick={openFilteringListModal}

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { withTranslation } from 'react-i18next';
import { sortIp } from '../../../helpers/helpers';
import { MODAL_TYPE } from '../../../helpers/constants';
import { MODAL_TYPE, TABLES_MIN_ROWS } from '../../../helpers/constants';
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';
class Table extends Component {
@@ -88,7 +88,7 @@ class Table extends Component {
showPagination
defaultPageSize={LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE) || 10}
onPageSizeChange={(size) => LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.REWRITES_PAGE_SIZE, size)}
minRows={5}
minRows={TABLES_MIN_ROWS}
ofText="/"
previousText={t('previous_btn')}
nextText={t('next_btn')}

View File

@@ -26,9 +26,7 @@ const ClientCell = ({
const { t } = useTranslation();
const dispatch = useDispatch();
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
const processingRules = useSelector((state) => state.filtering.processingRules);
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
const processingSet = useSelector((state) => state.access.processingSet);
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
const [isOptionsOpened, setOptionsOpened] = useState(false);
@@ -84,11 +82,23 @@ const ClientCell = ({
const blockingForClientKey = isFiltered ? 'unblock_for_this_client_only' : 'block_for_this_client_only';
const clientNameBlockingFor = getBlockingClientName(clients, client);
const onClick = async () => {
await dispatch(toggleBlocking(buttonType, domain));
await dispatch(getStats());
setOptionsOpened(false);
};
const BUTTON_OPTIONS = [
{
name: buttonType,
onClick,
className: isFiltered ? 'bg--green' : 'bg--danger',
},
{
name: blockingForClientKey,
onClick: () => {
dispatch(toggleBlockingForClient(buttonType, domain, clientNameBlockingFor));
setOptionsOpened(false);
},
},
{
@@ -101,27 +111,25 @@ const ClientCell = ({
client_info?.disallowed_rule || '',
));
await dispatch(updateLogs());
setOptionsOpened(false);
}
},
disabled: processingSet || lastRuleInAllowlist,
disabled: lastRuleInAllowlist,
},
];
const onClick = async () => {
await dispatch(toggleBlocking(buttonType, domain));
await dispatch(getStats());
};
const getOptions = (options) => {
if (options.length === 0) {
return null;
}
return (
<>
{options.map(({ name, onClick, disabled }) => (
{options.map(({
name, onClick, disabled, className,
}) => (
<button
key={name}
className="button-action--arrow-option px-4 py-1"
className={classNames('button-action--arrow-option px-4 py-1', className)}
onClick={onClick}
disabled={disabled}
>
@@ -134,17 +142,6 @@ const ClientCell = ({
const content = getOptions(BUTTON_OPTIONS);
const buttonClass = classNames('button-action button-action--main', {
'button-action--unblock': isFiltered,
'button-action--with-options': content,
'button-action--active': isOptionsOpened,
});
const buttonArrowClass = classNames('button-action button-action--arrow', {
'button-action--unblock': isFiltered,
'button-action--active': isOptionsOpened,
});
const containerClass = classNames('button-action__container', {
'button-action__container--detailed': isDetailed,
});
@@ -153,25 +150,26 @@ const ClientCell = ({
<div className={containerClass}>
<button
type="button"
className={buttonClass}
onClick={onClick}
disabled={processingRules}
className="btn btn-icon btn-sm px-0"
onClick={() => setOptionsOpened(true)}
>
{t(buttonType)}
<svg className="icon24 icon--lightgray button-action__icon">
<use xlinkHref="#bullets" />
</svg>
</button>
{content && (
<button className={buttonArrowClass} disabled={processingRules}>
<IconTooltip
className="icon24"
tooltipClass="button-action--arrow-option-container"
xlinkHref="chevron-down"
triggerClass="button-action--icon"
content={content}
placement="bottom-end"
trigger="click"
onVisibilityChange={setOptionsOpened}
/>
</button>
{isOptionsOpened && (
<IconTooltip
className="icon24"
tooltipClass="button-action--arrow-option-container"
xlinkHref="bullets"
triggerClass="btn btn-icon btn-sm px-0 button-action__hidden-trigger"
content={content}
placement="bottom-end"
trigger="click"
onVisibilityChange={setOptionsOpened}
defaultTooltipShown={true}
delayHide={0}
/>
)}
</div>
);
@@ -198,7 +196,7 @@ const ClientCell = ({
</div>
{isDetailed && clientName && !whoisAvailable && (
<Link
className="detailed-info d-none d-sm-block logs__text logs__text--link"
className="detailed-info d-none d-sm-block logs__text logs__text--link logs__text--client"
to={`logs?search="${encodeURIComponent(clientName)}"`}
title={clientName}
>

View File

@@ -1,4 +1,5 @@
.tooltip-custom__container {
min-width: 150px;
padding: 1rem 1.5rem 1.25rem 1.5rem;
font-size: 16px !important;
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);

View File

@@ -21,6 +21,8 @@ const IconTooltip = ({
content,
trigger,
onVisibilityChange,
defaultTooltipShown,
delayHide,
renderContent = content ? React.Children.map(
processContent(content),
(item, idx) => <div key={idx} className={contentItemClass}>
@@ -44,6 +46,8 @@ const IconTooltip = ({
trigger={trigger}
onVisibilityChange={onVisibilityChange}
delayShow={trigger === 'click' ? 0 : SHOW_TOOLTIP_DELAY}
delayHide={delayHide}
defaultTooltipShown={defaultTooltipShown}
>
{xlinkHref && <svg className={className}>
<use xlinkHref={`#${xlinkHref}`} />
@@ -65,6 +69,8 @@ IconTooltip.propTypes = {
content: PropTypes.node,
renderContent: PropTypes.arrayOf(PropTypes.element),
onVisibilityChange: PropTypes.func,
defaultTooltipShown: PropTypes.bool,
delayHide: PropTypes.number,
};
export default IconTooltip;

View File

@@ -80,6 +80,10 @@
color: var(--gray-f3);
}
.logs__table .logs__text--client {
padding-right: 32px;
}
.icon--selected {
background-color: var(--gray-f3);
border: solid 1px var(--gray-d8);
@@ -261,9 +265,8 @@
.button-action__container {
display: flex;
position: absolute;
right: 0;
right: 2px;
bottom: 0.5rem;
height: 1.6rem;
}
@media screen and (max-width: 1024px) {
@@ -307,45 +310,10 @@
border-bottom-right-radius: 0;
}
.button-action--arrow {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 1px solid var(--white);
width: 1.5625rem;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.button-action:hover {
cursor: pointer;
}
.button-action--arrow .button-action--icon {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
}
.button-action:active {
background: var(--btn-block-active);
}
.button-action--unblock:active {
background: var(--btn-unblock-active);
}
.button-action:disabled {
background: var(--btn-block-disabled);
cursor: default;
}
.button-action--unblock:disabled {
background: var(--btn-unblock-disabled);
}
.button-action--arrow-option {
background: transparent;
border: 0;
@@ -551,3 +519,20 @@
padding: 1rem 1.5rem;
background-color: var(--card-bgcolor);
}
.button-action__hidden-trigger {
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 33px;
margin: -1px;
padding: 0;
overflow: hidden;
border: 0;
clip: rect(0 0 0 0);
}
[data-theme="dark"] .button-action__icon {
color: var(--gray-f3);
}

View File

@@ -10,6 +10,7 @@ import whoisCell from './whoisCell';
import LogsSearchLink from '../../ui/LogsSearchLink';
import { sortIp } from '../../../helpers/helpers';
import { LocalStorageHelper, LOCAL_STORAGE_KEYS } from '../../../helpers/localStorageHelper';
import { TABLES_MIN_ROWS } from '../../../helpers/constants';
const COLUMN_MIN_WIDTH = 200;
@@ -90,7 +91,7 @@ class AutoClients extends Component {
onPageSizeChange={(size) => (
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE, size)
)}
minRows={5}
minRows={TABLES_MIN_ROWS}
ofText="/"
previousText={t('previous_btn')}
nextText={t('next_btn')}

View File

@@ -14,7 +14,7 @@ import {
sortIp,
getService,
} from '../../../../helpers/helpers';
import { MODAL_TYPE, LOCAL_TIMEZONE_VALUE } from '../../../../helpers/constants';
import { MODAL_TYPE, LOCAL_TIMEZONE_VALUE, TABLES_MIN_ROWS } from '../../../../helpers/constants';
import Card from '../../../ui/Card';
import CellWrap from '../../../ui/CellWrap';
import LogsSearchLink from '../../../ui/LogsSearchLink';
@@ -347,7 +347,7 @@ const ClientsTable = ({
onPageSizeChange={(size) => (
LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE, size)
)}
minRows={5}
minRows={TABLES_MIN_ROWS}
ofText="/"
previousText={t('previous_btn')}
nextText={t('next_btn')}

View File

@@ -137,6 +137,22 @@ const Examples = (props) => (
example_upstream_reserved
</Trans>
</li>
<li>
<code>[/example.local/]94.140.14.140 2a10:50c0::1:ff</code>: <Trans
components={[
<a
href="https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#upstreams-for-domains"
target="_blank"
rel="noopener noreferrer"
key="0"
>
Link
</a>,
]}
>
example_multiple_upstreams_reserved
</Trans>
</li>
<li>
<code>{COMMENT_LINE_DEFAULT_TOKEN} comment</code>: <Trans>
example_upstream_comment

View File

@@ -2,6 +2,7 @@
align-items: center;
justify-content: space-between;
padding: 0.6rem 1.5rem;
flex-shrink: 0;
}
.card-subtitle {
@@ -19,8 +20,16 @@
max-height: 17.5rem;
}
.dashboard .card-table {
overflow: hidden;
}
.dashboard .card-table-overflow--limited {
max-height: 18rem;
max-height: 292px;
}
.dashboard .ReactTable .rt-tr-group {
min-height: 52px;
}
.card-actions {
@@ -125,7 +134,7 @@
@media (min-width: 992px) {
.dashboard .card:not(.card--full) {
height: 22rem;
height: 360px;
}
}
@@ -140,3 +149,7 @@
.card .logs__row--blue {
background-color: #ecf7ff;
}
[data-theme="dark"] .card .logs__row--blue {
background-color: var(--logs__row--blue-bgcolor);
}

View File

@@ -24,6 +24,13 @@
height: var(--size);
}
.icon--15 {
--size: 0.95rem;
width: var(--size);
height: var(--size);
}
.icon--gray {
color: var(--gray-a5);
}

View File

@@ -239,6 +239,12 @@ const Icons = () => (
<circle cx="12" cy="12" r="9" />
<path d="M16.1215 12.1213H11.8789V7.87866" />
</symbol>
<symbol id="bullets" width="24" height="24" viewBox="0 0 24 24">
<path fillRule="evenodd" clipRule="evenodd" d="M12 7C11.1716 7 10.5 6.32843 10.5 5.5C10.5 4.67157 11.1716 4 12 4C12.8284 4 13.5 4.67157 13.5 5.5C13.5 6.32843 12.8284 7 12 7Z" fill="currentColor" />
<path fillRule="evenodd" clipRule="evenodd" d="M12 13.5C11.1716 13.5 10.5 12.8284 10.5 12C10.5 11.1716 11.1716 10.5 12 10.5C12.8284 10.5 13.5 11.1716 13.5 12C13.5 12.8284 12.8284 13.5 12 13.5Z" fill="currentColor" />
<path fillRule="evenodd" clipRule="evenodd" d="M12 20C11.1716 20 10.5 19.3284 10.5 18.5C10.5 17.6716 11.1716 17 12 17C12.8284 17 13.5 17.6716 13.5 18.5C13.5 19.3284 12.8284 20 12 20Z" fill="currentColor" />
</symbol>
</svg>
);

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { ResponsiveLine } from '@nivo/line';
import addDays from 'date-fns/add_days';
import addHours from 'date-fns/add_hours';
import subDays from 'date-fns/sub_days';
import subHours from 'date-fns/sub_hours';
import dateFormat from 'date-fns/format';
@@ -9,12 +8,14 @@ import round from 'lodash/round';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import './Line.css';
import { msToDays } from '../../helpers/helpers';
import { msToDays, msToHours } from '../../helpers/helpers';
import { TIME_UNITS } from '../../helpers/constants';
const Line = ({
data, color = 'black',
}) => {
const interval = msToDays(useSelector((state) => state.stats.interval));
const interval = useSelector((state) => state.stats.interval);
const timeUnits = useSelector((state) => state.stats.timeUnits);
return <ResponsiveLine
enableArea
@@ -44,12 +45,12 @@ const Line = ({
enableGridY={false}
enablePoints={false}
xFormat={(x) => {
if (interval >= 0 && interval <= 7) {
const hoursAgo = subHours(Date.now(), 24 * interval);
return dateFormat(addHours(hoursAgo, x), 'D MMM HH:00');
if (timeUnits === TIME_UNITS.HOURS) {
const hoursAgo = msToHours(interval) - x - 1;
return dateFormat(subHours(Date.now(), hoursAgo), 'D MMM HH:00');
}
const daysAgo = subDays(Date.now(), interval - 1);
const daysAgo = subDays(Date.now(), msToDays(interval) - 1);
return dateFormat(addDays(daysAgo, x), 'D MMM YYYY');
}}
yFormat={(y) => round(y, 2)}

View File

@@ -9,10 +9,6 @@
overflow: visible;
}
.ReactTable .rt-tbody {
overflow: visible;
}
.ReactTable .rt-noData {
color: var(--rt-nodata-color);
background-color: var(--rt-nodata-bgcolor);

View File

@@ -21,6 +21,7 @@ const Tooltip = ({
delayShow = SHOW_TOOLTIP_DELAY,
delayHide = HIDE_TOOLTIP_DELAY,
onVisibilityChange,
defaultTooltipShown,
}) => {
const { t } = useTranslation();
const touchEventsAvailable = 'ontouchstart' in window;
@@ -75,6 +76,7 @@ const Tooltip = ({
delayShow={delayShowValue}
tooltip={renderTooltip}
onVisibilityChange={onVisibilityChange}
defaultTooltipShown={defaultTooltipShown}
>
{renderTrigger}
</TooltipTrigger>
@@ -97,6 +99,7 @@ Tooltip.propTypes = {
className: propTypes.string,
triggerClass: propTypes.string,
onVisibilityChange: propTypes.func,
defaultTooltipShown: propTypes.bool,
};
export default Tooltip;

View File

@@ -554,3 +554,12 @@ export const DISABLE_PROTECTION_TIMINGS = {
};
export const LOCAL_TIMEZONE_VALUE = 'Local';
export const TABLES_MIN_ROWS = 5;
export const DASHBOARD_TABLES_DEFAULT_PAGE_SIZE = 100;
export const TIME_UNITS = {
HOURS: 'hours',
DAYS: 'days',
};

View File

@@ -43,7 +43,7 @@ export const renderFormattedClientCell = (value, info, isDetailed = false, isLog
const whoisAvailable = whois_info && Object.keys(whois_info).length > 0;
if (name) {
const nameValue = <div className="logs__text logs__text--link logs__text--nowrap" title={`${name} (${value})`}>
const nameValue = <div className="logs__text logs__text--link logs__text--nowrap logs__text--client" title={`${name} (${value})`}>
{name}&nbsp;<small>{`(${value})`}</small>
</div>;

View File

@@ -1,5 +1,5 @@
{
"timeUpdated": "2023-10-08T00:09:45.691Z",
"timeUpdated": "2023-10-15T12:13:01.838Z",
"categories": {
"0": "audio_video_player",
"1": "comments",
@@ -243,7 +243,7 @@
"name": "Australian Broadcasting Corporation",
"categoryId": 8,
"url": "https://www.abc.net.au/",
"companyId": "abc",
"companyId": "australian_government",
"source": "AdGuard"
},
"ablida": {
@@ -3593,6 +3593,13 @@
"url": null,
"companyId": null
},
"bom": {
"name": "Bureau of Meteorology",
"categoryId": 9,
"url": "http://bom.gov.au/",
"companyId": "australian_government",
"source": "AdGuard"
},
"bombora": {
"name": "Bombora",
"categoryId": 6,
@@ -5943,6 +5950,20 @@
"url": "https://discordapp.com/",
"companyId": null
},
"disneyplus": {
"name": "Disney+",
"categoryId": 0,
"url": "https://www.disneyplus.com/",
"companyId": "disney",
"source": "AdGuard"
},
"disneystreaming": {
"name": "Disney Streaming",
"categoryId": 0,
"url": "https://press.disneyplus.com",
"companyId": "disney",
"source": "AdGuard"
},
"display_block": {
"name": "display block",
"categoryId": 4,
@@ -6160,6 +6181,13 @@
"url": "http://www.drawbrid.ge/",
"companyId": "drawbridge"
},
"dreame_tech": {
"name": "Dreame Technology",
"categoryId": 8,
"url": "https://www.dreame.tech/",
"companyId": "xiaomi",
"source": "AdGuard"
},
"dreamlab.pl": {
"name": "DreamLab.pl",
"categoryId": 4,
@@ -10372,7 +10400,7 @@
"name": "Let's Encrypt",
"categoryId": 5,
"url": "https://letsencrypt.org/",
"companyId": "lets_encrypt",
"companyId": "isrg",
"source": "AdGuard"
},
"letv": {
@@ -19444,6 +19472,13 @@
"companyId": "xhamster",
"source": "AdGuard"
},
"xiaomi": {
"name": "Xiaomi",
"categoryId": 8,
"url": "https://www.mi.com/",
"companyId": "xiaomi",
"source": "AdGuard"
},
"xing": {
"name": "Xing",
"categoryId": 6,
@@ -20047,8 +20082,8 @@
"2leep.com": "2leep",
"33across.com": "33across",
"3dstats.com": "3dstats",
"3gppnetwork.org": "3gpp",
"3gpp.org": "3gpp",
"3gppnetwork.org": "3gpp",
"4cdn.org": "4chan",
"4finance.com": "4finance_com",
"4wnet.com": "4w_marketplace",
@@ -20069,7 +20104,6 @@
"aaxads.com": "aaxads.com",
"abtasty.com": "ab_tasty",
"d1447tq2m68ekg.cloudfront.net": "ab_tasty",
"abc.net.au": "abc",
"ab.co": "abc",
"abc-cdn.net.au": "abc",
"abc-host.net": "abc",
@@ -20077,6 +20111,7 @@
"abc-prod.net.au": "abc",
"abc-stage.net.au": "abc",
"abc-test.net.au": "abc",
"abc.net.au": "abc",
"abcaustralia.net.au": "abc",
"abcradio.net.au": "abc",
"ablida.de": "ablida",
@@ -20238,17 +20273,17 @@
"adgorithms.com": "adgorithms",
"adgoto.com": "adgoto",
"adguard.com": "adguard",
"adtidy.org": "adguard",
"agrd.io": "adguard",
"adguard.app": "adguard",
"adguard.info": "adguard",
"adguard.io": "adguard",
"adguard.org": "adguard",
"adtidy.org": "adguard",
"agrd.io": "adguard",
"adguard-dns.com": "adguard_dns",
"adguard-dns.io": "adguard_dns",
"adguardvpn.com": "adguard_vpn",
"adguard-vpn.com": "adguard_vpn",
"adguard-vpn.online": "adguard_vpn",
"adguardvpn.com": "adguard_vpn",
"adhands.ru": "adhands",
"adhese.be": "adhese",
"adhese.com": "adhese",
@@ -20264,9 +20299,9 @@
"cdn.adjs.net": "adjs",
"adjug.com": "adjug",
"adjust.com": "adjust",
"adjust.net.in": "adjust",
"adj.st": "adjust",
"adjust.io": "adjust",
"adjust.net.in": "adjust",
"adjust.world": "adjust",
"apptrace.com": "adjust",
"adk2.com": "adk2",
@@ -20616,17 +20651,17 @@
"amazon.fr": "amazon",
"amazon.it": "amazon",
"d3io1k5o0zdpqr.cloudfront.net": "amazon",
"amazoncrl.com": "amazon",
"aamazoncognito.com": "amazon",
"amazonbrowserapp.es": "amazon",
"amazonbrowserapp.co.uk": "amazon",
"amazon.sa": "amazon",
"amazon.nl": "amazon",
"amazon.in": "amazon",
"amazon.com.mx": "amazon",
"amazon.com.au": "amazon",
"amazon-corp.com": "amazon",
"a2z.com": "amazon",
"aamazoncognito.com": "amazon",
"amazon-corp.com": "amazon",
"amazon.com.au": "amazon",
"amazon.com.mx": "amazon",
"amazon.in": "amazon",
"amazon.nl": "amazon",
"amazon.sa": "amazon",
"amazonbrowserapp.co.uk": "amazon",
"amazonbrowserapp.es": "amazon",
"amazoncrl.com": "amazon",
"firetvcaptiveportal.com": "amazon",
"amazon-adsystem.com": "amazon_adsystem",
"serving-sys.com": "amazon_adsystem",
@@ -20692,31 +20727,32 @@
"eum-appdynamics.com": "appdynamics",
"jscdn.appier.net": "appier",
"apple.com": "apple",
"me.com": "apple",
"apple.news": "apple",
"apple-dns.net": "apple",
"aaplimg.com": "apple",
"icloud.com": "apple",
"itunes.com": "apple",
"icloud-content.com": "apple",
"mzstatic.com": "apple",
"cdn-apple.com": "apple",
"apple-mapkit.com": "apple",
"icons.axm-usercontent-apple.com": "apple",
"apple-cloudkit.com": "apple",
"apzones.com": "apple",
"apple-dns.net": "apple",
"apple-livephotoskit.com": "apple",
"apple-mapkit.com": "apple",
"apple.news": "apple",
"apzones.com": "apple",
"cdn-apple.com": "apple",
"icloud-content.com": "apple",
"icloud.com": "apple",
"icons.axm-usercontent-apple.com": "apple",
"itunes.com": "apple",
"me.com": "apple",
"mzstatic.com": "apple",
"safebrowsing.apple": "apple",
"safebrowsing.g.applimg.com": "apple",
"iadsdk.apple.com": "apple_ads",
"applifier.com": "applifier",
"assets.applovin.com": "applovin",
"applvn.com": "applovin",
"applovin.com": "applovin",
"applvn.com": "applovin",
"appmetrx.com": "appmetrx",
"adnxs.com": "appnexus",
"adnxs.net": "appnexus",
"appsflyer.com": "appsflyer",
"appsflyersdk.com": "appsflyer",
"adne.tv": "apptv",
"readserver.net": "apptv",
"www.apture.com": "apture",
@@ -20926,6 +20962,7 @@
"secure.apps.shappify.com": "bold",
"boldchat.com": "boldchat",
"boltdns.net": "boltdns.net",
"bom.gov.au": "bom",
"ml314.com": "bombora",
"bongacams.com": "bongacams.com",
"bonial.com": "bonial",
@@ -21170,8 +21207,8 @@
"cloud-media.fr": "cloud-media.fr",
"cloudflare.com": "cloudflare",
"cloudflare.net": "cloudflare",
"cloudflare-dns.com": "cloudflare",
"cloudflare-dm-cmpimg.com": "cloudflare",
"cloudflare-dns.com": "cloudflare",
"cloudflare-ipfs.com": "cloudflare",
"cloudflare-quic.com": "cloudflare",
"cloudflare-terms-of-service-abuse.com": "cloudflare",
@@ -21184,8 +21221,10 @@
"cloudflareresolve.com": "cloudflare",
"cloudflaressl.com": "cloudflare",
"cloudflarestatus.com": "cloudflare",
"cloudflarestream.com": "cloudflare",
"pacloudflare.com": "cloudflare",
"sn-cloudflare.com": "cloudflare",
"videodelivery.net": "cloudflare",
"cloudimg.io": "cloudimage.io",
"cloudinary.com": "cloudinary",
"clovenetwork.com": "clove_network",
@@ -21454,6 +21493,10 @@
"directadvert.ru": "directadvert",
"directrev.com": "directrev",
"discordapp.com": "discord",
"disneyplus.com": "disneyplus",
"bamgrid.com": "disneystreaming",
"dssedge.com": "disneystreaming",
"dssott.com": "disneystreaming",
"d81mfvml8p5ml.cloudfront.net": "display_block",
"disqus.com": "disqus",
"disquscdn.com": "disqus",
@@ -21497,6 +21540,8 @@
"doubleverify.com": "doubleverify",
"wrating.com": "dratio",
"adsymptotic.com": "drawbridge",
"dreame.tech": "dreame_tech",
"dreametech.com": "dreame_tech",
"dreamlab.pl": "dreamlab.pl",
"drift.com": "drift",
"js.driftt.com": "drift",
@@ -21717,18 +21762,18 @@
"findologic.com": "findologic.com",
"app-measurement.com": "firebase",
"fcm.googleapis.com": "firebase",
"firebaseappcheck.googleapis.com": "firebase",
"firebaseapp.com": "firebase",
"firebase.com": "firebase",
"firebasedynamiclinks.googleapis.com": "firebase",
"firebase.google.com": "firebase",
"firebase.googleapis.com": "firebase",
"firebaseapp.com": "firebase",
"firebaseappcheck.googleapis.com": "firebase",
"firebasedynamiclinks-ipv4.googleapis.com": "firebase",
"firebasedynamiclinks-ipv6.googleapis.com": "firebase",
"firebase.googleapis.com": "firebase",
"firebase.google.com": "firebase",
"firebasedynamiclinks.googleapis.com": "firebase",
"firebaseinappmessaging.googleapis.com": "firebase",
"firebaseinstallations.googleapis.com": "firebase",
"firebaselogging.googleapis.com": "firebase",
"firebaselogging-pa.googleapis.com": "firebase",
"firebaselogging.googleapis.com": "firebase",
"firebaseperusertopics-pa.googleapis.com": "firebase",
"firebaseremoteconfig.googleapis.com": "firebase",
"firebaseio.com": "firebaseio.com",
@@ -21806,9 +21851,9 @@
"freegeoip.net": "freegeoip_net",
"freenet.de": "freenet_de",
"freent.de": "freenet_de",
"freeviewaustralia.tv": "freeview",
"freeview.com.au": "freeview",
"freeview.com": "freeview",
"freeview.com.au": "freeview",
"freeviewaustralia.tv": "freeview",
"fwmrm.net": "freewheel",
"heimdall.fresh8.co": "fresh8",
"d36mpcpuzc4ztk.cloudfront.net": "freshdesk",
@@ -21958,7 +22003,6 @@
"google.ae": "google",
"google.al": "google",
"google.am": "google",
"googleapis.cn": "google",
"google.as": "google",
"google.az": "google",
"google.ba": "google",
@@ -21981,11 +22025,20 @@
"google.co.bw": "google",
"google.co.ck": "google",
"google.co.cr": "google",
"googlecode.com": "google",
"google.co.il": "google",
"google.co.ke": "google",
"google.co.kr": "google",
"google.co.ls": "google",
"google.co.mz": "google",
"google.co.nz": "google",
"google.co.tz": "google",
"google.co.ug": "google",
"google.co.uz": "google",
"google.co.ve": "google",
"google.co.vi": "google",
"google.co.za": "google",
"google.co.zm": "google",
"google.co.zw": "google",
"google.com.af": "google",
"google.com.ag": "google",
"google.com.ai": "google",
@@ -22033,20 +22086,9 @@
"google.com.uy": "google",
"google.com.vc": "google",
"google.com.vn": "google",
"google.co.mz": "google",
"google.co.nz": "google",
"google.co.tz": "google",
"google.co.ug": "google",
"google.co.uz": "google",
"google.co.ve": "google",
"google.co.vi": "google",
"google.co.za": "google",
"google.co.zm": "google",
"google.co.zw": "google",
"google.cv": "google",
"google.dj": "google",
"google.dm": "google",
"googledownloads.cn": "google",
"google.ee": "google",
"google.fm": "google",
"google.ga": "google",
@@ -22088,7 +22130,6 @@
"google.net": "google",
"google.nr": "google",
"google.nu": "google",
"googleoptimize.com": "google",
"google.org": "google",
"google.pn": "google",
"google.ps": "google",
@@ -22112,8 +22153,12 @@
"google.us": "google",
"google.vg": "google",
"google.vu": "google",
"googleweblight.in": "google",
"google.ws": "google",
"googleapis.cn": "google",
"googlecode.com": "google",
"googledownloads.cn": "google",
"googleoptimize.com": "google",
"googleweblight.in": "google",
"googlezip.net": "google",
"gstatic.cn": "google",
"news.google.com": "google",
@@ -22140,10 +22185,10 @@
"alt7-mtalk.google.com": "google_chat",
"alt8-mtalk.google.com": "google_chat",
"chat.google.com": "google_chat",
"mobile-gtalk4.l.google.com": "google_chat",
"mobile-gtalk.l.google.com": "google_chat",
"mtalk4.google.com": "google_chat",
"mobile-gtalk4.l.google.com": "google_chat",
"mtalk.google.com": "google_chat",
"mtalk4.google.com": "google_chat",
"talk.google.com": "google_chat",
"talk.l.google.com": "google_chat",
"talkx.l.google.com": "google_chat",
@@ -22163,10 +22208,10 @@
"mail-ads.google.com": "google_email",
"fonts.googleapis.com": "google_fonts",
"cloudfunctions.net": "google_hosted",
"ghs46.googlehosted.com": "google_hosted",
"ghs4.googlehosted.com": "google_hosted",
"ghs6.googlehosted.com": "google_hosted",
"ghs.googlehosted.com": "google_hosted",
"ghs4.googlehosted.com": "google_hosted",
"ghs46.googlehosted.com": "google_hosted",
"ghs6.googlehosted.com": "google_hosted",
"googlehosted.l.googleusercontent.com": "google_hosted",
"run.app": "google_hosted",
"supl.google.com": "google_location",
@@ -22180,9 +22225,9 @@
"maps.google.ca": "google_maps",
"maps.google.ch": "google_maps",
"maps.google.co.jp": "google_maps",
"maps.google.co.uk": "google_maps",
"maps.google.com": "google_maps",
"maps.google.com.mx": "google_maps",
"maps.google.co.uk": "google_maps",
"maps.google.es": "google_maps",
"maps.google.se": "google_maps",
"maps.gstatic.com": "google_maps",
@@ -22190,6 +22235,8 @@
"adservice.google.ca": "google_marketing",
"adservice.google.co.in": "google_marketing",
"adservice.google.co.kr": "google_marketing",
"adservice.google.co.uk": "google_marketing",
"adservice.google.co.za": "google_marketing",
"adservice.google.com": "google_marketing",
"adservice.google.com.ar": "google_marketing",
"adservice.google.com.au": "google_marketing",
@@ -22203,8 +22250,6 @@
"adservice.google.com.tr": "google_marketing",
"adservice.google.com.tw": "google_marketing",
"adservice.google.com.vn": "google_marketing",
"adservice.google.co.uk": "google_marketing",
"adservice.google.co.za": "google_marketing",
"adservice.google.de": "google_marketing",
"adservice.google.dk": "google_marketing",
"adservice.google.es": "google_marketing",
@@ -22219,17 +22264,17 @@
"googlesyndication-cn.com": "google_marketing",
"duo.google.com": "google_meet",
"hangouts.clients6.google.com": "google_meet",
"hangouts.googleapis.com": "google_meet",
"hangouts.google.com": "google_meet",
"hangouts.googleapis.com": "google_meet",
"meet.google.com": "google_meet",
"meetings.googleapis.com": "google_meet",
"stun1.l.google.com": "google_meet",
"stun.l.google.com": "google_meet",
"stun1.l.google.com": "google_meet",
"ggpht.com": "google_photos",
"play-fe.googleapis.com": "google_play",
"play.googleapis.com": "google_play",
"play.google.com": "google_play",
"play-lh.googleusercontent.com": "google_play",
"play.google.com": "google_play",
"play.googleapis.com": "google_play",
"1e100cdn.net": "google_servers",
"gvt1.com": "google_servers",
"gvt2.com": "google_servers",
@@ -22531,9 +22576,9 @@
"iprom.net": "iprom",
"ipromote.com": "ipromote",
"clickmanage.com": "iprospect",
"qy.net": "iqiyi",
"iqiyi.com": "iqiyi",
"iq.com": "iqiyi",
"iqiyi.com": "iqiyi",
"qy.net": "iqiyi",
"addelive.com": "ironsource",
"afdads.com": "ironsource",
"delivery47.com": "ironsource",
@@ -22772,11 +22817,11 @@
"footprint.net": "level3_communications",
"alphonso.tv": "lgads",
"lgads.tv": "lgads",
"lg.com": "lgtv",
"lge.com": "lgtv",
"lgsmartad.com": "lgtv",
"lgtvcommon.com": "lgtv",
"lgtvsdp.com": "lgtv",
"lge.com": "lgtv",
"lg.com": "lgtv",
"licensebuttons.net": "licensebuttons.net",
"lfstmedia.com": "lifestreet_media",
"content-recommendation.net": "ligatus",
@@ -22971,8 +23016,8 @@
"s1.mediaad.org": "mediaad",
"mlnadvertising.com": "mediaglu",
"fhserve.com": "mediahub",
"medialab.la": "medialab",
"media-lab.ai": "medialab",
"medialab.la": "medialab",
"adnet.ru": "medialand",
"medialand.ru": "medialand",
"medialead.de": "medialead",
@@ -23352,12 +23397,12 @@
"opinary.com": "opinary",
"opinionbar.com": "opinionbar",
"emagazines.com": "oplytic",
"oppomobile.com": "oppo",
"heytapmobi.com": "oppo",
"heytapmobile.com": "oppo",
"heytapdl.com": "oppo",
"allawnos.com": "oppo",
"allawntech.com": "oppo",
"heytapdl.com": "oppo",
"heytapmobi.com": "oppo",
"heytapmobile.com": "oppo",
"oppomobile.com": "oppo",
"opta.net": "opta.net",
"optaim.com": "optaim",
"cookielaw.org": "optanaon",
@@ -23484,8 +23529,9 @@
"loveadvert.ru": "play_by_mamba",
"playbuzz.com": "playbuzz.com",
"pof.com": "plenty_of_fish",
"plex.tv": "plex",
"plex.bz": "plex",
"plex.direct": "plex",
"plex.tv": "plex",
"analytics.plex.tv": "plex_metrics",
"metrics.plex.tv": "plex_metrics",
"plista.com": "plista",
@@ -23896,6 +23942,7 @@
"samsungsds.com": "samsungsds",
"internetat.tv": "samsungtv",
"samsungcloud.tv": "samsungtv",
"tizenservice.com": "samsungtv",
"ilsemedia.nl": "sanoma.fi",
"sanoma.fi": "sanoma.fi",
"d13im3ek7neeqp.cloudfront.net": "sap_crm",
@@ -24000,12 +24047,12 @@
"cdn.shopify.com": "shopify",
"myshopify.com": "shopify",
"shop.app": "shopify",
"shopifyapps.com": "shopify",
"shopifycdn.net": "shopify",
"shopify.co.za": "shopify",
"shopify.com.au": "shopify",
"shopify.com.mx": "shopify",
"shopify.co.za": "shopify",
"shopify.dev": "shopify",
"shopifyapps.com": "shopify",
"shopifycdn.net": "shopify",
"shopifynetwork.com": "shopify",
"shopifypreview.com": "shopify",
"shopifysvc.com": "shopify_stats",
@@ -24042,8 +24089,8 @@
"pages04.net": "silverpop",
"pages05.net": "silverpop",
"similardeals.net": "similardeals.net",
"similarweb.io": "similarweb",
"similarweb.com": "similarweb",
"similarweb.io": "similarweb",
"d8rk54i4mohrb.cloudfront.net": "simplereach",
"simplereach.com": "simplereach",
"simpli.fi": "simpli.fi",
@@ -24082,10 +24129,10 @@
"skypeassets.com": "skype",
"skysa.com": "skysa",
"skyscnr.com": "skyscnr.com",
"slack.com": "slack",
"slackb.com": "slack",
"slack-edge.com": "slack",
"slack-imgs.com": "slack",
"slack.com": "slack",
"slackb.com": "slack",
"slashdot.org": "slashdot_widget",
"sleeknotestaticcontent.sleeknote.com": "sleeknote",
"resultspage.com": "sli_systems",
@@ -24356,8 +24403,8 @@
"teaser.cc": "teaser.cc",
"emailretargeting.com": "tedemis",
"tracking.dsmmadvantage.com": "teletech",
"telstra.com.au": "telstra",
"telstra.com": "telstra",
"telstra.com.au": "telstra",
"tenderapp.com": "tender",
"tensitionschoo.club": "tensitionschoo.club",
"watch.teroti.com": "teroti",
@@ -24728,9 +24775,9 @@
"tools.vpscash.nl": "vpscash",
"vsassets.io": "vs",
"exp-tas.com": "vscode",
"vscode-unpkg.net": "vscode",
"v0cdn.net": "vscode",
"vscode-cdn.net": "vscode",
"vscode-unpkg.net": "vscode",
"vtracy.de": "vtracy.de",
"liftoff.io": "vungle",
"vungle.com": "vungle",
@@ -24803,8 +24850,8 @@
"wetter.com": "wetter_com",
"wettercomassets.com": "wetter_com",
"whatsbroadcast.com": "whatbroadcast",
"whatsapp.net": "whatsapp",
"whatsapp.com": "whatsapp",
"whatsapp.net": "whatsapp",
"whisper.onelink.me": "whisper",
"whisper.sh": "whisper",
"amung.us": "whos.amung.us",
@@ -24876,6 +24923,13 @@
"xhamsterlive.com": "xhamster",
"xhamsterpremium.com": "xhamster",
"xhcdn.com": "xhamster",
"huami.com": "xiaomi",
"mi-img.com": "xiaomi",
"mi.com": "xiaomi",
"miui.com": "xiaomi",
"xiaomi.com": "xiaomi",
"xiaomi.net": "xiaomi",
"xiaomiyoupin.com": "xiaomi",
"xing-share.com": "xing",
"xing.com": "xing",
"xmediaclicks.com": "xmediaclicks",

View File

@@ -1,6 +1,11 @@
import { handleActions } from 'redux-actions';
import { normalizeTopClients } from '../helpers/helpers';
import { DAY, HOUR, STATS_INTERVALS_DAYS } from '../helpers/constants';
import {
DAY,
HOUR,
STATS_INTERVALS_DAYS,
TIME_UNITS,
} from '../helpers/constants';
import * as actions from '../actions/stats';
@@ -18,6 +23,7 @@ const defaultStats = {
numReplacedSafebrowsing: 0,
numReplacedSafesearch: 0,
avgProcessingTime: 0,
timeUnits: TIME_UNITS.HOURS,
};
const stats = handleActions(
@@ -60,6 +66,7 @@ const stats = handleActions(
avg_processing_time: avgProcessingTime,
top_upstreams_responses: topUpstreamsResponses,
top_upstrems_avg_time: topUpstreamsAvgTime,
time_units: timeUnits,
} = payload;
const newState = {
@@ -81,6 +88,7 @@ const stats = handleActions(
avgProcessingTime,
topUpstreamsResponses,
topUpstreamsAvgTime,
timeUnits,
};
return newState;

12
go.mod
View File

@@ -3,9 +3,9 @@ module github.com/AdguardTeam/AdGuardHome
go 1.20
require (
github.com/AdguardTeam/dnsproxy v0.56.1
github.com/AdguardTeam/golibs v0.17.1
github.com/AdguardTeam/urlfilter v0.17.0
github.com/AdguardTeam/dnsproxy v0.56.3
github.com/AdguardTeam/golibs v0.17.2
github.com/AdguardTeam/urlfilter v0.17.3
github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.2.7
github.com/bluele/gcache v0.0.2
@@ -17,7 +17,7 @@ require (
github.com/google/gopacket v1.1.19
github.com/google/renameio/v2 v2.0.0
github.com/google/uuid v1.3.1
github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a
github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
github.com/kardianos/service v1.2.2
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
@@ -27,9 +27,9 @@ require (
// own code for that. Perhaps, use gopacket.
github.com/mdlayher/raw v0.1.0
github.com/miekg/dns v1.1.56
github.com/quic-go/quic-go v0.39.0
github.com/quic-go/quic-go v0.39.2
github.com/stretchr/testify v1.8.4
github.com/ti-mo/netfilter v0.5.0
github.com/ti-mo/netfilter v0.5.1
go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.14.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d

24
go.sum
View File

@@ -1,9 +1,9 @@
github.com/AdguardTeam/dnsproxy v0.56.1 h1:QltfyWO7k4mxWERCEYDzkQnKzvZX/zkneWjbuJ0TU6o=
github.com/AdguardTeam/dnsproxy v0.56.1/go.mod h1:fqmehcE3cHFNqKbWQpIjGk7GqBy7ur1v5At499lFjRc=
github.com/AdguardTeam/golibs v0.17.1 h1:j3Ehhld5GI/amcHYG+CF0sJ4OOzAQ06BY3N/iBYJZ1M=
github.com/AdguardTeam/golibs v0.17.1/go.mod h1:DKhCIXHcUYtBhU8ibTLKh1paUL96n5zhQBlx763sj+U=
github.com/AdguardTeam/urlfilter v0.17.0 h1:tUzhtR9wMx704GIP3cibsDQJrixlMHfwoQbYJfPdFow=
github.com/AdguardTeam/urlfilter v0.17.0/go.mod h1:bbuZjPUzm/Ip+nz5qPPbwIP+9rZyQbQad8Lt/0fCulU=
github.com/AdguardTeam/dnsproxy v0.56.3 h1:WP1FooLfZQPHEH2SuwMtJsOurDt32rubGx0OddcsKT0=
github.com/AdguardTeam/dnsproxy v0.56.3/go.mod h1:ZvkbM71HwpilgkCnTubDiR4Ba6x5Qvnhy2iasMWaTDM=
github.com/AdguardTeam/golibs v0.17.2 h1:vg6wHMjUKscnyPGRvxS5kAt7Uw4YxcJiITZliZ476W8=
github.com/AdguardTeam/golibs v0.17.2/go.mod h1:DKhCIXHcUYtBhU8ibTLKh1paUL96n5zhQBlx763sj+U=
github.com/AdguardTeam/urlfilter v0.17.3 h1:fg/ObbnO0Cv6aw0tW6N/ETDMhhNvmcUUOZ7HlmKC3rw=
github.com/AdguardTeam/urlfilter v0.17.3/go.mod h1:Jru7jFfeH2CoDf150uDs+rRYcZBzHHBz05r9REyDKyE=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
@@ -49,8 +49,8 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a h1:S33o3djA1nPRd+d/bf7jbbXytXuK/EoXow7+aa76grQ=
github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a/go.mod h1:zmdm3sTSDP3vOOX3CEWRkkRHtKr1DxBx+J1OQFoDQQs=
github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c h1:PgxFEySCI41sH0mB7/2XswdXbUykQsRUGod8Rn+NubM=
github.com/insomniacslk/dhcp v0.0.0-20231016090811-6a2c8fbdcc1c/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
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.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
@@ -94,8 +94,8 @@ 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/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg=
github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.39.0 h1:AgP40iThFMY0bj8jGxROhw3S0FMGa8ryqsmi9tBH3So=
github.com/quic-go/quic-go v0.39.0/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q=
github.com/quic-go/quic-go v0.39.2 h1:hmwAf8zAHlvan0Y5PXxeeBFZEW17IW99sXLry8I2kjk=
github.com/quic-go/quic-go v0.39.2/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q=
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -105,8 +105,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ti-mo/netfilter v0.2.0/go.mod h1:8GbBGsY/8fxtyIdfwy29JiluNcPK4K7wIT+x42ipqUU=
github.com/ti-mo/netfilter v0.5.0 h1:MZmsUw5bFRecOb0AeyjOPxTHg4UxYzyEs0Ek/6Lxoy8=
github.com/ti-mo/netfilter v0.5.0/go.mod h1:nt+8B9hx/QpqHr7Hazq+2qMCCA8u2OTkyc/7+U9ARz8=
github.com/ti-mo/netfilter v0.5.1 h1:cqamEd1c1zmpfpqvInLOro0Znq/RAfw2QL5wL2rAR/8=
github.com/ti-mo/netfilter v0.5.1/go.mod h1:h9UPQ3ZrTZGBitay+LETMxZvNgWGK/efTUcqES2YiLw=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg=

View File

@@ -182,6 +182,7 @@ func (s *Server) accessListJSON() (j accessListJSON) {
}
}
// handleAccessList handles requests to the GET /control/access/list endpoint.
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
aghhttp.WriteJSONResponseOK(w, r, s.accessListJSON())
}
@@ -224,6 +225,7 @@ func validateStrUniq(clients []string) (uc aghalg.UniqChecker[string], err error
return uc, uc.Validate()
}
// handleAccessSet handles requests to the POST /control/access/set endpoint.
func (s *Server) handleAccessSet(w http.ResponseWriter, r *http.Request) {
list := &accessListJSON{}
err := json.NewDecoder(r.Body).Decode(&list)

View File

@@ -8,6 +8,7 @@ import (
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/quic-go/quic-go"
)
@@ -151,6 +152,8 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
// DNS-over-HTTPS requests, it will return the hostname part of the Host header
// if there is one.
func clientServerName(pctx *proxy.DNSContext, proto proxy.Proto) (srvName string, err error) {
from := "tls conn"
switch proto {
case proxy.ProtoHTTPS:
r := pctx.HTTPRequest
@@ -164,6 +167,7 @@ func clientServerName(pctx *proxy.DNSContext, proto proxy.Proto) (srvName string
}
srvName = host
from = "host header"
}
case proxy.ProtoQUIC:
qConn := pctx.QUICConnection
@@ -183,5 +187,7 @@ func clientServerName(pctx *proxy.DNSContext, proto proxy.Proto) (srvName string
srvName = tc.ConnectionState().ServerName
}
log.Debug("dnsforward: got client server name %q from %s", srvName, from)
return srvName, nil
}

View File

@@ -7,6 +7,8 @@ import (
"net/http"
"net/netip"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@@ -444,19 +446,10 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
return nil, nil
}
for _, u := range upstreams {
var ups string
var domains []string
ups, domains, err = separateUpstream(u)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
_, err = validateUpstream(ups, domains)
if err != nil {
return nil, fmt.Errorf("validating upstream %q: %w", u, err)
}
err = validateUpstreamConfig(upstreams)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
conf, err = proxy.ParseUpstreamsConfig(
@@ -467,6 +460,7 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
},
)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
} else if len(conf.Upstreams) == 0 {
return nil, errors.Error("no default upstreams specified")
@@ -475,6 +469,31 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
return conf, nil
}
// validateUpstreamConfig validates each upstream from the upstream
// configuration and returns an error if any upstream is invalid.
//
// TODO(e.burkov): Move into aghnet or even into dnsproxy.
func validateUpstreamConfig(conf []string) (err error) {
for _, u := range conf {
var ups []string
var domains []string
ups, domains, err = separateUpstream(u)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
for _, addr := range ups {
_, err = validateUpstream(addr, len(domains) > 0)
if err != nil {
return fmt.Errorf("validating upstream %q: %w", addr, err)
}
}
}
return nil
}
// ValidateUpstreams validates each upstream and returns an error if any
// upstream is invalid or if there are no default upstreams specified.
//
@@ -534,14 +553,14 @@ var protocols = []string{
}
// validateUpstream returns an error if u alongside with domains is not a valid
// upstream configuration. useDefault is true if the upstream is
// upstream configuration. usesDefault is true if the upstream is
// domain-specific and is configured to point at the default upstream server
// which is validated separately. The upstream is considered domain-specific
// only if domains is at least not nil.
func validateUpstream(u string, domains []string) (useDefault bool, err error) {
// which is validated separately. specific reflects if the upstream is
// domain-specific.
func validateUpstream(u string, specific bool) (usesDefault bool, err error) {
// The special server address '#' means that default server must be used.
if useDefault = u == "#" && domains != nil; useDefault {
return useDefault, nil
if u == "#" && specific {
return true, nil
}
// Check if the upstream has a valid protocol prefix.
@@ -567,12 +586,12 @@ func validateUpstream(u string, domains []string) (useDefault bool, err error) {
return false, err
}
// separateUpstream returns the upstream and the specified domains. domains is
// nil when the upstream is not domains-specific. Otherwise it may also be
// separateUpstream returns the upstreams and the specified domains. domains
// is nil when the upstream is not domains-specific. Otherwise it may also be
// empty.
func separateUpstream(upstreamStr string) (ups string, domains []string, err error) {
func separateUpstream(upstreamStr string) (upstreams, domains []string, err error) {
if !strings.HasPrefix(upstreamStr, "[/") {
return upstreamStr, nil, nil
return []string{upstreamStr}, nil, nil
}
defer func() { err = errors.Annotate(err, "bad upstream for domain %q: %w", upstreamStr) }()
@@ -582,9 +601,9 @@ func separateUpstream(upstreamStr string) (ups string, domains []string, err err
case 2:
// Go on.
case 1:
return "", nil, errors.Error("missing separator")
return nil, nil, errors.Error("missing separator")
default:
return "", []string{}, errors.Error("duplicated separator")
return nil, nil, errors.Error("duplicated separator")
}
for i, host := range strings.Split(parts[0], "/") {
@@ -594,21 +613,22 @@ func separateUpstream(upstreamStr string) (ups string, domains []string, err err
err = netutil.ValidateDomainName(strings.TrimPrefix(host, "*."))
if err != nil {
return "", domains, fmt.Errorf("domain at index %d: %w", i, err)
return nil, nil, fmt.Errorf("domain at index %d: %w", i, err)
}
domains = append(domains, host)
}
return parts[1], domains, nil
return strings.Fields(parts[1]), domains, nil
}
// healthCheckFunc is a signature of function to check if upstream exchanges
// properly.
type healthCheckFunc func(u upstream.Upstream) (err error)
// checkDNSUpstreamExc checks if the DNS upstream exchanges correctly.
func checkDNSUpstreamExc(u upstream.Upstream) (err error) {
// checkExchange is a [healthCheckFunc] that checks if the DNS upstream
// exchanges correctly.
func checkExchange(u upstream.Upstream) (err error) {
// testTLD is the special-use fully-qualified domain name for testing the
// DNS server reachability.
//
@@ -638,11 +658,11 @@ func checkDNSUpstreamExc(u upstream.Upstream) (err error) {
return nil
}
// checkPrivateUpstreamExc checks if the upstream for resolving private
// addresses exchanges correctly.
// checkPrivateExchange is a [healthCheckFunc] that checks if the upstream for
// resolving private addresses exchanges correctly.
//
// TODO(e.burkov): Think about testing the ip6.arpa. as well.
func checkPrivateUpstreamExc(u upstream.Upstream) (err error) {
func checkPrivateExchange(u upstream.Upstream) (err error) {
// inAddrArpaTLD is the special-use fully-qualified domain name for PTR IP
// address resolution.
//
@@ -683,75 +703,153 @@ func (err domainSpecificTestError) Error() (msg string) {
return fmt.Sprintf("WARNING: %s", err.error)
}
// parseUpstreamLine parses line and creates the [upstream.Upstream] using opts
// and information from [s.dnsFilter.EtcHosts]. It returns an error if the line
// is not a valid upstream line, see [upstream.AddressToUpstream]. It's a
// caller's responsibility to close u.
func (s *Server) parseUpstreamLine(
line string,
opts *upstream.Options,
) (u upstream.Upstream, specific bool, err error) {
// Separate upstream from domains list.
upstreamAddr, domains, err := separateUpstream(line)
// checkUpstreamAddr creates the upstream using opts and, possibly, information
// from system hosts files, then checks if the DNS upstream exchanges correctly.
// It returns an error if addr is not valid DNS upstream address or the upstream
// is not exchanging correctly.
//
// TODO(e.burkov): Remove the receiver.
func (s *Server) checkUpstreamAddr(
addr string,
specific bool,
basicOpts *upstream.Options,
check healthCheckFunc,
) (err error) {
usesDefault, err := validateUpstream(addr, specific)
if err != nil {
return nil, false, fmt.Errorf("wrong upstream format: %w", err)
}
specific = len(domains) > 0
useDefault, err := validateUpstream(upstreamAddr, domains)
if err != nil {
return nil, specific, fmt.Errorf("wrong upstream format: %w", err)
} else if useDefault {
return nil, specific, nil
}
log.Debug("dnsforward: checking if upstream %q works", upstreamAddr)
opts = &upstream.Options{
Bootstrap: opts.Bootstrap,
Timeout: opts.Timeout,
PreferIPv6: opts.PreferIPv6,
}
// dnsFilter can be nil during application update.
if s.dnsFilter != nil {
recs := s.dnsFilter.EtcHostsRecords(extractUpstreamHost(upstreamAddr))
for _, rec := range recs {
opts.ServerIPAddrs = append(opts.ServerIPAddrs, rec.Addr.AsSlice())
}
sortNetIPAddrs(opts.ServerIPAddrs, opts.PreferIPv6)
}
u, err = upstream.AddressToUpstream(upstreamAddr, opts)
if err != nil {
return nil, specific, fmt.Errorf("creating upstream for %q: %w", upstreamAddr, err)
}
return u, specific, nil
}
func (s *Server) checkDNS(line string, opts *upstream.Options, check healthCheckFunc) (err error) {
if IsCommentOrEmpty(line) {
return fmt.Errorf("wrong upstream format: %w", err)
} else if usesDefault {
return nil
}
var u upstream.Upstream
var specific bool
log.Debug("dnsforward: checking if upstream %q works", addr)
defer func() {
if err != nil && specific {
err = domainSpecificTestError{error: err}
}
}()
u, specific, err = s.parseUpstreamLine(line, opts)
if err != nil || u == nil {
return err
opts := &upstream.Options{
Bootstrap: basicOpts.Bootstrap,
Timeout: basicOpts.Timeout,
PreferIPv6: basicOpts.PreferIPv6,
}
// dnsFilter can be nil during application update.
//
// TODO(e.burkov): Remove when update dnsproxy.
if s.dnsFilter != nil {
recs := s.dnsFilter.EtcHostsRecords(extractUpstreamHost(addr))
for _, rec := range recs {
opts.ServerIPAddrs = append(opts.ServerIPAddrs, rec.Addr.AsSlice())
}
sortNetIPAddrs(opts.ServerIPAddrs, opts.PreferIPv6)
}
u, err := upstream.AddressToUpstream(addr, opts)
if err != nil {
return fmt.Errorf("creating upstream for %q: %w", addr, err)
}
defer func() { err = errors.WithDeferred(err, u.Close()) }()
return check(u)
}
// checkResult is a result of checking an upstream server.
type checkResult = struct {
// status is an error message if the upstream server is not working. It's
// nil for working upstreams.
status error
// address is the upstream server address as given in the request. It may
// appear to be a whole line if it's incorrect itself.
address string
}
// checkDNS parses an upstream configuration line using opts and checks if the
// specified upstreams are working using check. countWG is decremented when the
// expected number of results added to resNum, then results are sent to resCh.
//
// TODO(e.burkov): Remove the receiver.
func (s *Server) checkDNS(
line string,
opts *upstream.Options,
check healthCheckFunc,
countWG *sync.WaitGroup,
resNum *atomic.Int32,
resCh chan<- checkResult,
) {
defer log.OnPanic("dnsforward: checking upstreams")
addrs, domains, err := separateUpstream(line)
if err != nil {
resNum.Add(1)
countWG.Done()
resCh <- checkResult{
address: line,
status: fmt.Errorf("wrong upstream format: %w", err),
}
return
}
resNum.Add(int32(len(addrs)))
countWG.Done()
specific := len(domains) > 0
for _, addr := range addrs {
resCh <- checkResult{
address: addr,
status: s.checkUpstreamAddr(addr, specific, opts, check),
}
}
}
// check returns the mapping of upstream addresses to their check results.
func (s *Server) check(req *upstreamJSON, opts *upstream.Options) (result map[string]string) {
req.Upstreams = stringutil.FilterOut(req.Upstreams, IsCommentOrEmpty)
req.FallbackDNS = stringutil.FilterOut(req.FallbackDNS, IsCommentOrEmpty)
req.PrivateUpstreams = stringutil.FilterOut(req.PrivateUpstreams, IsCommentOrEmpty)
countWG := &sync.WaitGroup{}
countWG.Add(len(req.Upstreams) + len(req.FallbackDNS) + len(req.PrivateUpstreams))
resNum := &atomic.Int32{}
resCh := make(chan checkResult)
for _, addr := range req.Upstreams {
go s.checkDNS(addr, opts, checkExchange, countWG, resNum, resCh)
}
for _, addr := range req.FallbackDNS {
go s.checkDNS(addr, opts, checkExchange, countWG, resNum, resCh)
}
for _, addr := range req.PrivateUpstreams {
go s.checkDNS(addr, opts, checkPrivateExchange, countWG, resNum, resCh)
}
// Wait until all the servers are counted and enqueued.
countWG.Wait()
n := resNum.Load()
result = make(map[string]string, n)
for i := int32(0); i < n; i++ {
// TODO(e.burkov): Upstreams intended for different purposes should
// be distinguished in the result, even if specified equally.
res := <-resCh
if res.status != nil {
result[res.address] = res.status.Error()
} else {
result[res.address] = "OK"
}
}
return result
}
// handleTestUpstreamDNS handles requests to the POST /control/test_upstream_dns
// endpoint.
func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
req := &upstreamJSON{}
err := json.NewDecoder(r.Body).Decode(req)
@@ -761,65 +859,18 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
return
}
bootstrapAddrs := stringutil.FilterOut(req.BootstrapDNS, IsCommentOrEmpty)
if len(bootstrapAddrs) == 0 {
bootstrapAddrs = defaultBootstrap
}
opts := &upstream.Options{
Bootstrap: req.BootstrapDNS,
Bootstrap: bootstrapAddrs,
Timeout: s.conf.UpstreamTimeout,
PreferIPv6: s.conf.BootstrapPreferIPv6,
}
if len(opts.Bootstrap) == 0 {
opts.Bootstrap = defaultBootstrap
}
type upsCheckResult = struct {
err error
host string
}
req.Upstreams = stringutil.FilterOut(req.Upstreams, IsCommentOrEmpty)
req.FallbackDNS = stringutil.FilterOut(req.FallbackDNS, IsCommentOrEmpty)
req.PrivateUpstreams = stringutil.FilterOut(req.PrivateUpstreams, IsCommentOrEmpty)
upsNum := len(req.Upstreams) + len(req.FallbackDNS) + len(req.PrivateUpstreams)
result := make(map[string]string, upsNum)
resCh := make(chan upsCheckResult, upsNum)
for _, ups := range req.Upstreams {
go func(ups string) {
resCh <- upsCheckResult{
host: ups,
err: s.checkDNS(ups, opts, checkDNSUpstreamExc),
}
}(ups)
}
for _, ups := range req.FallbackDNS {
go func(ups string) {
resCh <- upsCheckResult{
host: ups,
err: s.checkDNS(ups, opts, checkDNSUpstreamExc),
}
}(ups)
}
for _, ups := range req.PrivateUpstreams {
go func(ups string) {
resCh <- upsCheckResult{
host: ups,
err: s.checkDNS(ups, opts, checkPrivateUpstreamExc),
}
}(ups)
}
for i := 0; i < upsNum; i++ {
// TODO(e.burkov): The upstreams used for both common and private
// resolving should be reported separately.
pair := <-resCh
if pair.err != nil {
result[pair.host] = pair.err.Error()
} else {
result[pair.host] = "OK"
}
}
aghhttp.WriteJSONResponseOK(w, r, result)
aghhttp.WriteJSONResponseOK(w, r, s.check(req, opts))
}
// handleCacheClear is the handler for the POST /control/cache_clear HTTP API.

View File

@@ -49,13 +49,18 @@ func loadTestData(t *testing.T, casesFileName string, cases any) {
require.NoError(t, err)
}
const jsonExt = ".json"
const (
jsonExt = ".json"
// testBlockedRespTTL is the TTL for blocked responses to use in tests.
testBlockedRespTTL = 10
)
func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
filterConf := &filtering.Config{
ProtectionEnabled: true,
BlockingMode: filtering.BlockingModeDefault,
BlockedResponseTTL: 10,
BlockedResponseTTL: testBlockedRespTTL,
SafeBrowsingEnabled: true,
SafeBrowsingCacheSize: 1000,
SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
@@ -133,7 +138,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
filterConf := &filtering.Config{
ProtectionEnabled: true,
BlockingMode: filtering.BlockingModeDefault,
BlockedResponseTTL: 10,
BlockedResponseTTL: testBlockedRespTTL,
SafeBrowsingEnabled: true,
SafeBrowsingCacheSize: 1000,
SafeSearchConf: filtering.SafeSearchConfig{Enabled: true},
@@ -229,6 +234,9 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
}, {
name: "blocked_response_ttl",
wantSet: "",
}, {
name: "multiple_domain_specific_upstreams",
wantSet: "",
}}
var data map[string]struct {
@@ -250,6 +258,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
s.dnsFilter.SetBlockingMode(filtering.BlockingModeDefault, netip.Addr{}, netip.Addr{})
s.conf = defaultConf
s.conf.Config.EDNSClientSubnet = &EDNSClientSubnet{}
s.dnsFilter.SetBlockedResponseTTL(testBlockedRespTTL)
})
rBody := io.NopCloser(bytes.NewReader(caseData.Req))
@@ -470,6 +479,8 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) {
Host: newLocalUpstreamListener(t, 0, badHandler).String(),
}).String()
goodAndBadUps := strings.Join([]string{goodUps, badUps}, " ")
const (
upsTimeout = 100 * time.Millisecond
@@ -547,7 +558,7 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) {
"upstream_dns": []string{"[/domain.example/]" + badUps},
},
wantResp: map[string]any{
"[/domain.example/]" + badUps: `WARNING: couldn't communicate ` +
badUps: `WARNING: couldn't communicate ` +
`with upstream: exchanging with ` + badUps + ` over tcp: ` +
`dns: id mismatch`,
},
@@ -585,6 +596,40 @@ func TestServer_HandleTestUpstreamDNS(t *testing.T) {
goodUps: "OK",
},
name: "fallback_comment_mix",
}, {
body: map[string]any{
"upstream_dns": []string{"[/domain.example/]" + goodUps + " " + badUps},
},
wantResp: map[string]any{
goodUps: "OK",
badUps: `WARNING: couldn't communicate ` +
`with upstream: exchanging with ` + badUps + ` over tcp: ` +
`dns: id mismatch`,
},
name: "multiple_domain_specific_upstreams",
}, {
body: map[string]any{
"upstream_dns": []string{"[/domain.example/]/]1.2.3.4"},
},
wantResp: map[string]any{
"[/domain.example/]/]1.2.3.4": `wrong upstream format: ` +
`bad upstream for domain "[/domain.example/]/]1.2.3.4": ` +
`duplicated separator`,
},
name: "bad_specification",
}, {
body: map[string]any{
"upstream_dns": []string{"[/domain.example/]" + goodAndBadUps},
"fallback_dns": []string{"[/domain.example/]" + goodAndBadUps},
"private_upstream": []string{"[/domain.example/]" + goodAndBadUps},
},
wantResp: map[string]any{
goodUps: "OK",
badUps: `WARNING: couldn't communicate ` +
`with upstream: exchanging with ` + badUps + ` over tcp: ` +
`dns: id mismatch`,
},
name: "all_different",
}}
for _, tc := range testCases {

View File

@@ -839,5 +839,47 @@
"edns_cs_use_custom": false,
"edns_cs_custom_ip": ""
}
},
"multiple_domain_specific_upstreams": {
"req": {
"upstream_dns": [
"8.8.8.8:77",
"[/example.com/]8.8.4.4:77 9.9.9.10 https://1.1.1.1"
]
},
"want": {
"upstream_dns": [
"8.8.8.8:77",
"[/example.com/]8.8.4.4:77 9.9.9.10 https://1.1.1.1"
],
"upstream_dns_file": "",
"bootstrap_dns": [
"9.9.9.10",
"149.112.112.10",
"2620:fe::10",
"2620:fe::fe:10"
],
"fallback_dns": [],
"protection_enabled": true,
"protection_disabled_until": null,
"ratelimit": 0,
"blocking_mode": "default",
"blocking_ipv4": "",
"blocking_ipv6": "",
"blocked_response_ttl": 10,
"edns_cs_enabled": false,
"dnssec_enabled": false,
"disable_ipv6": false,
"upstream_mode": "",
"cache_size": 0,
"cache_ttl_min": 0,
"cache_ttl_max": 0,
"cache_optimistic": false,
"resolve_clients": false,
"use_private_ptr_resolvers": false,
"local_ptr_upstreams": [],
"edns_cs_use_custom": false,
"edns_cs_custom_ip": ""
}
}
}

View File

@@ -263,30 +263,6 @@ func assignUniqueFilterID() int64 {
return value
}
// Sets up a timer that will be checking for filters updates periodically
func (d *DNSFilter) periodicallyRefreshFilters() {
const maxInterval = 1 * 60 * 60
ivl := 5 // use a dynamically increasing time interval
for {
isNetErr, ok := false, false
if d.conf.FiltersUpdateIntervalHours != 0 {
_, isNetErr, ok = d.tryRefreshFilters(true, true, false)
if ok && !isNetErr {
ivl = maxInterval
}
}
if isNetErr {
ivl *= 2
if ivl > maxInterval {
ivl = maxInterval
}
}
time.Sleep(time.Duration(ivl) * time.Second)
}
}
// tryRefreshFilters is like [refreshFilters], but backs down if the update is
// already going on.
//

View File

@@ -257,6 +257,9 @@ type DNSFilter struct {
// conf contains filtering parameters.
conf *Config
// done is the channel to signal to stop running filters updates loop.
done chan struct{}
// Channel for passing data to filters-initializer goroutine
filtersInitializerChan chan filtersInitializerParams
filtersInitializerLock sync.Mutex
@@ -424,24 +427,15 @@ func (d *DNSFilter) setFilters(blockFilters, allowFilters []Filter, async bool)
return d.initFiltering(allowFilters, blockFilters)
}
// Starts initializing new filters by signal from channel
func (d *DNSFilter) filtersInitializer() {
for {
params := <-d.filtersInitializerChan
err := d.initFiltering(params.allowFilters, params.blockFilters)
if err != nil {
log.Error("filtering: initializing: %s", err)
continue
}
}
}
// Close - close the object
func (d *DNSFilter) Close() {
d.engineLock.Lock()
defer d.engineLock.Unlock()
if d.done != nil {
d.done <- struct{}{}
}
d.reset()
}
@@ -1131,19 +1125,64 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
return d, nil
}
// Start - start the module:
// . start async filtering initializer goroutine
// . register web handlers
// Start registers web handlers and starts filters updates loop.
func (d *DNSFilter) Start() {
d.filtersInitializerChan = make(chan filtersInitializerParams, 1)
go d.filtersInitializer()
d.done = make(chan struct{}, 1)
d.RegisterFilteringHandlers()
// Here we should start updating filters,
// but currently we can't wake up the periodic task to do so.
// So for now we just start this periodic task from here.
go d.periodicallyRefreshFilters()
go d.updatesLoop()
}
// updatesLoop initializes new filters and checks for filters updates in a loop.
func (d *DNSFilter) updatesLoop() {
defer log.OnPanic("filtering: updates loop")
ivl := time.Second * 5
t := time.NewTimer(ivl)
for {
select {
case params := <-d.filtersInitializerChan:
err := d.initFiltering(params.allowFilters, params.blockFilters)
if err != nil {
log.Error("filtering: initializing: %s", err)
continue
}
case <-t.C:
ivl = d.periodicallyRefreshFilters(ivl)
t.Reset(ivl)
case <-d.done:
t.Stop()
return
}
}
}
// periodicallyRefreshFilters checks for filters updates and returns time
// interval for the next update.
func (d *DNSFilter) periodicallyRefreshFilters(ivl time.Duration) (nextIvl time.Duration) {
const maxInterval = time.Hour
if d.conf.FiltersUpdateIntervalHours == 0 {
return ivl
}
isNetErr, ok := false, false
_, isNetErr, ok = d.tryRefreshFilters(true, true, false)
if ok && !isNetErr {
ivl = maxInterval
} else if isNetErr {
ivl *= 2
// TODO(s.chzhen): Use built-in function max in Go 1.21.
ivl = mathutil.Max(ivl, maxInterval)
}
return ivl
}
// Safe browsing and parental control methods.

View File

@@ -256,6 +256,13 @@ var blockedServices = []blockedService{{
"||z.cn^",
"||zappos^",
},
}, {
ID: "amino",
Name: "Amino",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M9 4C6.24 4 4 6.24 4 9v32c0 2.76 2.24 5 5 5h32c2.76 0 5-2.24 5-5V9c0-2.76-2.24-5-5-5H9zm8.17 3.78a1.001 1.001 0 0 1 1.01 1c0 .37-.21.73-.57.9-.14.07-.29.1-.43.1a.987.987 0 0 1-.9-.56 1 1 0 0 1 .89-1.44zm15.66 0c.14 0 .29.03.43.1.36.17.56.53.56.9a.987.987 0 0 1-1 1c-.14 0-.29-.03-.43-.1a.998.998 0 0 1-.57-.9c0-.14.03-.29.1-.43a1 1 0 0 1 .91-.57zM13.77 9.93c.29 0 .58.13.78.38.15.18.22.4.22.62 0 .3-.13.59-.38.78a.963.963 0 0 1-.62.22c-.29 0-.58-.13-.78-.37a1.001 1.001 0 0 1 .78-1.63zm22.46 0c.22 0 .44.07.63.22a1.01 1.01 0 0 1 .15 1.41c-.2.24-.49.37-.78.37a.963.963 0 0 1-.62-.22.972.972 0 0 1-.38-.78c0-.22.07-.44.22-.62.2-.25.49-.38.78-.38zM25 10c8.27 0 15 6.73 15 15s-6.73 15-15 15-15-6.73-15-15 6.73-15 15-15zm-14.08 2.78a1.01 1.01 0 0 1 1.01 1 1.005 1.005 0 0 1-1.629.781 1 1 0 0 1 .619-1.782zm28.16 0c.29 0 .58.13.78.38.14.18.22.4.22.62 0 .29-.13.58-.38.78a1.005 1.005 0 0 1-1.629-.781 1.01 1.01 0 0 1 1.01-1zm-14.434 3.222a2.185 2.185 0 0 0-2.175 1.398l-5.09 13.59a.75.75 0 0 0 .7 1.01h1.458a.973.973 0 0 0 .75-.35c1.75-2 4.35-3.58 6.64-4.7l1.821 4.43a1.003 1.003 0 0 0 .92.62h1.25a.75.75 0 0 0 .7-1.01l-1.97-5.24c1.3-.52 2.19-.79 2.22-.79.8-.21 1.29-1.02 1.08-1.83a1.502 1.502 0 0 0-1.81-1.09c-.12.03-1.13.3-2.57.82l-2.01-5.35c-.25-.69-.79-1.25-1.5-1.44a2.223 2.223 0 0 0-.414-.068zm-15.867.187a1 1 0 0 1 1 1.01c0 .14-.03.292-.1.432a1.004 1.004 0 0 1-1.34.459.991.991 0 0 1-.458-1.33c.17-.36.528-.57.898-.57zm32.442 0c.37 0 .728.21.898.57a.985.985 0 0 1-.459 1.33 1.004 1.004 0 0 1-1.34-.459.971.971 0 0 1-.1-.43 1 1 0 0 1 1-1.01zM24.5 22.04c.24 0 .48.13.59.39l.66 1.622a27.9 27.9 0 0 0-1.61.83c-.543.304-1.09.633-1.63.988l1.4-3.44c.11-.26.35-.39.59-.39zM8.78 31.811c.37 0 .73.208.9.558a1 1 0 1 1-1.9.432c0-.36.2-.72.56-.89.14-.07.29-.1.44-.1zm32.44 0a.991.991 0 0 1 .898 1.43.995.995 0 0 1-1.329.47 1 1 0 0 1-.568-.91c0-.14.03-.292.1-.432.17-.35.53-.558.9-.558zm-30.3 3.41a1.005 1.005 0 0 1 1.01 1 1.01 1.01 0 0 1-1.01 1 1 1 0 0 1-.78-.381c-.14-.18-.22-.4-.22-.62 0-.29.13-.58.38-.78.18-.15.4-.22.62-.22zm28.16 0a1 1 0 1 1-.63 1.78.996.996 0 0 1-.38-.78 1.005 1.005 0 0 1 1.01-1zm-25.31 2.85c.22 0 .44.068.62.218.25.19.38.481.38.781 0 .22-.07.44-.22.62a1.002 1.002 0 0 1-1.41.16 1.01 1.01 0 0 1-.15-1.41c.2-.24.49-.37.78-.37zm22.46 0c.29 0 .58.128.78.368a1.001 1.001 0 0 1-.78 1.631c-.29 0-.58-.13-.78-.38a.958.958 0 0 1-.22-.62c0-.3.13-.59.38-.78a.963.963 0 0 1 .62-.22zm-19.05 2.15c.14 0 .29.03.43.1.36.17.57.53.57.9 0 .14-.03.29-.1.43a1.001 1.001 0 0 1-1.34.468.986.986 0 0 1-.56-.898.987.987 0 0 1 1-1zm15.64 0c.37 0 .73.198.9.558a1 1 0 1 1-1.9.442c0-.37.21-.73.57-.9.14-.07.29-.1.43-.1z\"/></svg>"),
Rules: []string{
"||aminoapps.com^",
},
}, {
ID: "apple_streaming",
Name: "Apple Streaming",
@@ -337,6 +344,16 @@ var blockedServices = []blockedService{{
"||betwaysatta.com^",
"||vietnambetway88.com^",
},
}, {
ID: "bigo_live",
Name: "Bigo Live",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"3 3.5 41 41\" fill=\"currentColor\"><g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M29.18 26.13c5.84-2.13 10.03-4.81 5.7-10.34.3-11.15-18.48-13.27-23.18-.61-.54 2.44-.36 7.7 1.48 11.92.68 1.6.15 8.1-2.23 5.16-1.12-1.52-2.16-1.58-2.6-1.42-2.14 3.95 3.39 7.32 4.6 7.34 3.02 3.5 5.59 3.07 7.77 3.23\"/><path d=\"M32.03 24.95c5 7.69 2.45 12.6-3.85 14.2M18.16 25.72c.97.92 2.4.74 3.38.06.19-1.83 1.67-1.76 1.92.11 1.2-.02 2.44-.16 2.67.7.14.9-.63 1.44-1.8 1.28.06 1.84-.86 1.78-2.93 1.6-.68.3-1.3.42-1.93.55m3.33-8.6c5.01 1.97 9.5 1.55 12.19-2.67\"/><ellipse cx=\"25.08\" cy=\"18.77\" rx=\"1.12\" ry=\"1.28\"/><ellipse cx=\"32.83\" cy=\"16.88\" rx=\".93\" ry=\"1.22\"/><path d=\"M20.52 40c6.87 13.72 14.2-18.17.53-6.2m13.52-4.57c11.28-7.01 2.68 13.07-1.86 8.09M25.83 6.57c-4.11-3.58-6.75-2.2-8.56 1.68-7.73-1.63-7.5 4.7-5.57 6.93-3.37 2.43-4.1 4.87.12 7.3-1.62.75-3.3 3.43 1.36 5.35-1.18.95-2.77 1.83-.12 3.4m24.07-15.36h2.63m-1.31-1.5v2.92\"/></g></svg>"),
Rules: []string{
"||bigo.sg^",
"||bigo.tv^",
"||bigolive.tv^",
"||bigovideo.tv^",
},
}, {
ID: "bilibili",
Name: "Bilibili",
@@ -508,6 +525,13 @@ var blockedServices = []blockedService{{
"||deezer.com^",
"||dzcdn.net^",
},
}, {
ID: "directvgo",
Name: "DirecTV Go",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"-2 0 20 20\"><path d=\"M17 9.97A10.12 10.12 0 0 1 6.82 20H.46a.44.44 0 0 1-.34-.16.42.42 0 0 1-.11-.36A5.03 5.03 0 0 1 5 15.32h1.82a5.16 5.16 0 0 0 5.17-5.35v-.36a5.2 5.2 0 0 0-5.25-4.93H5a5.1 5.1 0 0 1-3.57-1.46A4.98 4.98 0 0 1 0 .54.45.45 0 0 1 .1.16.45.45 0 0 1 .47 0h6.26C12.36 0 16.95 4.44 17 9.97z\"/><path d=\"M12 9.97a9.95 9.95 0 0 1-2.9 7.09A9.85 9.85 0 0 1 2.04 20H.45a.43.43 0 0 1-.34-.16.43.43 0 0 1-.1-.36 4.94 4.94 0 0 1 4.86-4.16h1.77a5.36 5.36 0 0 0 5.27-4.89 4.32 4.32 0 0 0 0-.49v-.3a5.36 5.36 0 0 0-5.34-4.96h-1.7A4.92 4.92 0 0 1 1.4 3.22 5.02 5.02 0 0 1 .01.54.46.46 0 0 1 .45 0h1.5c5.51-.02 10 4.44 10.05 9.97z\"/><path d=\"M4 10a3 3 0 1 0 6 0 3 3 0 0 0-6 0z\"/></svg>"),
Rules: []string{
"||directvgo.com^",
},
}, {
ID: "discord",
Name: "Discord",
@@ -1975,6 +1999,23 @@ var blockedServices = []blockedService{{
"||sonyentertainmentnetwork.com",
"||station.sony.com",
},
}, {
ID: "plenty_of_fish",
Name: "Plenty of Fish",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"-8 -3 30 30\"><path d=\"M13.96 4.88C11.3.68 7.04 1.25.22 1.25v.09c.52.2.95.59 1.21 1.08.26.47.38 1.04.38 1.72v16.44c.01.61-.11 1.21-.38 1.76-.25.5-.68.9-1.2 1.1v.09h7.13v-.09c-.5-.22-.92-.6-1.17-1.1a3.78 3.78 0 0 1-.4-1.76V16c.54.13 1.08.2 1.62.19a7.66 7.66 0 0 0 6.84-4.3c.47-.97.7-2.1.7-3.41a6.6 6.6 0 0 0-1-3.6ZM9.45 13.6a3.33 3.33 0 0 1-2.96 1.65c-.24 0-.49-.02-.73-.05V7.56l.01-4.63a3 3 0 0 0-.1-.88c1.62 0 2.43.5 3.16 1.19.74.68 1.86 2.68 1.86 5.52a9.4 9.4 0 0 1-1.24 4.85Z\"/></svg>"),
Rules: []string{
"||pof.com^",
},
}, {
ID: "plex",
Name: "Plex",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 48 48\"><path d=\"M11.5 6A5.5 5.5 0 0 0 6 11.5v25a5.5 5.5 0 0 0 5.5 5.5h25a5.5 5.5 0 0 0 5.5-5.5v-25A5.5 5.5 0 0 0 36.5 6h-25zm6.67 7.08h6.47L31.75 24l-7.1 10.92h-6.48L25.2 24l-7.03-10.92z\"/></svg>"),
Rules: []string{
"||plex.bz^",
"||plex.direct^",
"||plex.tv^",
"||plexapp.com^",
},
}, {
ID: "pluto_tv",
Name: "Pluto TV",
@@ -2086,6 +2127,14 @@ var blockedServices = []blockedService{{
"||shopeemobile.com^",
"||shp.ee^",
},
}, {
ID: "signal",
Name: "Signal",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 32 32\"><path d=\"M26.6 16c-.2 4-1.9 7.2-5.5 9.2a10.6 10.6 0 0 1-10.5-.2h-.5l-3.8.9c-.2.1-.3 0-.2-.2l.9-3.8-.1-.5a10.6 10.6 0 1 1 19.5-7.2l.2 1.8z\"/><path d=\"M4.6 28.6c-.8 0-1.4-.7-1.2-1.4l.6-2.5.2-.1.9.2c.2 0 .1.1.1.2l-.5 2c-.1.3-.1.3.3.2l2-.5c.2 0 .2 0 .3.2l.2.8-.1.2-2.8.7c.1 0 .1 0 0 0zm5.1-1.2-1 .2c-.2.1-.3 0-.3-.2l-.2-.8.1-.2 1.5-.3h.2c.9.5 1.9.9 2.9 1.2.1 0 .2.1.1.2l-.2.7c0 .2-.1.2-.3.2-.9-.2-1.8-.6-2.6-1 0 .1 0 0-.2 0zm-3.8-5.3-.3 1.3c-.1.5 0 .4-.5.3l-.6-.1c-.1 0-.2-.1-.1-.2l.2-.9v-.4c-.4-.8-.8-1.7-1-2.6-.1-.3-.1-.3.2-.3l.7-.2.2.1c.3 1 .7 2 1.2 2.9-.1 0 0 0 0 .1zM26.4 8.3l-.1.1-.7.5c-.1.1-.2.1-.2 0-.7-.9-1.4-1.6-2.3-2.3-.1-.1-.1-.1 0-.2l.5-.7c.1-.1.1-.1.2 0 1 .7 1.8 1.5 2.6 2.6 0-.1 0-.1 0 0zm-7.2-4.9h.1c1.1.3 2.2.7 3.2 1.3.2.1.2.2.1.3l-.4.7-.2.1a10 10 0 0 0-3-1.2c-.1 0-.2-.1-.1-.2l.2-.9.1-.1zM6.5 9s-.1-.1 0 0l-.8-.6v-.3l1-1.3 1.4-1.2h.3l.5.7v.3C8 7.3 7.3 8 6.6 8.9l-.1.1zm21.1 9.9.9.2.1.2-.8 2.2-.5 1-.3.1-.7-.4-.1-.2c.5-.9 1-1.9 1.2-3 0-.1.1-.2.2-.1zM4.4 13.1l-.9-.2-.1-.2c.2-.9.6-1.8 1-2.6l.3-.6c.2-.2.2-.2.3-.1l.8.5c.1.1.1.1 0 .2-.5.9-1 1.9-1.2 3 0 0 0 .1-.2 0zM3 16l.1-1.7c0-.2.1-.2.3-.2l.8.1.2.2c-.1 1.1-.1 2.1 0 3.2l-.1.2-.9.1c-.2 0-.2-.1-.2-.2C3 17.2 3 16.6 3 16zm25.6-3.2-.1.1-.9.2c-.2 0-.2-.1-.2-.2-.2-.9-.6-1.7-1-2.5l-.2-.4c-.1-.1 0-.1 0-.2l.8-.5h.2a15 15 0 0 1 1.4 3.5zm-2.2 10.9-.1.1c-.7.9-1.6 1.8-2.5 2.5-.1.1-.2.1-.2 0l-.5-.7v-.3c.8-.6 1.6-1.4 2.2-2.2h.3l.7.5.1.1zM16 3l1.7.1c.2 0 .2.1.2.3l-.1.7-.2.2a16 16 0 0 0-3.1 0c-.2 0-.2 0-.2-.2l-.1-.8c0-.1 0-.2.2-.2L16 3zm0 26-1.8-.1c-.2 0-.2-.1-.2-.3l.1-.8.2-.2c1.1.1 2.1.1 3.2 0 .1 0 .2 0 .2.2l.1.8c0 .2-.1.2-.2.2-.4.2-1 .2-1.6.2zM12.8 3.4l.1.1.2.9c0 .2-.1.2-.2.2-.9.2-1.8.6-2.6 1-.3.2-.5.2-.7-.1l-.2-.4c-.1-.2-.1-.2.1-.3.8-.5 1.7-.9 2.6-1.2l.7-.2zM29 16l-.1 1.7c0 .2-.1.2-.3.2l-.7-.1c-.3 0-.3 0-.2-.3v-3.1c0-.1 0-.2.2-.2l.8-.1c.2 0 .2 0 .2.2L29 16zm-9.8 12.6-.1-.1-.2-.9.2-.2c.9-.2 1.7-.6 2.5-1l.4-.2c.1-.1.1 0 .2 0l.5.8v.2c-1.1.6-2.2 1.1-3.5 1.4z\"/></svg>"),
Rules: []string{
"||signal.org^",
"||whispersystems.org^",
},
}, {
ID: "skype",
Name: "Skype",
@@ -2210,6 +2259,14 @@ var blockedServices = []blockedService{{
"||tx.me^",
"||usercontent.dev^",
},
}, {
ID: "temu",
Name: "Temu",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 256 256\"><path d=\"M199.1 0C230.525 0 256 25.475 256 56.9v142.2c0 31.425-25.475 56.9-56.9 56.9H56.9C25.475 256 0 230.525 0 199.1V56.9C0 25.475 25.475 0 56.9 0zm-64 132.98h-3.4c-3.025 0-5.5 2.45-5.475 5.475v37.625c0 3.025 2.45 5.5 5.475 5.5s5.5-2.45 5.5-5.5v-24.7l9.25 13.05c1.925 2.7 5.925 2.7 7.875 0l9.25-13.05v24.7a5.5 5.5 0 0 0 5.5 5.5c3.025 0 5.5-2.45 5.475-5.5v-37.625c0-3.025-2.45-5.5-5.475-5.475h-3.4c-1.3 0-2.55.625-3.3 1.7l-11.975 18-12-18a3.997 3.997 0 0 0-3.3-1.7zm85.05 0c-3.025 0-5.5 2.45-5.5 5.475v22.975c0 7.225-4.075 10.925-10.775 10.9-6.7 0-10.775-3.825-10.75-11.225v-22.65c0-3.025-2.45-5.5-5.5-5.475-3.025 0-5.5 2.45-5.475 5.475v22.9c0 13.4 8.2 20.225 21.6 20.225s21.9-6.75 21.875-20.55v-22.575c0-3.025-2.45-5.5-5.475-5.475zm-154.22 0H33.855c-3.025 0-5.5 2.45-5.5 5.475s2.45 5.5 5.5 5.5h10.55v32.075c0 3.025 2.45 5.5 5.475 5.5s5.5-2.45 5.5-5.5v-32.075h10.55a5.5 5.5 0 0 0 5.5-5.5c0-3.025-2.45-5.5-5.5-5.475zm47.475 0H83.68c-3.025 0-5.5 2.45-5.5 5.475v37.575a5.5 5.5 0 0 0 5.5 5.5h29.725c3.025 0 5.5-2.45 5.475-5.5 0-3.025-2.45-5.5-5.475-5.5h-24.25v-7.8h21.1c3.025 0 5.5-2.45 5.5-5.475s-2.45-5.5-5.5-5.5h-21.1v-7.8h24.25c3.025 0 5.5-2.45 5.475-5.5 0-3.025-2.45-5.5-5.475-5.475zM59.78 75.63l-1.025.025c-4.275.275-7.2 2.125-8.85 4.625-1.925-2.875-5.525-4.9-10.95-4.6l-.125.175c-.625 1-2.975 5.475.825 10.35.775.825 2.675 3.15 1.9 6.125L30.53 110.155c-.9 1.45-.5 3.325.875 4.3 2.85 2 8.575 4.75 18.5 4.75 9.9 0 15.625-2.75 18.475-4.75l.375-.325a3.179 3.179 0 0 0 .5-3.975l-11-17.825.075.325-.125-.5c-.6-2.675.9-4.8 1.725-5.75l.2-.2c3.825-4.875 1.45-9.325.825-10.35l-.1-.175zm35.7 8.35c-3.775-7.5-8.675-8.775-11.125-6.825-1.875 1.5-6.2 7.425-6.5 7.825-4.775 6.775-4.5 8.425 1.625 12.275 3.45 2.175 6.225-.625 7.425-1.45-.575 3.575-2.325 9.2-4.95 13.15-1.425-1.075-2.475-1.9-3.125-2.5-.825-.75-2.075-.7-2.875.075a1.865 1.865 0 0 0-.55 1.425c.025.525.25 1.025.625 1.375 6.375 5.825 14.75 9.125 23.675 9.15 8.95 0 17.375-3.3 23.75-9.15.825-.75.85-2 .1-2.8a2.07 2.07 0 0 0-2.875-.075c-.5.45-1 .875-1.525 1.3l-2.8-6.25c-.45-1.075-.95-2.425-1.5-4.05.275-.675.85-1.325 1.675-2.175.6-.6 1.1-1.2 1.475-1.775 1.85-2.925.8-4.65.225-5.8-1.325-2.7-3.4-1.825-4.9-.225-1.85 1.95-3.65 2.8-6.55 3.45-2.425.55-4.3.275-5.85-.7-2.15-1.325-5.45-6.25-5.45-6.25zm69.325-7.625c-8 7.6-.325 24.125-14.875 31.15-1.6.775-2.925-1.775-5.075-1.775-6.075.05-17.675 5.4-18.125 8.1-.375 2.225 4.575 4 19.175 4.025 12.7 0 16.8-19.325 21.25-19.35 4.45 0 2.375 17.525 1.9 19.35h4.65c-.4-1.825-.7-7.325-.675-15.1 0-7.775 1.4-9.5 2.525-15.375.975-5.1-6.575-9.525-10.75-11.025zm45.6.625H197.38c-8.425 0-15.425 6.525-16 14.925l-.95 13.475c-.45 6.4 4.625 11.825 11.025 11.85h24.85c6.425 0 11.475-5.425 11.05-11.85l-.95-13.475c-.6-8.4-7.575-14.925-16-14.925zm-110.65 31c3.925 0 6.925 1.925 8.025 5.5-2.675.7-5.35 1.05-8.075 1.025-4.1 0-5.55-.375-8.175-1.075 1.05-3.15 4.525-5.45 8.225-5.45zm98.225-19.825v.375c0 3.25 2.65 5.925 5.9 5.925s5.925-2.65 5.925-5.925v-.375c0-1.45 5.25-1.45 5.25 0v.375c0 6.15-5 11.15-11.175 11.15-6.15 0-11.15-5-11.15-11.15v-.375c0-1.45 5.225-1.45 5.25 0z\"/></svg>"),
Rules: []string{
"||kwcdn.com^",
"||temu.com^",
},
}, {
ID: "tidal",
Name: "Tidal",
@@ -2256,6 +2313,13 @@ var blockedServices = []blockedService{{
"||tinder.com^",
"||tindersparks.com^",
},
}, {
ID: "tumblr",
Name: "Tumblr",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M40 0H10A10 10 0 0 0 0 10v30a10 10 0 0 0 10 10h30a10 10 0 0 0 10-10V10A10 10 0 0 0 40 0Zm-6 40.24c0 .12-.05.24-.14.32-.12.1-2.85 2.44-9.12 2.44-7.51 0-7.74-8.38-7.74-9.34V23.01L13.43 23a.42.42 0 0 1-.43-.42V18.8c0-.18.1-.34.27-.4.07-.03 6.79-2.64 6.79-8.98 0-.24.2-.43.43-.43h4.09c.24 0 .43.2.43.43L25 17h6.56c.24 0 .43.2.43.45v5.1c0 .24-.19.45-.43.45H25v10.5c0 .25.23 3.27 3.43 3.27a10.3 10.3 0 0 0 4.91-1.39c.14-.08.3-.09.44 0 .13.07.22.21.22.37Z\"/></svg>"),
Rules: []string{
"||tumblr.com^",
},
}, {
ID: "twitch",
Name: "Twitch",
@@ -2434,6 +2498,15 @@ var blockedServices = []blockedService{{
"||whatsapp.tv^",
"||whatsappbrand.com^",
},
}, {
ID: "wizz",
Name: "Wizz",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 -297 867 867\"><path d=\"M94.66 31.7c0-11.49-7.72-21.78-17.23-23.05Q47.26 4.74 17.18.16C7.7-1.34 0 7.77 0 20.57v207.77c0 25.69 15.38 43.88 34.38 40.93q44.73-6.52 89.6-11.57c17.66-2.09 32.49-18.81 34.34-38.88q4.86-52.95 9.73-105.04c.54-5.82 8.04-5.63 8.59.16q4.86 51.9 9.73 103.01c1.85 19.55 16.69 33.01 34.4 31.58q44.97-3.47 90.02-5.46a36.76 36.76 0 0 0 34.66-36.57V47.86a18.27 18.27 0 0 0-17.33-18.23q-30.32-1.15-60.63-2.97c-9.56-.59-17.31 7.13-17.31 17.32v112.25c0 5.52-7.5 6.48-8.57 1.06q-10.13-51.32-20.26-104c-3.38-17.49-17.5-31.07-33.85-32.59q-15.16-1.35-30.32-2.87c-16.34-1.7-30.42 10.04-33.79 28.63q-10.1 55.6-20.2 113.34c-1.06 6.1-8.52 5.4-8.52-.87Zm297.01-.38c-12.77-.2-23.11 7.4-23.12 17.07V223.6c0 9.69 10.35 17.28 23.12 17.08q41.4-.63 82.8 0c12.77.2 23.11-7.4 23.11-17.08V48.4c0-9.69-10.34-17.28-23.11-17.08q-41.4.62-82.8 0ZM520.7 47.87a18.28 18.28 0 0 1 17.33-18.23q64.98-2.47 129.85-8c9.55-.84 17.3 7.09 17.3 17.78v65.23c0 18.33-11.46 34.12-27.47 37.67q-27.56 6.23-55.16 12.04c-5.08 1.07-4.31 8.95.9 9.04q32.23.5 64.44 1.2c9.55.17 17.29 9 17.29 19.7v48.28c0 10.7-7.74 18.62-17.3 17.78q-64.84-5.53-129.84-8a18.27 18.27 0 0 1-17.33-18.23V164.9a35.26 35.26 0 0 1 27.86-34.62q29.6-6.15 59.18-12.68c5.12-1.14 4.38-9.15-.85-9.06q-34.42.55-68.86.87a17.3 17.3 0 0 1-17.33-17.48Zm181.75-9.81c0-10.8 7.74-20.32 17.28-21.34Q784.49 9.97 848.97.16c9.5-1.49 17.18 7.62 17.18 20.41v77.96c0 21.94-11.38 40.83-27.3 44.95q-27.4 7.28-54.87 13.76c-5.06 1.2-4.29 10.3.9 10.47q32.08 1.03 64.1 2.25c9.49.31 17.18 10.96 17.18 23.75v57.72c0 12.8-7.7 21.9-17.19 20.41q-64.46-9.8-129.24-16.56c-9.54-1.03-17.27-10.55-17.27-21.34v-65.82c0-18.61 11.63-34.66 27.77-38.57q29.49-7.02 58.92-14.88c5.1-1.37 4.36-10.67-.85-10.49q-34.25 1.11-68.57 2c-9.54.22-17.27-8.36-17.27-19.15Z\"/></svg>"),
Rules: []string{
"||getwizz.io^",
"||wizz.chat^",
"||wizzapp.com^",
},
}, {
ID: "xboxlive",
Name: "Xbox Live",
@@ -2448,6 +2521,14 @@ var blockedServices = []blockedService{{
"||xboxlive.com^",
"||xboxservices.com^",
},
}, {
ID: "xiaohongshu",
Name: "Xiaohongshu",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 50 50\"><path d=\"M35 22v2h1v-2h-1zm0 0v2h1v-2h-1zm9-18H6c-1.09 0-2 .91-2 2v38c0 1.09.91 2 2 2h38c1.09 0 2-.91 2-2V6c0-1.09-.91-2-2-2zM12 24c0 1.38-.19 5.89-2.61 6.24l-.28-1.98c.39-.19.89-2.14.89-4.26v-2h2v2zm3 6h-2V19h2v11zm2.29-.29c-1.2-1.2-1.29-4.73-1.29-5.78V22h2v1.93c0 1.91.34 3.99.71 4.36l-1.42 1.42zM22 31h-3l1-2h3l-1 2zm9 0h-7l1-2h2v-7h-2l-2.1 4.38h1.72l-1 2H21a1 1 0 0 1-.82-1.57L22 24h-2a1 1 0 0 1-.86-1.51l3-5 1.72 1.02L21.77 22H25v-2h6v2h-2v7h2v2zm9-2.5a2.5 2.5 0 0 1-2.5 2.5c-1.21 0-1.22-.86-1.45-2H38v-3h-3v5h-2v-5h-2v-2h2v-2h-1v-2h1v-1h2v1h1a2 2 0 0 1 2 2v2a2 2 0 0 1 2 2v2.5zm0-6.5h-1v-1c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1zm-5 2h1v-2h-1v2zm0-2v2h1v-2h-1z\"/></svg>"),
Rules: []string{
"||xhscdn.com^",
"||xiaohongshu.com^",
},
}, {
ID: "youtube",
Name: "YouTube",

View File

@@ -4,32 +4,17 @@ import (
"crypto/rand"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"net/http"
"path"
"strconv"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
"go.etcd.io/bbolt"
"golang.org/x/crypto/bcrypt"
)
// cookieTTL is the time-to-live of the session cookie.
const cookieTTL = 365 * timeutil.Day
// sessionCookieName is the name of the session cookie.
const sessionCookieName = "agh_session"
// sessionTokenSize is the length of session token in bytes.
const sessionTokenSize = 16
@@ -69,7 +54,7 @@ func (s *session) deserialize(data []byte) bool {
// Auth - global object
type Auth struct {
db *bbolt.DB
raleLimiter *authRateLimiter
rateLimiter *authRateLimiter
sessions map[string]*session
users []webUser
lock sync.Mutex
@@ -77,6 +62,8 @@ type Auth struct {
}
// webUser represents a user of the Web UI.
//
// TODO(s.chzhen): Improve naming.
type webUser struct {
Name string `yaml:"name"`
PasswordHash string `yaml:"password"`
@@ -88,7 +75,7 @@ func InitAuth(dbFilename string, users []webUser, sessionTTL uint32, rateLimiter
a := &Auth{
sessionTTL: sessionTTL,
raleLimiter: rateLimiter,
rateLimiter: rateLimiter,
sessions: make(map[string]*session),
users: users,
}
@@ -216,8 +203,8 @@ func (a *Auth) storeSession(data []byte, s *session) bool {
return true
}
// remove session from file
func (a *Auth) removeSession(sess []byte) {
// removeSessionFromFile removes a stored session from the DB file on disk.
func (a *Auth) removeSessionFromFile(sess []byte) {
tx, err := a.db.Begin(true)
if err != nil {
log.Error("auth: bbolt.Begin: %s", err)
@@ -279,7 +266,7 @@ func (a *Auth) checkSession(sess string) (res checkSessionResult) {
if s.expire <= now {
delete(a.sessions, sess)
key, _ := hex.DecodeString(sess)
a.removeSession(key)
a.removeSessionFromFile(key)
return checkSessionExpired
}
@@ -301,351 +288,17 @@ func (a *Auth) checkSession(sess string) (res checkSessionResult) {
return checkSessionOK
}
// RemoveSession - remove session
func (a *Auth) RemoveSession(sess string) {
// removeSession removes the session from the active sessions and the disk.
func (a *Auth) removeSession(sess string) {
key, _ := hex.DecodeString(sess)
a.lock.Lock()
delete(a.sessions, sess)
a.lock.Unlock()
a.removeSession(key)
a.removeSessionFromFile(key)
}
type loginJSON struct {
Name string `json:"name"`
Password string `json:"password"`
}
// newSessionToken returns cryptographically secure randomly generated slice of
// bytes of sessionTokenSize length.
//
// TODO(e.burkov): Think about using byte array instead of byte slice.
func newSessionToken() (data []byte, err error) {
randData := make([]byte, sessionTokenSize)
_, err = rand.Read(randData)
if err != nil {
return nil, err
}
return randData, nil
}
// newCookie creates a new authentication cookie.
func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error) {
rateLimiter := a.raleLimiter
u, ok := a.findUser(req.Name, req.Password)
if !ok {
if rateLimiter != nil {
rateLimiter.inc(addr)
}
return nil, errors.Error("invalid username or password")
}
if rateLimiter != nil {
rateLimiter.remove(addr)
}
sess, err := newSessionToken()
if err != nil {
return nil, fmt.Errorf("generating token: %w", err)
}
now := time.Now().UTC()
a.addSession(sess, &session{
userName: u.Name,
expire: uint32(now.Unix()) + a.sessionTTL,
})
return &http.Cookie{
Name: sessionCookieName,
Value: hex.EncodeToString(sess),
Path: "/",
Expires: now.Add(cookieTTL),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}, nil
}
// realIP extracts the real IP address of the client from an HTTP request using
// the known HTTP headers.
//
// TODO(a.garipov): Currently, this is basically a copy of a similar function in
// module dnsproxy. This should really become a part of module golibs and be
// replaced both here and there. Or be replaced in both places by
// a well-maintained third-party module.
//
// TODO(a.garipov): Support header Forwarded from RFC 7329.
func realIP(r *http.Request) (ip net.IP, err error) {
proxyHeaders := []string{
httphdr.CFConnectingIP,
httphdr.TrueClientIP,
httphdr.XRealIP,
}
for _, h := range proxyHeaders {
v := r.Header.Get(h)
ip = net.ParseIP(v)
if ip != nil {
return ip, nil
}
}
// If none of the above yielded any results, get the leftmost IP address
// from the X-Forwarded-For header.
s := r.Header.Get(httphdr.XForwardedFor)
ipStrs := strings.SplitN(s, ", ", 2)
ip = net.ParseIP(ipStrs[0])
if ip != nil {
return ip, nil
}
// When everything else fails, just return the remote address as understood
// by the stdlib.
ipStr, err := netutil.SplitHost(r.RemoteAddr)
if err != nil {
return nil, fmt.Errorf("getting ip from client addr: %w", err)
}
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) {
req := loginJSON{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
return
}
var remoteIP string
// realIP cannot be used here without taking TrustedProxies into account due
// to security issues.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
//
// TODO(e.burkov): Use realIP when the issue will be fixed.
if remoteIP, err = netutil.SplitHost(r.RemoteAddr); err != nil {
writeErrorWithIP(
r,
w,
http.StatusBadRequest,
r.RemoteAddr,
"auth: getting remote address: %s",
err,
)
return
}
if rateLimiter := Context.auth.raleLimiter; rateLimiter != nil {
if left := rateLimiter.check(remoteIP); left > 0 {
w.Header().Set(httphdr.RetryAfter, strconv.Itoa(int(left.Seconds())))
writeErrorWithIP(
r,
w,
http.StatusTooManyRequests,
remoteIP,
"auth: blocked for %s",
left,
)
return
}
}
cookie, err := Context.auth.newCookie(req, remoteIP)
if err != nil {
writeErrorWithIP(r, w, http.StatusForbidden, remoteIP, "%s", err)
return
}
// Use realIP here, since this IP address is only used for logging.
ip, err := realIP(r)
if err != nil {
log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
}
log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
http.SetCookie(w, cookie)
h := w.Header()
h.Set(httphdr.CacheControl, "no-store, no-cache, must-revalidate, proxy-revalidate")
h.Set(httphdr.Pragma, "no-cache")
h.Set(httphdr.Expires, "0")
aghhttp.OK(w)
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
respHdr := w.Header()
c, err := r.Cookie(sessionCookieName)
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
// The user is already logged out.
respHdr.Set(httphdr.Location, "/login.html")
w.WriteHeader(http.StatusFound)
return
}
Context.auth.RemoveSession(c.Value)
c = &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
respHdr.Set(httphdr.Location, "/login.html")
respHdr.Set(httphdr.SetCookie, c.String())
w.WriteHeader(http.StatusFound)
}
// RegisterAuthHandlers - register handlers
func RegisterAuthHandlers() {
Context.mux.Handle("/control/login", postInstallHandler(ensureHandler(http.MethodPost, handleLogin)))
httpRegister(http.MethodGet, "/control/logout", handleLogout)
}
// optionalAuthThird return true if user should authenticate first.
func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
if glProcessCookie(r) {
log.Debug("auth: authentication is handled by GL-Inet submodule")
return false
}
// redirect to login page if not authenticated
isAuthenticated := false
cookie, err := r.Cookie(sessionCookieName)
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
// Check Basic authentication.
user, pass, hasBasic := r.BasicAuth()
if hasBasic {
_, isAuthenticated = Context.auth.findUser(user, pass)
if !isAuthenticated {
log.Info("auth: invalid Basic Authorization value")
}
}
} else {
res := Context.auth.checkSession(cookie.Value)
isAuthenticated = res == checkSessionOK
if !isAuthenticated {
log.Debug("auth: invalid cookie value: %s", cookie)
}
}
if isAuthenticated {
return false
}
if p := r.URL.Path; p == "/" || p == "/index.html" {
if glProcessRedirect(w, r) {
log.Debug("auth: redirected to login page by GL-Inet submodule")
} else {
log.Debug("auth: redirected to login page")
http.Redirect(w, r, "login.html", http.StatusFound)
}
} else {
log.Debug("auth: responded with forbidden to %s %s", r.Method, p)
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("Forbidden"))
}
return true
}
// TODO(a.garipov): Use [http.Handler] consistently everywhere throughout the
// project.
func optionalAuth(
h func(http.ResponseWriter, *http.Request),
) (wrapped func(http.ResponseWriter, *http.Request)) {
return func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
authRequired := Context.auth != nil && Context.auth.AuthRequired()
if p == "/login.html" {
cookie, err := r.Cookie(sessionCookieName)
if authRequired && err == nil {
// Redirect to the dashboard if already authenticated.
res := Context.auth.checkSession(cookie.Value)
if res == checkSessionOK {
http.Redirect(w, r, "", http.StatusFound)
return
}
log.Debug("auth: invalid cookie value: %s", cookie)
}
} else if isPublicResource(p) {
// Process as usual, no additional auth requirements.
} else if authRequired {
if optionalAuthThird(w, r) {
return
}
}
h(w, r)
}
}
// isPublicResource returns true if p is a path to a public resource.
func isPublicResource(p string) (ok bool) {
isAsset, err := path.Match("/assets/*", p)
if err != nil {
// The only error that is returned from path.Match is
// [path.ErrBadPattern]. This is a programmer error.
panic(fmt.Errorf("bad asset pattern: %w", err))
}
isLogin, err := path.Match("/login.*", p)
if err != nil {
// Same as above.
panic(fmt.Errorf("bad login pattern: %w", err))
}
return isAsset || isLogin
}
type authHandler struct {
handler http.Handler
}
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
optionalAuth(a.handler.ServeHTTP)(w, r)
}
func optionalAuthHandler(handler http.Handler) http.Handler {
return &authHandler{handler}
}
// Add adds a new user with the given password.
func (a *Auth) Add(u *webUser, password string) (err error) {
// addUser adds a new user with the given password.
func (a *Auth) addUser(u *webUser, password string) (err error) {
if len(password) == 0 {
return errors.Error("empty password")
}
@@ -715,22 +368,40 @@ func (a *Auth) getCurrentUser(r *http.Request) (u webUser) {
return webUser{}
}
// GetUsers - get users
func (a *Auth) GetUsers() []webUser {
// usersList returns a copy of a users list.
func (a *Auth) usersList() (users []webUser) {
a.lock.Lock()
users := a.users
a.lock.Unlock()
defer a.lock.Unlock()
users = make([]webUser, len(a.users))
copy(users, a.users)
return users
}
// AuthRequired - if authentication is required
func (a *Auth) AuthRequired() bool {
// authRequired returns true if a authentication is required.
func (a *Auth) authRequired() bool {
if GLMode {
return true
}
a.lock.Lock()
r := (len(a.users) != 0)
a.lock.Unlock()
return r
defer a.lock.Unlock()
return len(a.users) != 0
}
// newSessionToken returns cryptographically secure randomly generated slice of
// bytes of sessionTokenSize length.
//
// TODO(e.burkov): Think about using byte array instead of byte slice.
func newSessionToken() (data []byte, err error) {
randData := make([]byte, sessionTokenSize)
_, err = rand.Read(randData)
if err != nil {
return nil, err
}
return randData, nil
}

View File

@@ -0,0 +1,89 @@
package home
import (
"bytes"
"crypto/rand"
"encoding/hex"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewSessionToken(t *testing.T) {
// Successful case.
token, err := newSessionToken()
require.NoError(t, err)
assert.Len(t, token, sessionTokenSize)
// Break the rand.Reader.
prevReader := rand.Reader
t.Cleanup(func() { rand.Reader = prevReader })
rand.Reader = &bytes.Buffer{}
// Unsuccessful case.
token, err = newSessionToken()
require.Error(t, err)
assert.Empty(t, token)
}
func TestAuth(t *testing.T) {
dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db")
users := []webUser{{
Name: "name",
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
}}
a := InitAuth(fn, nil, 60, nil)
s := session{}
user := webUser{Name: "name"}
err := a.addUser(&user, "password")
require.NoError(t, err)
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
a.removeSession("notfound")
sess, err := newSessionToken()
require.NoError(t, err)
sessStr := hex.EncodeToString(sess)
now := time.Now().UTC().Unix()
// check expiration
s.expire = uint32(now)
a.addSession(sess, &s)
assert.Equal(t, checkSessionExpired, a.checkSession(sessStr))
// add session with TTL = 2 sec
s = session{}
s.expire = uint32(time.Now().UTC().Unix() + 2)
a.addSession(sess, &s)
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
a.Close()
// load saved session
a = InitAuth(fn, users, 60, nil)
// the session is still alive
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
// reset our expiration time because checkSession() has just updated it
s.expire = uint32(time.Now().UTC().Unix() + 2)
a.storeSession(sess, &s)
a.Close()
u, ok := a.findUser("name", "password")
assert.True(t, ok)
assert.NotEmpty(t, u.Name)
time.Sleep(3 * time.Second)
// load and remove expired sessions
a = InitAuth(fn, users, 60, nil)
assert.Equal(t, checkSessionNotFound, a.checkSession(sessStr))
a.Close()
}

352
internal/home/authhttp.go Normal file
View File

@@ -0,0 +1,352 @@
package home
import (
"encoding/hex"
"encoding/json"
"fmt"
"net"
"net/http"
"path"
"strconv"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/timeutil"
)
// cookieTTL is the time-to-live of the session cookie.
const cookieTTL = 365 * timeutil.Day
// sessionCookieName is the name of the session cookie.
const sessionCookieName = "agh_session"
// loginJSON is the JSON structure for authentication.
type loginJSON struct {
Name string `json:"name"`
Password string `json:"password"`
}
// newCookie creates a new authentication cookie.
func (a *Auth) newCookie(req loginJSON, addr string) (c *http.Cookie, err error) {
rateLimiter := a.rateLimiter
u, ok := a.findUser(req.Name, req.Password)
if !ok {
if rateLimiter != nil {
rateLimiter.inc(addr)
}
return nil, errors.Error("invalid username or password")
}
if rateLimiter != nil {
rateLimiter.remove(addr)
}
sess, err := newSessionToken()
if err != nil {
return nil, fmt.Errorf("generating token: %w", err)
}
now := time.Now().UTC()
a.addSession(sess, &session{
userName: u.Name,
expire: uint32(now.Unix()) + a.sessionTTL,
})
return &http.Cookie{
Name: sessionCookieName,
Value: hex.EncodeToString(sess),
Path: "/",
Expires: now.Add(cookieTTL),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}, nil
}
// realIP extracts the real IP address of the client from an HTTP request using
// the known HTTP headers.
//
// TODO(a.garipov): Currently, this is basically a copy of a similar function in
// module dnsproxy. This should really become a part of module golibs and be
// replaced both here and there. Or be replaced in both places by
// a well-maintained third-party module.
//
// TODO(a.garipov): Support header Forwarded from RFC 7329.
func realIP(r *http.Request) (ip net.IP, err error) {
proxyHeaders := []string{
httphdr.CFConnectingIP,
httphdr.TrueClientIP,
httphdr.XRealIP,
}
for _, h := range proxyHeaders {
v := r.Header.Get(h)
ip = net.ParseIP(v)
if ip != nil {
return ip, nil
}
}
// If none of the above yielded any results, get the leftmost IP address
// from the X-Forwarded-For header.
s := r.Header.Get(httphdr.XForwardedFor)
ipStrs := strings.SplitN(s, ", ", 2)
ip = net.ParseIP(ipStrs[0])
if ip != nil {
return ip, nil
}
// When everything else fails, just return the remote address as understood
// by the stdlib.
ipStr, err := netutil.SplitHost(r.RemoteAddr)
if err != nil {
return nil, fmt.Errorf("getting ip from client addr: %w", err)
}
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)
}
// handleLogin is the handler for the POST /control/login HTTP API.
func handleLogin(w http.ResponseWriter, r *http.Request) {
req := loginJSON{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json decode: %s", err)
return
}
var remoteIP string
// realIP cannot be used here without taking TrustedProxies into account due
// to security issues.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
//
// TODO(e.burkov): Use realIP when the issue will be fixed.
if remoteIP, err = netutil.SplitHost(r.RemoteAddr); err != nil {
writeErrorWithIP(
r,
w,
http.StatusBadRequest,
r.RemoteAddr,
"auth: getting remote address: %s",
err,
)
return
}
if rateLimiter := Context.auth.rateLimiter; rateLimiter != nil {
if left := rateLimiter.check(remoteIP); left > 0 {
w.Header().Set(httphdr.RetryAfter, strconv.Itoa(int(left.Seconds())))
writeErrorWithIP(
r,
w,
http.StatusTooManyRequests,
remoteIP,
"auth: blocked for %s",
left,
)
return
}
}
cookie, err := Context.auth.newCookie(req, remoteIP)
if err != nil {
writeErrorWithIP(r, w, http.StatusForbidden, remoteIP, "%s", err)
return
}
// Use realIP here, since this IP address is only used for logging.
ip, err := realIP(r)
if err != nil {
log.Error("auth: getting real ip from request with remote ip %s: %s", remoteIP, err)
}
log.Info("auth: user %q successfully logged in from ip %v", req.Name, ip)
http.SetCookie(w, cookie)
h := w.Header()
h.Set(httphdr.CacheControl, "no-store, no-cache, must-revalidate, proxy-revalidate")
h.Set(httphdr.Pragma, "no-cache")
h.Set(httphdr.Expires, "0")
aghhttp.OK(w)
}
// handleLogout is the handler for the GET /control/logout HTTP API.
func handleLogout(w http.ResponseWriter, r *http.Request) {
respHdr := w.Header()
c, err := r.Cookie(sessionCookieName)
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
// The user is already logged out.
respHdr.Set(httphdr.Location, "/login.html")
w.WriteHeader(http.StatusFound)
return
}
Context.auth.removeSession(c.Value)
c = &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
respHdr.Set(httphdr.Location, "/login.html")
respHdr.Set(httphdr.SetCookie, c.String())
w.WriteHeader(http.StatusFound)
}
// RegisterAuthHandlers - register handlers
func RegisterAuthHandlers() {
Context.mux.Handle("/control/login", postInstallHandler(ensureHandler(http.MethodPost, handleLogin)))
httpRegister(http.MethodGet, "/control/logout", handleLogout)
}
// optionalAuthThird returns true if a user should authenticate first.
func optionalAuthThird(w http.ResponseWriter, r *http.Request) (mustAuth bool) {
pref := fmt.Sprintf("auth: raddr %s", r.RemoteAddr)
if glProcessCookie(r) {
log.Debug("%s: authentication is handled by gl-inet submodule", pref)
return false
}
// redirect to login page if not authenticated
isAuthenticated := false
cookie, err := r.Cookie(sessionCookieName)
if err != nil {
// The only error that is returned from r.Cookie is [http.ErrNoCookie].
// Check Basic authentication.
user, pass, hasBasic := r.BasicAuth()
if hasBasic {
_, isAuthenticated = Context.auth.findUser(user, pass)
if !isAuthenticated {
log.Info("%s: invalid basic authorization value", pref)
}
}
} else {
res := Context.auth.checkSession(cookie.Value)
isAuthenticated = res == checkSessionOK
if !isAuthenticated {
log.Debug("%s: invalid cookie value: %q", pref, cookie)
}
}
if isAuthenticated {
return false
}
if p := r.URL.Path; p == "/" || p == "/index.html" {
if glProcessRedirect(w, r) {
log.Debug("%s: redirected to login page by gl-inet submodule", pref)
} else {
log.Debug("%s: redirected to login page", pref)
http.Redirect(w, r, "login.html", http.StatusFound)
}
} else {
log.Debug("%s: responded with forbidden to %s %s", pref, r.Method, p)
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("Forbidden"))
}
return true
}
// TODO(a.garipov): Use [http.Handler] consistently everywhere throughout the
// project.
func optionalAuth(
h func(http.ResponseWriter, *http.Request),
) (wrapped func(http.ResponseWriter, *http.Request)) {
return func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
authRequired := Context.auth != nil && Context.auth.authRequired()
if p == "/login.html" {
cookie, err := r.Cookie(sessionCookieName)
if authRequired && err == nil {
// Redirect to the dashboard if already authenticated.
res := Context.auth.checkSession(cookie.Value)
if res == checkSessionOK {
http.Redirect(w, r, "", http.StatusFound)
return
}
log.Debug("auth: raddr %s: invalid cookie value: %q", r.RemoteAddr, cookie)
}
} else if isPublicResource(p) {
// Process as usual, no additional auth requirements.
} else if authRequired {
if optionalAuthThird(w, r) {
return
}
}
h(w, r)
}
}
// isPublicResource returns true if p is a path to a public resource.
func isPublicResource(p string) (ok bool) {
isAsset, err := path.Match("/assets/*", p)
if err != nil {
// The only error that is returned from path.Match is
// [path.ErrBadPattern]. This is a programmer error.
panic(fmt.Errorf("bad asset pattern: %w", err))
}
isLogin, err := path.Match("/login.*", p)
if err != nil {
// Same as above.
panic(fmt.Errorf("bad login pattern: %w", err))
}
return isAsset || isLogin
}
// authHandler is a helper structure that implements [http.Handler].
type authHandler struct {
handler http.Handler
}
// ServeHTTP implements the [http.Handler] interface for *authHandler.
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
optionalAuth(a.handler.ServeHTTP)(w, r)
}
// optionalAuthHandler returns a authentication handler.
func optionalAuthHandler(handler http.Handler) http.Handler {
return &authHandler{handler}
}

View File

@@ -1,16 +1,12 @@
package home
import (
"bytes"
"crypto/rand"
"encoding/hex"
"net"
"net/http"
"net/textproto"
"net/url"
"path/filepath"
"testing"
"time"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/testutil"
@@ -18,82 +14,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestNewSessionToken(t *testing.T) {
// Successful case.
token, err := newSessionToken()
require.NoError(t, err)
assert.Len(t, token, sessionTokenSize)
// Break the rand.Reader.
prevReader := rand.Reader
t.Cleanup(func() { rand.Reader = prevReader })
rand.Reader = &bytes.Buffer{}
// Unsuccessful case.
token, err = newSessionToken()
require.Error(t, err)
assert.Empty(t, token)
}
func TestAuth(t *testing.T) {
dir := t.TempDir()
fn := filepath.Join(dir, "sessions.db")
users := []webUser{{
Name: "name",
PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2",
}}
a := InitAuth(fn, nil, 60, nil)
s := session{}
user := webUser{Name: "name"}
err := a.Add(&user, "password")
require.NoError(t, err)
assert.Equal(t, checkSessionNotFound, a.checkSession("notfound"))
a.RemoveSession("notfound")
sess, err := newSessionToken()
require.NoError(t, err)
sessStr := hex.EncodeToString(sess)
now := time.Now().UTC().Unix()
// check expiration
s.expire = uint32(now)
a.addSession(sess, &s)
assert.Equal(t, checkSessionExpired, a.checkSession(sessStr))
// add session with TTL = 2 sec
s = session{}
s.expire = uint32(time.Now().UTC().Unix() + 2)
a.addSession(sess, &s)
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
a.Close()
// load saved session
a = InitAuth(fn, users, 60, nil)
// the session is still alive
assert.Equal(t, checkSessionOK, a.checkSession(sessStr))
// reset our expiration time because checkSession() has just updated it
s.expire = uint32(time.Now().UTC().Unix() + 2)
a.storeSession(sess, &s)
a.Close()
u, ok := a.findUser("name", "password")
assert.True(t, ok)
assert.NotEmpty(t, u.Name)
time.Sleep(3 * time.Second)
// load and remove expired sessions
a = InitAuth(fn, users, 60, nil)
assert.Equal(t, checkSessionNotFound, a.checkSession(sessStr))
a.Close()
}
// implements http.ResponseWriter
type testResponseWriter struct {
hdr http.Header

View File

@@ -587,7 +587,7 @@ func (c *configuration) write() (err error) {
defer c.Unlock()
if Context.auth != nil {
config.Users = Context.auth.GetUsers()
config.Users = Context.auth.usersList()
}
if Context.tls != nil {

View File

@@ -420,7 +420,7 @@ func (web *webAPI) handleInstallConfigure(w http.ResponseWriter, r *http.Request
u := &webUser{
Name: req.Username,
}
err = Context.auth.Add(u, req.Password)
err = Context.auth.addUser(u, req.Password)
if err != nil {
Context.firstRun = true
copyInstallSettings(config, curConfig)

View File

@@ -3,6 +3,7 @@
package ipset
import (
"bytes"
"fmt"
"net"
"strings"
@@ -38,19 +39,69 @@ func newManager(ipsetConf []string) (set Manager, err error) {
// defaultDial is the default netfilter dialing function.
func defaultDial(pf netfilter.ProtoFamily, conf *netlink.Config) (conn ipsetConn, err error) {
conn, err = ipset.Dial(pf, conf)
c, err := ipset.Dial(pf, conf)
if err != nil {
return nil, err
}
return conn, nil
return &queryConn{c}, nil
}
// queryConn is the [ipsetConn] implementation with listAll method, which
// returns the list of properties of all available ipsets.
type queryConn struct {
*ipset.Conn
}
// type check
var _ ipsetConn = (*queryConn)(nil)
// listAll returns the list of properties of all available ipsets.
//
// TODO(s.chzhen): Use https://github.com/vishvananda/netlink.
func (qc *queryConn) listAll() (sets []props, err error) {
msg, err := netfilter.MarshalNetlink(
netfilter.Header{
// The family doesn't seem to matter. See TODO on parseIpsetConfig.
Family: qc.Conn.Family,
SubsystemID: netfilter.NFSubsysIPSet,
MessageType: netfilter.MessageType(ipset.CmdList),
Flags: netlink.Request | netlink.Dump,
},
[]netfilter.Attribute{{
Type: uint16(ipset.AttrProtocol),
Data: []byte{ipset.Protocol},
}},
)
if err != nil {
return nil, fmt.Errorf("marshaling netlink msg: %w", err)
}
// We assume it's OK to call a method of an unexported type
// [ipset.connector], since there is no negative effects.
ms, err := qc.Conn.Conn.Query(msg)
if err != nil {
return nil, fmt.Errorf("querying netlink msg: %w", err)
}
for i, s := range ms {
p := props{}
err = p.unmarshalMessage(s)
if err != nil {
return nil, fmt.Errorf("unmarshaling netlink msg at index %d: %w", i, err)
}
sets = append(sets, p)
}
return sets, nil
}
// ipsetConn is the ipset conn interface.
type ipsetConn interface {
Add(name string, entries ...*ipset.Entry) (err error)
Close() (err error)
Header(name string) (p *ipset.HeaderPolicy, err error)
listAll() (sets []props, err error)
}
// dialer creates an ipsetConn.
@@ -58,8 +109,75 @@ type dialer func(pf netfilter.ProtoFamily, conf *netlink.Config) (conn ipsetConn
// props contains one Linux Netfilter ipset properties.
type props struct {
name string
// name of the ipset.
name string
// family of the IP addresses in the ipset.
family netfilter.ProtoFamily
// isPersistent indicates that ipset has no timeout parameter and all
// entries are added permanently.
isPersistent bool
}
// unmarshalMessage unmarshals netlink message and sets the properties of the
// ipset.
func (p *props) unmarshalMessage(msg netlink.Message) (err error) {
_, attrs, err := netfilter.UnmarshalNetlink(msg)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
// By default ipset has no timeout parameter.
p.isPersistent = true
for _, a := range attrs {
p.parseAttribute(a)
}
return nil
}
// parseAttribute parses netfilter attribute and sets the name and family of
// the ipset.
func (p *props) parseAttribute(a netfilter.Attribute) {
switch ipset.AttributeType(a.Type) {
case ipset.AttrData:
p.parseAttrData(a)
case ipset.AttrSetName:
// Trim the null character.
p.name = string(bytes.Trim(a.Data, "\x00"))
case ipset.AttrFamily:
p.family = netfilter.ProtoFamily(a.Data[0])
default:
// Go on.
}
}
// parseAttrData parses attribute data and sets the timeout of the ipset.
func (p *props) parseAttrData(a netfilter.Attribute) {
for _, a := range a.Children {
switch ipset.AttributeType(a.Type) {
case ipset.AttrTimeout:
timeout := a.Uint32()
p.isPersistent = timeout == 0
default:
// Go on.
}
}
}
// unit is a convenient alias for struct{}.
type unit = struct{}
// ipsInIpset is the type of a set of IP-address-to-ipset mappings.
type ipsInIpset map[ipInIpsetEntry]unit
// ipInIpsetEntry is the type for entries in an ipsInIpset set.
type ipInIpsetEntry struct {
ipsetName string
ipArr [net.IPv6len]byte
}
// manager is the Linux Netfilter ipset manager.
@@ -72,6 +190,13 @@ type manager struct {
// mu protects all properties below.
mu *sync.Mutex
// TODO(a.garipov): Currently, the ipset list is static, and we don't
// read the IPs already in sets, so we can assume that all incoming IPs
// are either added to all corresponding ipsets or not. When that stops
// being the case, for example if we add dynamic reconfiguration of
// ipsets, this map will need to become a per-ipset-name one.
addedIPs ipsInIpset
ipv4Conn ipsetConn
ipv6Conn ipsetConn
}
@@ -96,8 +221,8 @@ func (m *manager) dialNetfilter(conf *netlink.Config) (err error) {
return nil
}
// parseIpsetConfig parses one ipset configuration string.
func parseIpsetConfig(confStr string) (hosts, ipsetNames []string, err error) {
// parseIpsetConfigLine parses one ipset configuration line.
func parseIpsetConfigLine(confStr string) (hosts, ipsetNames []string, err error) {
confStr = strings.TrimSpace(confStr)
hostsAndNames := strings.Split(confStr, "/")
if len(hostsAndNames) != 2 {
@@ -125,50 +250,53 @@ func parseIpsetConfig(confStr string) (hosts, ipsetNames []string, err error) {
return hosts, ipsetNames, nil
}
// ipsetProps returns the properties of an ipset with the given name.
func (m *manager) ipsetProps(name string) (set props, err error) {
// The family doesn't seem to matter when we use a header query, so
// query only the IPv4 one.
// parseIpsetConfig parses the ipset configuration and stores ipsets. It
// returns an error if the configuration can't be used.
func (m *manager) parseIpsetConfig(ipsetConf []string) (err error) {
// The family doesn't seem to matter when we use a header query, so query
// only the IPv4 one.
//
// TODO(a.garipov): Find out if this is a bug or a feature.
var res *ipset.HeaderPolicy
res, err = m.ipv4Conn.Header(name)
all, err := m.ipv4Conn.listAll()
if err != nil {
return set, err
// Don't wrap the error since it's informative enough as is.
return err
}
if res == nil || res.Family == nil {
return set, errors.Error("empty response or no family data")
for _, p := range all {
m.nameToIpset[p.name] = p
}
family := netfilter.ProtoFamily(res.Family.Value)
if family != netfilter.ProtoIPv4 && family != netfilter.ProtoIPv6 {
return set, fmt.Errorf("unexpected ipset family %d", family)
for i, confStr := range ipsetConf {
var hosts, ipsetNames []string
hosts, ipsetNames, err = parseIpsetConfigLine(confStr)
if err != nil {
return fmt.Errorf("config line at idx %d: %w", i, err)
}
var ipsets []props
ipsets, err = m.ipsets(ipsetNames)
if err != nil {
return fmt.Errorf("getting ipsets from config line at idx %d: %w", i, err)
}
for _, host := range hosts {
m.domainToIpsets[host] = append(m.domainToIpsets[host], ipsets...)
}
}
return props{
name: name,
family: family,
}, nil
return nil
}
// ipsets returns currently known ipsets.
func (m *manager) ipsets(names []string) (sets []props, err error) {
for _, name := range names {
set, ok := m.nameToIpset[name]
if ok {
sets = append(sets, set)
continue
for _, n := range names {
p, ok := m.nameToIpset[n]
if !ok {
return nil, fmt.Errorf("unknown ipset %q", n)
}
set, err = m.ipsetProps(name)
if err != nil {
return nil, fmt.Errorf("querying ipset %q: %w", name, err)
}
m.nameToIpset[name] = set
sets = append(sets, set)
sets = append(sets, p)
}
return sets, nil
@@ -186,6 +314,8 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err
domainToIpsets: make(map[string][]props),
dial: dial,
addedIPs: make(ipsInIpset),
}
err = m.dialNetfilter(&netlink.Config{})
@@ -201,26 +331,9 @@ func newManagerWithDialer(ipsetConf []string, dial dialer) (mgr Manager, err err
return nil, fmt.Errorf("dialing netfilter: %w", err)
}
for i, confStr := range ipsetConf {
var hosts, ipsetNames []string
hosts, ipsetNames, err = parseIpsetConfig(confStr)
if err != nil {
return nil, fmt.Errorf("config line at idx %d: %w", i, err)
}
var ipsets []props
ipsets, err = m.ipsets(ipsetNames)
if err != nil {
return nil, fmt.Errorf(
"getting ipsets from config line at idx %d: %w",
i,
err,
)
}
for _, host := range hosts {
m.domainToIpsets[host] = append(m.domainToIpsets[host], ipsets...)
}
err = m.parseIpsetConfig(ipsetConf)
if err != nil {
return nil, fmt.Errorf("getting ipsets: %w", err)
}
return m, nil
@@ -259,8 +372,19 @@ func (m *manager) addIPs(host string, set props, ips []net.IP) (n int, err error
}
var entries []*ipset.Entry
var newAddedEntries []ipInIpsetEntry
for _, ip := range ips {
e := ipInIpsetEntry{
ipsetName: set.name,
}
copy(e.ipArr[:], ip.To16())
if _, added := m.addedIPs[e]; added {
continue
}
entries = append(entries, ipset.NewEntry(ipset.EntryIP(ip)))
newAddedEntries = append(newAddedEntries, e)
}
n = len(entries)
@@ -283,6 +407,15 @@ func (m *manager) addIPs(host string, set props, ips []net.IP) (n int, err error
return 0, fmt.Errorf("adding %q%s to ipset %q: %w", host, ips, set.name, err)
}
// Only add these to the cache once we're sure that all of them were
// actually sent to the ipset.
for _, e := range newAddedEntries {
s := m.nameToIpset[e.ipsetName]
if s.isPersistent {
m.addedIPs[e] = unit{}
}
}
return n, nil
}

View File

@@ -21,8 +21,12 @@ type fakeConn struct {
ipv4Entries *[]*ipset.Entry
ipv6Header *ipset.HeaderPolicy
ipv6Entries *[]*ipset.Entry
sets []props
}
// type check
var _ ipsetConn = (*fakeConn)(nil)
// Add implements the [ipsetConn] interface for *fakeConn.
func (c *fakeConn) Add(name string, entries ...*ipset.Entry) (err error) {
if strings.Contains(name, "ipv4") {
@@ -43,15 +47,9 @@ func (c *fakeConn) Close() (err error) {
return nil
}
// Header implements the [ipsetConn] interface for *fakeConn.
func (c *fakeConn) Header(name string) (p *ipset.HeaderPolicy, err error) {
if strings.Contains(name, "ipv4") {
return c.ipv4Header, nil
} else if strings.Contains(name, "ipv6") {
return c.ipv6Header, nil
}
return nil, errors.Error("test: ipset not found")
// listAll implements the [ipsetConn] interface for *fakeConn.
func (c *fakeConn) listAll() (sets []props, err error) {
return c.sets, nil
}
func TestManager_Add(t *testing.T) {
@@ -76,6 +74,13 @@ func TestManager_Add(t *testing.T) {
Family: ipset.NewUInt8Box(uint8(netfilter.ProtoIPv6)),
},
ipv6Entries: &ipv6Entries,
sets: []props{{
name: "ipv4set",
family: netfilter.ProtoIPv4,
}, {
name: "ipv6set",
family: netfilter.ProtoIPv6,
}},
}, nil
}

View File

@@ -139,7 +139,7 @@ func (l *queryLog) clear() {
l.bufferLock.Lock()
defer l.bufferLock.Unlock()
l.buffer = nil
l.buffer.Clear()
l.flushPending = false
}()

View File

@@ -8,10 +8,8 @@ require (
github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601
github.com/kisielk/errcheck v1.6.3
github.com/kyoh86/looppointer v0.2.1
github.com/securego/gosec/v2 v2.18.0
// TODO(a.garipov): Return to latest once the release is tagged
// correctly. See uudashr/gocognit#31.
github.com/uudashr/gocognit v1.0.8-0.20230906062305-bc9ca12659bf
github.com/securego/gosec/v2 v2.18.1
github.com/uudashr/gocognit v1.1.1
golang.org/x/tools v0.14.0
golang.org/x/vuln v1.0.1
honnef.co/go/tools v0.4.6

View File

@@ -33,11 +33,11 @@ github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA
github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/securego/gosec/v2 v2.18.0 h1:PEQsNSe8NjGrQ6oF3LrmMhZTm2iqYKUOYf+OWXzo+XQ=
github.com/securego/gosec/v2 v2.18.0/go.mod h1:06rgh4+5IrlRpi573DJRZ6y/tlIE+a0rFgMlJDxFIyQ=
github.com/securego/gosec/v2 v2.18.1 h1:xnnehWg7dIW8qrRPGm8ykY21zp2MueKyC99Vlcuj96I=
github.com/securego/gosec/v2 v2.18.1/go.mod h1:ZUTcKD9gAFip1lLGHWCjkoBQJyaEzePTNzjwlL2HHoE=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/uudashr/gocognit v1.0.8-0.20230906062305-bc9ca12659bf h1:LA5CHw6L5BvI0RjqvBYa9+3hXAL2rhuAJPtS5rS2a2k=
github.com/uudashr/gocognit v1.0.8-0.20230906062305-bc9ca12659bf/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY=
github.com/uudashr/gocognit v1.1.1 h1:qIj6KhmcGQGBiWtaKH6ZlIyDGa6br2febZNZ6MDzqMw=
github.com/uudashr/gocognit v1.1.1/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -63,7 +63,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@@ -35,7 +35,7 @@ set -f -u
go_version="$( "${GO:-go}" version )"
readonly go_version
go_min_version='go1.20.10'
go_min_version='go1.20.11'
go_version_msg="
warning: your go version (${go_version}) is different from the recommended minimal one (${go_min_version}).
if you have the version installed, please set the GO environment variable.