Compare commits

..

40 Commits

Author SHA1 Message Date
Dimitry Kolyshev
c1be2bab4d rewrite: imp code 2023-01-09 18:51:05 +07:00
Dimitry Kolyshev
53cd9b7a1a Merge remote-tracking branch 'origin/master' into 2499-rewrites-3
# Conflicts:
#	internal/home/dns.go
2023-01-09 18:07:26 +07:00
Eugene Burkov
d8d7a5c335 Pull request: 5191-update-flag
Merge in DNS/adguard-home from 5191-update-flag to master

Updates #5191.
Updates #4223.

Squashed commit of the following:

commit fbace4942844dc67f2467479385e06843c3abb6a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Jan 9 12:05:16 2023 +0400

    all: imp code, docs

commit 8237dceb771ba95f545f79565d76cbb4ebd0d805
Merge: ca9518f2 bbdcc673
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Dec 30 14:45:55 2022 +0400

    Merge branch 'master' into 5191-update-flag

commit ca9518f20e5643572adf9734b93a5436ba30c865
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Dec 29 20:36:33 2022 +0400

    home: imp code

commit 1dc6c7c3480df3df4a5f3f923f1feab7761a7945
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Dec 29 18:26:08 2022 +0400

    all: imp code, docs

commit 7bbe893e98063b956482fd6f1c6be95a4f1956cf
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Dec 29 03:44:48 2022 +0400

    home: restart service on update

commit e0d3c287a7e1c05b1e397f4727c447a1fcd9f7f6
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Dec 29 03:22:49 2022 +0400

    all: update on first run

commit 0aa4e78f03bf3819425accb468ce59e747506ef3
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Dec 29 02:47:30 2022 +0400

    all: move some code to init less

commit 68aebfa050b9965afef26653e9b699ff4aaf5b8b
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Dec 29 00:36:00 2022 +0400

    WIP

commit 2c7fb97d701ac158613c5a3a4d4d35c5b79b3d59
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Dec 28 14:15:59 2022 +0400

    home: imp logs

commit 4b06d089da835d6d187803bbb5ca1caf9973e2d3
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Dec 27 19:21:17 2022 +0400

    all: fix update flag
2023-01-09 13:38:31 +03:00
Dimitry Kolyshev
18a6066df5 rewrite: qtype matching 2022-12-30 23:42:55 +07:00
Dimitry Kolyshev
18392943fa all: rewrite package dependency 2022-12-30 12:36:47 +07:00
Dimitry Kolyshev
c2abedec70 Merge remote-tracking branch 'origin/master' into 2499-rewrites-3 2022-12-30 11:21:05 +07:00
Dimitry Kolyshev
bbdcc673a2 Pull request: 5270-updater-package-url
Merge in DNS/adguard-home from 5270-updater-package-url to master

Squashed commit of the following:

commit 50ee8edb6270e750ed82b14c174f88922aff86bb
Author: Dimitry Kolyshev <dkolyshev@adguard.com>
Date:   Wed Dec 28 12:21:24 2022 +0700

    updater: package url
2022-12-28 15:14:08 +03:00
Dimitry Kolyshev
d3bf5fcb05 rewrite: imp code 2022-12-28 11:16:07 +07:00
Dimitry Kolyshev
5a794411d9 Merge remote-tracking branch 'origin/master' into 2499-rewrites-3 2022-12-27 10:09:33 +07:00
Dimitry Kolyshev
8e058b8042 filtering: rewrite tests 2022-12-26 22:59:17 +07:00
Dimitry Kolyshev
d76834f843 filtering: imp code 2022-12-26 22:44:19 +07:00
Ildar Kamalov
e7fc61a997 Pull request: 4898 reload page on tls settings save
Merge in DNS/adguard-home from 4898-reload-page to master

Closes #4898.

Squashed commit of the following:

commit c2d78804d96d00b5ff10e23c0f275a6c73455b93
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 26 18:03:31 2022 +0400

    all: fix changelog

commit e55db6a620e4aa74eb3562a4eb35bbd3d13c8712
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 26 18:02:37 2022 +0400

    all: log changes

commit 3985dc4a42d1dc717def3011d7dfd24dd49da0da
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Dec 26 15:20:38 2022 +0300

    client: reload page on tls settings save
2022-12-26 17:19:51 +03:00
Eugene Burkov
97af23b0af Pull request: 5290-rules-count
Merge in DNS/adguard-home from 5290-rules-count to master

Closes #5290.

Squashed commit of the following:

commit c29fd668dd8f25dbfe978fb95f850acbbd632b8b
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Dec 23 14:42:57 2022 +0400

    all: log changes

commit fba4fe7cc046578f17cdf72dff93523558b8aa1f
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Fri Dec 23 14:33:57 2022 +0400

    filtering: fix rules count on err
2022-12-23 17:11:11 +03:00
Dimitry Kolyshev
5480bed1f7 Merge remote-tracking branch 'origin/master' into 2499-rewrites-3 2022-12-23 13:28:28 +07:00
Dimitry Kolyshev
c5fb7e6b0d rewrite: tests subdomains matching 2022-12-23 13:26:29 +07:00
Dimitry Kolyshev
9efc381224 rewrite: test matching cnames 2022-12-23 13:18:04 +07:00
Ildar Kamalov
e481922d91 Pull request: 4898 fix redirect to current protocol on tls settings change
Updates #4898

Squashed commit of the following:

commit b10b0a5adeeb44375912d34a9ef60a7f4ff9688c
Merge: 9d4bc0ef defde7d0
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Dec 21 17:49:52 2022 +0300

    Merge branch 'master' into 4898-tls-redirect

commit 9d4bc0efaf1639380325f75684514386ec581206
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Dec 21 16:39:17 2022 +0300

    fix helper

commit cb5b43e65c6224b6d9fe012b52a18ee8e2e1c19a
Author: Ildar Kamalov <ik@adguard.com>
Date:   Mon Dec 19 19:16:07 2022 +0300

    client: reload page on tls enable
2022-12-21 18:13:07 +03:00
Ildar Kamalov
defde7d0fe Pull request: 5249 fix search input outline
Updates #5249

Squashed commit of the following:

commit f1a16e79073d0b887a1c27becab0aeb039d7268b
Merge: b752c58e 0c03063c
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Dec 21 17:06:59 2022 +0300

    Merge branch 'master' into 5249-outline

commit b752c58e0a79815d22853cd85419ffa64f38f530
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Dec 21 12:34:15 2022 +0300

    fix input borders

commit 1c7d5da85a30ba951b6ad0a0b8e70780309686fa
Merge: 40fb0273 0ddd8e3d
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Dec 21 12:27:25 2022 +0300

    Merge branch 'master' into 5249-outline

commit 40fb02734de1a525681d54295600897088c02ac9
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Dec 20 13:29:15 2022 +0300

    5249 fix search input outline
2022-12-21 17:48:35 +03:00
Ildar Kamalov
0c03063c8a Pull request: 4962 trim client upstream field whitespace
Updates #4962

Squashed commit of the following:

commit d1382e197455987cf1539b28a09da7b5377dd784
Merge: 510d80bd 0ddd8e3d
Author: Ildar Kamalov <ik@adguard.com>
Date:   Wed Dec 21 12:26:05 2022 +0300

    Merge branch 'master' into 4962-trim-whitespace

commit 510d80bde01fb027f72b2371bda35c62c6b02ca6
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Dec 20 13:57:13 2022 +0300

    4962 trim client upstream field whitespace
2022-12-21 12:38:51 +03:00
Eugene Burkov
0ddd8e3dcc Pull request: 5258-changelog
Merge in DNS/adguard-home from 5258-changelog to master

Updates #5258.

Squashed commit of the following:

commit a46f9435b91cfdd9f1da6132526707fe81004e51
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Dec 20 18:36:41 2022 +0400

    all: log changes
2022-12-20 17:59:55 +03:00
Eugene Burkov
48cbc7bdf0 Pull request: 5258-good-old-filters
Merge in DNS/adguard-home from 5258-good-old-filters to master

Updates #5258.

Squashed commit of the following:

commit 8555e685a104713e552f017de63281749f41b6b2
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Dec 20 16:07:52 2022 +0400

    filtering: imp tests, docs

commit 2ecfc18fc69850a06461620a24527158603cd7b8
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Tue Dec 20 11:00:59 2022 +0400

    filtering: fix docs

commit 1ea8d45a85f3fb6794b44134e8fdcbe2044d2199
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 19 23:19:37 2022 +0400

    filtering: imp naming, docs

commit c52a3bba48738c002111c234fb4c312380e49cfc
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 19 23:13:37 2022 +0400

    filtering: imp logic

commit 3ad4276ace40f05db47b49fb033d1b0fa208ec4e
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 19 17:49:15 2022 +0400

    filtering: imp docs

commit 1bc3cc443bc8ec988532effaaf5f50474a1a69ab
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 19 17:45:37 2022 +0400

    filtering: imp more

commit 7908339a0c9fcc29e8fe12b6c5d8c14bbfa51364
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 19 16:57:42 2022 +0400

    filtering: imp code

commit 21bbd18b4ded83f354210ac32010d8fd1073452f
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Mon Dec 19 12:11:21 2022 +0400

    filtering: imp src reading
2022-12-20 16:40:42 +03:00
Dimitry Kolyshev
299371e0fd rewrite: imp code 2022-12-19 12:50:00 +07:00
Dimitry Kolyshev
12f52f07c5 Merge remote-tracking branch 'origin/master' into 2499-rewrites-3 2022-12-16 11:51:08 +07:00
Ainar Garipov
de08ef0077 Pull request: unignore-snap
Merge in DNS/adguard-home from unignore-snap to master

Squashed commit of the following:

commit 2901080c92d5316bcbf536ebdfeaff88cea0edd4
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Dec 15 19:08:46 2022 +0300

    all: unignore snap
2022-12-15 19:13:59 +03:00
Ainar Garipov
cfab157146 Pull request: upd-chlog
Merge in DNS/adguard-home from upd-chlog to master

Squashed commit of the following:

commit 6d99abd2144219393be3997b723288c36ee72faf
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Dec 15 18:14:07 2022 +0300

    all: upd chlog
2022-12-15 18:19:00 +03:00
Ainar Garipov
ec05ee16fe Pull request: skip-snap
Merge in DNS/adguard-home from skip-snap to master

Squashed commit of the following:

commit 3859612645826ccad025ab4ba7e03adac6d09842
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Dec 15 17:19:48 2022 +0300

    all: skip snap upload temporarily; imp issue tmpl
2022-12-15 17:38:01 +03:00
Ainar Garipov
c1b537c14b Pull request: upd-all
Merge in DNS/adguard-home from upd-all to master

Squashed commit of the following:

commit 4031b9ca886c41f07ac949dfae8dd00d0969138f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Dec 15 15:19:01 2022 +0300

    all: upd blocked svcs, i18n, vetted flts
2022-12-15 15:31:42 +03:00
Dimitry Kolyshev
990311c9e0 Merge remote-tracking branch 'origin/master' into 2499-rewrites-3 2022-12-15 12:43:45 +07:00
Dimitry Kolyshev
526c358697 all: rewrites 2022-12-15 12:13:14 +07:00
Eugene Burkov
d77b743c7b Pull request: 5251-close-ups
Merge in DNS/adguard-home from 5251-close-ups to master

Updates #5251.

Squashed commit of the following:

commit 98a4a9a45ae702df3cf26cab0b28bd83a6556085
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Dec 14 18:31:17 2022 +0300

    all: log changes better

commit af25803925c15ba2d9b07865c3deb58033006c52
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Dec 14 18:24:42 2022 +0300

    all: log changes

commit 65bb12d8b6d6bd2e37ee83bc4aca63aa573da63a
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Dec 14 18:15:41 2022 +0300

    all: upd dnsproxy

commit 24039cd7f9c64ee5d9806e1146dbd1e76c298a20
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Dec 14 17:19:15 2022 +0300

    dnsforward: imp code, docs

commit a40bbd55267c9904c14b89568408f86ccb3ef6c9
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Dec 14 16:43:10 2022 +0300

    dnsforward: log errs instead of return
2022-12-14 18:39:31 +03:00
Dimitry Kolyshev
e657899c32 rewrite: storage 2022-12-13 13:59:43 +07:00
Dimitry Kolyshev
fb3602853a rewrite: storage 2022-12-13 13:48:33 +07:00
Dimitry Kolyshev
2cf171f21e all: rewrite http 2022-12-13 13:05:11 +07:00
Dimitry Kolyshev
e56f465ad8 filtering: rewrite http 2022-12-13 12:34:56 +07:00
Dimitry Kolyshev
a8e80bc583 filtering: rewrite http 2022-12-12 21:32:48 +07:00
Dimitry Kolyshev
9a186d0a8a all: upd deps 2022-12-12 21:30:27 +07:00
Dimitry Kolyshev
2d29455d7f Merge remote-tracking branch 'origin/master' into 2499-rewrites-3 2022-12-12 12:27:15 +07:00
Ainar Garipov
8d453e75a4 Pull request: 5238-default-filter-urls
Updates #5238.

Squashed commit of the following:

commit 6a1b58bfa0f0bb8cc924e294485e8f650ce7f7aa
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Dec 8 14:18:54 2022 +0300

    all: upd go in github actions

commit 98366880dc32290258dfae152be069cb84ed86c6
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Dec 8 14:08:36 2022 +0300

    home: sync default filter urls
2022-12-08 16:27:10 +03:00
Dimitry Kolyshev
55a0dec144 all: imp code 2022-12-06 12:38:05 +02:00
Dimitry Kolyshev
6b607e982b all: rewrites 2022-12-05 14:37:55 +02:00
70 changed files with 1085 additions and 1476 deletions

View File

@@ -7,9 +7,9 @@
'name': 'AdGuard filters issues' 'name': 'AdGuard filters issues'
'url': 'https://link.adtidy.org/forward.html?action=report&app=home&from=github' 'url': 'https://link.adtidy.org/forward.html?action=report&app=home&from=github'
- 'about': > - 'about': >
Please send requests for addition to the vetted filtering lists to the Please send requests for new blocked services and vetted filtering lists
Hostlists Registry repository. to the Hostlists Registry repository
'name': 'AdGuard Hostlists Registry' 'name': 'Blocked services and vetted filtering rule lists: AdGuard Hostlists Registry'
'url': 'https://github.com/AdguardTeam/HostlistsRegistry' 'url': 'https://github.com/AdguardTeam/HostlistsRegistry'
- 'about': > - 'about': >
Please use GitHub Discussions for questions Please use GitHub Discussions for questions

View File

@@ -1,7 +1,7 @@
'name': 'build' 'name': 'build'
'env': 'env':
'GO_VERSION': '1.18.8' 'GO_VERSION': '1.18.9'
'NODE_VERSION': '14' 'NODE_VERSION': '14'
'on': 'on':

View File

@@ -1,7 +1,7 @@
'name': 'lint' 'name': 'lint'
'env': 'env':
'GO_VERSION': '1.18.8' 'GO_VERSION': '1.18.9'
'on': 'on':
'push': 'push':

View File

@@ -18,12 +18,48 @@ and this project adheres to
<!-- <!--
## [v0.107.21] - 2122-12-28 (APPROX.) ## [v0.107.22] - 2222-12-28 (APPROX.)
See also the [v0.107.22 GitHub milestone][ms-v0.107.22].
[ms-v0.107.22]: https://github.com/AdguardTeam/AdGuardHome/milestone/58?closed=1
-->
## [v0.107.21] - 2122-12-15
See also the [v0.107.21 GitHub milestone][ms-v0.107.21]. See also the [v0.107.21 GitHub milestone][ms-v0.107.21].
### Changed
- The URLs of the default filters for new installations are synchronized to
those introduced in v0.107.20 ([#5238]).
**NOTE:** Some users may need to re-add the lists from the vetted filter lists
to update the URLs to the new ones. Custom filters added by users themselves
do not require re-adding.
### Fixed
- `AdGuardHome --update` freezing when another instance of AdGuard Home is
running ([#4223], [#5191]).
- The `--update` flag performing an update even with the same version.
- Failing HTTPS redirection on saving the encryption settings ([#4898]).
- Zeroing rules counter of erroneusly edited filtering rule lists ([#5290]).
- Filters updating strategy, which could sometimes lead to use of broken or
incompletely downloaded lists ([#5258]).
- Errors popping up during updates of settings, which could sometimes cause the
server to stop responding ([#5251]).
[#4898]: https://github.com/AdguardTeam/AdGuardHome/issues/4898
[#5191]: https://github.com/AdguardTeam/AdGuardHome/issues/5191
[#5238]: https://github.com/AdguardTeam/AdGuardHome/issues/5238
[#5251]: https://github.com/AdguardTeam/AdGuardHome/issues/5251
[#5258]: https://github.com/AdguardTeam/AdGuardHome/issues/5258
[#5290]: https://github.com/AdguardTeam/AdGuardHome/issues/5290
[ms-v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/milestone/57?closed=1 [ms-v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/milestone/57?closed=1
-->
@@ -1482,11 +1518,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
<!-- <!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.21...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.22...HEAD
[v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.20...v0.107.21 [v0.107.22]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.21...v0.107.22
--> -->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.20...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.21...HEAD
[v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.20...v0.107.21
[v0.107.20]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.19...v0.107.20 [v0.107.20]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.19...v0.107.20
[v0.107.19]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.18...v0.107.19 [v0.107.19]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.18...v0.107.19
[v0.107.18]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.17...v0.107.18 [v0.107.18]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.17...v0.107.18

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Налады DHCP IPv6", "dhcp_ipv6_settings": "Налады DHCP IPv6",
"form_error_required": "Абавязковае поле", "form_error_required": "Абавязковае поле",
"form_error_ip4_format": "Няслушны IPv4-адрас", "form_error_ip4_format": "Няслушны IPv4-адрас",
"form_error_ip4_range_start_format": "Няслушны IPv4-адрас пачатку дыяпазону",
"form_error_ip4_range_end_format": "Няслушны IPv4-адрас канца дыяпазону",
"form_error_ip4_gateway_format": "Няслушны IPv4-адрас шлюза", "form_error_ip4_gateway_format": "Няслушны IPv4-адрас шлюза",
"form_error_ip6_format": "Няслушны IPv6-адрас", "form_error_ip6_format": "Няслушны IPv6-адрас",
"form_error_ip_format": "Няслушны IP-адрас", "form_error_ip_format": "Няслушны IP-адрас",
@@ -51,7 +49,6 @@
"out_of_range_error": "Павінна быць па-за дыяпазонам «{{start}}»-«{{end}}»", "out_of_range_error": "Павінна быць па-за дыяпазонам «{{start}}»-«{{end}}»",
"lower_range_start_error": "Павінна быць менш за пачатак дыяпазону", "lower_range_start_error": "Павінна быць менш за пачатак дыяпазону",
"greater_range_start_error": "Павінна быць больш за пачатак дыяпазону", "greater_range_start_error": "Павінна быць больш за пачатак дыяпазону",
"greater_range_end_error": "Павінна быць больш за канец дыяпазону",
"subnet_error": "Адрасы павінны быць усярэдзіне адной падсеткі", "subnet_error": "Адрасы павінны быць усярэдзіне адной падсеткі",
"gateway_or_subnet_invalid": "Некарэктная маска падсеткі", "gateway_or_subnet_invalid": "Некарэктная маска падсеткі",
"dhcp_form_gateway_input": "IP-адрас шлюза", "dhcp_form_gateway_input": "IP-адрас шлюза",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Nastavení DHCP IPv6", "dhcp_ipv6_settings": "Nastavení DHCP IPv6",
"form_error_required": "Povinné pole", "form_error_required": "Povinné pole",
"form_error_ip4_format": "Neplatná adresa IPv4", "form_error_ip4_format": "Neplatná adresa IPv4",
"form_error_ip4_range_start_format": "Neplatná adresa IPv4 na začátku rozsahu",
"form_error_ip4_range_end_format": "Neplatná adresa IPv4 na konci rozsahu",
"form_error_ip4_gateway_format": "Neplatná adresa IPv4 brány", "form_error_ip4_gateway_format": "Neplatná adresa IPv4 brány",
"form_error_ip6_format": "Neplatná adresa IPv6", "form_error_ip6_format": "Neplatná adresa IPv6",
"form_error_ip_format": "Neplatná IP adresa", "form_error_ip_format": "Neplatná IP adresa",
@@ -51,7 +49,6 @@
"out_of_range_error": "Musí být mimo rozsah \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Musí být mimo rozsah \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Musí být menší než začátek rozsahu", "lower_range_start_error": "Musí být menší než začátek rozsahu",
"greater_range_start_error": "Musí být větší než začátek rozsahu", "greater_range_start_error": "Musí být větší než začátek rozsahu",
"greater_range_end_error": "Musí být větší než konec rozsahu",
"subnet_error": "Adresy musí být v jedné podsíti", "subnet_error": "Adresy musí být v jedné podsíti",
"gateway_or_subnet_invalid": "Neplatná maska podsítě", "gateway_or_subnet_invalid": "Neplatná maska podsítě",
"dhcp_form_gateway_input": "IP brána", "dhcp_form_gateway_input": "IP brána",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6-indstillinger", "dhcp_ipv6_settings": "DHCP IPv6-indstillinger",
"form_error_required": "Obligatorisk felt", "form_error_required": "Obligatorisk felt",
"form_error_ip4_format": "Ugyldig IPv4-adresse", "form_error_ip4_format": "Ugyldig IPv4-adresse",
"form_error_ip4_range_start_format": "Ugyldig IPv4-startadresse for området",
"form_error_ip4_range_end_format": "Ugyldig IPv4-slutadresse for området",
"form_error_ip4_gateway_format": "Ugyldig IPv4 gateway-adresse", "form_error_ip4_gateway_format": "Ugyldig IPv4 gateway-adresse",
"form_error_ip6_format": "Ugyldig IPv6-adresse", "form_error_ip6_format": "Ugyldig IPv6-adresse",
"form_error_ip_format": "Ugyldig IP-adresse", "form_error_ip_format": "Ugyldig IP-adresse",
@@ -51,9 +49,8 @@
"out_of_range_error": "Skal være uden for området \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Skal være uden for området \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Skal være mindre end starten på området", "lower_range_start_error": "Skal være mindre end starten på området",
"greater_range_start_error": "Skal være større end starten på området", "greater_range_start_error": "Skal være større end starten på området",
"greater_range_end_error": "Skal være større end områdeslutning",
"subnet_error": "Adresser ska være i ét undernet", "subnet_error": "Adresser ska være i ét undernet",
"gateway_or_subnet_invalid": "Undernetmaske ugyldig", "gateway_or_subnet_invalid": "Ugyldig undernetmaske",
"dhcp_form_gateway_input": "Gateway IP", "dhcp_form_gateway_input": "Gateway IP",
"dhcp_form_subnet_input": "Undernetmaske", "dhcp_form_subnet_input": "Undernetmaske",
"dhcp_form_range_title": "Interval af IP-adresser", "dhcp_form_range_title": "Interval af IP-adresser",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP-IPv6-Einstellungen", "dhcp_ipv6_settings": "DHCP-IPv6-Einstellungen",
"form_error_required": "Pflichtfeld", "form_error_required": "Pflichtfeld",
"form_error_ip4_format": "Ungültige IPv4-Adresse", "form_error_ip4_format": "Ungültige IPv4-Adresse",
"form_error_ip4_range_start_format": "Ungültiger Bereichsbeginn der IPv4-Adresse",
"form_error_ip4_range_end_format": "Ungültiges Bereichsende der IPv4-Adresse",
"form_error_ip4_gateway_format": "Ungültige IPv4-Adresse des Gateways", "form_error_ip4_gateway_format": "Ungültige IPv4-Adresse des Gateways",
"form_error_ip6_format": "Ungültige IPv6-Adresse", "form_error_ip6_format": "Ungültige IPv6-Adresse",
"form_error_ip_format": "Ungültige IP-Adresse", "form_error_ip_format": "Ungültige IP-Adresse",
@@ -51,7 +49,6 @@
"out_of_range_error": "Muss außerhalb des Bereichs „{{start}}“-„{{end}}“ liegen", "out_of_range_error": "Muss außerhalb des Bereichs „{{start}}“-„{{end}}“ liegen",
"lower_range_start_error": "Muss niedriger als der Bereichsbeginn sein", "lower_range_start_error": "Muss niedriger als der Bereichsbeginn sein",
"greater_range_start_error": "Muss größer als der Bereichsbeginn sein", "greater_range_start_error": "Muss größer als der Bereichsbeginn sein",
"greater_range_end_error": "Muss größer als das Bereichsende sein",
"subnet_error": "Die Adressen müssen innerhalb eines Subnetzes liegen", "subnet_error": "Die Adressen müssen innerhalb eines Subnetzes liegen",
"gateway_or_subnet_invalid": "Ungültige Subnetzmaske", "gateway_or_subnet_invalid": "Ungültige Subnetzmaske",
"dhcp_form_gateway_input": "Gateway-IP", "dhcp_form_gateway_input": "Gateway-IP",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Configuración DHCP IPv6", "dhcp_ipv6_settings": "Configuración DHCP IPv6",
"form_error_required": "Campo obligatorio", "form_error_required": "Campo obligatorio",
"form_error_ip4_format": "Dirección IPv4 no válida", "form_error_ip4_format": "Dirección IPv4 no válida",
"form_error_ip4_range_start_format": "Dirección IPv4 no válida del inicio de rango",
"form_error_ip4_range_end_format": "Dirección IPv4 no válida del final de rango",
"form_error_ip4_gateway_format": "Dirección IPv4 no válida de la puerta de enlace", "form_error_ip4_gateway_format": "Dirección IPv4 no válida de la puerta de enlace",
"form_error_ip6_format": "Dirección IPv6 no válida", "form_error_ip6_format": "Dirección IPv6 no válida",
"form_error_ip_format": "Dirección IP no válida", "form_error_ip_format": "Dirección IP no válida",
@@ -51,7 +49,6 @@
"out_of_range_error": "Debe estar fuera del rango \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Debe estar fuera del rango \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Debe ser inferior que el inicio de rango", "lower_range_start_error": "Debe ser inferior que el inicio de rango",
"greater_range_start_error": "Debe ser mayor que el inicio de rango", "greater_range_start_error": "Debe ser mayor que el inicio de rango",
"greater_range_end_error": "Debe ser mayor que el final de rango",
"subnet_error": "Las direcciones deben estar en una subred", "subnet_error": "Las direcciones deben estar en una subred",
"gateway_or_subnet_invalid": "Máscara de subred no válida", "gateway_or_subnet_invalid": "Máscara de subred no válida",
"dhcp_form_gateway_input": "IP de puerta de enlace", "dhcp_form_gateway_input": "IP de puerta de enlace",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP:n IPv6-asetukset", "dhcp_ipv6_settings": "DHCP:n IPv6-asetukset",
"form_error_required": "Pakollinen kenttä", "form_error_required": "Pakollinen kenttä",
"form_error_ip4_format": "Virheellinen IPv4-osoite", "form_error_ip4_format": "Virheellinen IPv4-osoite",
"form_error_ip4_range_start_format": "Virheellinen IPv4-osoitealueen aloitusosoite",
"form_error_ip4_range_end_format": "Virheellinen IPv4-osoitealueen päätösosoite",
"form_error_ip4_gateway_format": "Virheellinen yhdyskäytävän IPv4-osoite", "form_error_ip4_gateway_format": "Virheellinen yhdyskäytävän IPv4-osoite",
"form_error_ip6_format": "Virheellinen IPv6-osoite", "form_error_ip6_format": "Virheellinen IPv6-osoite",
"form_error_ip_format": "Virheellinen IP-osoite", "form_error_ip_format": "Virheellinen IP-osoite",
@@ -51,7 +49,6 @@
"out_of_range_error": "Oltava alueen \"{{start}}\" - \"{{end}}\" ulkopuolella", "out_of_range_error": "Oltava alueen \"{{start}}\" - \"{{end}}\" ulkopuolella",
"lower_range_start_error": "Oltava alueen aloitusarvoa pienempi", "lower_range_start_error": "Oltava alueen aloitusarvoa pienempi",
"greater_range_start_error": "Oltava alueen aloitusarvoa suurempi", "greater_range_start_error": "Oltava alueen aloitusarvoa suurempi",
"greater_range_end_error": "Oltava alueen päätösarvoa pienempi",
"subnet_error": "Osoitteiden tulee olla yhdessä aliverkossa", "subnet_error": "Osoitteiden tulee olla yhdessä aliverkossa",
"gateway_or_subnet_invalid": "Virheellinen aliverkon peite", "gateway_or_subnet_invalid": "Virheellinen aliverkon peite",
"dhcp_form_gateway_input": "Yhdyskäytävän IP-osoite", "dhcp_form_gateway_input": "Yhdyskäytävän IP-osoite",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Paramètres IPv6 du DHCP", "dhcp_ipv6_settings": "Paramètres IPv6 du DHCP",
"form_error_required": "Champ requis", "form_error_required": "Champ requis",
"form_error_ip4_format": "Adresse IPv4 invalide", "form_error_ip4_format": "Adresse IPv4 invalide",
"form_error_ip4_range_start_format": "Adresse de début de plage IPv4 incorrecte",
"form_error_ip4_range_end_format": "Adresse de fin de plage IPv4 incorrecte",
"form_error_ip4_gateway_format": "Adresse de passerelle IPv4 invalide", "form_error_ip4_gateway_format": "Adresse de passerelle IPv4 invalide",
"form_error_ip6_format": "Adresse IPv6 invalide", "form_error_ip6_format": "Adresse IPv6 invalide",
"form_error_ip_format": "Adresse IP invalide", "form_error_ip_format": "Adresse IP invalide",
@@ -51,9 +49,8 @@
"out_of_range_error": "Doit être hors plage « {{start}} » - « {{end}} »", "out_of_range_error": "Doit être hors plage « {{start}} » - « {{end}} »",
"lower_range_start_error": "Doit être inférieur au début de plage", "lower_range_start_error": "Doit être inférieur au début de plage",
"greater_range_start_error": "Doit être supérieur au début de plage", "greater_range_start_error": "Doit être supérieur au début de plage",
"greater_range_end_error": "Doit être supérieur à la fin de plage",
"subnet_error": "Les adresses doivent être dans le même sous-réseau", "subnet_error": "Les adresses doivent être dans le même sous-réseau",
"gateway_or_subnet_invalid": "Masque de sous-réseau invalide", "gateway_or_subnet_invalid": "Masque de sous-réseau invalide.",
"dhcp_form_gateway_input": "IP de la passerelle", "dhcp_form_gateway_input": "IP de la passerelle",
"dhcp_form_subnet_input": "Masque de sous-réseau", "dhcp_form_subnet_input": "Masque de sous-réseau",
"dhcp_form_range_title": "Rangée des adresses IP", "dhcp_form_range_title": "Rangée des adresses IP",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 postavke", "dhcp_ipv6_settings": "DHCP IPv6 postavke",
"form_error_required": "Obavezno polje", "form_error_required": "Obavezno polje",
"form_error_ip4_format": "Nevažeća IPv4 adresa", "form_error_ip4_format": "Nevažeća IPv4 adresa",
"form_error_ip4_range_start_format": "Nepravilan početak ranga IPv4 adresa",
"form_error_ip4_range_end_format": "Nepravilan kraj ranga IPv4 adresa",
"form_error_ip4_gateway_format": "Nepravilna IPV4 adresa čvora", "form_error_ip4_gateway_format": "Nepravilna IPV4 adresa čvora",
"form_error_ip6_format": "Nevažeći IPv6 adresa", "form_error_ip6_format": "Nevažeći IPv6 adresa",
"form_error_ip_format": "Nepravilna IP adresa", "form_error_ip_format": "Nepravilna IP adresa",
@@ -51,9 +49,8 @@
"out_of_range_error": "Mora biti izvan ranga \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Mora biti izvan ranga \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Mora biti niže od početnog ranga", "lower_range_start_error": "Mora biti niže od početnog ranga",
"greater_range_start_error": "Mora biti veće od krajnjeg ranga", "greater_range_start_error": "Mora biti veće od krajnjeg ranga",
"greater_range_end_error": "Mora biti veće od krajnjeg ranga",
"subnet_error": "Adrese moraju biti iz iste podmreže", "subnet_error": "Adrese moraju biti iz iste podmreže",
"gateway_or_subnet_invalid": "Maska podmreže je neprvilna", "gateway_or_subnet_invalid": "Nevažeća podmrežna maska",
"dhcp_form_gateway_input": "Gateway IP", "dhcp_form_gateway_input": "Gateway IP",
"dhcp_form_subnet_input": "Subnet maskiranje", "dhcp_form_subnet_input": "Subnet maskiranje",
"dhcp_form_range_title": "Raspon IP adresa", "dhcp_form_range_title": "Raspon IP adresa",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 Beállítások", "dhcp_ipv6_settings": "DHCP IPv6 Beállítások",
"form_error_required": "Kötelező mező", "form_error_required": "Kötelező mező",
"form_error_ip4_format": "Érvénytelen IPv4 cím", "form_error_ip4_format": "Érvénytelen IPv4 cím",
"form_error_ip4_range_start_format": "Érvénytelen IPv4-cím a tartomány kezdetéhez",
"form_error_ip4_range_end_format": "Érvénytelen IPv4-cím a tartomány végén",
"form_error_ip4_gateway_format": "Az átjáróhoz (gateway) érvénytelen IPv4 cím lett megadva", "form_error_ip4_gateway_format": "Az átjáróhoz (gateway) érvénytelen IPv4 cím lett megadva",
"form_error_ip6_format": "Érvénytelen IPv6 cím", "form_error_ip6_format": "Érvénytelen IPv6 cím",
"form_error_ip_format": "Érvénytelen IP-cím", "form_error_ip_format": "Érvénytelen IP-cím",
@@ -51,7 +49,6 @@
"out_of_range_error": "A következő tartományon kívül legyen: \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "A következő tartományon kívül legyen: \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Kisebb legyen, mint a tartomány kezdete", "lower_range_start_error": "Kisebb legyen, mint a tartomány kezdete",
"greater_range_start_error": "Nagyobbnak kell lennie, mint a tartomány kezdete", "greater_range_start_error": "Nagyobbnak kell lennie, mint a tartomány kezdete",
"greater_range_end_error": "Nagyobb legyen, mint a tartomány vége",
"subnet_error": "A címeknek egy alhálózatban kell lenniük", "subnet_error": "A címeknek egy alhálózatban kell lenniük",
"gateway_or_subnet_invalid": "Az alhálózati maszk érvénytelen", "gateway_or_subnet_invalid": "Az alhálózati maszk érvénytelen",
"dhcp_form_gateway_input": "Átjáró IP", "dhcp_form_gateway_input": "Átjáró IP",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Pengaturan DHCP IPv6", "dhcp_ipv6_settings": "Pengaturan DHCP IPv6",
"form_error_required": "Kolom yang harus diisi", "form_error_required": "Kolom yang harus diisi",
"form_error_ip4_format": "Alamat IPv4 tidak valid", "form_error_ip4_format": "Alamat IPv4 tidak valid",
"form_error_ip4_range_start_format": "Alamat IPv4 tidak valid dari rentang awal",
"form_error_ip4_range_end_format": "Alamat IPv4 tidak valid dari rentang akhir",
"form_error_ip4_gateway_format": "Alamat IPv4 gateway tidak valid", "form_error_ip4_gateway_format": "Alamat IPv4 gateway tidak valid",
"form_error_ip6_format": "Alamat IPv6 tidak valid", "form_error_ip6_format": "Alamat IPv6 tidak valid",
"form_error_ip_format": "Alamat IP tidak valid", "form_error_ip_format": "Alamat IP tidak valid",
@@ -51,7 +49,6 @@
"out_of_range_error": "Harus di luar rentang \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Harus di luar rentang \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Harus lebih rendah dari rentang awal", "lower_range_start_error": "Harus lebih rendah dari rentang awal",
"greater_range_start_error": "Harus lebih besar dari rentang awal", "greater_range_start_error": "Harus lebih besar dari rentang awal",
"greater_range_end_error": "Harus lebih besar dari rentang akhir",
"subnet_error": "Alamat harus dalam satu subnet", "subnet_error": "Alamat harus dalam satu subnet",
"gateway_or_subnet_invalid": "Subnet mask tidak valid", "gateway_or_subnet_invalid": "Subnet mask tidak valid",
"dhcp_form_gateway_input": "IP gateway", "dhcp_form_gateway_input": "IP gateway",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Impostazioni DHCP IPv6", "dhcp_ipv6_settings": "Impostazioni DHCP IPv6",
"form_error_required": "Campo richiesto", "form_error_required": "Campo richiesto",
"form_error_ip4_format": "Indirizzo IPv4 non valido", "form_error_ip4_format": "Indirizzo IPv4 non valido",
"form_error_ip4_range_start_format": "Indirizzo IPV4 non valido dell'intervallo iniziale",
"form_error_ip4_range_end_format": "Indirizzo IPV4 non valido dell'intervallo finale",
"form_error_ip4_gateway_format": "Indirizzo gateway IPv4 non valido", "form_error_ip4_gateway_format": "Indirizzo gateway IPv4 non valido",
"form_error_ip6_format": "Indirizzo IPv6 non valido", "form_error_ip6_format": "Indirizzo IPv6 non valido",
"form_error_ip_format": "Indirizzo IP non valido", "form_error_ip_format": "Indirizzo IP non valido",
@@ -51,7 +49,6 @@
"out_of_range_error": "Deve essere fuori intervallo \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Deve essere fuori intervallo \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Deve essere inferiore dell'intervallo di inizio", "lower_range_start_error": "Deve essere inferiore dell'intervallo di inizio",
"greater_range_start_error": "Deve essere maggiore dell'intervallo di inizio", "greater_range_start_error": "Deve essere maggiore dell'intervallo di inizio",
"greater_range_end_error": "Deve essere maggiore dell'intervallo di fine",
"subnet_error": "Gli indirizzi devono trovarsi in una sottorete", "subnet_error": "Gli indirizzi devono trovarsi in una sottorete",
"gateway_or_subnet_invalid": "Maschera di sottorete non valida", "gateway_or_subnet_invalid": "Maschera di sottorete non valida",
"dhcp_form_gateway_input": "IP Gateway", "dhcp_form_gateway_input": "IP Gateway",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 設定", "dhcp_ipv6_settings": "DHCP IPv6 設定",
"form_error_required": "必須項目です", "form_error_required": "必須項目です",
"form_error_ip4_format": "IPv4アドレスが無効です", "form_error_ip4_format": "IPv4アドレスが無効です",
"form_error_ip4_range_start_format": "範囲開始のIPv4アドレスが無効です",
"form_error_ip4_range_end_format": "範囲終了のIPv4アドレスが無効です",
"form_error_ip4_gateway_format": "ゲートウェイのIPv4アドレスが無効です", "form_error_ip4_gateway_format": "ゲートウェイのIPv4アドレスが無効です",
"form_error_ip6_format": "IPv6アドレスが無効です", "form_error_ip6_format": "IPv6アドレスが無効です",
"form_error_ip_format": "IPアドレスが無効です", "form_error_ip_format": "IPアドレスが無効です",
@@ -51,7 +49,6 @@
"out_of_range_error": "\"{{start}}\"〜\"{{end}}\" の範囲外である必要があります", "out_of_range_error": "\"{{start}}\"〜\"{{end}}\" の範囲外である必要があります",
"lower_range_start_error": "範囲開始よりも低い値である必要があります", "lower_range_start_error": "範囲開始よりも低い値である必要があります",
"greater_range_start_error": "範囲開始値より大きい値でなければなりません", "greater_range_start_error": "範囲開始値より大きい値でなければなりません",
"greater_range_end_error": "範囲終了値より大きい値でなければなりません",
"subnet_error": "両アドレスが同じサブネット内にある必要があります", "subnet_error": "両アドレスが同じサブネット内にある必要があります",
"gateway_or_subnet_invalid": "サブネットマスクが無効です", "gateway_or_subnet_invalid": "サブネットマスクが無効です",
"dhcp_form_gateway_input": "ゲートウェイIP", "dhcp_form_gateway_input": "ゲートウェイIP",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 설정", "dhcp_ipv6_settings": "DHCP IPv6 설정",
"form_error_required": "필수 영역", "form_error_required": "필수 영역",
"form_error_ip4_format": "잘못된 IPv4 형식", "form_error_ip4_format": "잘못된 IPv4 형식",
"form_error_ip4_range_start_format": "잘못된 범위 시작 IPv4 형식",
"form_error_ip4_range_end_format": "잘못된 범위 종료 IPv4 형식",
"form_error_ip4_gateway_format": "잘못된 게이트웨이 IPv4 형식", "form_error_ip4_gateway_format": "잘못된 게이트웨이 IPv4 형식",
"form_error_ip6_format": "잘못된 IPv6 주소", "form_error_ip6_format": "잘못된 IPv6 주소",
"form_error_ip_format": "잘못된 IP 주소", "form_error_ip_format": "잘못된 IP 주소",
@@ -51,7 +49,6 @@
"out_of_range_error": "'{{start}}'-'{{end}}' 범위 밖이어야 합니다", "out_of_range_error": "'{{start}}'-'{{end}}' 범위 밖이어야 합니다",
"lower_range_start_error": "범위 시작보다 작은 값이어야 합니다", "lower_range_start_error": "범위 시작보다 작은 값이어야 합니다",
"greater_range_start_error": "범위 시작보다 큰 값이어야 합니다", "greater_range_start_error": "범위 시작보다 큰 값이어야 합니다",
"greater_range_end_error": "범위 종료보다 큰 값이어야 합니다",
"subnet_error": "주소는 하나의 서브넷에 있어야 합니다", "subnet_error": "주소는 하나의 서브넷에 있어야 합니다",
"gateway_or_subnet_invalid": "잘못된 서브넷 마스크", "gateway_or_subnet_invalid": "잘못된 서브넷 마스크",
"dhcp_form_gateway_input": "게이트웨이 IP", "dhcp_form_gateway_input": "게이트웨이 IP",
@@ -223,7 +220,7 @@
"example_upstream_tcp_hostname": "일반 DNS (TCP를 통한, 호스트명);", "example_upstream_tcp_hostname": "일반 DNS (TCP를 통한, 호스트명);",
"all_lists_up_to_date_toast": "모든 리스트가 이미 최신입니다", "all_lists_up_to_date_toast": "모든 리스트가 이미 최신입니다",
"updated_upstream_dns_toast": "업스트림 서버가 성공적으로 저장되었습니다", "updated_upstream_dns_toast": "업스트림 서버가 성공적으로 저장되었습니다",
"dns_test_ok_toast": "특정 DNS 서버들은 정상적으로 동작 중입니다", "dns_test_ok_toast": "지정된 DNS 서버가 올바르게 작동하고 있습니다.",
"dns_test_not_ok_toast": "서버 '{{key}}': 사용할 수 없습니다, 제대로 작성했는지 확인하세요", "dns_test_not_ok_toast": "서버 '{{key}}': 사용할 수 없습니다, 제대로 작성했는지 확인하세요",
"dns_test_warning_toast": "업스트림 '{{key}}'이(가) 테스트 요청에 응답하지 않으며 제대로 작동하지 않을 수 있습니다", "dns_test_warning_toast": "업스트림 '{{key}}'이(가) 테스트 요청에 응답하지 않으며 제대로 작동하지 않을 수 있습니다",
"unblock": "차단 해제", "unblock": "차단 해제",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 instellingen", "dhcp_ipv6_settings": "DHCP IPv6 instellingen",
"form_error_required": "Vereist veld", "form_error_required": "Vereist veld",
"form_error_ip4_format": "Ongeldig IPv4-adres", "form_error_ip4_format": "Ongeldig IPv4-adres",
"form_error_ip4_range_start_format": "Ongeldig IPv4-adres start bereik",
"form_error_ip4_range_end_format": "Ongeldig IPv4-adres einde bereik",
"form_error_ip4_gateway_format": "Ongeldig IPv4-adres van de gateway", "form_error_ip4_gateway_format": "Ongeldig IPv4-adres van de gateway",
"form_error_ip6_format": "Ongeldig IPv6-adres", "form_error_ip6_format": "Ongeldig IPv6-adres",
"form_error_ip_format": "Ongeldig IP-adres", "form_error_ip_format": "Ongeldig IP-adres",
@@ -51,9 +49,8 @@
"out_of_range_error": "Moet buiten bereik zijn \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Moet buiten bereik zijn \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Moet lager zijn dan begin reeks", "lower_range_start_error": "Moet lager zijn dan begin reeks",
"greater_range_start_error": "Moet groter zijn dan begin reeks", "greater_range_start_error": "Moet groter zijn dan begin reeks",
"greater_range_end_error": "Moet groter zijn dan einde reeks",
"subnet_error": "Adressen moeten in één subnet vallen", "subnet_error": "Adressen moeten in één subnet vallen",
"gateway_or_subnet_invalid": "Subnetmasker ongeldig", "gateway_or_subnet_invalid": "Ongeldig subnetmasker",
"dhcp_form_gateway_input": "Gateway IP", "dhcp_form_gateway_input": "Gateway IP",
"dhcp_form_subnet_input": "Subnet mask", "dhcp_form_subnet_input": "Subnet mask",
"dhcp_form_range_title": "Bereik van IP adressen", "dhcp_form_range_title": "Bereik van IP adressen",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Ustawienia serwera DHCP IPv6", "dhcp_ipv6_settings": "Ustawienia serwera DHCP IPv6",
"form_error_required": "Pole wymagane", "form_error_required": "Pole wymagane",
"form_error_ip4_format": "Nieprawidłowy adres IPv4", "form_error_ip4_format": "Nieprawidłowy adres IPv4",
"form_error_ip4_range_start_format": "Nieprawidłowy adres IPv4 początku zakresu",
"form_error_ip4_range_end_format": "Nieprawidłowy adres IPv4 końca zakresu",
"form_error_ip4_gateway_format": "Nieprawidłowy adres IPv4 bramy", "form_error_ip4_gateway_format": "Nieprawidłowy adres IPv4 bramy",
"form_error_ip6_format": "Nieprawidłowy adres IPv6", "form_error_ip6_format": "Nieprawidłowy adres IPv6",
"form_error_ip_format": "Nieprawidłowy adres IP", "form_error_ip_format": "Nieprawidłowy adres IP",
@@ -51,7 +49,6 @@
"out_of_range_error": "Musi być spoza zakresu \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Musi być spoza zakresu \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Musi być niższy niż początek zakresu", "lower_range_start_error": "Musi być niższy niż początek zakresu",
"greater_range_start_error": "Musi być większy niż początek zakresu", "greater_range_start_error": "Musi być większy niż początek zakresu",
"greater_range_end_error": "Musi być większy niż koniec zakresu",
"subnet_error": "Adresy muszą należeć do jednej podsieci", "subnet_error": "Adresy muszą należeć do jednej podsieci",
"gateway_or_subnet_invalid": "Nieprawidłowa maska podsieci", "gateway_or_subnet_invalid": "Nieprawidłowa maska podsieci",
"dhcp_form_gateway_input": "Adres IP bramy", "dhcp_form_gateway_input": "Adres IP bramy",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Configurações DHCP IPv6", "dhcp_ipv6_settings": "Configurações DHCP IPv6",
"form_error_required": "Campo obrigatório", "form_error_required": "Campo obrigatório",
"form_error_ip4_format": "Endereço de IPv4 inválido", "form_error_ip4_format": "Endereço de IPv4 inválido",
"form_error_ip4_range_start_format": "Endereço IPv4 de início de intervalo inválido",
"form_error_ip4_range_end_format": "Endereço IPv4 de fim de intervalo inválido.",
"form_error_ip4_gateway_format": "Endereço IPv4 de gateway inválido", "form_error_ip4_gateway_format": "Endereço IPv4 de gateway inválido",
"form_error_ip6_format": "Endereço de IPv6 inválido", "form_error_ip6_format": "Endereço de IPv6 inválido",
"form_error_ip_format": "Endereço de IP inválido", "form_error_ip_format": "Endereço de IP inválido",
@@ -51,7 +49,6 @@
"out_of_range_error": "Deve estar fora do intervalo \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Deve estar fora do intervalo \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Deve ser inferior ao início do intervalo", "lower_range_start_error": "Deve ser inferior ao início do intervalo",
"greater_range_start_error": "Deve ser maior que o início do intervalo", "greater_range_start_error": "Deve ser maior que o início do intervalo",
"greater_range_end_error": "Deve ser maior que o fim do intervalo",
"subnet_error": "Endereços devem estar em uma sub-rede", "subnet_error": "Endereços devem estar em uma sub-rede",
"gateway_or_subnet_invalid": "Máscara de sub-rede inválida", "gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
"dhcp_form_gateway_input": "IP do gateway", "dhcp_form_gateway_input": "IP do gateway",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Definições DHCP IPv6", "dhcp_ipv6_settings": "Definições DHCP IPv6",
"form_error_required": "Campo obrigatório", "form_error_required": "Campo obrigatório",
"form_error_ip4_format": "Endereço de IPv4 inválido", "form_error_ip4_format": "Endereço de IPv4 inválido",
"form_error_ip4_range_start_format": "Endereço IPv4 de início de intervalo inválido",
"form_error_ip4_range_end_format": "Endereço IPv4 de fim de intervalo inválido",
"form_error_ip4_gateway_format": "Endereço IPv4 de gateway inválido", "form_error_ip4_gateway_format": "Endereço IPv4 de gateway inválido",
"form_error_ip6_format": "Endereço de IPv6 inválido", "form_error_ip6_format": "Endereço de IPv6 inválido",
"form_error_ip_format": "Endereço de email inválido", "form_error_ip_format": "Endereço de email inválido",
@@ -51,7 +49,6 @@
"out_of_range_error": "Deve estar fora do intervalo \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Deve estar fora do intervalo \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Deve ser inferior ao início do intervalo", "lower_range_start_error": "Deve ser inferior ao início do intervalo",
"greater_range_start_error": "Deve ser maior que o início do intervalo", "greater_range_start_error": "Deve ser maior que o início do intervalo",
"greater_range_end_error": "Deve ser maior que o fim do intervalo",
"subnet_error": "Os endereços devem estar em uma sub-rede", "subnet_error": "Os endereços devem estar em uma sub-rede",
"gateway_or_subnet_invalid": "Máscara de sub-rede inválida", "gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
"dhcp_form_gateway_input": "IP do gateway", "dhcp_form_gateway_input": "IP do gateway",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Setări DHCP IPv6", "dhcp_ipv6_settings": "Setări DHCP IPv6",
"form_error_required": "Câmp obligatoriu", "form_error_required": "Câmp obligatoriu",
"form_error_ip4_format": "Adresă IPv4 nevalidă", "form_error_ip4_format": "Adresă IPv4 nevalidă",
"form_error_ip4_range_start_format": "Adresă IPv4 nevalidă pentru începutul intervalului",
"form_error_ip4_range_end_format": "Adresă IPv4 nevalidă a sfârșitului intervalului",
"form_error_ip4_gateway_format": "Adresă IPv4 nevalidă a gateway-ului", "form_error_ip4_gateway_format": "Adresă IPv4 nevalidă a gateway-ului",
"form_error_ip6_format": "Adresa IPv6 nevalidă", "form_error_ip6_format": "Adresa IPv6 nevalidă",
"form_error_ip_format": "Adresă IP nevalidă", "form_error_ip_format": "Adresă IP nevalidă",
@@ -51,7 +49,6 @@
"out_of_range_error": "Trebuie să fie în afara intervalului „{{start}}”-„{{end}}”", "out_of_range_error": "Trebuie să fie în afara intervalului „{{start}}”-„{{end}}”",
"lower_range_start_error": "Trebuie să fie mai mică decât începutul intervalului", "lower_range_start_error": "Trebuie să fie mai mică decât începutul intervalului",
"greater_range_start_error": "Trebuie să fie mai mare decât începutul intervalului", "greater_range_start_error": "Trebuie să fie mai mare decât începutul intervalului",
"greater_range_end_error": "Trebuie să fie mai mare decât sfârșitul intervalului",
"subnet_error": "Adresele trebuie să fie în aceeași subrețea", "subnet_error": "Adresele trebuie să fie în aceeași subrețea",
"gateway_or_subnet_invalid": "Mască de subrețea nevalidă", "gateway_or_subnet_invalid": "Mască de subrețea nevalidă",
"dhcp_form_gateway_input": "IP Gateway", "dhcp_form_gateway_input": "IP Gateway",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Настройки DHCP IPv6", "dhcp_ipv6_settings": "Настройки DHCP IPv6",
"form_error_required": "Обязательное поле", "form_error_required": "Обязательное поле",
"form_error_ip4_format": "Некорректный IPv4-адрес", "form_error_ip4_format": "Некорректный IPv4-адрес",
"form_error_ip4_range_start_format": "Некорректный IPv4-адрес начала диапазона",
"form_error_ip4_range_end_format": "Некорректный IPv4-адрес конца диапазона",
"form_error_ip4_gateway_format": "Некорректный IPv4-адрес шлюза", "form_error_ip4_gateway_format": "Некорректный IPv4-адрес шлюза",
"form_error_ip6_format": "Некорректный IPv6-адрес", "form_error_ip6_format": "Некорректный IPv6-адрес",
"form_error_ip_format": "Некорректный IP-адрес", "form_error_ip_format": "Некорректный IP-адрес",
@@ -51,7 +49,6 @@
"out_of_range_error": "Должно быть вне диапазона «{{start}}»-«{{end}}»", "out_of_range_error": "Должно быть вне диапазона «{{start}}»-«{{end}}»",
"lower_range_start_error": "Должно быть меньше начала диапазона", "lower_range_start_error": "Должно быть меньше начала диапазона",
"greater_range_start_error": "Должно быть больше начала диапазона", "greater_range_start_error": "Должно быть больше начала диапазона",
"greater_range_end_error": "Должно быть больше конца диапазона",
"subnet_error": "Адреса должны быть внутри одной подсети", "subnet_error": "Адреса должны быть внутри одной подсети",
"gateway_or_subnet_invalid": "Некорректная маска подсети", "gateway_or_subnet_invalid": "Некорректная маска подсети",
"dhcp_form_gateway_input": "IP-адрес шлюза", "dhcp_form_gateway_input": "IP-адрес шлюза",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Nastavenia DHCP IPv6", "dhcp_ipv6_settings": "Nastavenia DHCP IPv6",
"form_error_required": "Povinná položka.", "form_error_required": "Povinná položka.",
"form_error_ip4_format": "Neplatná IPv4 adresa", "form_error_ip4_format": "Neplatná IPv4 adresa",
"form_error_ip4_range_start_format": "Neplatný začiatok rozsahu IPv4 formátu",
"form_error_ip4_range_end_format": "Neplatný koniec rozsahu IPv4 formátu",
"form_error_ip4_gateway_format": "Neplatná IPv4 adresa brány", "form_error_ip4_gateway_format": "Neplatná IPv4 adresa brány",
"form_error_ip6_format": "Neplatná IPv6 adresa", "form_error_ip6_format": "Neplatná IPv6 adresa",
"form_error_ip_format": "Neplatná IP adresa", "form_error_ip_format": "Neplatná IP adresa",
@@ -51,7 +49,6 @@
"out_of_range_error": "Musí byť mimo rozsahu \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Musí byť mimo rozsahu \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Musí byť nižšie ako začiatok rozsahu", "lower_range_start_error": "Musí byť nižšie ako začiatok rozsahu",
"greater_range_start_error": "Musí byť väčšie ako začiatok rozsahu", "greater_range_start_error": "Musí byť väčšie ako začiatok rozsahu",
"greater_range_end_error": "Musí byť väčšie ako koniec rozsahu",
"subnet_error": "Adresy musia byť v spoločnej podsieti", "subnet_error": "Adresy musia byť v spoločnej podsieti",
"gateway_or_subnet_invalid": "Maska podsiete je neplatná", "gateway_or_subnet_invalid": "Maska podsiete je neplatná",
"dhcp_form_gateway_input": "IP brána", "dhcp_form_gateway_input": "IP brána",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Nastavitve DHCP IPv6", "dhcp_ipv6_settings": "Nastavitve DHCP IPv6",
"form_error_required": "Zahtevano polje.", "form_error_required": "Zahtevano polje.",
"form_error_ip4_format": "Neveljaven naslov IPv4.", "form_error_ip4_format": "Neveljaven naslov IPv4.",
"form_error_ip4_range_start_format": "Neveljaven začetek razpona naslova IPv4",
"form_error_ip4_range_end_format": "Neveljaven konec razpona naslova IPv4",
"form_error_ip4_gateway_format": "Neveljaven naslov IPv4 prehoda", "form_error_ip4_gateway_format": "Neveljaven naslov IPv4 prehoda",
"form_error_ip6_format": "Neveljaven naslov IPv6", "form_error_ip6_format": "Neveljaven naslov IPv6",
"form_error_ip_format": "Neveljaven naslov IP", "form_error_ip_format": "Neveljaven naslov IP",
@@ -51,7 +49,6 @@
"out_of_range_error": "Mora biti izven razpona \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Mora biti izven razpona \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Mora biti manjši od začetka razpona", "lower_range_start_error": "Mora biti manjši od začetka razpona",
"greater_range_start_error": "Mora biti večji od začetka razpona", "greater_range_start_error": "Mora biti večji od začetka razpona",
"greater_range_end_error": "Mora biti večji od konca razpona",
"subnet_error": "Naslovi morajo biti v enem podomrežju", "subnet_error": "Naslovi morajo biti v enem podomrežju",
"gateway_or_subnet_invalid": "Maska podomrežja ni veljavna", "gateway_or_subnet_invalid": "Maska podomrežja ni veljavna",
"dhcp_form_gateway_input": "IP prehoda", "dhcp_form_gateway_input": "IP prehoda",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 postavke", "dhcp_ipv6_settings": "DHCP IPv6 postavke",
"form_error_required": "Obavezno polje", "form_error_required": "Obavezno polje",
"form_error_ip4_format": "Nevažeća IPv4 adresa", "form_error_ip4_format": "Nevažeća IPv4 adresa",
"form_error_ip4_range_start_format": "Nevažeća IPv4 addresa početnog opsega",
"form_error_ip4_range_end_format": "Nevažeća IPv4 addresa završnog opsega",
"form_error_ip4_gateway_format": "Nevažeća IPv4 addresa prozala", "form_error_ip4_gateway_format": "Nevažeća IPv4 addresa prozala",
"form_error_ip6_format": "Nevažeća IPv6 adresa", "form_error_ip6_format": "Nevažeća IPv6 adresa",
"form_error_ip_format": "Nevažeća IP adresa", "form_error_ip_format": "Nevažeća IP adresa",
@@ -51,7 +49,6 @@
"out_of_range_error": "Mora biti izvan opsega \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Mora biti izvan opsega \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Mora biti manje od početnog opsega", "lower_range_start_error": "Mora biti manje od početnog opsega",
"greater_range_start_error": "Mora biti veće od početnog opsega", "greater_range_start_error": "Mora biti veće od početnog opsega",
"greater_range_end_error": "Mora biti veće od završnog opsega",
"subnet_error": "Asrese moraju biti u jednoj subnet", "subnet_error": "Asrese moraju biti u jednoj subnet",
"gateway_or_subnet_invalid": "Subnet mask nevažeća", "gateway_or_subnet_invalid": "Subnet mask nevažeća",
"dhcp_form_gateway_input": "IP mrežnog prolaza", "dhcp_form_gateway_input": "IP mrežnog prolaza",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 inställningar", "dhcp_ipv6_settings": "DHCP IPv6 inställningar",
"form_error_required": "Obligatoriskt fält", "form_error_required": "Obligatoriskt fält",
"form_error_ip4_format": "Ogiltig IPv4-adress", "form_error_ip4_format": "Ogiltig IPv4-adress",
"form_error_ip4_range_start_format": "Ogiltig IPv4-adress för starten av intervallet",
"form_error_ip4_range_end_format": "Ogiltig IPv4-adress för slutet av intervallet",
"form_error_ip4_gateway_format": "Ogiltig IPv4 adress för gatewayen", "form_error_ip4_gateway_format": "Ogiltig IPv4 adress för gatewayen",
"form_error_ip6_format": "Ogiltig IPv6-adress", "form_error_ip6_format": "Ogiltig IPv6-adress",
"form_error_ip_format": "Ogiltig IP-adress", "form_error_ip_format": "Ogiltig IP-adress",
@@ -51,7 +49,6 @@
"out_of_range_error": "Måste vara utanför intervallet \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Måste vara utanför intervallet \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Måste vara lägre än starten på intervallet", "lower_range_start_error": "Måste vara lägre än starten på intervallet",
"greater_range_start_error": "Måste vara högre än starten på intervallet", "greater_range_start_error": "Måste vara högre än starten på intervallet",
"greater_range_end_error": "Måste vara större än intervallets slut",
"subnet_error": "Adresser måste finnas i ett subnät", "subnet_error": "Adresser måste finnas i ett subnät",
"gateway_or_subnet_invalid": "Subnätmask ogiltig", "gateway_or_subnet_invalid": "Subnätmask ogiltig",
"dhcp_form_gateway_input": "Gateway-IP", "dhcp_form_gateway_input": "Gateway-IP",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 Ayarları", "dhcp_ipv6_settings": "DHCP IPv6 Ayarları",
"form_error_required": "Gerekli alan", "form_error_required": "Gerekli alan",
"form_error_ip4_format": "Geçersiz IPv4 adresi", "form_error_ip4_format": "Geçersiz IPv4 adresi",
"form_error_ip4_range_start_format": "Geçersiz başlangıç aralığı IPv4 biçimi",
"form_error_ip4_range_end_format": "Geçersiz bitiş aralığı IPv4 adresi",
"form_error_ip4_gateway_format": "Geçersiz ağ geçidi IPv4 adresi", "form_error_ip4_gateway_format": "Geçersiz ağ geçidi IPv4 adresi",
"form_error_ip6_format": "Geçersiz IPv6 adresi", "form_error_ip6_format": "Geçersiz IPv6 adresi",
"form_error_ip_format": "Geçersiz IP adresi", "form_error_ip_format": "Geçersiz IP adresi",
@@ -51,9 +49,8 @@
"out_of_range_error": "\"{{start}}\"-\"{{end}}\" aralığının dışında olmalıdır", "out_of_range_error": "\"{{start}}\"-\"{{end}}\" aralığının dışında olmalıdır",
"lower_range_start_error": "Başlangıç aralığından daha düşük olmalıdır", "lower_range_start_error": "Başlangıç aralığından daha düşük olmalıdır",
"greater_range_start_error": "Başlangıç aralığından daha büyük olmalıdır", "greater_range_start_error": "Başlangıç aralığından daha büyük olmalıdır",
"greater_range_end_error": "Bitiş aralığından daha büyük olmalıdır",
"subnet_error": "Adresler bir alt ağda olmalıdır", "subnet_error": "Adresler bir alt ağda olmalıdır",
"gateway_or_subnet_invalid": "Alt ağ maskesi geçersiz", "gateway_or_subnet_invalid": "Geçersiz alt ağ maskesi",
"dhcp_form_gateway_input": "Ağ geçidi IP", "dhcp_form_gateway_input": "Ağ geçidi IP",
"dhcp_form_subnet_input": "Alt ağ maskesi", "dhcp_form_subnet_input": "Alt ağ maskesi",
"dhcp_form_range_title": "IP adresi aralığı", "dhcp_form_range_title": "IP adresi aralığı",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Налаштування DHCP IPv6", "dhcp_ipv6_settings": "Налаштування DHCP IPv6",
"form_error_required": "Обов'язкове поле", "form_error_required": "Обов'язкове поле",
"form_error_ip4_format": "Неправильна IPv4-адреса", "form_error_ip4_format": "Неправильна IPv4-адреса",
"form_error_ip4_range_start_format": "Неправильна IPv4-адреса початку діапазону",
"form_error_ip4_range_end_format": "Неправильна IPv4-адреса кінця діапазону",
"form_error_ip4_gateway_format": "Неправильна IPv4-адреса шлюзу", "form_error_ip4_gateway_format": "Неправильна IPv4-адреса шлюзу",
"form_error_ip6_format": "Неправильна IPv6-адреса", "form_error_ip6_format": "Неправильна IPv6-адреса",
"form_error_ip_format": "Неправильна IP-адреса", "form_error_ip_format": "Неправильна IP-адреса",
@@ -51,7 +49,6 @@
"out_of_range_error": "Не повинна бути в діапазоні «{{start}}»−«{{end}}»", "out_of_range_error": "Не повинна бути в діапазоні «{{start}}»−«{{end}}»",
"lower_range_start_error": "Має бути меншим за початкову адресу", "lower_range_start_error": "Має бути меншим за початкову адресу",
"greater_range_start_error": "Має бути більшим за початкову адресу", "greater_range_start_error": "Має бути більшим за початкову адресу",
"greater_range_end_error": "Має бути більшим за кінцеву адресу",
"subnet_error": "Адреси повинні бути в одній підмережі", "subnet_error": "Адреси повинні бути в одній підмережі",
"gateway_or_subnet_invalid": "Неправильна маска підмережі", "gateway_or_subnet_invalid": "Неправильна маска підмережі",
"dhcp_form_gateway_input": "IP-адреса шлюзу", "dhcp_form_gateway_input": "IP-адреса шлюзу",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Cài đặt DHCP IPv6", "dhcp_ipv6_settings": "Cài đặt DHCP IPv6",
"form_error_required": "Trường bắt buộc", "form_error_required": "Trường bắt buộc",
"form_error_ip4_format": "Địa chỉ IPv4 không hợp lệ", "form_error_ip4_format": "Địa chỉ IPv4 không hợp lệ",
"form_error_ip4_range_start_format": "Địa chỉ IPv4 không hợp lệ của phạm vi bắt đầu",
"form_error_ip4_range_end_format": "Địa chỉ IPv4 không hợp lệ của cuối phạm vi",
"form_error_ip4_gateway_format": "Địa chỉ IPv4 không hợp lệ của cổng kết nối", "form_error_ip4_gateway_format": "Địa chỉ IPv4 không hợp lệ của cổng kết nối",
"form_error_ip6_format": "Địa chỉ IPv6 không hợp lệ", "form_error_ip6_format": "Địa chỉ IPv6 không hợp lệ",
"form_error_ip_format": "Địa chỉ IP không hợp lệ", "form_error_ip_format": "Địa chỉ IP không hợp lệ",
@@ -51,7 +49,6 @@
"out_of_range_error": "Phải nằm ngoài phạm vi \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "Phải nằm ngoài phạm vi \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Phải thấp hơn khởi động phạm vi", "lower_range_start_error": "Phải thấp hơn khởi động phạm vi",
"greater_range_start_error": "Phải lớn hơn khoảng bắt đầu", "greater_range_start_error": "Phải lớn hơn khoảng bắt đầu",
"greater_range_end_error": "Phải lớn hơn phạm vi kết thúc",
"subnet_error": "Địa chỉ phải nằm trong một mạng con", "subnet_error": "Địa chỉ phải nằm trong một mạng con",
"gateway_or_subnet_invalid": "Mặt nạ mạng con không hợp lệ", "gateway_or_subnet_invalid": "Mặt nạ mạng con không hợp lệ",
"dhcp_form_gateway_input": "Cổng IP", "dhcp_form_gateway_input": "Cổng IP",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6设置", "dhcp_ipv6_settings": "DHCP IPv6设置",
"form_error_required": "必填字段", "form_error_required": "必填字段",
"form_error_ip4_format": "无效的 IPv4 地址", "form_error_ip4_format": "无效的 IPv4 地址",
"form_error_ip4_range_start_format": "范围起始值的 IPv4 地址无效",
"form_error_ip4_range_end_format": "范围终值的 IPv4 地址无效",
"form_error_ip4_gateway_format": "网关 IPv4 地址无效", "form_error_ip4_gateway_format": "网关 IPv4 地址无效",
"form_error_ip6_format": "无效的 IPv6 地址", "form_error_ip6_format": "无效的 IPv6 地址",
"form_error_ip_format": "无效的 IP 地址", "form_error_ip_format": "无效的 IP 地址",
@@ -51,7 +49,6 @@
"out_of_range_error": "必定超出了范围 \"{{start}}\"-\"{{end}}\"", "out_of_range_error": "必定超出了范围 \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "必须小于范围起始值", "lower_range_start_error": "必须小于范围起始值",
"greater_range_start_error": "必须大于范围起始值", "greater_range_start_error": "必须大于范围起始值",
"greater_range_end_error": "必须大于范围终值",
"subnet_error": "地址必须在一个子网内", "subnet_error": "地址必须在一个子网内",
"gateway_or_subnet_invalid": "子网掩码无效", "gateway_or_subnet_invalid": "子网掩码无效",
"dhcp_form_gateway_input": "网关 IP", "dhcp_form_gateway_input": "网关 IP",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 設定", "dhcp_ipv6_settings": "DHCP IPv6 設定",
"form_error_required": "必填的欄位", "form_error_required": "必填的欄位",
"form_error_ip4_format": "無效的 IPv4 位址", "form_error_ip4_format": "無效的 IPv4 位址",
"form_error_ip4_range_start_format": "無效起始範圍的 IPv4 位址",
"form_error_ip4_range_end_format": "無效結束範圍的 IPv4 位址",
"form_error_ip4_gateway_format": "無效閘道的 IPv4 位址", "form_error_ip4_gateway_format": "無效閘道的 IPv4 位址",
"form_error_ip6_format": "無效的 IPv6 位址", "form_error_ip6_format": "無效的 IPv6 位址",
"form_error_ip_format": "無效的 IP 位址", "form_error_ip_format": "無效的 IP 位址",
@@ -51,7 +49,6 @@
"out_of_range_error": "必須在\"{{start}}\"-\"{{end}}\"範圍之外", "out_of_range_error": "必須在\"{{start}}\"-\"{{end}}\"範圍之外",
"lower_range_start_error": "必須低於起始範圍", "lower_range_start_error": "必須低於起始範圍",
"greater_range_start_error": "必須大於起始範圍", "greater_range_start_error": "必須大於起始範圍",
"greater_range_end_error": "必須大於結束範圍",
"subnet_error": "位址必須在子網路中", "subnet_error": "位址必須在子網路中",
"gateway_or_subnet_invalid": "無效的子網路遮罩", "gateway_or_subnet_invalid": "無效的子網路遮罩",
"dhcp_form_gateway_input": "閘道 IP", "dhcp_form_gateway_input": "閘道 IP",

View File

@@ -41,6 +41,12 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
response.certificate_chain = atob(response.certificate_chain); response.certificate_chain = atob(response.certificate_chain);
response.private_key = atob(response.private_key); response.private_key = atob(response.private_key);
if (values.enabled && values.force_https && window.location.protocol === 'http:') {
window.location.reload();
return;
}
redirectToCurrentProtocol(response, httpPort);
const dnsStatus = await apiClient.getGlobalStatus(); const dnsStatus = await apiClient.getGlobalStatus();
if (dnsStatus) { if (dnsStatus) {
dispatch(dnsStatusSuccess(dnsStatus)); dispatch(dnsStatusSuccess(dnsStatus));
@@ -48,7 +54,6 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
dispatch(setTlsConfigSuccess(response)); dispatch(setTlsConfigSuccess(response));
dispatch(addSuccessToast('encryption_config_saved')); dispatch(addSuccessToast('encryption_config_saved'));
redirectToCurrentProtocol(response, httpPort);
} catch (error) { } catch (error) {
dispatch(addErrorToast({ error })); dispatch(addErrorToast({ error }));
dispatch(setTlsConfigFailure()); dispatch(setTlsConfigFailure());

View File

@@ -155,7 +155,7 @@ const Form = (props) => {
name={FORM_NAMES.search} name={FORM_NAMES.search}
component={renderFilterField} component={renderFilterField}
type="text" type="text"
className={classNames('form-control--search form-control--transparent', className)} className={classNames('form-control form-control--search form-control--transparent', className)}
placeholder={t('domain_or_client')} placeholder={t('domain_or_client')}
tooltip={t('query_log_strict_search')} tooltip={t('query_log_strict_search')}
onClearInputClick={onInputClear} onClearInputClick={onInputClear}

View File

@@ -103,14 +103,12 @@
} }
.form-control--search { .form-control--search {
box-shadow: 0 1px 0 #ddd;
padding: 0 2.5rem; padding: 0 2.5rem;
height: 2.25rem; height: 2.25rem;
flex-grow: 1; flex-grow: 1;
} }
.form-control--transparent { .form-control--transparent {
border: 0 solid transparent !important;
background-color: transparent !important; background-color: transparent !important;
} }
@@ -174,10 +172,8 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 2.5rem;
--size: 2.5rem; height: 2.5rem;
width: var(--size);
height: var(--size);
padding: 0; padding: 0;
margin-left: 0.9375rem; margin-left: 0.9375rem;
background-color: transparent; background-color: transparent;
@@ -474,7 +470,7 @@
.filteringRules__filter { .filteringRules__filter {
font-style: italic; font-style: italic;
font-weight: normal; font-weight: 400;
margin-bottom: 1rem; margin-bottom: 1rem;
} }

View File

@@ -11,12 +11,13 @@ import Select from 'react-select';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import Tabs from '../../ui/Tabs'; import Tabs from '../../ui/Tabs';
import Examples from '../Dns/Upstream/Examples'; import Examples from '../Dns/Upstream/Examples';
import { toggleAllServices } from '../../../helpers/helpers'; import { toggleAllServices, trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
import { import {
renderInputField, renderInputField,
renderGroupField, renderGroupField,
CheckboxField, CheckboxField,
renderServiceField, renderServiceField,
renderTextareaField,
} from '../../../helpers/form'; } from '../../../helpers/form';
import { validateClientId, validateRequiredValue } from '../../../helpers/validators'; import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
import { CLIENT_ID_LINK, FORM_NAME } from '../../../helpers/constants'; import { CLIENT_ID_LINK, FORM_NAME } from '../../../helpers/constants';
@@ -230,10 +231,11 @@ let Form = (props) => {
<Field <Field
id="upstreams" id="upstreams"
name="upstreams" name="upstreams"
component="textarea" component={renderTextareaField}
type="text" type="text"
className="form-control form-control--textarea mb-5" className="form-control form-control--textarea mb-5"
placeholder={t('upstream_dns')} placeholder={t('upstream_dns')}
normalizeOnBlur={trimLinesAndRemoveEmpty}
/> />
<Examples /> <Examples />
</div>, </div>,

View File

@@ -390,6 +390,7 @@ export const SPECIAL_FILTER_ID = {
PARENTAL: -3, PARENTAL: -3,
SAFE_BROWSING: -4, SAFE_BROWSING: -4,
SAFE_SEARCH: -5, SAFE_SEARCH: -5,
REWRITES: -6,
}; };
export const BLOCK_ACTIONS = { export const BLOCK_ACTIONS = {

View File

@@ -28,6 +28,12 @@ export default {
"homepage": "https://badmojr.github.io/1Hosts/", "homepage": "https://badmojr.github.io/1Hosts/",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_24.txt" "source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_24.txt"
}, },
"1hosts_mini": {
"name": "1Hosts (mini)",
"categoryId": "general",
"homepage": "https://badmojr.github.io/1Hosts/",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_38.txt"
},
"CHN_adrules": { "CHN_adrules": {
"name": "CHN: AdRules DNS List", "name": "CHN: AdRules DNS List",
"categoryId": "regional", "categoryId": "regional",
@@ -40,6 +46,12 @@ export default {
"homepage": "https://anti-ad.net/", "homepage": "https://anti-ad.net/",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_21.txt" "source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_21.txt"
}, },
"HUN_hufilter": {
"name": "HUN: Hufilter",
"categoryId": "regional",
"homepage": "https://github.com/hufilter/hufilter",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_35.txt"
},
"IDN_abpindo": { "IDN_abpindo": {
"name": "IDN: ABPindo", "name": "IDN: ABPindo",
"categoryId": "regional", "categoryId": "regional",
@@ -70,6 +82,12 @@ export default {
"homepage": "https://github.com/yous/YousList", "homepage": "https://github.com/yous/YousList",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_15.txt" "source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_15.txt"
}, },
"LIT_easylist_lithuania": {
"name": "LIT: EasyList Lithuania",
"categoryId": "regional",
"homepage": "https://github.com/EasyList-Lithuania/easylist_lithuania",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_36.txt"
},
"MKD_macedonian_pi_hole_blocklist": { "MKD_macedonian_pi_hole_blocklist": {
"name": "MKD: Macedonian Pi-hole Blocklist", "name": "MKD: Macedonian Pi-hole Blocklist",
"categoryId": "regional", "categoryId": "regional",
@@ -148,6 +166,18 @@ export default {
"homepage": "https://energized.pro/", "homepage": "https://energized.pro/",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_28.txt" "source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_28.txt"
}, },
"hagezi_personal": {
"name": "HaGeZi Personal Black \u0026 White",
"categoryId": "general",
"homepage": "https://github.com/hagezi/dns-blocklists",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_34.txt"
},
"no_google": {
"name": "No Google",
"categoryId": "other",
"homepage": "https://github.com/nickspaargaren/no-google",
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_37.txt"
},
"nocoin_filter_list": { "nocoin_filter_list": {
"name": "NoCoin Filter List", "name": "NoCoin Filter List",
"categoryId": "security", "categoryId": "security",

8
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
go 1.18 go 1.18
require ( require (
github.com/AdguardTeam/dnsproxy v0.46.4 github.com/AdguardTeam/dnsproxy v0.46.5
github.com/AdguardTeam/golibs v0.11.3 github.com/AdguardTeam/golibs v0.11.3
github.com/AdguardTeam/urlfilter v0.16.0 github.com/AdguardTeam/urlfilter v0.16.0
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
@@ -30,8 +30,8 @@ require (
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.1.0 golang.org/x/crypto v0.1.0
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 golang.org/x/exp v0.0.0-20221106115401-f9659909a136
golang.org/x/net v0.1.0 golang.org/x/net v0.4.0
golang.org/x/sys v0.2.0 golang.org/x/sys v0.3.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.0 howett.net/plist v1.0.0
@@ -61,6 +61,6 @@ require (
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
golang.org/x/mod v0.6.0 // indirect golang.org/x/mod v0.6.0 // indirect
golang.org/x/sync v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect golang.org/x/text v0.5.0 // indirect
golang.org/x/tools v0.2.0 // indirect golang.org/x/tools v0.2.0 // indirect
) )

16
go.sum
View File

@@ -1,5 +1,5 @@
github.com/AdguardTeam/dnsproxy v0.46.4 h1:/+wnTG0q2TkGQyA1PeSsjv4/f5ZprGduKPSoOcG+rOU= github.com/AdguardTeam/dnsproxy v0.46.5 h1:TiJZhwaIDDaKkqEfJ9AD9aroFjcHN8oEbKB8WfTjSIs=
github.com/AdguardTeam/dnsproxy v0.46.4/go.mod h1:yYDMAH6ay2PxLcLtfVM3FUiyv/U9B/zYO+cIIssuJNU= github.com/AdguardTeam/dnsproxy v0.46.5/go.mod h1:yKBVgFlE6CqTQtye++3e7SATaMPc4Ixij+KkHsM6HhM=
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw= github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.11.3 h1:Oif+REq2WLycQ2Xm3ZPmJdfftptss0HbGWbxdFaC310= github.com/AdguardTeam/golibs v0.11.3 h1:Oif+REq2WLycQ2Xm3ZPmJdfftptss0HbGWbxdFaC310=
@@ -187,8 +187,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -228,8 +228,8 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -237,8 +237,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

View File

@@ -530,14 +530,14 @@ func validateBlockingMode(mode BlockingMode, blockingIPv4, blockingIPv6 net.IP)
// prepareInternalProxy initializes the DNS proxy that is used for internal DNS // prepareInternalProxy initializes the DNS proxy that is used for internal DNS
// queries, such as public clients PTR resolving and updater hostname resolving. // queries, such as public clients PTR resolving and updater hostname resolving.
func (s *Server) prepareInternalProxy() (err error) { func (s *Server) prepareInternalProxy() (err error) {
srvConf := s.conf
conf := &proxy.Config{ conf := &proxy.Config{
CacheEnabled: true, CacheEnabled: true,
CacheSizeBytes: 4096, CacheSizeBytes: 4096,
UpstreamConfig: s.conf.UpstreamConfig, UpstreamConfig: srvConf.UpstreamConfig,
MaxGoroutines: int(s.conf.MaxGoroutines), MaxGoroutines: int(s.conf.MaxGoroutines),
} }
srvConf := s.conf
setProxyUpstreamMode( setProxyUpstreamMode(
conf, conf,
srvConf.AllServers, srvConf.AllServers,
@@ -570,46 +570,32 @@ func (s *Server) Stop() error {
// stopLocked stops the DNS server without locking. For internal use only. // stopLocked stops the DNS server without locking. For internal use only.
func (s *Server) stopLocked() (err error) { func (s *Server) stopLocked() (err error) {
// TODO(e.burkov, a.garipov): Return critical errors, not just log them.
// This will require filtering all the non-critical errors in
// [upstream.Upstream] implementations.
if s.dnsProxy != nil { if s.dnsProxy != nil {
err = s.dnsProxy.Stop() err = s.dnsProxy.Stop()
if err != nil { if err != nil {
return fmt.Errorf("closing primary resolvers: %w", err) log.Error("dnsforward: closing primary resolvers: %s", err)
} }
} }
var errs []error
if upsConf := s.internalProxy.UpstreamConfig; upsConf != nil { if upsConf := s.internalProxy.UpstreamConfig; upsConf != nil {
const action = "closing internal resolvers"
err = upsConf.Close() err = upsConf.Close()
if err != nil { if err != nil {
if errors.Is(err, net.ErrClosed) { log.Error("dnsforward: closing internal resolvers: %s", err)
log.Debug("dnsforward: %s: %s", action, err)
} else {
errs = append(errs, fmt.Errorf("%s: %w", action, err))
}
} }
} }
if upsConf := s.localResolvers.UpstreamConfig; upsConf != nil { if upsConf := s.localResolvers.UpstreamConfig; upsConf != nil {
const action = "closing local resolvers"
err = upsConf.Close() err = upsConf.Close()
if err != nil { if err != nil {
if errors.Is(err, net.ErrClosed) { log.Error("dnsforward: closing local resolvers: %s", err)
log.Debug("dnsforward: %s: %s", action, err)
} else {
errs = append(errs, fmt.Errorf("%s: %w", action, err))
}
} }
} }
if len(errs) > 0 { s.isRunning = false
return errors.List("stopping dns server", errs...)
} else {
s.isRunning = false
}
return nil return nil
} }

View File

@@ -22,6 +22,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest" "github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rewrite"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil"
@@ -67,7 +68,7 @@ func createTestServer(
ID: 0, Data: []byte(rules), ID: 0, Data: []byte(rules),
}} }}
f, err := filtering.New(filterConf, filters) f, err := filtering.New(filterConf, filters, nil)
require.NoError(t, err) require.NoError(t, err)
f.SetEnabled(true) f.SetEnabled(true)
@@ -760,7 +761,7 @@ func TestBlockedCustomIP(t *testing.T) {
Data: []byte(rules), Data: []byte(rules),
}} }}
f, err := filtering.New(&filtering.Config{}, filters) f, err := filtering.New(&filtering.Config{}, filters, nil)
require.NoError(t, err) require.NoError(t, err)
s, err := NewServer(DNSCreateParams{ s, err := NewServer(DNSCreateParams{
@@ -880,21 +881,22 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
func TestRewrite(t *testing.T) { func TestRewrite(t *testing.T) {
c := &filtering.Config{ c := &filtering.Config{
Rewrites: []*filtering.LegacyRewrite{{ Rewrites: []*filtering.RewriteItem{{
Domain: "test.com", Domain: "test.com",
Answer: "1.2.3.4", Answer: "1.2.3.4",
Type: dns.TypeA,
}, { }, {
Domain: "alias.test.com", Domain: "alias.test.com",
Answer: "test.com", Answer: "test.com",
Type: dns.TypeCNAME,
}, { }, {
Domain: "my.alias.example.org", Domain: "my.alias.example.org",
Answer: "example.org", Answer: "example.org",
Type: dns.TypeCNAME,
}}, }},
} }
f, err := filtering.New(c, nil)
rewriteStorage, err := rewrite.NewDefaultStorage(c.Rewrites)
require.NoError(t, err)
f, err := filtering.New(c, nil, rewriteStorage)
require.NoError(t, err) require.NoError(t, err)
f.SetEnabled(true) f.SetEnabled(true)
@@ -945,6 +947,12 @@ func TestRewrite(t *testing.T) {
assert.Empty(t, reply.Answer) assert.Empty(t, reply.Answer)
req = createTestMessageWithType("test.com.", dns.TypeTXT)
reply, eerr = dns.Exchange(req, addr.String())
require.NoError(t, eerr)
assert.Empty(t, reply.Answer)
req = createTestMessageWithType("alias.test.com.", dns.TypeA) req = createTestMessageWithType("alias.test.com.", dns.TypeA)
reply, eerr = dns.Exchange(req, addr.String()) reply, eerr = dns.Exchange(req, addr.String())
require.NoError(t, eerr) require.NoError(t, eerr)
@@ -952,8 +960,15 @@ func TestRewrite(t *testing.T) {
require.Len(t, reply.Answer, 2) require.Len(t, reply.Answer, 2)
assert.Equal(t, "test.com.", reply.Answer[0].(*dns.CNAME).Target) assert.Equal(t, "test.com.", reply.Answer[0].(*dns.CNAME).Target)
assert.Equal(t, dns.TypeA, reply.Answer[1].Header().Rrtype)
assert.True(t, net.IP{1, 2, 3, 4}.Equal(reply.Answer[1].(*dns.A).A)) assert.True(t, net.IP{1, 2, 3, 4}.Equal(reply.Answer[1].(*dns.A).A))
req = createTestMessageWithType("alias.test.com.", dns.TypeTXT)
reply, eerr = dns.Exchange(req, addr.String())
require.NoError(t, eerr)
assert.Empty(t, reply.Answer)
req = createTestMessageWithType("my.alias.example.org.", dns.TypeA) req = createTestMessageWithType("my.alias.example.org.", dns.TypeA)
reply, eerr = dns.Exchange(req, addr.String()) reply, eerr = dns.Exchange(req, addr.String())
require.NoError(t, eerr) require.NoError(t, eerr)
@@ -967,6 +982,12 @@ func TestRewrite(t *testing.T) {
assert.Equal(t, "example.org.", reply.Answer[0].(*dns.CNAME).Target) assert.Equal(t, "example.org.", reply.Answer[0].(*dns.CNAME).Target)
assert.Equal(t, dns.TypeA, reply.Answer[1].Header().Rrtype) assert.Equal(t, dns.TypeA, reply.Answer[1].Header().Rrtype)
req = createTestMessageWithType("my.alias.test.com.", dns.TypeTXT)
reply, eerr = dns.Exchange(req, addr.String())
require.NoError(t, eerr)
assert.Empty(t, reply.Answer)
} }
for _, protect := range []bool{true, false} { for _, protect := range []bool{true, false} {
@@ -1011,7 +1032,7 @@ var testDHCP = &dhcpd.MockInterface{
func TestPTRResponseFromDHCPLeases(t *testing.T) { func TestPTRResponseFromDHCPLeases(t *testing.T) {
const localDomain = "lan" const localDomain = "lan"
flt, err := filtering.New(&filtering.Config{}, nil) flt, err := filtering.New(&filtering.Config{}, nil, nil)
require.NoError(t, err) require.NoError(t, err)
s, err := NewServer(DNSCreateParams{ s, err := NewServer(DNSCreateParams{
@@ -1085,7 +1106,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
flt, err := filtering.New(&filtering.Config{ flt, err := filtering.New(&filtering.Config{
EtcHosts: hc, EtcHosts: hc,
}, nil) }, nil, nil)
require.NoError(t, err) require.NoError(t, err)
flt.SetEnabled(true) flt.SetEnabled(true)

View File

@@ -35,7 +35,7 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
ID: 0, Data: []byte(rules), ID: 0, Data: []byte(rules),
}} }}
f, err := filtering.New(&filtering.Config{}, filters) f, err := filtering.New(&filtering.Config{}, filters, nil)
require.NoError(t, err) require.NoError(t, err)
f.SetEnabled(true) f.SetEnabled(true)

View File

@@ -2,6 +2,7 @@ package filtering
import ( import (
"bufio" "bufio"
"bytes"
"fmt" "fmt"
"hash/crc32" "hash/crc32"
"io" "io"
@@ -12,6 +13,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
@@ -97,14 +99,15 @@ func (d *DNSFilter) filterSetProperties(
filt.URL, filt.URL,
) )
defer func(oldURL, oldName string, oldEnabled bool, oldUpdated time.Time) { defer func(oldURL, oldName string, oldEnabled bool, oldUpdated time.Time, oldRulesCount int) {
if err != nil { if err != nil {
filt.URL = oldURL filt.URL = oldURL
filt.Name = oldName filt.Name = oldName
filt.Enabled = oldEnabled filt.Enabled = oldEnabled
filt.LastUpdated = oldUpdated filt.LastUpdated = oldUpdated
filt.RulesCount = oldRulesCount
} }
}(filt.URL, filt.Name, filt.Enabled, filt.LastUpdated) }(filt.URL, filt.Name, filt.Enabled, filt.LastUpdated, filt.RulesCount)
filt.Name = newList.Name filt.Name = newList.Name
@@ -134,8 +137,8 @@ func (d *DNSFilter) filterSetProperties(
// TODO(e.burkov): The validation of the contents of the new URL is // TODO(e.burkov): The validation of the contents of the new URL is
// currently skipped if the rule list is disabled. This makes it // currently skipped if the rule list is disabled. This makes it
// possible to set a bad rules source, but the validation should still // possible to set a bad rules source, but the validation should still
// kick in when the filter is enabled. Consider making changing this // kick in when the filter is enabled. Consider changing this behavior
// behavior to be stricter. // to be stricter.
filt.unload() filt.unload()
} }
@@ -269,10 +272,10 @@ func (d *DNSFilter) periodicallyRefreshFilters() {
// already going on. // already going on.
// //
// TODO(e.burkov): Get rid of the concurrency pattern which requires the // TODO(e.burkov): Get rid of the concurrency pattern which requires the
// sync.Mutex.TryLock. // [sync.Mutex.TryLock].
func (d *DNSFilter) tryRefreshFilters(block, allow, force bool) (updated int, isNetworkErr, ok bool) { func (d *DNSFilter) tryRefreshFilters(block, allow, force bool) (updated int, isNetworkErr, ok bool) {
if ok = d.refreshLock.TryLock(); !ok { if ok = d.refreshLock.TryLock(); !ok {
return 0, false, ok return 0, false, false
} }
defer d.refreshLock.Unlock() defer d.refreshLock.Unlock()
@@ -427,52 +430,124 @@ func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
return updNum, false return updNum, false
} }
// Allows printable UTF-8 text with CR, LF, TAB characters // isPrintableText returns true if data is printable UTF-8 text with CR, LF, TAB
func isPrintableText(data []byte, len int) bool { // characters.
for i := 0; i < len; i++ { //
c := data[i] // TODO(e.burkov): Investigate the purpose of this and improve the
// implementation. Perhaps, use something from the unicode package.
func isPrintableText(data string) (ok bool) {
for _, c := range []byte(data) {
if (c >= ' ' && c != 0x7f) || c == '\n' || c == '\r' || c == '\t' { if (c >= ' ' && c != 0x7f) || c == '\n' || c == '\r' || c == '\t' {
continue continue
} }
return false return false
} }
return true return true
} }
// A helper function that parses filter contents and returns a number of rules and a filter name (if there's any) // scanLinesWithBreak is essentially a [bufio.ScanLines] which keeps trailing
func (d *DNSFilter) parseFilterContents(file io.Reader) (int, uint32, string) { // line breaks.
rulesCount := 0 func scanLinesWithBreak(data []byte, atEOF bool) (advance int, token []byte, err error) {
name := "" if atEOF && len(data) == 0 {
seenTitle := false return 0, nil, nil
r := bufio.NewReader(file) }
checksum := uint32(0)
for { if i := bytes.IndexByte(data, '\n'); i >= 0 {
line, err := r.ReadString('\n') return i + 1, data[0 : i+1], nil
checksum = crc32.Update(checksum, crc32.IEEETable, []byte(line)) }
line = strings.TrimSpace(line) if atEOF {
if len(line) == 0 { return len(data), data, nil
// }
} else if line[0] == '!' {
m := d.filterTitleRegexp.FindAllStringSubmatch(line, -1)
if len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
name = m[0][1]
seenTitle = true
}
} else if line[0] == '#' { // Request more data.
// return 0, nil, nil
} else { }
rulesCount++
// parseFilter copies filter's content from src to dst and returns the number of
// rules, name, number of bytes written, checksum, and title of the parsed list.
// dst must not be nil.
func (d *DNSFilter) parseFilter(
src io.Reader,
dst io.Writer,
) (rulesNum, written int, checksum uint32, title string, err error) {
scanner := bufio.NewScanner(src)
scanner.Split(scanLinesWithBreak)
titleFound := false
for n := 0; scanner.Scan(); written += n {
line := scanner.Text()
var isRule bool
var likelyTitle string
isRule, likelyTitle, err = d.parseFilterLine(line, !titleFound, written == 0)
if err != nil {
return 0, written, 0, "", err
} }
if isRule {
rulesNum++
} else if likelyTitle != "" {
title, titleFound = likelyTitle, true
}
checksum = crc32.Update(checksum, crc32.IEEETable, []byte(line))
n, err = dst.Write([]byte(line))
if err != nil { if err != nil {
break return 0, written, 0, "", fmt.Errorf("writing filter line: %w", err)
} }
} }
return rulesCount, checksum, name if err = scanner.Err(); err != nil {
return 0, written, 0, "", fmt.Errorf("scanning filter contents: %w", err)
}
return rulesNum, written, checksum, title, nil
}
// parseFilterLine returns true if the passed line is a rule. line is
// considered a rule if it's not a comment and contains no title.
func (d *DNSFilter) parseFilterLine(
line string,
lookForTitle bool,
testHTML bool,
) (isRule bool, title string, err error) {
if !isPrintableText(line) {
return false, "", errors.Error("filter contains non-printable characters")
}
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
return false, "", nil
}
if testHTML && isHTML(line) {
return false, "", errors.Error("data is HTML, not plain text")
}
if line[0] == '!' && lookForTitle {
match := d.filterTitleRegexp.FindStringSubmatch(line)
if len(match) > 1 {
title = match[1]
}
return false, title, nil
}
return true, "", nil
}
// isHTML returns true if the line contains HTML tags instead of plain text.
// line shouldn have no leading space symbols.
//
// TODO(ameshkov): It actually gives too much false-positives. Perhaps, just
// check if trimmed string begins with angle bracket.
func isHTML(line string) (ok bool) {
line = strings.ToLower(line)
return strings.HasPrefix(line, "<html") || strings.HasPrefix(line, "<!doctype")
} }
// Perform upgrade on a filter and update LastUpdated value // Perform upgrade on a filter and update LastUpdated value
@@ -485,57 +560,10 @@ func (d *DNSFilter) update(filter *FilterYAML) (bool, error) {
log.Error("os.Chtimes(): %v", e) log.Error("os.Chtimes(): %v", e)
} }
} }
return b, err return b, err
} }
func (d *DNSFilter) read(reader io.Reader, tmpFile *os.File, filter *FilterYAML) (int, error) {
htmlTest := true
firstChunk := make([]byte, 4*1024)
firstChunkLen := 0
buf := make([]byte, 64*1024)
total := 0
for {
n, err := reader.Read(buf)
total += n
if htmlTest {
num := len(firstChunk) - firstChunkLen
if n < num {
num = n
}
copied := copy(firstChunk[firstChunkLen:], buf[:num])
firstChunkLen += copied
if firstChunkLen == len(firstChunk) || err == io.EOF {
if !isPrintableText(firstChunk, firstChunkLen) {
return total, fmt.Errorf("data contains non-printable characters")
}
s := strings.ToLower(string(firstChunk))
if strings.Contains(s, "<html") || strings.Contains(s, "<!doctype") {
return total, fmt.Errorf("data is HTML, not plain text")
}
htmlTest = false
firstChunk = nil
}
}
_, err2 := tmpFile.Write(buf[:n])
if err2 != nil {
return total, err2
}
if err == io.EOF {
return total, nil
}
if err != nil {
log.Printf("Couldn't fetch filter contents from URL %s, skipping: %s", filter.URL, err)
return total, err
}
}
}
// finalizeUpdate closes and gets rid of temporary file f with filter's content // finalizeUpdate closes and gets rid of temporary file f with filter's content
// according to updated. It also saves new values of flt's name, rules number // according to updated. It also saves new values of flt's name, rules number
// and checksum if sucсeeded. // and checksum if sucсeeded.
@@ -552,7 +580,8 @@ func (d *DNSFilter) finalizeUpdate(
// Close the file before renaming it because it's required on Windows. // Close the file before renaming it because it's required on Windows.
// //
// See https://github.com/adguardTeam/adGuardHome/issues/1553. // See https://github.com/adguardTeam/adGuardHome/issues/1553.
if err = file.Close(); err != nil { err = file.Close()
if err != nil {
return fmt.Errorf("closing temporary file: %w", err) return fmt.Errorf("closing temporary file: %w", err)
} }
@@ -564,38 +593,18 @@ func (d *DNSFilter) finalizeUpdate(
log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path(d.DataDir)) log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path(d.DataDir))
if err = os.Rename(tmpFileName, flt.Path(d.DataDir)); err != nil { // Don't use renamio or maybe packages, since those will require loading the
// whole filter content to the memory on Windows.
err = os.Rename(tmpFileName, flt.Path(d.DataDir))
if err != nil {
return errors.WithDeferred(err, os.Remove(tmpFileName)) return errors.WithDeferred(err, os.Remove(tmpFileName))
} }
flt.Name = stringutil.Coalesce(flt.Name, name) flt.Name, flt.checksum, flt.RulesCount = aghalg.Coalesce(flt.Name, name), cs, rnum
flt.checksum = cs
flt.RulesCount = rnum
return nil return nil
} }
// processUpdate copies filter's content from src to dst and returns the name,
// rules number, and checksum for it. It also returns the number of bytes read
// from src.
func (d *DNSFilter) processUpdate(
src io.Reader,
dst *os.File,
flt *FilterYAML,
) (name string, rnum int, cs uint32, n int, err error) {
if n, err = d.read(src, dst, flt); err != nil {
return "", 0, 0, 0, err
}
if _, err = dst.Seek(0, io.SeekStart); err != nil {
return "", 0, 0, 0, err
}
rnum, cs, name = d.parseFilterContents(dst)
return name, rnum, cs, n, nil
}
// updateIntl updates the flt rewriting it's actual file. It returns true if // updateIntl updates the flt rewriting it's actual file. It returns true if
// the actual update has been performed. // the actual update has been performed.
func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) { func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
@@ -612,31 +621,21 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
} }
defer func() { defer func() {
err = errors.WithDeferred(err, d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs)) err = errors.WithDeferred(err, d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
ok = ok && err == nil if ok && err == nil {
if ok {
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum) log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
} }
}() }()
// Change the default 0o600 permission to something more acceptable by // Change the default 0o600 permission to something more acceptable by end
// end users. // users.
// //
// See https://github.com/AdguardTeam/AdGuardHome/issues/3198. // See https://github.com/AdguardTeam/AdGuardHome/issues/3198.
if err = tmpFile.Chmod(0o644); err != nil { if err = tmpFile.Chmod(0o644); err != nil {
return false, fmt.Errorf("changing file mode: %w", err) return false, fmt.Errorf("changing file mode: %w", err)
} }
var r io.Reader var rc io.ReadCloser
if filepath.IsAbs(flt.URL) { if !filepath.IsAbs(flt.URL) {
var file io.ReadCloser
file, err = os.Open(flt.URL)
if err != nil {
return false, fmt.Errorf("open file: %w", err)
}
defer func() { err = errors.WithDeferred(err, file.Close()) }()
r = file
} else {
var resp *http.Response var resp *http.Response
resp, err = d.HTTPClient.Get(flt.URL) resp, err = d.HTTPClient.Get(flt.URL)
if err != nil { if err != nil {
@@ -649,24 +648,30 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
log.Printf("got status code %d from %s, skip", resp.StatusCode, flt.URL) log.Printf("got status code %d from %s, skip", resp.StatusCode, flt.URL)
return false, fmt.Errorf("got status code != 200: %d", resp.StatusCode) return false, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
} }
r = resp.Body rc = resp.Body
} else {
rc, err = os.Open(flt.URL)
if err != nil {
return false, fmt.Errorf("open file: %w", err)
}
defer func() { err = errors.WithDeferred(err, rc.Close()) }()
} }
name, rnum, cs, n, err = d.processUpdate(r, tmpFile, flt) rnum, n, cs, name, err = d.parseFilter(rc, tmpFile)
return cs != flt.checksum, err return cs != flt.checksum && err == nil, err
} }
// loads filter contents from the file in dataDir // loads filter contents from the file in dataDir
func (d *DNSFilter) load(filter *FilterYAML) (err error) { func (d *DNSFilter) load(flt *FilterYAML) (err error) {
filterFilePath := filter.Path(d.DataDir) fileName := flt.Path(d.DataDir)
log.Tracef("filtering: loading filter %d from %s", filter.ID, filterFilePath) log.Debug("filtering: loading filter %d from %s", flt.ID, fileName)
file, err := os.Open(filterFilePath) file, err := os.Open(fileName)
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
// Do nothing, file doesn't exist. // Do nothing, file doesn't exist.
return nil return nil
@@ -680,13 +685,14 @@ func (d *DNSFilter) load(filter *FilterYAML) (err error) {
return fmt.Errorf("getting filter file stat: %w", err) return fmt.Errorf("getting filter file stat: %w", err)
} }
log.Tracef("filtering: File %s, id %d, length %d", filterFilePath, filter.ID, st.Size()) log.Debug("filtering: file %s, id %d, length %d", fileName, flt.ID, st.Size())
rulesCount, checksum, _ := d.parseFilterContents(file) rulesCount, _, checksum, _, err := d.parseFilter(file, io.Discard)
if err != nil {
return fmt.Errorf("parsing filter file: %w", err)
}
filter.RulesCount = rulesCount flt.RulesCount, flt.checksum, flt.LastUpdated = rulesCount, checksum, st.ModTime()
filter.checksum = checksum
filter.LastUpdated = st.ModTime()
return nil return nil
} }

View File

@@ -4,33 +4,23 @@ import (
"io/fs" "io/fs"
"net" "net"
"net/http" "net/http"
"net/netip"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/testutil" "github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// serveFiltersLocally is a helper that concurrently listens on a free port to // serveHTTPLocally starts a new HTTP server, that handles its index with h. It
// respond with fltContent. It also gracefully closes the listener when the // also gracefully closes the listener when the test under t finishes.
// test under t finishes. func serveHTTPLocally(t *testing.T, h http.Handler) (urlStr string) {
func serveFiltersLocally(t *testing.T, fltContent []byte) (ipp netip.AddrPort) {
t.Helper() t.Helper()
h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
pt := testutil.PanicT{}
n, werr := w.Write(fltContent)
require.NoError(pt, werr)
require.Equal(pt, len(fltContent), n)
})
l, err := net.Listen("tcp", ":0") l, err := net.Listen("tcp", ":0")
require.NoError(t, err) require.NoError(t, err)
@@ -38,9 +28,26 @@ func serveFiltersLocally(t *testing.T, fltContent []byte) (ipp netip.AddrPort) {
testutil.CleanupAndRequireSuccess(t, l.Close) testutil.CleanupAndRequireSuccess(t, l.Close)
addr := l.Addr() addr := l.Addr()
require.IsType(t, new(net.TCPAddr), addr) require.IsType(t, (*net.TCPAddr)(nil), addr)
return netip.AddrPortFrom(netutil.IPv4Localhost(), uint16(addr.(*net.TCPAddr).Port)) return (&url.URL{
Scheme: aghhttp.SchemeHTTP,
Host: addr.String(),
}).String()
}
// serveFiltersLocally is a helper that concurrently listens on a free port to
// respond with fltContent.
func serveFiltersLocally(t *testing.T, fltContent []byte) (urlStr string) {
t.Helper()
return serveHTTPLocally(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
pt := testutil.PanicT{}
n, werr := w.Write(fltContent)
require.NoError(pt, werr)
require.Equal(pt, len(fltContent), n)
}))
} }
func TestFilters(t *testing.T) { func TestFilters(t *testing.T) {
@@ -61,14 +68,11 @@ func TestFilters(t *testing.T) {
HTTPClient: &http.Client{ HTTPClient: &http.Client{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
}, },
}, nil) }, nil, nil)
require.NoError(t, err) require.NoError(t, err)
f := &FilterYAML{ f := &FilterYAML{
URL: (&url.URL{ URL: addr,
Scheme: "http",
Host: addr.String(),
}).String(),
} }
updateAndAssert := func(t *testing.T, want require.BoolAssertionFunc, wantRulesCount int) { updateAndAssert := func(t *testing.T, want require.BoolAssertionFunc, wantRulesCount int) {
@@ -103,11 +107,7 @@ func TestFilters(t *testing.T) {
anotherContent := []byte(`||example.com^`) anotherContent := []byte(`||example.com^`)
oldURL := f.URL oldURL := f.URL
ipp := serveFiltersLocally(t, anotherContent) f.URL = serveFiltersLocally(t, anotherContent)
f.URL = (&url.URL{
Scheme: "http",
Host: ipp.String(),
}).String()
t.Cleanup(func() { f.URL = oldURL }) t.Cleanup(func() { f.URL = oldURL })
updateAndAssert(t, require.True, 1) updateAndAssert(t, require.True, 1)

View File

@@ -33,7 +33,6 @@ import (
// The IDs of built-in filter lists. // The IDs of built-in filter lists.
// //
// Keep in sync with client/src/helpers/constants.js. // Keep in sync with client/src/helpers/constants.js.
// TODO(d.kolyshev): Add RewritesListID and don't forget to keep in sync.
const ( const (
CustomListID = -iota CustomListID = -iota
SysHostsListID SysHostsListID
@@ -41,6 +40,7 @@ const (
ParentalListID ParentalListID
SafeBrowsingListID SafeBrowsingListID
SafeSearchListID SafeSearchListID
RewritesListID
) )
// ServiceEntry - blocked service array element // ServiceEntry - blocked service array element
@@ -90,7 +90,7 @@ type Config struct {
ParentalCacheSize uint `yaml:"parental_cache_size"` // (in bytes) ParentalCacheSize uint `yaml:"parental_cache_size"` // (in bytes)
CacheTime uint `yaml:"cache_time"` // Element's TTL (in minutes) CacheTime uint `yaml:"cache_time"` // Element's TTL (in minutes)
Rewrites []*LegacyRewrite `yaml:"rewrites"` Rewrites []*RewriteItem `yaml:"rewrites"`
// Names of services to block (globally). // Names of services to block (globally).
// Per-client settings can override this configuration. // Per-client settings can override this configuration.
@@ -190,8 +190,12 @@ type DNSFilter struct {
// filterTitleRegexp is the regular expression to retrieve a name of a // filterTitleRegexp is the regular expression to retrieve a name of a
// filter list. // filter list.
//
// TODO(e.burkov): Don't use regexp for such a simple text processing task.
filterTitleRegexp *regexp.Regexp filterTitleRegexp *regexp.Regexp
rewriteStorage RewriteStorage
hostCheckers []hostChecker hostCheckers []hostChecker
} }
@@ -313,7 +317,7 @@ func (d *DNSFilter) WriteDiskConfig(c *Config) {
defer d.confLock.Unlock() defer d.confLock.Unlock()
*c = d.Config *c = d.Config
c.Rewrites = cloneRewrites(c.Rewrites) c.Rewrites = slices.Clone(c.Rewrites)
}() }()
d.filtersMu.RLock() d.filtersMu.RLock()
@@ -324,16 +328,6 @@ func (d *DNSFilter) WriteDiskConfig(c *Config) {
c.UserRules = slices.Clone(d.UserRules) c.UserRules = slices.Clone(d.UserRules)
} }
// cloneRewrites returns a deep copy of entries.
func cloneRewrites(entries []*LegacyRewrite) (clone []*LegacyRewrite) {
clone = make([]*LegacyRewrite, len(entries))
for i, rw := range entries {
clone[i] = rw.clone()
}
return clone
}
// SetFilters sets new filters, synchronously or asynchronously. When filters // SetFilters sets new filters, synchronously or asynchronously. When filters
// are set asynchronously, the old filters continue working until the new // are set asynchronously, the old filters continue working until the new
// filters are ready. // filters are ready.
@@ -544,75 +538,52 @@ func (d *DNSFilter) matchSysHosts(
// CNAME, breaking loops in the process. // CNAME, breaking loops in the process.
// //
// Secondly, it finds A or AAAA rewrites for host and, if found, sets res.IPList // Secondly, it finds A or AAAA rewrites for host and, if found, sets res.IPList
// accordingly. If the found rewrite has a special value of "A" or "AAAA", the // accordingly.
// result is an exception.
func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) { func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
d.confLock.RLock() d.confLock.RLock()
defer d.confLock.RUnlock() defer d.confLock.RUnlock()
rewrites, matched := findRewrites(d.Rewrites, host, qtype) if d.rewriteStorage == nil {
if !matched { return res
return Result{}
} }
res.Reason = Rewritten dnsr := d.rewriteStorage.MatchRequest(&urlfilter.DNSRequest{
Hostname: host,
DNSType: qtype,
})
cnames := stringutil.NewSet() setRewriteResult(&res, host, dnsr, qtype)
origHost := host
for matched && len(rewrites) > 0 && rewrites[0].Type == dns.TypeCNAME {
rw := rewrites[0]
rwPat := rw.Domain
rwAns := rw.Answer
log.Debug("rewrite: cname for %s is %s", host, rwAns)
if origHost == rwAns || rwPat == rwAns {
// Either a request for the hostname itself or a rewrite of
// a pattern onto itself, both of which are an exception rules.
// Return a not filtered result.
return Result{}
} else if host == rwAns && isWildcard(rwPat) {
// An "*.example.com → sub.example.com" rewrite matching in a loop.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/4016.
res.CanonName = host
break
}
host = rwAns
if cnames.Has(host) {
log.Info("rewrite: cname loop for %q on %q", origHost, host)
return res
}
cnames.Add(host)
res.CanonName = host
rewrites, matched = findRewrites(d.Rewrites, host, qtype)
}
setRewriteResult(&res, host, rewrites, qtype)
return res return res
} }
// setRewriteResult sets the Reason or IPList of res if necessary. res must not // setRewriteResult sets the Reason or IPList of res if necessary. res must not
// be nil. // be nil.
func setRewriteResult(res *Result, host string, rewrites []*LegacyRewrite, qtype uint16) { func setRewriteResult(res *Result, host string, dnsr []*rules.DNSRewrite, qtype uint16) {
for _, rw := range rewrites { if len(dnsr) == 0 {
if rw.Type == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) { res.Reason = NotFilteredNotFound
if rw.IP == nil {
// "A"/"AAAA" exception: allow getting from upstream.
res.Reason = NotFilteredNotFound
return return
}
res.Reason = Rewritten
for _, dnsRewrite := range dnsr {
if dnsRewrite.RRType == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) {
ip, ok := dnsRewrite.Value.(net.IP)
if !ok || ip == nil {
continue
} }
res.IPList = append(res.IPList, rw.IP) if qtype == dns.TypeA {
ip = ip.To4()
}
log.Debug("rewrite: a/aaaa for %s is %s", host, rw.IP) res.IPList = append(res.IPList, ip)
log.Debug("rewrite: a/aaaa for %s is %s", host, ip)
} else if dnsRewrite.NewCNAME != "" {
res.CanonName = dnsRewrite.NewCNAME
} }
} }
} }
@@ -925,7 +896,7 @@ func InitModule() {
// New creates properly initialized DNS Filter that is ready to be used. c must // New creates properly initialized DNS Filter that is ready to be used. c must
// be non-nil. // be non-nil.
func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) { func New(c *Config, blockFilters []Filter, rewriteStorage RewriteStorage) (d *DNSFilter, err error) {
d = &DNSFilter{ d = &DNSFilter{
resolver: net.DefaultResolver, resolver: net.DefaultResolver,
refreshLock: &sync.Mutex{}, refreshLock: &sync.Mutex{},
@@ -978,11 +949,7 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
d.Config = *c d.Config = *c
d.filtersMu = &sync.RWMutex{} d.filtersMu = &sync.RWMutex{}
d.rewriteStorage = rewriteStorage
err = d.prepareRewrites()
if err != nil {
return nil, fmt.Errorf("rewrites: preparing: %s", err)
}
bsvcs := []string{} bsvcs := []string{}
for _, s := range d.BlockedServices { for _, s := range d.BlockedServices {

View File

@@ -46,6 +46,7 @@ func newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts
ProtectionEnabled: true, ProtectionEnabled: true,
FilteringEnabled: true, FilteringEnabled: true,
} }
if c != nil { if c != nil {
c.SafeBrowsingCacheSize = 10000 c.SafeBrowsingCacheSize = 10000
c.ParentalCacheSize = 10000 c.ParentalCacheSize = 10000
@@ -58,7 +59,8 @@ func newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts
// It must not be nil. // It must not be nil.
c = &Config{} c = &Config{}
} }
f, err := New(c, filters)
f, err := New(c, filters, nil)
require.NoError(t, err) require.NoError(t, err)
purgeCaches(f) purgeCaches(f)
@@ -417,274 +419,275 @@ func TestMatching(t *testing.T) {
host string host string
wantReason Reason wantReason Reason
wantIsFiltered bool wantIsFiltered bool
wantDNSType uint16 qtype uint16
}{{ }{{
name: "sanity", name: "sanity",
rules: "||doubleclick.net^", rules: "||doubleclick.net^",
host: "www.doubleclick.net", host: "www.doubleclick.net",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "sanity", name: "sanity",
rules: "||doubleclick.net^", rules: "||doubleclick.net^",
host: "nodoubleclick.net", host: "nodoubleclick.net",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "sanity", name: "sanity",
rules: "||doubleclick.net^", rules: "||doubleclick.net^",
host: "doubleclick.net.ru", host: "doubleclick.net.ru",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "sanity", name: "sanity",
rules: "||doubleclick.net^", rules: "||doubleclick.net^",
host: sbBlocked, host: sbBlocked,
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "blocking", name: "blocking",
rules: blockingRules, rules: blockingRules,
host: "example.org", host: "example.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "blocking", name: "blocking",
rules: blockingRules, rules: blockingRules,
host: "test.example.org", host: "test.example.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "blocking", name: "blocking",
rules: blockingRules, rules: blockingRules,
host: "test.test.example.org", host: "test.test.example.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "blocking", name: "blocking",
rules: blockingRules, rules: blockingRules,
host: "testexample.org", host: "testexample.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "blocking", name: "blocking",
rules: blockingRules, rules: blockingRules,
host: "onemoreexample.org", host: "onemoreexample.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "allowlist", name: "allowlist",
rules: allowlistRules, rules: allowlistRules,
host: "example.org", host: "example.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "allowlist", name: "allowlist",
rules: allowlistRules, rules: allowlistRules,
host: "test.example.org", host: "test.example.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredAllowList, wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "allowlist", name: "allowlist",
rules: allowlistRules, rules: allowlistRules,
host: "test.test.example.org", host: "test.test.example.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredAllowList, wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "allowlist", name: "allowlist",
rules: allowlistRules, rules: allowlistRules,
host: "testexample.org", host: "testexample.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "allowlist", name: "allowlist",
rules: allowlistRules, rules: allowlistRules,
host: "onemoreexample.org", host: "onemoreexample.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "important", name: "important",
rules: importantRules, rules: importantRules,
host: "example.org", host: "example.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredAllowList, wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "important", name: "important",
rules: importantRules, rules: importantRules,
host: "test.example.org", host: "test.example.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "important", name: "important",
rules: importantRules, rules: importantRules,
host: "test.test.example.org", host: "test.test.example.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "important", name: "important",
rules: importantRules, rules: importantRules,
host: "testexample.org", host: "testexample.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "important", name: "important",
rules: importantRules, rules: importantRules,
host: "onemoreexample.org", host: "onemoreexample.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "regex", name: "regex",
rules: regexRules, rules: regexRules,
host: "example.org", host: "example.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "regex", name: "regex",
rules: regexRules, rules: regexRules,
host: "test.example.org", host: "test.example.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredAllowList, wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "regex", name: "regex",
rules: regexRules, rules: regexRules,
host: "test.test.example.org", host: "test.test.example.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredAllowList, wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "regex", name: "regex",
rules: regexRules, rules: regexRules,
host: "testexample.org", host: "testexample.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "regex", name: "regex",
rules: regexRules, rules: regexRules,
host: "onemoreexample.org", host: "onemoreexample.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "mask", name: "mask",
rules: maskRules, rules: maskRules,
host: "test.example.org", host: "test.example.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "mask", name: "mask",
rules: maskRules, rules: maskRules,
host: "test2.example.org", host: "test2.example.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "mask", name: "mask",
rules: maskRules, rules: maskRules,
host: "example.com", host: "example.com",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "mask", name: "mask",
rules: maskRules, rules: maskRules,
host: "exampleeee.com", host: "exampleeee.com",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "mask", name: "mask",
rules: maskRules, rules: maskRules,
host: "onemoreexamsite.com", host: "onemoreexamsite.com",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "mask", name: "mask",
rules: maskRules, rules: maskRules,
host: "example.org", host: "example.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "mask", name: "mask",
rules: maskRules, rules: maskRules,
host: "testexample.org", host: "testexample.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "mask", name: "mask",
rules: maskRules, rules: maskRules,
host: "example.co.uk", host: "example.co.uk",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "dnstype", name: "dnstype",
rules: dnstypeRules, rules: dnstypeRules,
host: "onemoreexample.org", host: "onemoreexample.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "dnstype", name: "dnstype",
rules: dnstypeRules, rules: dnstypeRules,
host: "example.org", host: "example.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredNotFound, wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "dnstype", name: "dnstype",
rules: dnstypeRules, rules: dnstypeRules,
host: "example.org", host: "example.org",
wantIsFiltered: true, wantIsFiltered: true,
wantReason: FilteredBlockList, wantReason: FilteredBlockList,
wantDNSType: dns.TypeAAAA, qtype: dns.TypeAAAA,
}, { }, {
name: "dnstype", name: "dnstype",
rules: dnstypeRules, rules: dnstypeRules,
host: "test.example.org", host: "test.example.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredAllowList, wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA, qtype: dns.TypeA,
}, { }, {
name: "dnstype", name: "dnstype",
rules: dnstypeRules, rules: dnstypeRules,
host: "test.example.org", host: "test.example.org",
wantIsFiltered: false, wantIsFiltered: false,
wantReason: NotFilteredAllowList, wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeAAAA, qtype: dns.TypeAAAA,
}} }}
for _, tc := range testCases { for _, tc := range testCases {
t.Run(fmt.Sprintf("%s-%s", tc.name, tc.host), func(t *testing.T) { t.Run(fmt.Sprintf("%s-%s", tc.name, tc.host), func(t *testing.T) {
filters := []Filter{{ID: 0, Data: []byte(tc.rules)}} filters := []Filter{{ID: 0, Data: []byte(tc.rules)}}
d, setts := newForTest(t, nil, filters) d, setts := newForTest(t, nil, filters)
t.Cleanup(d.Close) t.Cleanup(d.Close)
res, err := d.CheckHost(tc.host, tc.wantDNSType, setts) res, err := d.CheckHost(tc.host, tc.qtype, setts)
require.NoError(t, err) require.NoError(t, err)
assert.Equalf(t, tc.wantIsFiltered, res.IsFiltered, "Hostname %s has wrong result (%v must be %v)", tc.host, res.IsFiltered, tc.wantIsFiltered) assert.Equalf(t, tc.wantIsFiltered, res.IsFiltered, "Hostname %s has wrong result (%v must be %v)", tc.host, res.IsFiltered, tc.wantIsFiltered)

View File

@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"testing" "testing"
"time" "time"
@@ -30,11 +29,7 @@ func TestDNSFilter_handleFilteringSetURL(t *testing.T) {
endpoint: &badRulesEndpoint, endpoint: &badRulesEndpoint,
content: []byte(`<html></html>`), content: []byte(`<html></html>`),
}} { }} {
ipp := serveFiltersLocally(t, rulesSource.content) *rulesSource.endpoint = serveFiltersLocally(t, rulesSource.content)
*rulesSource.endpoint = (&url.URL{
Scheme: "http",
Host: ipp.String(),
}).String()
} }
testCases := []struct { testCases := []struct {
@@ -110,7 +105,7 @@ func TestDNSFilter_handleFilteringSetURL(t *testing.T) {
}, },
ConfigModified: func() { confModifiedCalled = true }, ConfigModified: func() { confModifiedCalled = true },
DataDir: filtersDir, DataDir: filtersDir,
}, nil) }, nil, nil)
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(d.Close) t.Cleanup(d.Close)

View File

@@ -0,0 +1,42 @@
package filtering
import (
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/rules"
)
// RewriteStorage is a storage for rewrite rules.
type RewriteStorage interface {
// MatchRequest returns matching dnsrewrites for the specified request.
MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.DNSRewrite)
// Add adds item to the storage.
Add(item *RewriteItem) (err error)
// Remove deletes item from the storage.
Remove(item *RewriteItem) (err error)
// List returns all items from the storage.
List() (items []*RewriteItem)
}
// RewriteItem is a single DNS rewrite record.
type RewriteItem struct {
// Domain is the domain pattern for which this rewrite should work.
Domain string `yaml:"domain" json:"domain"`
// Answer is the IP address, canonical name, or one of the special
// values: "A" or "AAAA".
Answer string `yaml:"answer" json:"answer"`
}
// Equal returns true if rw is Equal to other.
func (rw *RewriteItem) Equal(other *RewriteItem) (ok bool) {
if rw == nil {
return other == nil
} else if other == nil {
return false
}
return *rw == *other
}

View File

@@ -1,73 +0,0 @@
package rewrite
import (
"fmt"
"net/netip"
"strings"
"github.com/miekg/dns"
)
// Item is a single DNS rewrite record.
type Item struct {
// Domain is the domain pattern for which this rewrite should work.
Domain string `yaml:"domain"`
// Answer is the IP address, canonical name, or one of the special
// values: "A" or "AAAA".
Answer string `yaml:"answer"`
}
// equal returns true if rw is equal to other.
func (rw *Item) equal(other *Item) (ok bool) {
if rw == nil {
return other == nil
} else if other == nil {
return false
}
return *rw == *other
}
// toRule converts rw to a filter rule.
func (rw *Item) toRule() (res string) {
if rw == nil {
return ""
}
domain := strings.ToLower(rw.Domain)
dType, exception := rw.rewriteParams()
dTypeKey := dns.TypeToString[dType]
if exception {
return fmt.Sprintf("@@||%s^$dnstype=%s,dnsrewrite", domain, dTypeKey)
}
return fmt.Sprintf("|%s^$dnsrewrite=NOERROR;%s;%s", domain, dTypeKey, rw.Answer)
}
// rewriteParams returns dns request type and exception flag for rw.
func (rw *Item) rewriteParams() (dType uint16, exception bool) {
switch rw.Answer {
case "AAAA":
return dns.TypeAAAA, true
case "A":
return dns.TypeA, true
default:
// Go on.
}
addr, err := netip.ParseAddr(rw.Answer)
if err != nil {
// TODO(d.kolyshev): Validate rw.Answer as a domain name.
return dns.TypeCNAME, false
}
if addr.Is4() {
dType = dns.TypeA
} else {
dType = dns.TypeAAAA
}
return dType, false
}

View File

@@ -1,124 +0,0 @@
package rewrite
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestItem_equal(t *testing.T) {
const (
testDomain = "example.org"
testAnswer = "1.1.1.1"
)
testItem := &Item{
Domain: testDomain,
Answer: testAnswer,
}
testCases := []struct {
name string
left *Item
right *Item
want bool
}{{
name: "nil_left",
left: nil,
right: testItem,
want: false,
}, {
name: "nil_right",
left: testItem,
right: nil,
want: false,
}, {
name: "nils",
left: nil,
right: nil,
want: true,
}, {
name: "equal",
left: testItem,
right: testItem,
want: true,
}, {
name: "distinct",
left: testItem,
right: &Item{
Domain: "other",
Answer: "other",
},
want: false,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res := tc.left.equal(tc.right)
assert.Equal(t, tc.want, res)
})
}
}
func TestItem_toRule(t *testing.T) {
const testDomain = "example.org"
testCases := []struct {
name string
item *Item
want string
}{{
name: "nil",
item: nil,
want: "",
}, {
name: "a_rule",
item: &Item{
Domain: testDomain,
Answer: "1.1.1.1",
},
want: "|example.org^$dnsrewrite=NOERROR;A;1.1.1.1",
}, {
name: "aaaa_rule",
item: &Item{
Domain: testDomain,
Answer: "1:2:3::4",
},
want: "|example.org^$dnsrewrite=NOERROR;AAAA;1:2:3::4",
}, {
name: "cname_rule",
item: &Item{
Domain: testDomain,
Answer: "other.org",
},
want: "|example.org^$dnsrewrite=NOERROR;CNAME;other.org",
}, {
name: "wildcard_rule",
item: &Item{
Domain: "*.example.org",
Answer: "other.org",
},
want: "|*.example.org^$dnsrewrite=NOERROR;CNAME;other.org",
}, {
name: "aaaa_exception",
item: &Item{
Domain: testDomain,
Answer: "A",
},
want: "@@||example.org^$dnstype=A,dnsrewrite",
}, {
name: "aaaa_exception",
item: &Item{
Domain: testDomain,
Answer: "AAAA",
},
want: "@@||example.org^$dnstype=AAAA,dnsrewrite",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res := tc.item.toRule()
assert.Equal(t, tc.want, res)
})
}
}

View File

@@ -3,9 +3,11 @@ package rewrite
import ( import (
"fmt" "fmt"
"net/netip"
"strings" "strings"
"sync" "sync"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/urlfilter" "github.com/AdguardTeam/urlfilter"
@@ -15,21 +17,6 @@ import (
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
// Storage is a storage for rewrite rules.
type Storage interface {
// MatchRequest returns matching dnsrewrites for the specified request.
MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.DNSRewrite)
// Add adds item to the storage.
Add(item *Item) (err error)
// Remove deletes item from the storage.
Remove(item *Item) (err error)
// List returns all items from the storage.
List() (items []*Item)
}
// DefaultStorage is the default storage for rewrite rules. // DefaultStorage is the default storage for rewrite rules.
type DefaultStorage struct { type DefaultStorage struct {
// mu protects items. // mu protects items.
@@ -42,7 +29,7 @@ type DefaultStorage struct {
ruleList filterlist.RuleList ruleList filterlist.RuleList
// rewrites stores the rewrite entries from configuration. // rewrites stores the rewrite entries from configuration.
rewrites []*Item rewrites []*filtering.RewriteItem
// urlFilterID is the synthetic integer identifier for the urlfilter engine. // urlFilterID is the synthetic integer identifier for the urlfilter engine.
// //
@@ -53,16 +40,13 @@ type DefaultStorage struct {
// NewDefaultStorage returns new rewrites storage. listID is used as an // NewDefaultStorage returns new rewrites storage. listID is used as an
// identifier of the underlying rules list. rewrites must not be nil. // identifier of the underlying rules list. rewrites must not be nil.
func NewDefaultStorage(listID int, rewrites []*Item) (s *DefaultStorage, err error) { func NewDefaultStorage(rewrites []*filtering.RewriteItem) (s *DefaultStorage, err error) {
s = &DefaultStorage{ s = &DefaultStorage{
mu: &sync.RWMutex{}, mu: &sync.RWMutex{},
urlFilterID: listID, urlFilterID: filtering.RewritesListID,
rewrites: rewrites, rewrites: rewrites,
} }
s.mu.Lock()
defer s.mu.Unlock()
err = s.resetRules() err = s.resetRules()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -72,9 +56,9 @@ func NewDefaultStorage(listID int, rewrites []*Item) (s *DefaultStorage, err err
} }
// type check // type check
var _ Storage = (*DefaultStorage)(nil) var _ filtering.RewriteStorage = (*DefaultStorage)(nil)
// MatchRequest implements the [Storage] interface for *DefaultStorage. // MatchRequest implements the [RewriteStorage] interface for *DefaultStorage.
func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.DNSRewrite) { func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.DNSRewrite) {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
@@ -84,28 +68,32 @@ func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.
return nil return nil
} }
// TODO(a.garipov): Check cnames for cycles on initialisation. // TODO(a.garipov): Check cnames for cycles on initialization.
cnames := stringutil.NewSet() cnames := stringutil.NewSet()
host := dReq.Hostname host := dReq.Hostname
var lastCNAMERule *rules.NetworkRule
for len(rrules) > 0 && rrules[0].DNSRewrite != nil && rrules[0].DNSRewrite.NewCNAME != "" { for len(rrules) > 0 && rrules[0].DNSRewrite != nil && rrules[0].DNSRewrite.NewCNAME != "" {
rule := rrules[0] lastCNAMERule = rrules[0]
rwAns := rule.DNSRewrite.NewCNAME lastDNSRewrite := lastCNAMERule.DNSRewrite
rwAns := lastDNSRewrite.NewCNAME
log.Debug("rewrite: cname for %s is %s", host, rwAns) log.Debug("rewrite: cname for %s is %s", host, rwAns)
if dReq.Hostname == rwAns { if dReq.Hostname == rwAns {
// A request for the hostname itself is an exception rule. // A request for the hostname itself.
// TODO(d.kolyshev): Check rewrite of a pattern onto itself. // TODO(d.kolyshev): Check rewrite of a pattern onto itself.
log.Debug("rewrite: request for hostname itself for %q", dReq.Hostname)
return nil return nil
} }
if host == rwAns && isWildcard(rule.RuleText) { if host == rwAns && isWildcard(lastCNAMERule.RuleText) {
// An "*.example.com → sub.example.com" rewrite matching in a loop. // An "*.example.com → sub.example.com" rewrite matching in a loop.
// //
// See https://github.com/AdguardTeam/AdGuardHome/issues/4016. // See https://github.com/AdguardTeam/AdGuardHome/issues/4016.
log.Debug("rewrite: cname wildcard loop for %q on %q", dReq.Hostname, rwAns)
return []*rules.DNSRewrite{rule.DNSRewrite} return []*rules.DNSRewrite{lastDNSRewrite}
} }
if cnames.Has(rwAns) { if cnames.Has(rwAns) {
@@ -120,21 +108,28 @@ func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.
Hostname: rwAns, Hostname: rwAns,
DNSType: dReq.DNSType, DNSType: dReq.DNSType,
}) })
if drules != nil {
rrules = drules if drules == nil {
break
} }
rrules = drules
host = rwAns host = rwAns
} }
return s.collectDNSRewrites(rrules, dReq.DNSType) return s.collectDNSRewrites(rrules, lastCNAMERule, dReq.DNSType)
} }
// collectDNSRewrites filters DNSRewrite by question type. // collectDNSRewrites filters DNSRewrite by question type.
func (s *DefaultStorage) collectDNSRewrites( func (s *DefaultStorage) collectDNSRewrites(
rewrites []*rules.NetworkRule, rewrites []*rules.NetworkRule,
cnameRule *rules.NetworkRule,
qtyp uint16, qtyp uint16,
) (rws []*rules.DNSRewrite) { ) (rws []*rules.DNSRewrite) {
if cnameRule != nil {
rewrites = append([]*rules.NetworkRule{cnameRule}, rewrites...)
}
for _, rewrite := range rewrites { for _, rewrite := range rewrites {
dnsRewrite := rewrite.DNSRewrite dnsRewrite := rewrite.DNSRewrite
if matchesQType(dnsRewrite, qtyp) { if matchesQType(dnsRewrite, qtyp) {
@@ -152,8 +147,8 @@ func (s *DefaultStorage) rewriteRulesForReq(dReq *urlfilter.DNSRequest) (rules [
return res.DNSRewrites() return res.DNSRewrites()
} }
// Add implements the [Storage] interface for *DefaultStorage. // Add implements the [RewriteStorage] interface for *DefaultStorage.
func (s *DefaultStorage) Add(item *Item) (err error) { func (s *DefaultStorage) Add(item *filtering.RewriteItem) (err error) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@@ -163,16 +158,16 @@ func (s *DefaultStorage) Add(item *Item) (err error) {
return s.resetRules() return s.resetRules()
} }
// Remove implements the [Storage] interface for *DefaultStorage. // Remove implements the [RewriteStorage] interface for *DefaultStorage.
func (s *DefaultStorage) Remove(item *Item) (err error) { func (s *DefaultStorage) Remove(item *filtering.RewriteItem) (err error) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
arr := []*Item{} arr := []*filtering.RewriteItem{}
// TODO(d.kolyshev): Use slices.IndexFunc + slices.Delete? // TODO(d.kolyshev): Use slices.IndexFunc + slices.Delete?
for _, ent := range s.rewrites { for _, ent := range s.rewrites {
if ent.equal(item) { if ent.Equal(item) {
log.Debug("rewrite: removed element: %s -> %s", ent.Domain, ent.Answer) log.Debug("rewrite: removed element: %s -> %s", ent.Domain, ent.Answer)
continue continue
@@ -185,8 +180,8 @@ func (s *DefaultStorage) Remove(item *Item) (err error) {
return s.resetRules() return s.resetRules()
} }
// List implements the [Storage] interface for *DefaultStorage. // List implements the [RewriteStorage] interface for *DefaultStorage.
func (s *DefaultStorage) List() (items []*Item) { func (s *DefaultStorage) List() (items []*filtering.RewriteItem) {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
@@ -198,7 +193,7 @@ func (s *DefaultStorage) resetRules() (err error) {
// TODO(a.garipov): Use strings.Builder. // TODO(a.garipov): Use strings.Builder.
var rulesText []string var rulesText []string
for _, rewrite := range s.rewrites { for _, rewrite := range s.rewrites {
rulesText = append(rulesText, rewrite.toRule()) rulesText = append(rulesText, toRule(rewrite))
} }
strList := &filterlist.StringRuleList{ strList := &filterlist.StringRuleList{
@@ -222,20 +217,60 @@ func (s *DefaultStorage) resetRules() (err error) {
// matchesQType returns true if dnsrewrite matches the question type qt. // matchesQType returns true if dnsrewrite matches the question type qt.
func matchesQType(dnsrr *rules.DNSRewrite, qt uint16) (ok bool) { func matchesQType(dnsrr *rules.DNSRewrite, qt uint16) (ok bool) {
// Add CNAMEs, since they match for all types requests. switch qt {
if dnsrr.RRType == dns.TypeCNAME { case dns.TypeA:
return dnsrr.RRType != dns.TypeAAAA
case dns.TypeAAAA:
return dnsrr.RRType != dns.TypeA
default:
return true return true
} }
// Reject types other than A and AAAA.
if qt != dns.TypeA && qt != dns.TypeAAAA {
return false
}
return dnsrr.RRType == qt
} }
// isWildcard returns true if pat is a wildcard domain pattern. // isWildcard returns true if pat is a wildcard domain pattern.
func isWildcard(pat string) (res bool) { func isWildcard(pat string) (res bool) {
return strings.HasPrefix(pat, "|*.") return strings.HasPrefix(pat, "|*.")
} }
// toRule converts rw to a filter rule.
func toRule(rw *filtering.RewriteItem) (res string) {
if rw == nil {
return ""
}
domain := strings.ToLower(rw.Domain)
dType, exception := rewriteParams(rw)
dTypeKey := dns.TypeToString[dType]
if exception {
return fmt.Sprintf("@@||%s^$dnstype=%s,dnsrewrite", domain, dTypeKey)
}
return fmt.Sprintf("|%s^$dnsrewrite=NOERROR;%s;%s", domain, dTypeKey, rw.Answer)
}
// RewriteParams returns dns request type and exception flag for rw.
func rewriteParams(rw *filtering.RewriteItem) (dType uint16, exception bool) {
switch rw.Answer {
case "AAAA":
return dns.TypeAAAA, true
case "A":
return dns.TypeA, true
default:
// Go on.
}
addr, err := netip.ParseAddr(rw.Answer)
if err != nil {
// TODO(d.kolyshev): Validate rw.Answer as a domain name.
return dns.TypeCNAME, false
}
if addr.Is4() {
dType = dns.TypeA
} else {
dType = dns.TypeAAAA
}
return dType, false
}

View File

@@ -4,6 +4,7 @@ import (
"net" "net"
"testing" "testing"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/urlfilter" "github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/rules" "github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns" "github.com/miekg/dns"
@@ -12,32 +13,32 @@ import (
) )
func TestNewDefaultStorage(t *testing.T) { func TestNewDefaultStorage(t *testing.T) {
items := []*Item{{ items := []*filtering.RewriteItem{{
Domain: "example.com", Domain: "example.com",
Answer: "answer.com", Answer: "answer.com",
}} }}
s, err := NewDefaultStorage(-1, items) s, err := NewDefaultStorage(items)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, s.List(), 1) require.Len(t, s.List(), 1)
} }
func TestDefaultStorage_CRUD(t *testing.T) { func TestDefaultStorage_CRUD(t *testing.T) {
var items []*Item var items []*filtering.RewriteItem
s, err := NewDefaultStorage(-1, items) s, err := NewDefaultStorage(items)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, s.List(), 0) require.Len(t, s.List(), 0)
item := &Item{Domain: "example.com", Answer: "answer.com"} item := &filtering.RewriteItem{Domain: "example.com", Answer: "answer.com"}
err = s.Add(item) err = s.Add(item)
require.NoError(t, err) require.NoError(t, err)
list := s.List() list := s.List()
require.Len(t, list, 1) require.Len(t, list, 1)
require.True(t, item.equal(list[0])) require.True(t, item.Equal(list[0]))
err = s.Remove(item) err = s.Remove(item)
require.NoError(t, err) require.NoError(t, err)
@@ -45,7 +46,7 @@ func TestDefaultStorage_CRUD(t *testing.T) {
} }
func TestDefaultStorage_MatchRequest(t *testing.T) { func TestDefaultStorage_MatchRequest(t *testing.T) {
items := []*Item{{ items := []*filtering.RewriteItem{{
// This one and below are about CNAME, A and AAAA. // This one and below are about CNAME, A and AAAA.
Domain: "somecname", Domain: "somecname",
Answer: "somehost.com", Answer: "somehost.com",
@@ -101,7 +102,7 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
Answer: "sub.issue4016.com", Answer: "sub.issue4016.com",
}} }}
s, err := NewDefaultStorage(-1, items) s, err := NewDefaultStorage(items)
require.NoError(t, err) require.NoError(t, err)
testCases := []struct { testCases := []struct {
@@ -115,14 +116,39 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
wantDNSRewrites: nil, wantDNSRewrites: nil,
dtyp: dns.TypeA, dtyp: dns.TypeA,
}, { }, {
name: "not_filtered_qtype", name: "other_qtype",
host: "www.host.com", host: "www.host.com",
wantDNSRewrites: nil, wantDNSRewrites: []*rules.DNSRewrite{{
dtyp: dns.TypeMX, Value: nil,
NewCNAME: "host.com",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.IP{1, 2, 3, 4}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
RRType: dns.TypeA,
}, {
Value: net.IP{1, 2, 3, 5}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
RRType: dns.TypeA,
}, {
Value: net.ParseIP("1:2:3::4"),
NewCNAME: "",
RCode: dns.RcodeSuccess,
RRType: dns.TypeAAAA,
}},
dtyp: dns.TypeMX,
}, { }, {
name: "rewritten_a", name: "rewritten_a",
host: "www.host.com", host: "www.host.com",
wantDNSRewrites: []*rules.DNSRewrite{{ wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "host.com",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.IP{1, 2, 3, 4}.To16(), Value: net.IP{1, 2, 3, 4}.To16(),
NewCNAME: "", NewCNAME: "",
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
@@ -138,6 +164,11 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
name: "rewritten_aaaa", name: "rewritten_aaaa",
host: "www.host.com", host: "www.host.com",
wantDNSRewrites: []*rules.DNSRewrite{{ wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "host.com",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.ParseIP("1:2:3::4"), Value: net.ParseIP("1:2:3::4"),
NewCNAME: "", NewCNAME: "",
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
@@ -154,21 +185,30 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
RRType: dns.TypeA, RRType: dns.TypeA,
}}, }},
dtyp: dns.TypeA, dtyp: dns.TypeA,
//}, { }, {
// TODO(d.kolyshev): This is about matching in urlfilter. name: "wildcard_override",
// name: "wildcard_override", host: "a.host.com",
// host: "a.host.com", wantDNSRewrites: []*rules.DNSRewrite{{
// wantDNSRewrites: []*rules.DNSRewrite{{ Value: net.IP{1, 2, 3, 4}.To16(),
// Value: net.IP{1, 2, 3, 4}.To16(), NewCNAME: "",
// NewCNAME: "", RCode: dns.RcodeSuccess,
// RCode: dns.RcodeSuccess, RRType: dns.TypeA,
// RRType: dns.TypeA, }, {
// }}, Value: net.IP{1, 2, 3, 5}.To16(),
// dtyp: dns.TypeA, NewCNAME: "",
RCode: dns.RcodeSuccess,
RRType: dns.TypeA,
}},
dtyp: dns.TypeA,
}, { }, {
name: "wildcard_cname_interaction", name: "wildcard_cname_interaction",
host: "www.host2.com", host: "www.host2.com",
wantDNSRewrites: []*rules.DNSRewrite{{ wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "host.com",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.IP{1, 2, 3, 4}.To16(), Value: net.IP{1, 2, 3, 4}.To16(),
NewCNAME: "", NewCNAME: "",
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
@@ -184,6 +224,11 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
name: "two_cnames", name: "two_cnames",
host: "b.host.com", host: "b.host.com",
wantDNSRewrites: []*rules.DNSRewrite{{ wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "somehost.com",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.IP{0, 0, 0, 0}.To16(), Value: net.IP{0, 0, 0, 0}.To16(),
NewCNAME: "", NewCNAME: "",
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
@@ -194,6 +239,11 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
name: "two_cnames_and_wildcard", name: "two_cnames_and_wildcard",
host: "b.host3.com", host: "b.host3.com",
wantDNSRewrites: []*rules.DNSRewrite{{ wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "x.host.com",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.IP{1, 2, 3, 5}.To16(), Value: net.IP{1, 2, 3, 5}.To16(),
NewCNAME: "", NewCNAME: "",
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
@@ -221,10 +271,15 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
}}, }},
dtyp: dns.TypeA, dtyp: dns.TypeA,
}, { }, {
name: "issue4008", name: "issue4008",
host: "somehost.com", host: "somehost.com",
wantDNSRewrites: nil, wantDNSRewrites: []*rules.DNSRewrite{{
dtyp: dns.TypeHTTPS, Value: net.IP{0, 0, 0, 0}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
RRType: dns.TypeA,
}},
dtyp: dns.TypeHTTPS,
}, { }, {
name: "issue4016", name: "issue4016",
host: "www.issue4016.com", host: "www.issue4016.com",
@@ -256,7 +311,7 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
func TestDefaultStorage_MatchRequest_Levels(t *testing.T) { func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
// Exact host, wildcard L2, wildcard L3. // Exact host, wildcard L2, wildcard L3.
items := []*Item{{ items := []*filtering.RewriteItem{{
Domain: "host.com", Domain: "host.com",
Answer: "1.1.1.1", Answer: "1.1.1.1",
}, { }, {
@@ -267,7 +322,7 @@ func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
Answer: "3.3.3.3", Answer: "3.3.3.3",
}} }}
s, err := NewDefaultStorage(-1, items) s, err := NewDefaultStorage(items)
require.NoError(t, err) require.NoError(t, err)
testCases := []struct { testCases := []struct {
@@ -295,17 +350,21 @@ func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
RRType: dns.TypeA, RRType: dns.TypeA,
}}, }},
dtyp: dns.TypeA, dtyp: dns.TypeA,
//}, { }, {
// TODO(d.kolyshev): This is about matching in urlfilter. name: "l3_match",
// name: "l3_match", host: "my.sub.host.com",
// host: "my.sub.host.com", wantDNSRewrites: []*rules.DNSRewrite{{
// wantDNSRewrites: []*rules.DNSRewrite{{ Value: net.IP{3, 3, 3, 3}.To16(),
// Value: net.IP{3, 3, 3, 3}.To16(), NewCNAME: "",
// NewCNAME: "", RCode: dns.RcodeSuccess,
// RCode: dns.RcodeSuccess, RRType: dns.TypeA,
// RRType: dns.TypeA, }, {
// }}, Value: net.IP{2, 2, 2, 2}.To16(),
// dtyp: dns.TypeA, NewCNAME: "",
RCode: dns.RcodeSuccess,
RRType: dns.TypeA,
}},
dtyp: dns.TypeA,
}} }}
for _, tc := range testCases { for _, tc := range testCases {
@@ -322,7 +381,7 @@ func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) { func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
// Wildcard and exception for a sub-domain. // Wildcard and exception for a sub-domain.
items := []*Item{{ items := []*filtering.RewriteItem{{
Domain: "*.host.com", Domain: "*.host.com",
Answer: "2.2.2.2", Answer: "2.2.2.2",
}, { }, {
@@ -330,10 +389,10 @@ func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
Answer: "sub.host.com", Answer: "sub.host.com",
}, { }, {
Domain: "*.sub.host.com", Domain: "*.sub.host.com",
Answer: "*.sub.host.com", Answer: "sub.host.com",
}} }}
s, err := NewDefaultStorage(-1, items) s, err := NewDefaultStorage(items)
require.NoError(t, err) require.NoError(t, err)
testCases := []struct { testCases := []struct {
@@ -356,12 +415,79 @@ func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
host: "sub.host.com", host: "sub.host.com",
wantDNSRewrites: nil, wantDNSRewrites: nil,
dtyp: dns.TypeA, dtyp: dns.TypeA,
//}, { }, {
// TODO(d.kolyshev): This is about matching in urlfilter. name: "exception_wildcard",
// name: "exception_wildcard", host: "my.sub.host.com",
// host: "my.sub.host.com", wantDNSRewrites: nil,
// wantDNSRewrites: nil, dtyp: dns.TypeA,
// dtyp: dns.TypeA, }}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
dnsRewrites := s.MatchRequest(&urlfilter.DNSRequest{
Hostname: tc.host,
DNSType: tc.dtyp,
})
assert.Equal(t, tc.wantDNSRewrites, dnsRewrites)
})
}
}
func TestDefaultStorage_MatchRequest_CNAMEs(t *testing.T) {
// Two cname rules for one subdomain
items := []*filtering.RewriteItem{{
Domain: "cname.org",
Answer: "1.1.1.1",
}, {
Domain: "sub_cname.org",
Answer: "2.2.2.2",
}, {
Domain: "*.host.com",
Answer: "cname.org",
}, {
Domain: "*.sub.host.com",
Answer: "sub_cname.org",
}}
s, err := NewDefaultStorage(items)
require.NoError(t, err)
testCases := []struct {
name string
host string
wantDNSRewrites []*rules.DNSRewrite
dtyp uint16
}{{
name: "match_my_domain",
host: "my.host.com",
wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "cname.org",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.IP{1, 1, 1, 1}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
RRType: dns.TypeA,
}},
dtyp: dns.TypeA,
}, {
name: "match_sub_my_domain",
host: "my.sub.host.com",
wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "cname.org",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.IP{1, 1, 1, 1}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
RRType: dns.TypeA,
}},
dtyp: dns.TypeA,
}} }}
for _, tc := range testCases { for _, tc := range testCases {
@@ -378,7 +504,7 @@ func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
func TestDefaultStorage_MatchRequest_ExceptionIP(t *testing.T) { func TestDefaultStorage_MatchRequest_ExceptionIP(t *testing.T) {
// Exception for AAAA record. // Exception for AAAA record.
items := []*Item{{ items := []*filtering.RewriteItem{{
Domain: "host.com", Domain: "host.com",
Answer: "1.2.3.4", Answer: "1.2.3.4",
}, { }, {
@@ -395,7 +521,7 @@ func TestDefaultStorage_MatchRequest_ExceptionIP(t *testing.T) {
Answer: "A", Answer: "A",
}} }}
s, err := NewDefaultStorage(-1, items) s, err := NewDefaultStorage(items)
require.NoError(t, err) require.NoError(t, err)
testCases := []struct { testCases := []struct {
@@ -456,3 +582,66 @@ func TestDefaultStorage_MatchRequest_ExceptionIP(t *testing.T) {
}) })
} }
} }
func TestToRule(t *testing.T) {
const testDomain = "example.org"
testCases := []struct {
name string
item *filtering.RewriteItem
want string
}{{
name: "nil",
item: nil,
want: "",
}, {
name: "a_rule",
item: &filtering.RewriteItem{
Domain: testDomain,
Answer: "1.1.1.1",
},
want: "|example.org^$dnsrewrite=NOERROR;A;1.1.1.1",
}, {
name: "aaaa_rule",
item: &filtering.RewriteItem{
Domain: testDomain,
Answer: "1:2:3::4",
},
want: "|example.org^$dnsrewrite=NOERROR;AAAA;1:2:3::4",
}, {
name: "cname_rule",
item: &filtering.RewriteItem{
Domain: testDomain,
Answer: "other.org",
},
want: "|example.org^$dnsrewrite=NOERROR;CNAME;other.org",
}, {
name: "wildcard_rule",
item: &filtering.RewriteItem{
Domain: "*.example.org",
Answer: "other.org",
},
want: "|*.example.org^$dnsrewrite=NOERROR;CNAME;other.org",
}, {
name: "aaaa_exception",
item: &filtering.RewriteItem{
Domain: testDomain,
Answer: "A",
},
want: "@@||example.org^$dnstype=A,dnsrewrite",
}, {
name: "aaaa_exception",
item: &filtering.RewriteItem{
Domain: testDomain,
Answer: "AAAA",
},
want: "@@||example.org^$dnstype=AAAA,dnsrewrite",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res := toRule(tc.item)
assert.Equal(t, tc.want, res)
})
}
}

View File

@@ -0,0 +1,61 @@
package filtering
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestItem_equal(t *testing.T) {
const (
testDomain = "example.org"
testAnswer = "1.1.1.1"
)
testItem := &RewriteItem{
Domain: testDomain,
Answer: testAnswer,
}
testCases := []struct {
name string
left *RewriteItem
right *RewriteItem
want bool
}{{
name: "nil_left",
left: nil,
right: testItem,
want: false,
}, {
name: "nil_right",
left: testItem,
right: nil,
want: false,
}, {
name: "nils",
left: nil,
right: nil,
want: true,
}, {
name: "equal",
left: testItem,
right: testItem,
want: true,
}, {
name: "distinct",
left: testItem,
right: &RewriteItem{
Domain: "other",
Answer: "other",
},
want: false,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res := tc.left.Equal(tc.right)
assert.Equal(t, tc.want, res)
})
}
}

View File

@@ -8,85 +8,57 @@ import (
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
// TODO(d.kolyshev): Use [rewrite.Item] instead. // handleRewriteList is the handler for the GET /control/rewrite/list HTTP API.
type rewriteEntryJSON struct {
Domain string `json:"domain"`
Answer string `json:"answer"`
}
func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) { func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
arr := []*rewriteEntryJSON{} _ = aghhttp.WriteJSONResponse(w, r, d.rewriteStorage.List())
d.confLock.Lock()
for _, ent := range d.Config.Rewrites {
jsent := rewriteEntryJSON{
Domain: ent.Domain,
Answer: ent.Answer,
}
arr = append(arr, &jsent)
}
d.confLock.Unlock()
_ = aghhttp.WriteJSONResponse(w, r, arr)
} }
// handleRewriteAdd is the handler for the POST /control/rewrite/add HTTP API.
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) { func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
rwJSON := rewriteEntryJSON{} rw := &RewriteItem{}
err := json.NewDecoder(r.Body).Decode(&rwJSON) err := json.NewDecoder(r.Body).Decode(rw)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err) aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return return
} }
rw := &LegacyRewrite{ err = d.rewriteStorage.Add(rw)
Domain: rwJSON.Domain,
Answer: rwJSON.Answer,
}
err = rw.normalize()
if err != nil { if err != nil {
// Shouldn't happen currently, since normalize only returns a non-nil aghhttp.Error(r, w, http.StatusBadRequest, "add rewrite: %s", err)
// error when a rewrite is nil, but be change-proof.
aghhttp.Error(r, w, http.StatusBadRequest, "normalizing: %s", err)
return return
} }
log.Debug("rewrite: added element: %s -> %s", rw.Domain, rw.Answer)
d.confLock.Lock() d.confLock.Lock()
d.Config.Rewrites = append(d.Config.Rewrites, rw) d.Config.Rewrites = d.rewriteStorage.List()
d.confLock.Unlock() d.confLock.Unlock()
log.Debug("rewrite: added element: %s -> %s [%d]", rw.Domain, rw.Answer, len(d.Config.Rewrites))
d.Config.ConfigModified() d.Config.ConfigModified()
} }
// handleRewriteDelete is the handler for the POST /control/rewrite/delete HTTP
// API.
func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) { func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) {
jsent := rewriteEntryJSON{} entDel := RewriteItem{}
err := json.NewDecoder(r.Body).Decode(&jsent) err := json.NewDecoder(r.Body).Decode(&entDel)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err) aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return return
} }
entDel := &LegacyRewrite{ err = d.rewriteStorage.Remove(&entDel)
Domain: jsent.Domain, if err != nil {
Answer: jsent.Answer, aghhttp.Error(r, w, http.StatusBadRequest, "remove rewrite: %s", err)
return
} }
arr := []*LegacyRewrite{}
d.confLock.Lock() d.confLock.Lock()
for _, ent := range d.Config.Rewrites { d.Config.Rewrites = d.rewriteStorage.List()
if ent.equal(entDel) {
log.Debug("rewrite: removed element: %s -> %s", ent.Domain, ent.Answer)
continue
}
arr = append(arr, ent)
}
d.Config.Rewrites = arr
d.confLock.Unlock() d.confLock.Unlock()
d.Config.ConfigModified() d.Config.ConfigModified()

View File

@@ -1,219 +0,0 @@
// DNS Rewrites
package filtering
import (
"fmt"
"net"
"sort"
"strings"
"github.com/AdguardTeam/golibs/errors"
"github.com/miekg/dns"
"golang.org/x/exp/slices"
)
// LegacyRewrite is a single legacy DNS rewrite record.
//
// Instances of *LegacyRewrite must never be nil.
type LegacyRewrite struct {
// Domain is the domain pattern for which this rewrite should work.
Domain string `yaml:"domain"`
// Answer is the IP address, canonical name, or one of the special
// values: "A" or "AAAA".
Answer string `yaml:"answer"`
// IP is the IP address that should be used in the response if Type is
// dns.TypeA or dns.TypeAAAA.
IP net.IP `yaml:"-"`
// Type is the DNS record type: A, AAAA, or CNAME.
Type uint16 `yaml:"-"`
}
// clone returns a deep clone of rw.
func (rw *LegacyRewrite) clone() (cloneRW *LegacyRewrite) {
return &LegacyRewrite{
Domain: rw.Domain,
Answer: rw.Answer,
IP: slices.Clone(rw.IP),
Type: rw.Type,
}
}
// equal returns true if the rw is equal to the other.
func (rw *LegacyRewrite) equal(other *LegacyRewrite) (ok bool) {
return rw.Domain == other.Domain && rw.Answer == other.Answer
}
// matchesQType returns true if the entry matches the question type qt.
func (rw *LegacyRewrite) matchesQType(qt uint16) (ok bool) {
// Add CNAMEs, since they match for all types requests.
if rw.Type == dns.TypeCNAME {
return true
}
// Reject types other than A and AAAA.
if qt != dns.TypeA && qt != dns.TypeAAAA {
return false
}
// If the types match or the entry is set to allow only the other type,
// include them.
return rw.Type == qt || rw.IP == nil
}
// normalize makes sure that the a new or decoded entry is normalized with
// regards to domain name case, IP length, and so on.
//
// If rw is nil, it returns an errors.
func (rw *LegacyRewrite) normalize() (err error) {
if rw == nil {
return errors.Error("nil rewrite entry")
}
// TODO(a.garipov): Write a case-agnostic version of strings.HasSuffix and
// use it in matchDomainWildcard instead of using strings.ToLower
// everywhere.
rw.Domain = strings.ToLower(rw.Domain)
switch rw.Answer {
case "AAAA":
rw.IP = nil
rw.Type = dns.TypeAAAA
return nil
case "A":
rw.IP = nil
rw.Type = dns.TypeA
return nil
default:
// Go on.
}
ip := net.ParseIP(rw.Answer)
if ip == nil {
rw.Type = dns.TypeCNAME
return nil
}
ip4 := ip.To4()
if ip4 != nil {
rw.IP = ip4
rw.Type = dns.TypeA
} else {
rw.IP = ip
rw.Type = dns.TypeAAAA
}
return nil
}
// isWildcard returns true if pat is a wildcard domain pattern.
func isWildcard(pat string) bool {
return len(pat) > 1 && pat[0] == '*' && pat[1] == '.'
}
// matchDomainWildcard returns true if host matches the wildcard pattern.
func matchDomainWildcard(host, wildcard string) (ok bool) {
return isWildcard(wildcard) && strings.HasSuffix(host, wildcard[1:])
}
// rewritesSorted is a slice of legacy rewrites for sorting.
//
// The sorting priority:
//
// 1. A and AAAA > CNAME
// 2. wildcard > exact
// 3. lower level wildcard > higher level wildcard
//
// TODO(a.garipov): Replace with slices.Sort.
type rewritesSorted []*LegacyRewrite
// Len implements the sort.Interface interface for rewritesSorted.
func (a rewritesSorted) Len() (l int) { return len(a) }
// Swap implements the sort.Interface interface for rewritesSorted.
func (a rewritesSorted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
// Less implements the sort.Interface interface for rewritesSorted.
func (a rewritesSorted) Less(i, j int) (less bool) {
ith, jth := a[i], a[j]
if ith.Type == dns.TypeCNAME && jth.Type != dns.TypeCNAME {
return true
} else if ith.Type != dns.TypeCNAME && jth.Type == dns.TypeCNAME {
return false
}
if iw, jw := isWildcard(ith.Domain), isWildcard(jth.Domain); iw != jw {
return jw
}
// Both are either wildcards or not.
return len(ith.Domain) > len(jth.Domain)
}
// prepareRewrites normalizes and validates all legacy DNS rewrites.
func (d *DNSFilter) prepareRewrites() (err error) {
for i, r := range d.Rewrites {
err = r.normalize()
if err != nil {
return fmt.Errorf("at index %d: %w", i, err)
}
}
return nil
}
// findRewrites returns the list of matched rewrite entries. If rewrites are
// empty, but matched is true, the domain is found among the rewrite rules but
// not for this question type.
//
// The result priority is: CNAME, then A and AAAA; exact, then wildcard. If the
// host is matched exactly, wildcard entries aren't returned. If the host
// matched by wildcards, return the most specific for the question type.
func findRewrites(
entries []*LegacyRewrite,
host string,
qtype uint16,
) (rewrites []*LegacyRewrite, matched bool) {
for _, e := range entries {
if e.Domain != host && !matchDomainWildcard(host, e.Domain) {
continue
}
matched = true
if e.matchesQType(qtype) {
rewrites = append(rewrites, e)
}
}
if len(rewrites) == 0 {
return nil, matched
}
sort.Sort(rewritesSorted(rewrites))
for i, r := range rewrites {
if isWildcard(r.Domain) {
// Don't use rewrites[:0], because we need to return at least one
// item here.
rewrites = rewrites[:max(1, i)]
break
}
}
return rewrites, matched
}
func max(a, b int) int {
if a > b {
return a
}
return b
}

View File

@@ -1,371 +0,0 @@
package filtering
import (
"net"
"testing"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TODO(e.burkov): All the tests in this file may and should me merged together.
func TestRewrites(t *testing.T) {
d, _ := newForTest(t, nil, nil)
t.Cleanup(d.Close)
d.Rewrites = []*LegacyRewrite{{
// This one and below are about CNAME, A and AAAA.
Domain: "somecname",
Answer: "somehost.com",
}, {
Domain: "somehost.com",
Answer: "0.0.0.0",
}, {
Domain: "host.com",
Answer: "1.2.3.4",
}, {
Domain: "host.com",
Answer: "1.2.3.5",
}, {
Domain: "host.com",
Answer: "1:2:3::4",
}, {
Domain: "www.host.com",
Answer: "host.com",
}, {
// This one is a wildcard.
Domain: "*.host.com",
Answer: "1.2.3.5",
}, {
// This one and below are about wildcard overriding.
Domain: "a.host.com",
Answer: "1.2.3.4",
}, {
// This one is about CNAME and wildcard interacting.
Domain: "*.host2.com",
Answer: "host.com",
}, {
// This one and below are about 2 level CNAME.
Domain: "b.host.com",
Answer: "somecname",
}, {
// This one and below are about 2 level CNAME and wildcard.
Domain: "b.host3.com",
Answer: "a.host3.com",
}, {
Domain: "a.host3.com",
Answer: "x.host.com",
}, {
Domain: "*.hostboth.com",
Answer: "1.2.3.6",
}, {
Domain: "*.hostboth.com",
Answer: "1234::5678",
}, {
Domain: "BIGHOST.COM",
Answer: "1.2.3.7",
}, {
Domain: "*.issue4016.com",
Answer: "sub.issue4016.com",
}}
require.NoError(t, d.prepareRewrites())
testCases := []struct {
name string
host string
wantCName string
wantIPs []net.IP
wantReason Reason
dtyp uint16
}{{
name: "not_filtered_not_found",
host: "hoost.com",
wantCName: "",
wantIPs: nil,
wantReason: NotFilteredNotFound,
dtyp: dns.TypeA,
}, {
name: "rewritten_a",
host: "www.host.com",
wantCName: "host.com",
wantIPs: []net.IP{{1, 2, 3, 4}, {1, 2, 3, 5}},
wantReason: Rewritten,
dtyp: dns.TypeA,
}, {
name: "rewritten_aaaa",
host: "www.host.com",
wantCName: "host.com",
wantIPs: []net.IP{net.ParseIP("1:2:3::4")},
wantReason: Rewritten,
dtyp: dns.TypeAAAA,
}, {
name: "wildcard_match",
host: "abc.host.com",
wantCName: "",
wantIPs: []net.IP{{1, 2, 3, 5}},
wantReason: Rewritten,
dtyp: dns.TypeA,
}, {
name: "wildcard_override",
host: "a.host.com",
wantCName: "",
wantIPs: []net.IP{{1, 2, 3, 4}},
wantReason: Rewritten,
dtyp: dns.TypeA,
}, {
name: "wildcard_cname_interaction",
host: "www.host2.com",
wantCName: "host.com",
wantIPs: []net.IP{{1, 2, 3, 4}, {1, 2, 3, 5}},
wantReason: Rewritten,
dtyp: dns.TypeA,
}, {
name: "two_cnames",
host: "b.host.com",
wantCName: "somehost.com",
wantIPs: []net.IP{{0, 0, 0, 0}},
wantReason: Rewritten,
dtyp: dns.TypeA,
}, {
name: "two_cnames_and_wildcard",
host: "b.host3.com",
wantCName: "x.host.com",
wantIPs: []net.IP{{1, 2, 3, 5}},
wantReason: Rewritten,
dtyp: dns.TypeA,
}, {
name: "issue3343",
host: "www.hostboth.com",
wantCName: "",
wantIPs: []net.IP{net.ParseIP("1234::5678")},
wantReason: Rewritten,
dtyp: dns.TypeAAAA,
}, {
name: "issue3351",
host: "bighost.com",
wantCName: "",
wantIPs: []net.IP{{1, 2, 3, 7}},
wantReason: Rewritten,
dtyp: dns.TypeA,
}, {
name: "issue4008",
host: "somehost.com",
wantCName: "",
wantIPs: nil,
wantReason: Rewritten,
dtyp: dns.TypeHTTPS,
}, {
name: "issue4016",
host: "www.issue4016.com",
wantCName: "sub.issue4016.com",
wantIPs: nil,
wantReason: Rewritten,
dtyp: dns.TypeA,
}, {
name: "issue4016_self",
host: "sub.issue4016.com",
wantCName: "",
wantIPs: nil,
wantReason: NotFilteredNotFound,
dtyp: dns.TypeA,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := d.processRewrites(tc.host, tc.dtyp)
require.Equalf(t, tc.wantReason, r.Reason, "got %s", r.Reason)
if tc.wantCName != "" {
assert.Equal(t, tc.wantCName, r.CanonName)
}
assert.Equal(t, tc.wantIPs, r.IPList)
})
}
}
func TestRewritesLevels(t *testing.T) {
d, _ := newForTest(t, nil, nil)
t.Cleanup(d.Close)
// Exact host, wildcard L2, wildcard L3.
d.Rewrites = []*LegacyRewrite{{
Domain: "host.com",
Answer: "1.1.1.1",
Type: dns.TypeA,
}, {
Domain: "*.host.com",
Answer: "2.2.2.2",
Type: dns.TypeA,
}, {
Domain: "*.sub.host.com",
Answer: "3.3.3.3",
Type: dns.TypeA,
}}
require.NoError(t, d.prepareRewrites())
testCases := []struct {
name string
host string
want net.IP
}{{
name: "exact_match",
host: "host.com",
want: net.IP{1, 1, 1, 1},
}, {
name: "l2_match",
host: "sub.host.com",
want: net.IP{2, 2, 2, 2},
}, {
name: "l3_match",
host: "my.sub.host.com",
want: net.IP{3, 3, 3, 3},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := d.processRewrites(tc.host, dns.TypeA)
assert.Equal(t, Rewritten, r.Reason)
require.Len(t, r.IPList, 1)
})
}
}
func TestRewritesExceptionCNAME(t *testing.T) {
d, _ := newForTest(t, nil, nil)
t.Cleanup(d.Close)
// Wildcard and exception for a sub-domain.
d.Rewrites = []*LegacyRewrite{{
Domain: "*.host.com",
Answer: "2.2.2.2",
}, {
Domain: "sub.host.com",
Answer: "sub.host.com",
}, {
Domain: "*.sub.host.com",
Answer: "*.sub.host.com",
}}
require.NoError(t, d.prepareRewrites())
testCases := []struct {
name string
host string
want net.IP
}{{
name: "match_subdomain",
host: "my.host.com",
want: net.IP{2, 2, 2, 2},
}, {
name: "exception_cname",
host: "sub.host.com",
want: nil,
}, {
name: "exception_wildcard",
host: "my.sub.host.com",
want: nil,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := d.processRewrites(tc.host, dns.TypeA)
if tc.want == nil {
assert.Equal(t, NotFilteredNotFound, r.Reason, "got %s", r.Reason)
return
}
assert.Equal(t, Rewritten, r.Reason)
require.Len(t, r.IPList, 1)
assert.True(t, tc.want.Equal(r.IPList[0]))
})
}
}
func TestRewritesExceptionIP(t *testing.T) {
d, _ := newForTest(t, nil, nil)
t.Cleanup(d.Close)
// Exception for AAAA record.
d.Rewrites = []*LegacyRewrite{{
Domain: "host.com",
Answer: "1.2.3.4",
Type: dns.TypeA,
}, {
Domain: "host.com",
Answer: "AAAA",
Type: dns.TypeAAAA,
}, {
Domain: "host2.com",
Answer: "::1",
Type: dns.TypeAAAA,
}, {
Domain: "host2.com",
Answer: "A",
Type: dns.TypeA,
}, {
Domain: "host3.com",
Answer: "A",
Type: dns.TypeA,
}}
require.NoError(t, d.prepareRewrites())
testCases := []struct {
name string
host string
want []net.IP
dtyp uint16
}{{
name: "match_A",
host: "host.com",
want: []net.IP{{1, 2, 3, 4}},
dtyp: dns.TypeA,
}, {
name: "exception_AAAA_host.com",
host: "host.com",
want: nil,
dtyp: dns.TypeAAAA,
}, {
name: "exception_A_host2.com",
host: "host2.com",
want: nil,
dtyp: dns.TypeA,
}, {
name: "match_AAAA_host2.com",
host: "host2.com",
want: []net.IP{net.ParseIP("::1")},
dtyp: dns.TypeAAAA,
}, {
name: "exception_A_host3.com",
host: "host3.com",
want: nil,
dtyp: dns.TypeA,
}, {
name: "match_AAAA_host3.com",
host: "host3.com",
want: []net.IP{},
dtyp: dns.TypeAAAA,
}}
for _, tc := range testCases {
t.Run(tc.name+"_"+tc.host, func(t *testing.T) {
r := d.processRewrites(tc.host, tc.dtyp)
if tc.want == nil {
assert.Equal(t, NotFilteredNotFound, r.Reason)
return
}
assert.Equalf(t, Rewritten, r.Reason, "got %s", r.Reason)
require.Len(t, r.IPList, len(tc.want))
for _, ip := range tc.want {
assert.True(t, ip.Equal(r.IPList[0]))
}
})
}
}

View File

@@ -253,7 +253,6 @@ var blockedServices = []blockedService{{
Rules: []string{ Rules: []string{
"||aus.social^", "||aus.social^",
"||awscommunity.social^", "||awscommunity.social^",
"||dju.social^",
"||dresden.network^", "||dresden.network^",
"||fedibird.com^", "||fedibird.com^",
"||fosstodon.org^", "||fosstodon.org^",
@@ -261,11 +260,11 @@ var blockedServices = []blockedService{{
"||h4.io^", "||h4.io^",
"||hachyderm.io^", "||hachyderm.io^",
"||hessen.social^", "||hessen.social^",
"||hispagatos.space^",
"||home.social^", "||home.social^",
"||hostux.social^", "||hostux.social^",
"||ieji.de^", "||ieji.de^",
"||indieweb.social^", "||indieweb.social^",
"||infosec.exchange^",
"||ioc.exchange^", "||ioc.exchange^",
"||kolektiva.social^", "||kolektiva.social^",
"||livellosegreto.it^", "||livellosegreto.it^",
@@ -287,9 +286,11 @@ var blockedServices = []blockedService{{
"||mastodon.nu^", "||mastodon.nu^",
"||mastodon.nz^", "||mastodon.nz^",
"||mastodon.online^", "||mastodon.online^",
"||mastodon.online^",
"||mastodon.scot^", "||mastodon.scot^",
"||mastodon.sdf.org^", "||mastodon.sdf.org^",
"||mastodon.social^", "||mastodon.social^",
"||mastodon.social^",
"||mastodon.top^", "||mastodon.top^",
"||mastodon.uno^", "||mastodon.uno^",
"||mastodon.world^", "||mastodon.world^",
@@ -309,7 +310,6 @@ var blockedServices = []blockedService{{
"||mstdn.social^", "||mstdn.social^",
"||muenchen.social^", "||muenchen.social^",
"||muenster.im^", "||muenster.im^",
"||nerdculture.de^",
"||newsie.social^", "||newsie.social^",
"||noc.social^", "||noc.social^",
"||norden.social^", "||norden.social^",
@@ -335,21 +335,21 @@ var blockedServices = []blockedService{{
"||social.vivaldi.net^", "||social.vivaldi.net^",
"||sself.co^", "||sself.co^",
"||sueden.social^", "||sueden.social^",
"||tech.lgbt^",
"||techhub.social^", "||techhub.social^",
"||theblower.au^", "||theblower.au^",
"||tkz.one^", "||tkz.one^",
"||todon.eu^",
"||toot.aquilenet.fr^", "||toot.aquilenet.fr^",
"||toot.community^", "||toot.community^",
"||toot.funami.tech^", "||toot.funami.tech^",
"||toot.wales^", "||toot.wales^",
"||troet.cafe^", "||troet.cafe^",
"||uiuxdev.social^", "||twingyeo.kr^",
"||union.place^", "||union.place^",
"||universeodon.com^", "||universeodon.com^",
"||urbanists.social^", "||urbanists.social^",
"||vocalodon.net^",
"||wxw.moe^", "||wxw.moe^",
"||xarxa.cloud^",
}, },
}, { }, {
ID: "minecraft", ID: "minecraft",
@@ -540,6 +540,7 @@ var blockedServices = []blockedService{{
Name: "Twitter", Name: "Twitter",
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M22.398 5.55a8.583 8.583 0 0 1-2.449.673 4.252 4.252 0 0 0 1.875-2.364 8.66 8.66 0 0 1-2.71 1.04A4.251 4.251 0 0 0 16 3.546a4.27 4.27 0 0 0-4.266 4.27c0 .335.036.66.11.972a12.126 12.126 0 0 1-8.797-4.46 4.259 4.259 0 0 0-.578 2.148c0 1.48.754 2.785 1.898 3.55a4.273 4.273 0 0 1-1.933-.535v.055a4.27 4.27 0 0 0 3.425 4.183c-.359.098-.734.149-1.125.149-.273 0-.543-.027-.804-.074a4.276 4.276 0 0 0 3.988 2.965 8.562 8.562 0 0 1-5.3 1.824 8.82 8.82 0 0 1-1.02-.059 12.088 12.088 0 0 0 6.543 1.918c7.851 0 12.14-6.504 12.14-12.144 0-.184-.004-.368-.011-.551a8.599 8.599 0 0 0 2.128-2.207zm0 0\" /></svg>"), IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M22.398 5.55a8.583 8.583 0 0 1-2.449.673 4.252 4.252 0 0 0 1.875-2.364 8.66 8.66 0 0 1-2.71 1.04A4.251 4.251 0 0 0 16 3.546a4.27 4.27 0 0 0-4.266 4.27c0 .335.036.66.11.972a12.126 12.126 0 0 1-8.797-4.46 4.259 4.259 0 0 0-.578 2.148c0 1.48.754 2.785 1.898 3.55a4.273 4.273 0 0 1-1.933-.535v.055a4.27 4.27 0 0 0 3.425 4.183c-.359.098-.734.149-1.125.149-.273 0-.543-.027-.804-.074a4.276 4.276 0 0 0 3.988 2.965 8.562 8.562 0 0 1-5.3 1.824 8.82 8.82 0 0 1-1.02-.059 12.088 12.088 0 0 0 6.543 1.918c7.851 0 12.14-6.504 12.14-12.144 0-.184-.004-.368-.011-.551a8.599 8.599 0 0 0 2.128-2.207zm0 0\" /></svg>"),
Rules: []string{ Rules: []string{
"||pscp.tv^",
"||t.co^", "||t.co^",
"||twimg.com^", "||twimg.com^",
"||twitter.com^", "||twitter.com^",

View File

@@ -278,15 +278,20 @@ var config = &configuration{
PortDNSOverTLS: defaultPortTLS, // needs to be passed through to dnsproxy PortDNSOverTLS: defaultPortTLS, // needs to be passed through to dnsproxy
PortDNSOverQUIC: defaultPortQUIC, PortDNSOverQUIC: defaultPortQUIC,
}, },
// NOTE: Keep these parameters in sync with the one put into
// client/src/helpers/filters/filters.js by scripts/vetted-filters.
//
// TODO(a.garipov): Think of a way to make scripts/vetted-filters update
// these as well if necessary.
Filters: []filtering.FilterYAML{{ Filters: []filtering.FilterYAML{{
Filter: filtering.Filter{ID: 1}, Filter: filtering.Filter{ID: 1},
Enabled: true, Enabled: true,
URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt",
Name: "AdGuard DNS filter", Name: "AdGuard DNS filter",
}, { }, {
Filter: filtering.Filter{ID: 2}, Filter: filtering.Filter{ID: 2},
Enabled: false, Enabled: false,
URL: "https://adaway.org/hosts.txt", URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt",
Name: "AdAway Default Blocklist", Name: "AdAway Default Blocklist",
}}, }},
DHCP: &dhcpd.ServerConfig{ DHCP: &dhcpd.ServerConfig{

View File

@@ -123,7 +123,7 @@ func handleUpdate(w http.ResponseWriter, r *http.Request) {
return return
} }
err = Context.updater.Update() err = Context.updater.Update(false)
if err != nil { if err != nil {
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err) aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)

View File

@@ -9,9 +9,12 @@ import (
"path/filepath" "path/filepath"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rewrite"
"github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/stats" "github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
@@ -39,17 +42,13 @@ func onConfigModified() {
} }
} }
// initDNSServer creates an instance of the dnsforward.Server // initDNS updates all the fields of the [Context] needed to initialize the DNS
// Please note that we must do it even if we don't start it // server and initializes it at last. It also must not be called unless
// so that we had access to the query log and the stats // [config] and [Context] are initialized.
func initDNSServer() (err error) { func initDNS() (err error) {
baseDir := Context.getDataDir() baseDir := Context.getDataDir()
var anonFunc aghnet.IPMutFunc anonymizer := config.anonymizer()
if config.DNS.AnonymizeClientIP {
anonFunc = querylog.AnonymizeIP
}
anonymizer := aghnet.NewIPMut(anonFunc)
statsConf := stats.Config{ statsConf := stats.Config{
Filename: filepath.Join(baseDir, "stats.db"), Filename: filepath.Join(baseDir, "stats.db"),
@@ -76,40 +75,57 @@ func initDNSServer() (err error) {
} }
Context.queryLog = querylog.New(conf) Context.queryLog = querylog.New(conf)
Context.filters, err = filtering.New(config.DNS.DnsfilterConf, nil) rewriteStorage, err := rewrite.NewDefaultStorage(config.DNS.DnsfilterConf.Rewrites)
if err != nil {
return fmt.Errorf("rewrites: init: %w", err)
}
Context.filters, err = filtering.New(config.DNS.DnsfilterConf, nil, rewriteStorage)
if err != nil { if err != nil {
// Don't wrap the error, since it's informative enough as is. // Don't wrap the error, since it's informative enough as is.
return err return err
} }
var privateNets netutil.SubnetSet tlsConf := &tlsConfigSettings{}
switch len(config.DNS.PrivateNets) { Context.tls.WriteDiskConfig(tlsConf)
case 0:
// Use an optimized locally-served matcher.
privateNets = netutil.SubnetSetFunc(netutil.IsLocallyServed)
case 1:
privateNets, err = netutil.ParseSubnet(config.DNS.PrivateNets[0])
if err != nil {
return fmt.Errorf("preparing the set of private subnets: %w", err)
}
default:
var nets []*net.IPNet
nets, err = netutil.ParseSubnets(config.DNS.PrivateNets...)
if err != nil {
return fmt.Errorf("preparing the set of private subnets: %w", err)
}
privateNets = netutil.SliceSubnetSet(nets) return initDNSServer(
Context.filters,
Context.stats,
Context.queryLog,
Context.dhcpServer,
anonymizer,
httpRegister,
tlsConf,
)
}
// initDNSServer initializes the [context.dnsServer]. To only use the internal
// proxy, none of the arguments are required, but tlsConf still must not be nil,
// in other cases all the arguments also must not be nil. It also must not be
// called unless [config] and [Context] are initialized.
func initDNSServer(
filters *filtering.DNSFilter,
sts stats.Interface,
qlog querylog.QueryLog,
dhcpSrv dhcpd.Interface,
anonymizer *aghnet.IPMut,
httpReg aghhttp.RegisterFunc,
tlsConf *tlsConfigSettings,
) (err error) {
privateNets, err := parseSubnetSet(config.DNS.PrivateNets)
if err != nil {
return fmt.Errorf("preparing set of private subnets: %w", err)
} }
p := dnsforward.DNSCreateParams{ p := dnsforward.DNSCreateParams{
DNSFilter: Context.filters, DNSFilter: filters,
Stats: Context.stats, Stats: sts,
QueryLog: Context.queryLog, QueryLog: qlog,
PrivateNets: privateNets, PrivateNets: privateNets,
Anonymizer: anonymizer, Anonymizer: anonymizer,
LocalDomain: config.DHCP.LocalDomainName, LocalDomain: config.DHCP.LocalDomainName,
DHCPServer: Context.dhcpServer, DHCPServer: dhcpSrv,
} }
Context.dnsServer, err = dnsforward.NewServer(p) Context.dnsServer, err = dnsforward.NewServer(p)
@@ -120,15 +136,15 @@ func initDNSServer() (err error) {
} }
Context.clients.dnsServer = Context.dnsServer Context.clients.dnsServer = Context.dnsServer
var dnsConfig dnsforward.ServerConfig
dnsConfig, err = generateServerConfig() dnsConf, err := generateServerConfig(tlsConf, httpReg)
if err != nil { if err != nil {
closeDNSServer() closeDNSServer()
return fmt.Errorf("generateServerConfig: %w", err) return fmt.Errorf("generateServerConfig: %w", err)
} }
err = Context.dnsServer.Prepare(&dnsConfig) err = Context.dnsServer.Prepare(&dnsConf)
if err != nil { if err != nil {
closeDNSServer() closeDNSServer()
@@ -146,6 +162,32 @@ func initDNSServer() (err error) {
return nil return nil
} }
// parseSubnetSet parses a slice of subnets. If the slice is empty, it returns
// a subnet set that matches all locally served networks, see
// [netutil.IsLocallyServed].
func parseSubnetSet(nets []string) (s netutil.SubnetSet, err error) {
switch len(nets) {
case 0:
// Use an optimized function-based matcher.
return netutil.SubnetSetFunc(netutil.IsLocallyServed), nil
case 1:
s, err = netutil.ParseSubnet(nets[0])
if err != nil {
return nil, err
}
return s, nil
default:
var nets []*net.IPNet
nets, err = netutil.ParseSubnets(config.DNS.PrivateNets...)
if err != nil {
return nil, err
}
return netutil.SliceSubnetSet(nets), nil
}
}
func isRunning() bool { func isRunning() bool {
return Context.dnsServer != nil && Context.dnsServer.IsRunning() return Context.dnsServer != nil && Context.dnsServer.IsRunning()
} }
@@ -193,7 +235,10 @@ func ipsToUDPAddrs(ips []netip.Addr, port int) (udpAddrs []*net.UDPAddr) {
return udpAddrs return udpAddrs
} }
func generateServerConfig() (newConf dnsforward.ServerConfig, err error) { func generateServerConfig(
tlsConf *tlsConfigSettings,
httpReg aghhttp.RegisterFunc,
) (newConf dnsforward.ServerConfig, err error) {
dnsConf := config.DNS dnsConf := config.DNS
hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()}) hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()})
newConf = dnsforward.ServerConfig{ newConf = dnsforward.ServerConfig{
@@ -201,12 +246,10 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port), TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port),
FilteringConfig: dnsConf.FilteringConfig, FilteringConfig: dnsConf.FilteringConfig,
ConfigModified: onConfigModified, ConfigModified: onConfigModified,
HTTPRegister: httpRegister, HTTPRegister: httpReg,
OnDNSRequest: onDNSRequest, OnDNSRequest: onDNSRequest,
} }
tlsConf := tlsConfigSettings{}
Context.tls.WriteDiskConfig(&tlsConf)
if tlsConf.Enabled { if tlsConf.Enabled {
newConf.TLSConfig = tlsConf.TLSConfig newConf.TLSConfig = tlsConf.TLSConfig
newConf.TLSConfig.ServerName = tlsConf.ServerName newConf.TLSConfig.ServerName = tlsConf.ServerName
@@ -224,7 +267,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
} }
if tlsConf.PortDNSCrypt != 0 { if tlsConf.PortDNSCrypt != 0 {
newConf.DNSCryptConfig, err = newDNSCrypt(hosts, tlsConf) newConf.DNSCryptConfig, err = newDNSCrypt(hosts, *tlsConf)
if err != nil { if err != nil {
// Don't wrap the error, because it's already // Don't wrap the error, because it's already
// wrapped by newDNSCrypt. // wrapped by newDNSCrypt.
@@ -413,7 +456,11 @@ func startDNSServer() error {
func reconfigureDNSServer() (err error) { func reconfigureDNSServer() (err error) {
var newConf dnsforward.ServerConfig var newConf dnsforward.ServerConfig
newConf, err = generateServerConfig()
tlsConf := &tlsConfigSettings{}
Context.tls.WriteDiskConfig(tlsConf)
newConf, err = generateServerConfig(tlsConf, httpRegister)
if err != nil { if err != nil {
return fmt.Errorf("generating forwarding dns server config: %w", err) return fmt.Errorf("generating forwarding dns server config: %w", err)
} }

View File

@@ -455,6 +455,10 @@ func run(opts options, clientBuildFS fs.FS) {
err = setupConfig(opts) err = setupConfig(opts)
fatalOnError(err) fatalOnError(err)
// TODO(e.burkov): This could be made earlier, probably as the option's
// effect.
cmdlineUpdate(opts)
if !Context.firstRun { if !Context.firstRun {
// Save the updated config // Save the updated config
err = config.write() err = config.write()
@@ -522,7 +526,7 @@ func run(opts options, clientBuildFS fs.FS) {
fatalOnError(err) fatalOnError(err)
if !Context.firstRun { if !Context.firstRun {
err = initDNSServer() err = initDNS()
fatalOnError(err) fatalOnError(err)
Context.tls.start() Context.tls.start()
@@ -543,20 +547,24 @@ func run(opts options, clientBuildFS fs.FS) {
} }
} }
// TODO(a.garipov): This could be made much earlier and could be done on
// the first run as well, but to achieve this we need to bypass requests
// over dnsforward resolver.
cmdlineUpdate(opts)
Context.web.Start() Context.web.Start()
// wait indefinitely for other go-routines to complete their job // wait indefinitely for other go-routines to complete their job
select {} select {}
} }
func (c *configuration) anonymizer() (ipmut *aghnet.IPMut) {
var anonFunc aghnet.IPMutFunc
if c.DNS.AnonymizeClientIP {
anonFunc = querylog.AnonymizeIP
}
return aghnet.NewIPMut(anonFunc)
}
// startMods initializes and starts the DNS server after installation. // startMods initializes and starts the DNS server after installation.
func startMods() error { func startMods() (err error) {
err := initDNSServer() err = initDNS()
if err != nil { if err != nil {
return err return err
} }
@@ -927,8 +935,8 @@ func getHTTPProxy(_ *http.Request) (*url.URL, error) {
// jsonError is a generic JSON error response. // jsonError is a generic JSON error response.
// //
// TODO(a.garipov): Merge together with the implementations in .../dhcpd and // TODO(a.garipov): Merge together with the implementations in [dhcpd] and other
// other packages after refactoring the web handler registering. // packages after refactoring the web handler registering.
type jsonError struct { type jsonError struct {
// Message is the error message, an opaque string. // Message is the error message, an opaque string.
Message string `json:"message"` Message string `json:"message"`
@@ -940,30 +948,40 @@ func cmdlineUpdate(opts options) {
return return
} }
log.Info("starting update") // Initialize the DNS server to use the internal resolver which the updater
// needs to be able to resolve the update source hostname.
//
// TODO(e.burkov): We could probably initialize the internal resolver
// separately.
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{})
fatalOnError(err)
if Context.firstRun { log.Info("cmdline update: performing update")
log.Info("update not allowed on first run")
os.Exit(0) updater := Context.updater
} info, err := updater.VersionInfo(true)
_, err := Context.updater.VersionInfo(true)
if err != nil { if err != nil {
vcu := Context.updater.VersionCheckURL() vcu := updater.VersionCheckURL()
log.Error("getting version info from %s: %s", vcu, err) log.Error("getting version info from %s: %s", vcu, err)
os.Exit(0) os.Exit(1)
} }
if Context.updater.NewVersion() == "" { if info.NewVersion == version.Version() {
log.Info("no updates available") log.Info("no updates available")
os.Exit(0) os.Exit(0)
} }
err = Context.updater.Update() err = updater.Update(Context.firstRun)
fatalOnError(err) fatalOnError(err)
err = restartService()
if err != nil {
log.Debug("restarting service: %s", err)
log.Info("AdGuard Home was not installed as a service. " +
"Please restart running instances of AdGuardHome manually.")
}
os.Exit(0) os.Exit(0)
} }

View File

@@ -229,7 +229,7 @@ var cmdLineOpts = []cmdLineOpt{{
updateNoValue: func(o options) (options, error) { o.performUpdate = true; return o, nil }, updateNoValue: func(o options) (options, error) { o.performUpdate = true; return o, nil },
effect: nil, effect: nil,
serialize: func(o options) (val string, ok bool) { return "", o.performUpdate }, serialize: func(o options) (val string, ok bool) { return "", o.performUpdate },
description: "Update application and exit.", description: "Update the current binary and restart the service in case it's installed.",
longName: "update", longName: "update",
shortName: "", shortName: "",
}, { }, {

View File

@@ -159,6 +159,38 @@ func sendSigReload() {
log.Debug("service: sent signal to pid %d", pid) log.Debug("service: sent signal to pid %d", pid)
} }
// restartService restarts the service. It returns error if the service is not
// running.
func restartService() (err error) {
// Call chooseSystem explicitly to introduce OpenBSD support for service
// package. It's a noop for other GOOS values.
chooseSystem()
pwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("getting current directory: %w", err)
}
svcConfig := &service.Config{
Name: serviceName,
DisplayName: serviceDisplayName,
Description: serviceDescription,
WorkingDirectory: pwd,
}
configureService(svcConfig)
var s service.Service
if s, err = service.New(&program{}, svcConfig); err != nil {
return fmt.Errorf("initializing service: %w", err)
}
if err = svcAction(s, "restart"); err != nil {
return fmt.Errorf("restarting service: %w", err)
}
return nil
}
// handleServiceControlAction one of the possible control actions: // handleServiceControlAction one of the possible control actions:
// //
// - install: Installs a service/daemon. // - install: Installs a service/daemon.

View File

@@ -7,6 +7,8 @@ import (
"github.com/kardianos/service" "github.com/kardianos/service"
) )
// chooseSystem checks the current system detected and substitutes it with local
// implementation if needed.
func chooseSystem() { func chooseSystem() {
sys := service.ChosenSystem() sys := service.ChosenSystem()
// By default, package service uses the SysV system if it cannot detect // By default, package service uses the SysV system if it cannot detect

View File

@@ -30,6 +30,8 @@ import (
// sysVersion is the version of local service.System interface implementation. // sysVersion is the version of local service.System interface implementation.
const sysVersion = "openbsd-runcom" const sysVersion = "openbsd-runcom"
// chooseSystem checks the current system detected and substitutes it with local
// implementation if needed.
func chooseSystem() { func chooseSystem() {
service.ChooseSystem(openbsdSystem{}) service.ChooseSystem(openbsdSystem{})
} }

View File

@@ -180,7 +180,7 @@ func withRecovered(orig *error) {
// type check // type check
var _ Interface = (*StatsCtx)(nil) var _ Interface = (*StatsCtx)(nil)
// Start implements the Interface interface for *StatsCtx. // Start implements the [Interface] interface for *StatsCtx.
func (s *StatsCtx) Start() { func (s *StatsCtx) Start() {
s.initWeb() s.initWeb()

View File

@@ -61,7 +61,7 @@ func (u *Updater) VersionInfo(forceRecheck bool) (vi VersionInfo, err error) {
return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err) return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err)
} }
u.prevCheckTime = time.Now() u.prevCheckTime = now
u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body) u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body)
return u.prevCheckResult, u.prevCheckError return u.prevCheckResult, u.prevCheckError
@@ -92,7 +92,11 @@ func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) {
info.AnnouncementURL = versionJSON["announcement_url"] info.AnnouncementURL = versionJSON["announcement_url"]
packageURL, ok := u.downloadURL(versionJSON) packageURL, ok := u.downloadURL(versionJSON)
info.CanAutoUpdate = aghalg.BoolToNullBool(ok && info.NewVersion != u.version) if !ok {
return info, fmt.Errorf("version.json: packageURL not found")
}
info.CanAutoUpdate = aghalg.BoolToNullBool(info.NewVersion != u.version)
u.newVersion = info.NewVersion u.newVersion = info.NewVersion
u.packageURL = packageURL u.packageURL = packageURL

View File

@@ -104,49 +104,58 @@ func NewUpdater(conf *Config) *Updater {
} }
} }
// Update performs the auto-update. // Update performs the auto-update. It returns an error if the update failed.
func (u *Updater) Update() (err error) { // If firstRun is true, it assumes the configuration file doesn't exist.
func (u *Updater) Update(firstRun bool) (err error) {
u.mu.Lock() u.mu.Lock()
defer u.mu.Unlock() defer u.mu.Unlock()
log.Info("updater: updating") log.Info("updater: updating")
defer func() { log.Info("updater: finished; errors: %v", err) }() defer func() {
if err != nil {
log.Error("updater: failed: %v", err)
} else {
log.Info("updater: finished")
}
}()
execPath, err := os.Executable() execPath, err := os.Executable()
if err != nil { if err != nil {
return err return fmt.Errorf("getting executable path: %w", err)
} }
err = u.prepare(execPath) err = u.prepare(execPath)
if err != nil { if err != nil {
return err return fmt.Errorf("preparing: %w", err)
} }
defer u.clean() defer u.clean()
err = u.downloadPackageFile(u.packageURL, u.packageName) err = u.downloadPackageFile()
if err != nil { if err != nil {
return err return fmt.Errorf("downloading package file: %w", err)
} }
err = u.unpack() err = u.unpack()
if err != nil { if err != nil {
return err return fmt.Errorf("unpacking: %w", err)
} }
err = u.check() if !firstRun {
if err != nil { err = u.check()
return err if err != nil {
return fmt.Errorf("checking config: %w", err)
}
} }
err = u.backup() err = u.backup(firstRun)
if err != nil { if err != nil {
return err return fmt.Errorf("making backup: %w", err)
} }
err = u.replace() err = u.replace()
if err != nil { if err != nil {
return err return fmt.Errorf("replacing: %w", err)
} }
return nil return nil
@@ -174,7 +183,7 @@ func (u *Updater) prepare(exePath string) (err error) {
_, pkgNameOnly := filepath.Split(u.packageURL) _, pkgNameOnly := filepath.Split(u.packageURL)
if pkgNameOnly == "" { if pkgNameOnly == "" {
return fmt.Errorf("invalid PackageURL") return fmt.Errorf("invalid PackageURL: %q", u.packageURL)
} }
u.packageName = filepath.Join(u.updateDir, pkgNameOnly) u.packageName = filepath.Join(u.updateDir, pkgNameOnly)
@@ -204,6 +213,7 @@ func (u *Updater) prepare(exePath string) (err error) {
return nil return nil
} }
// unpack extracts the files from the downloaded archive.
func (u *Updater) unpack() error { func (u *Updater) unpack() error {
var err error var err error
_, pkgNameOnly := filepath.Split(u.packageURL) _, pkgNameOnly := filepath.Split(u.packageURL)
@@ -228,38 +238,48 @@ func (u *Updater) unpack() error {
return nil return nil
} }
// check returns an error if the configuration file couldn't be used with the
// version of AdGuard Home just downloaded.
func (u *Updater) check() error { func (u *Updater) check() error {
log.Debug("updater: checking configuration") log.Debug("updater: checking configuration")
err := copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml")) err := copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml"))
if err != nil { if err != nil {
return fmt.Errorf("copyFile() failed: %w", err) return fmt.Errorf("copyFile() failed: %w", err)
} }
cmd := exec.Command(u.updateExeName, "--check-config") cmd := exec.Command(u.updateExeName, "--check-config")
err = cmd.Run() err = cmd.Run()
if err != nil || cmd.ProcessState.ExitCode() != 0 { if err != nil || cmd.ProcessState.ExitCode() != 0 {
return fmt.Errorf("exec.Command(): %s %d", err, cmd.ProcessState.ExitCode()) return fmt.Errorf("exec.Command(): %s %d", err, cmd.ProcessState.ExitCode())
} }
return nil return nil
} }
func (u *Updater) backup() error { // backup makes a backup of the current configuration and supporting files. It
// ignores the configuration file if firstRun is true.
func (u *Updater) backup(firstRun bool) (err error) {
log.Debug("updater: backing up current configuration") log.Debug("updater: backing up current configuration")
_ = os.Mkdir(u.backupDir, 0o755) _ = os.Mkdir(u.backupDir, 0o755)
err := copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml")) if !firstRun {
if err != nil { err = copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml"))
return fmt.Errorf("copyFile() failed: %w", err) if err != nil {
return fmt.Errorf("copyFile() failed: %w", err)
}
} }
wd := u.workDir wd := u.workDir
err = copySupportingFiles(u.unpackedFiles, wd, u.backupDir) err = copySupportingFiles(u.unpackedFiles, wd, u.backupDir)
if err != nil { if err != nil {
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", wd, u.backupDir, err)
wd, u.backupDir, err)
} }
return nil return nil
} }
// replace moves the current executable with the updated one and also copies the
// supporting files.
func (u *Updater) replace() error { func (u *Updater) replace() error {
err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir) err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir)
if err != nil { if err != nil {
@@ -287,6 +307,7 @@ func (u *Updater) replace() error {
return nil return nil
} }
// clean removes the temporary directory itself and all it's contents.
func (u *Updater) clean() { func (u *Updater) clean() {
_ = os.RemoveAll(u.updateDir) _ = os.RemoveAll(u.updateDir)
} }
@@ -297,9 +318,9 @@ func (u *Updater) clean() {
const MaxPackageFileSize = 32 * 1024 * 1024 const MaxPackageFileSize = 32 * 1024 * 1024
// Download package file and save it to disk // Download package file and save it to disk
func (u *Updater) downloadPackageFile(url, filename string) (err error) { func (u *Updater) downloadPackageFile() (err error) {
var resp *http.Response var resp *http.Response
resp, err = u.client.Get(url) resp, err = u.client.Get(u.packageURL)
if err != nil { if err != nil {
return fmt.Errorf("http request failed: %w", err) return fmt.Errorf("http request failed: %w", err)
} }
@@ -321,7 +342,7 @@ func (u *Updater) downloadPackageFile(url, filename string) (err error) {
_ = os.Mkdir(u.updateDir, 0o755) _ = os.Mkdir(u.updateDir, 0o755)
log.Debug("updater: saving package to file") log.Debug("updater: saving package to file")
err = os.WriteFile(filename, body, 0o644) err = os.WriteFile(u.packageName, body, 0o644)
if err != nil { if err != nil {
return fmt.Errorf("os.WriteFile() failed: %w", err) return fmt.Errorf("os.WriteFile() failed: %w", err)
} }

View File

@@ -136,10 +136,10 @@ func TestUpdate(t *testing.T) {
u.packageURL = fakeURL.String() u.packageURL = fakeURL.String()
require.NoError(t, u.prepare(exePath)) require.NoError(t, u.prepare(exePath))
require.NoError(t, u.downloadPackageFile(u.packageURL, u.packageName)) require.NoError(t, u.downloadPackageFile())
require.NoError(t, u.unpack()) require.NoError(t, u.unpack())
// require.NoError(t, u.check()) // require.NoError(t, u.check())
require.NoError(t, u.backup()) require.NoError(t, u.backup(false))
require.NoError(t, u.replace()) require.NoError(t, u.replace())
u.clean() u.clean()
@@ -215,10 +215,10 @@ func TestUpdateWindows(t *testing.T) {
u.packageURL = fakeURL.String() u.packageURL = fakeURL.String()
require.NoError(t, u.prepare(exePath)) require.NoError(t, u.prepare(exePath))
require.NoError(t, u.downloadPackageFile(u.packageURL, u.packageName)) require.NoError(t, u.downloadPackageFile())
require.NoError(t, u.unpack()) require.NoError(t, u.unpack())
// assert.Nil(t, u.check()) // assert.Nil(t, u.check())
require.NoError(t, u.backup()) require.NoError(t, u.backup(false))
require.NoError(t, u.replace()) require.NoError(t, u.replace())
u.clean() u.clean()