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

View File

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

View File

@@ -1,7 +1,7 @@
'name': 'lint'
'env':
'GO_VERSION': '1.18.8'
'GO_VERSION': '1.18.9'
'on':
'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].
### 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
-->
@@ -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
[v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.20...v0.107.21
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.22...HEAD
[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.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

View File

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

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Nastavení DHCP IPv6",
"form_error_required": "Povinné pole",
"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_ip6_format": "Neplatná adresa IPv6",
"form_error_ip_format": "Neplatná IP adresa",
@@ -51,7 +49,6 @@
"out_of_range_error": "Musí být mimo rozsah \"{{start}}\"-\"{{end}}\"",
"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_end_error": "Musí být větší než konec rozsahu",
"subnet_error": "Adresy musí být v jedné podsíti",
"gateway_or_subnet_invalid": "Neplatná maska podsítě",
"dhcp_form_gateway_input": "IP brána",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6-indstillinger",
"form_error_required": "Obligatorisk felt",
"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_ip6_format": "Ugyldig IPv6-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}}\"",
"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_end_error": "Skal være større end områdeslutning",
"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_subnet_input": "Undernetmaske",
"dhcp_form_range_title": "Interval af IP-adresser",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP-IPv6-Einstellungen",
"form_error_required": "Pflichtfeld",
"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_ip6_format": "Ungültige IPv6-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",
"lower_range_start_error": "Muss niedriger 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",
"gateway_or_subnet_invalid": "Ungültige Subnetzmaske",
"dhcp_form_gateway_input": "Gateway-IP",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Configuración DHCP IPv6",
"form_error_required": "Campo obligatorio",
"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_ip6_format": "Dirección IPv6 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}}\"",
"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_end_error": "Debe ser mayor que el final de rango",
"subnet_error": "Las direcciones deben estar en una subred",
"gateway_or_subnet_invalid": "Máscara de subred no válida",
"dhcp_form_gateway_input": "IP de puerta de enlace",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP:n IPv6-asetukset",
"form_error_required": "Pakollinen kenttä",
"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_ip6_format": "Virheellinen IPv6-osoite",
"form_error_ip_format": "Virheellinen IP-osoite",
@@ -51,7 +49,6 @@
"out_of_range_error": "Oltava alueen \"{{start}}\" - \"{{end}}\" ulkopuolella",
"lower_range_start_error": "Oltava alueen aloitusarvoa pienempi",
"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",
"gateway_or_subnet_invalid": "Virheellinen aliverkon peite",
"dhcp_form_gateway_input": "Yhdyskäytävän IP-osoite",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Paramètres IPv6 du DHCP",
"form_error_required": "Champ requis",
"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_ip6_format": "Adresse IPv6 invalide",
"form_error_ip_format": "Adresse IP invalide",
@@ -51,9 +49,8 @@
"out_of_range_error": "Doit être hors plage « {{start}} » - « {{end}} »",
"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_end_error": "Doit être supérieur à la fin de plage",
"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_subnet_input": "Masque de sous-réseau",
"dhcp_form_range_title": "Rangée des adresses IP",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 postavke",
"form_error_required": "Obavezno polje",
"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_ip6_format": "Nevažeći IPv6 adresa",
"form_error_ip_format": "Nepravilna IP adresa",
@@ -51,9 +49,8 @@
"out_of_range_error": "Mora biti izvan ranga \"{{start}}\"-\"{{end}}\"",
"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_end_error": "Mora biti veće od krajnjeg ranga",
"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_subnet_input": "Subnet maskiranje",
"dhcp_form_range_title": "Raspon IP adresa",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 Beállítások",
"form_error_required": "Kötelező mező",
"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_ip6_format": "Érvénytelen IPv6 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}}\"",
"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_end_error": "Nagyobb legyen, mint a tartomány vége",
"subnet_error": "A címeknek egy alhálózatban kell lenniük",
"gateway_or_subnet_invalid": "Az alhálózati maszk érvénytelen",
"dhcp_form_gateway_input": "Átjáró IP",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Pengaturan DHCP IPv6",
"form_error_required": "Kolom yang harus diisi",
"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_ip6_format": "Alamat IPv6 tidak valid",
"form_error_ip_format": "Alamat IP tidak valid",
@@ -51,7 +49,6 @@
"out_of_range_error": "Harus di luar rentang \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Harus lebih rendah 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",
"gateway_or_subnet_invalid": "Subnet mask tidak valid",
"dhcp_form_gateway_input": "IP gateway",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Impostazioni DHCP IPv6",
"form_error_required": "Campo richiesto",
"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_ip6_format": "Indirizzo IPv6 non valido",
"form_error_ip_format": "Indirizzo IP non valido",
@@ -51,7 +49,6 @@
"out_of_range_error": "Deve essere fuori intervallo \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Deve essere inferiore 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",
"gateway_or_subnet_invalid": "Maschera di sottorete non valida",
"dhcp_form_gateway_input": "IP Gateway",

View File

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

View File

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

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 instellingen",
"form_error_required": "Vereist veld",
"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_ip6_format": "Ongeldig IPv6-adres",
"form_error_ip_format": "Ongeldig IP-adres",
@@ -51,9 +49,8 @@
"out_of_range_error": "Moet buiten bereik zijn \"{{start}}\"-\"{{end}}\"",
"lower_range_start_error": "Moet lager 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",
"gateway_or_subnet_invalid": "Subnetmasker ongeldig",
"gateway_or_subnet_invalid": "Ongeldig subnetmasker",
"dhcp_form_gateway_input": "Gateway IP",
"dhcp_form_subnet_input": "Subnet mask",
"dhcp_form_range_title": "Bereik van IP adressen",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Ustawienia serwera DHCP IPv6",
"form_error_required": "Pole wymagane",
"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_ip6_format": "Nieprawidłowy adres IPv6",
"form_error_ip_format": "Nieprawidłowy adres IP",
@@ -51,7 +49,6 @@
"out_of_range_error": "Musi być spoza zakresu \"{{start}}\"-\"{{end}}\"",
"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_end_error": "Musi być większy niż koniec zakresu",
"subnet_error": "Adresy muszą należeć do jednej podsieci",
"gateway_or_subnet_invalid": "Nieprawidłowa maska podsieci",
"dhcp_form_gateway_input": "Adres IP bramy",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Configurações DHCP IPv6",
"form_error_required": "Campo obrigatório",
"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_ip6_format": "Endereço de IPv6 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}}\"",
"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_end_error": "Deve ser maior que o fim do intervalo",
"subnet_error": "Endereços devem estar em uma sub-rede",
"gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
"dhcp_form_gateway_input": "IP do gateway",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Definições DHCP IPv6",
"form_error_required": "Campo obrigatório",
"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_ip6_format": "Endereço de IPv6 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}}\"",
"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_end_error": "Deve ser maior que o fim do intervalo",
"subnet_error": "Os endereços devem estar em uma sub-rede",
"gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
"dhcp_form_gateway_input": "IP do gateway",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Setări DHCP IPv6",
"form_error_required": "Câmp obligatoriu",
"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_ip6_format": "Adresa IPv6 nevalidă",
"form_error_ip_format": "Adresă IP nevalidă",
@@ -51,7 +49,6 @@
"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",
"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",
"gateway_or_subnet_invalid": "Mască de subrețea nevalidă",
"dhcp_form_gateway_input": "IP Gateway",

View File

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

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Nastavenia DHCP IPv6",
"form_error_required": "Povinná položka.",
"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_ip6_format": "Neplatná IPv6 adresa",
"form_error_ip_format": "Neplatná IP adresa",
@@ -51,7 +49,6 @@
"out_of_range_error": "Musí byť mimo rozsahu \"{{start}}\"-\"{{end}}\"",
"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_end_error": "Musí byť väčšie ako koniec rozsahu",
"subnet_error": "Adresy musia byť v spoločnej podsieti",
"gateway_or_subnet_invalid": "Maska podsiete je neplatná",
"dhcp_form_gateway_input": "IP brána",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Nastavitve DHCP IPv6",
"form_error_required": "Zahtevano polje.",
"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_ip6_format": "Neveljaven naslov IPv6",
"form_error_ip_format": "Neveljaven naslov IP",
@@ -51,7 +49,6 @@
"out_of_range_error": "Mora biti izven razpona \"{{start}}\"-\"{{end}}\"",
"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_end_error": "Mora biti večji od konca razpona",
"subnet_error": "Naslovi morajo biti v enem podomrežju",
"gateway_or_subnet_invalid": "Maska podomrežja ni veljavna",
"dhcp_form_gateway_input": "IP prehoda",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 postavke",
"form_error_required": "Obavezno polje",
"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_ip6_format": "Nevažeća IPv6 adresa",
"form_error_ip_format": "Nevažeća IP adresa",
@@ -51,7 +49,6 @@
"out_of_range_error": "Mora biti izvan opsega \"{{start}}\"-\"{{end}}\"",
"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_end_error": "Mora biti veće od završnog opsega",
"subnet_error": "Asrese moraju biti u jednoj subnet",
"gateway_or_subnet_invalid": "Subnet mask nevažeća",
"dhcp_form_gateway_input": "IP mrežnog prolaza",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 inställningar",
"form_error_required": "Obligatoriskt fält",
"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_ip6_format": "Ogiltig IPv6-adress",
"form_error_ip_format": "Ogiltig IP-adress",
@@ -51,7 +49,6 @@
"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",
"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",
"gateway_or_subnet_invalid": "Subnätmask ogiltig",
"dhcp_form_gateway_input": "Gateway-IP",

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 Ayarları",
"form_error_required": "Gerekli alan",
"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_ip6_format": "Geçersiz IPv6 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",
"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_end_error": "Bitiş aralığından daha büyük 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_subnet_input": "Alt ağ maskesi",
"dhcp_form_range_title": "IP adresi aralığı",

View File

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

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "Cài đặt DHCP IPv6",
"form_error_required": "Trường bắt buộc",
"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_ip6_format": "Địa chỉ IPv6 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}}\"",
"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_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",
"gateway_or_subnet_invalid": "Mặt nạ mạng con không hợp lệ",
"dhcp_form_gateway_input": "Cổng IP",

View File

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

View File

@@ -37,8 +37,6 @@
"dhcp_ipv6_settings": "DHCP IPv6 設定",
"form_error_required": "必填的欄位",
"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_ip6_format": "無效的 IPv6 位址",
"form_error_ip_format": "無效的 IP 位址",
@@ -51,7 +49,6 @@
"out_of_range_error": "必須在\"{{start}}\"-\"{{end}}\"範圍之外",
"lower_range_start_error": "必須低於起始範圍",
"greater_range_start_error": "必須大於起始範圍",
"greater_range_end_error": "必須大於結束範圍",
"subnet_error": "位址必須在子網路中",
"gateway_or_subnet_invalid": "無效的子網路遮罩",
"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.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();
if (dnsStatus) {
dispatch(dnsStatusSuccess(dnsStatus));
@@ -48,7 +54,6 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
dispatch(setTlsConfigSuccess(response));
dispatch(addSuccessToast('encryption_config_saved'));
redirectToCurrentProtocol(response, httpPort);
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setTlsConfigFailure());

View File

@@ -155,7 +155,7 @@ const Form = (props) => {
name={FORM_NAMES.search}
component={renderFilterField}
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')}
tooltip={t('query_log_strict_search')}
onClearInputClick={onInputClear}

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,12 @@ export default {
"homepage": "https://badmojr.github.io/1Hosts/",
"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": {
"name": "CHN: AdRules DNS List",
"categoryId": "regional",
@@ -40,6 +46,12 @@ export default {
"homepage": "https://anti-ad.net/",
"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": {
"name": "IDN: ABPindo",
"categoryId": "regional",
@@ -70,6 +82,12 @@ export default {
"homepage": "https://github.com/yous/YousList",
"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": {
"name": "MKD: Macedonian Pi-hole Blocklist",
"categoryId": "regional",
@@ -148,6 +166,18 @@ export default {
"homepage": "https://energized.pro/",
"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": {
"name": "NoCoin Filter List",
"categoryId": "security",

8
go.mod
View File

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

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.4/go.mod h1:yYDMAH6ay2PxLcLtfVM3FUiyv/U9B/zYO+cIIssuJNU=
github.com/AdguardTeam/dnsproxy v0.46.5 h1:TiJZhwaIDDaKkqEfJ9AD9aroFjcHN8oEbKB8WfTjSIs=
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.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
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-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.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
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-20210220032951-036812b2e83c/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-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.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.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.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
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
// queries, such as public clients PTR resolving and updater hostname resolving.
func (s *Server) prepareInternalProxy() (err error) {
srvConf := s.conf
conf := &proxy.Config{
CacheEnabled: true,
CacheSizeBytes: 4096,
UpstreamConfig: s.conf.UpstreamConfig,
UpstreamConfig: srvConf.UpstreamConfig,
MaxGoroutines: int(s.conf.MaxGoroutines),
}
srvConf := s.conf
setProxyUpstreamMode(
conf,
srvConf.AllServers,
@@ -570,46 +570,32 @@ func (s *Server) Stop() error {
// stopLocked stops the DNS server without locking. For internal use only.
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 {
err = s.dnsProxy.Stop()
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 {
const action = "closing internal resolvers"
err = upsConf.Close()
if err != nil {
if errors.Is(err, net.ErrClosed) {
log.Debug("dnsforward: %s: %s", action, err)
} else {
errs = append(errs, fmt.Errorf("%s: %w", action, err))
}
log.Error("dnsforward: closing internal resolvers: %s", err)
}
}
if upsConf := s.localResolvers.UpstreamConfig; upsConf != nil {
const action = "closing local resolvers"
err = upsConf.Close()
if err != nil {
if errors.Is(err, net.ErrClosed) {
log.Debug("dnsforward: %s: %s", action, err)
} else {
errs = append(errs, fmt.Errorf("%s: %w", action, err))
}
log.Error("dnsforward: closing local resolvers: %s", err)
}
}
if len(errs) > 0 {
return errors.List("stopping dns server", errs...)
} else {
s.isRunning = false
}
s.isRunning = false
return nil
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rewrite"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/netutil"
@@ -67,7 +68,7 @@ func createTestServer(
ID: 0, Data: []byte(rules),
}}
f, err := filtering.New(filterConf, filters)
f, err := filtering.New(filterConf, filters, nil)
require.NoError(t, err)
f.SetEnabled(true)
@@ -760,7 +761,7 @@ func TestBlockedCustomIP(t *testing.T) {
Data: []byte(rules),
}}
f, err := filtering.New(&filtering.Config{}, filters)
f, err := filtering.New(&filtering.Config{}, filters, nil)
require.NoError(t, err)
s, err := NewServer(DNSCreateParams{
@@ -880,21 +881,22 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
func TestRewrite(t *testing.T) {
c := &filtering.Config{
Rewrites: []*filtering.LegacyRewrite{{
Rewrites: []*filtering.RewriteItem{{
Domain: "test.com",
Answer: "1.2.3.4",
Type: dns.TypeA,
}, {
Domain: "alias.test.com",
Answer: "test.com",
Type: dns.TypeCNAME,
}, {
Domain: "my.alias.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)
f.SetEnabled(true)
@@ -945,6 +947,12 @@ func TestRewrite(t *testing.T) {
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)
reply, eerr = dns.Exchange(req, addr.String())
require.NoError(t, eerr)
@@ -952,8 +960,15 @@ func TestRewrite(t *testing.T) {
require.Len(t, reply.Answer, 2)
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))
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)
reply, eerr = dns.Exchange(req, addr.String())
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, 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} {
@@ -1011,7 +1032,7 @@ var testDHCP = &dhcpd.MockInterface{
func TestPTRResponseFromDHCPLeases(t *testing.T) {
const localDomain = "lan"
flt, err := filtering.New(&filtering.Config{}, nil)
flt, err := filtering.New(&filtering.Config{}, nil, nil)
require.NoError(t, err)
s, err := NewServer(DNSCreateParams{
@@ -1085,7 +1106,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
flt, err := filtering.New(&filtering.Config{
EtcHosts: hc,
}, nil)
}, nil, nil)
require.NoError(t, err)
flt.SetEnabled(true)

View File

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

View File

@@ -2,6 +2,7 @@ package filtering
import (
"bufio"
"bytes"
"fmt"
"hash/crc32"
"io"
@@ -12,6 +13,7 @@ import (
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
@@ -97,14 +99,15 @@ func (d *DNSFilter) filterSetProperties(
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 {
filt.URL = oldURL
filt.Name = oldName
filt.Enabled = oldEnabled
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
@@ -134,8 +137,8 @@ func (d *DNSFilter) filterSetProperties(
// TODO(e.burkov): The validation of the contents of the new URL is
// currently skipped if the rule list is disabled. This makes it
// possible to set a bad rules source, but the validation should still
// kick in when the filter is enabled. Consider making changing this
// behavior to be stricter.
// kick in when the filter is enabled. Consider changing this behavior
// to be stricter.
filt.unload()
}
@@ -269,10 +272,10 @@ func (d *DNSFilter) periodicallyRefreshFilters() {
// already going on.
//
// 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) {
if ok = d.refreshLock.TryLock(); !ok {
return 0, false, ok
return 0, false, false
}
defer d.refreshLock.Unlock()
@@ -427,52 +430,124 @@ func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
return updNum, false
}
// Allows printable UTF-8 text with CR, LF, TAB characters
func isPrintableText(data []byte, len int) bool {
for i := 0; i < len; i++ {
c := data[i]
// isPrintableText returns true if data is printable UTF-8 text with CR, LF, TAB
// characters.
//
// 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' {
continue
}
return false
}
return true
}
// A helper function that parses filter contents and returns a number of rules and a filter name (if there's any)
func (d *DNSFilter) parseFilterContents(file io.Reader) (int, uint32, string) {
rulesCount := 0
name := ""
seenTitle := false
r := bufio.NewReader(file)
checksum := uint32(0)
// scanLinesWithBreak is essentially a [bufio.ScanLines] which keeps trailing
// line breaks.
func scanLinesWithBreak(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
for {
line, err := r.ReadString('\n')
checksum = crc32.Update(checksum, crc32.IEEETable, []byte(line))
if i := bytes.IndexByte(data, '\n'); i >= 0 {
return i + 1, data[0 : i+1], nil
}
line = strings.TrimSpace(line)
if len(line) == 0 {
//
} 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
}
if atEOF {
return len(data), data, nil
}
} else if line[0] == '#' {
//
} else {
rulesCount++
// Request more data.
return 0, nil, nil
}
// 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 {
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
@@ -485,57 +560,10 @@ func (d *DNSFilter) update(filter *FilterYAML) (bool, error) {
log.Error("os.Chtimes(): %v", e)
}
}
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
// according to updated. It also saves new values of flt's name, rules number
// 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.
//
// 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)
}
@@ -564,38 +593,18 @@ func (d *DNSFilter) finalizeUpdate(
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))
}
flt.Name = stringutil.Coalesce(flt.Name, name)
flt.checksum = cs
flt.RulesCount = rnum
flt.Name, flt.checksum, flt.RulesCount = aghalg.Coalesce(flt.Name, name), cs, rnum
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
// the actual update has been performed.
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() {
err = errors.WithDeferred(err, d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
ok = ok && err == nil
if ok {
if ok && err == nil {
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
}
}()
// Change the default 0o600 permission to something more acceptable by
// end users.
// Change the default 0o600 permission to something more acceptable by end
// users.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/3198.
if err = tmpFile.Chmod(0o644); err != nil {
return false, fmt.Errorf("changing file mode: %w", err)
}
var r io.Reader
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 rc io.ReadCloser
if !filepath.IsAbs(flt.URL) {
var resp *http.Response
resp, err = d.HTTPClient.Get(flt.URL)
if err != nil {
@@ -649,24 +648,30 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
if resp.StatusCode != http.StatusOK {
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
func (d *DNSFilter) load(filter *FilterYAML) (err error) {
filterFilePath := filter.Path(d.DataDir)
func (d *DNSFilter) load(flt *FilterYAML) (err error) {
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) {
// Do nothing, file doesn't exist.
return nil
@@ -680,13 +685,14 @@ func (d *DNSFilter) load(filter *FilterYAML) (err error) {
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
filter.checksum = checksum
filter.LastUpdated = st.ModTime()
flt.RulesCount, flt.checksum, flt.LastUpdated = rulesCount, checksum, st.ModTime()
return nil
}

View File

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

View File

@@ -33,7 +33,6 @@ import (
// The IDs of built-in filter lists.
//
// Keep in sync with client/src/helpers/constants.js.
// TODO(d.kolyshev): Add RewritesListID and don't forget to keep in sync.
const (
CustomListID = -iota
SysHostsListID
@@ -41,6 +40,7 @@ const (
ParentalListID
SafeBrowsingListID
SafeSearchListID
RewritesListID
)
// ServiceEntry - blocked service array element
@@ -90,7 +90,7 @@ type Config struct {
ParentalCacheSize uint `yaml:"parental_cache_size"` // (in bytes)
CacheTime uint `yaml:"cache_time"` // Element's TTL (in minutes)
Rewrites []*LegacyRewrite `yaml:"rewrites"`
Rewrites []*RewriteItem `yaml:"rewrites"`
// Names of services to block (globally).
// 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
// filter list.
//
// TODO(e.burkov): Don't use regexp for such a simple text processing task.
filterTitleRegexp *regexp.Regexp
rewriteStorage RewriteStorage
hostCheckers []hostChecker
}
@@ -313,7 +317,7 @@ func (d *DNSFilter) WriteDiskConfig(c *Config) {
defer d.confLock.Unlock()
*c = d.Config
c.Rewrites = cloneRewrites(c.Rewrites)
c.Rewrites = slices.Clone(c.Rewrites)
}()
d.filtersMu.RLock()
@@ -324,16 +328,6 @@ func (d *DNSFilter) WriteDiskConfig(c *Config) {
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
// are set asynchronously, the old filters continue working until the new
// filters are ready.
@@ -544,75 +538,52 @@ func (d *DNSFilter) matchSysHosts(
// CNAME, breaking loops in the process.
//
// 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
// result is an exception.
// accordingly.
func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
d.confLock.RLock()
defer d.confLock.RUnlock()
rewrites, matched := findRewrites(d.Rewrites, host, qtype)
if !matched {
return Result{}
if d.rewriteStorage == nil {
return res
}
res.Reason = Rewritten
dnsr := d.rewriteStorage.MatchRequest(&urlfilter.DNSRequest{
Hostname: host,
DNSType: qtype,
})
cnames := stringutil.NewSet()
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)
setRewriteResult(&res, host, dnsr, qtype)
return res
}
// setRewriteResult sets the Reason or IPList of res if necessary. res must not
// be nil.
func setRewriteResult(res *Result, host string, rewrites []*LegacyRewrite, qtype uint16) {
for _, rw := range rewrites {
if rw.Type == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) {
if rw.IP == nil {
// "A"/"AAAA" exception: allow getting from upstream.
res.Reason = NotFilteredNotFound
func setRewriteResult(res *Result, host string, dnsr []*rules.DNSRewrite, qtype uint16) {
if len(dnsr) == 0 {
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
// 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{
resolver: net.DefaultResolver,
refreshLock: &sync.Mutex{},
@@ -978,11 +949,7 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
d.Config = *c
d.filtersMu = &sync.RWMutex{}
err = d.prepareRewrites()
if err != nil {
return nil, fmt.Errorf("rewrites: preparing: %s", err)
}
d.rewriteStorage = rewriteStorage
bsvcs := []string{}
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,
FilteringEnabled: true,
}
if c != nil {
c.SafeBrowsingCacheSize = 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.
c = &Config{}
}
f, err := New(c, filters)
f, err := New(c, filters, nil)
require.NoError(t, err)
purgeCaches(f)
@@ -417,274 +419,275 @@ func TestMatching(t *testing.T) {
host string
wantReason Reason
wantIsFiltered bool
wantDNSType uint16
qtype uint16
}{{
name: "sanity",
rules: "||doubleclick.net^",
host: "www.doubleclick.net",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "sanity",
rules: "||doubleclick.net^",
host: "nodoubleclick.net",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "sanity",
rules: "||doubleclick.net^",
host: "doubleclick.net.ru",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "sanity",
rules: "||doubleclick.net^",
host: sbBlocked,
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "blocking",
rules: blockingRules,
host: "example.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "blocking",
rules: blockingRules,
host: "test.example.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "blocking",
rules: blockingRules,
host: "test.test.example.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "blocking",
rules: blockingRules,
host: "testexample.org",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "blocking",
rules: blockingRules,
host: "onemoreexample.org",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "allowlist",
rules: allowlistRules,
host: "example.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "allowlist",
rules: allowlistRules,
host: "test.example.org",
wantIsFiltered: false,
wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "allowlist",
rules: allowlistRules,
host: "test.test.example.org",
wantIsFiltered: false,
wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "allowlist",
rules: allowlistRules,
host: "testexample.org",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "allowlist",
rules: allowlistRules,
host: "onemoreexample.org",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "important",
rules: importantRules,
host: "example.org",
wantIsFiltered: false,
wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "important",
rules: importantRules,
host: "test.example.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "important",
rules: importantRules,
host: "test.test.example.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "important",
rules: importantRules,
host: "testexample.org",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "important",
rules: importantRules,
host: "onemoreexample.org",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "regex",
rules: regexRules,
host: "example.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "regex",
rules: regexRules,
host: "test.example.org",
wantIsFiltered: false,
wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "regex",
rules: regexRules,
host: "test.test.example.org",
wantIsFiltered: false,
wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "regex",
rules: regexRules,
host: "testexample.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "regex",
rules: regexRules,
host: "onemoreexample.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "mask",
rules: maskRules,
host: "test.example.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "mask",
rules: maskRules,
host: "test2.example.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "mask",
rules: maskRules,
host: "example.com",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "mask",
rules: maskRules,
host: "exampleeee.com",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "mask",
rules: maskRules,
host: "onemoreexamsite.com",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "mask",
rules: maskRules,
host: "example.org",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "mask",
rules: maskRules,
host: "testexample.org",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "mask",
rules: maskRules,
host: "example.co.uk",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "dnstype",
rules: dnstypeRules,
host: "onemoreexample.org",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "dnstype",
rules: dnstypeRules,
host: "example.org",
wantIsFiltered: false,
wantReason: NotFilteredNotFound,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "dnstype",
rules: dnstypeRules,
host: "example.org",
wantIsFiltered: true,
wantReason: FilteredBlockList,
wantDNSType: dns.TypeAAAA,
qtype: dns.TypeAAAA,
}, {
name: "dnstype",
rules: dnstypeRules,
host: "test.example.org",
wantIsFiltered: false,
wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeA,
qtype: dns.TypeA,
}, {
name: "dnstype",
rules: dnstypeRules,
host: "test.example.org",
wantIsFiltered: false,
wantReason: NotFilteredAllowList,
wantDNSType: dns.TypeAAAA,
qtype: dns.TypeAAAA,
}}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s-%s", tc.name, tc.host), func(t *testing.T) {
filters := []Filter{{ID: 0, Data: []byte(tc.rules)}}
d, setts := newForTest(t, nil, filters)
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)
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"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
@@ -30,11 +29,7 @@ func TestDNSFilter_handleFilteringSetURL(t *testing.T) {
endpoint: &badRulesEndpoint,
content: []byte(`<html></html>`),
}} {
ipp := serveFiltersLocally(t, rulesSource.content)
*rulesSource.endpoint = (&url.URL{
Scheme: "http",
Host: ipp.String(),
}).String()
*rulesSource.endpoint = serveFiltersLocally(t, rulesSource.content)
}
testCases := []struct {
@@ -110,7 +105,7 @@ func TestDNSFilter_handleFilteringSetURL(t *testing.T) {
},
ConfigModified: func() { confModifiedCalled = true },
DataDir: filtersDir,
}, nil)
}, nil, nil)
require.NoError(t, err)
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 (
"fmt"
"net/netip"
"strings"
"sync"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/urlfilter"
@@ -15,21 +17,6 @@ import (
"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.
type DefaultStorage struct {
// mu protects items.
@@ -42,7 +29,7 @@ type DefaultStorage struct {
ruleList filterlist.RuleList
// rewrites stores the rewrite entries from configuration.
rewrites []*Item
rewrites []*filtering.RewriteItem
// 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
// 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{
mu: &sync.RWMutex{},
urlFilterID: listID,
urlFilterID: filtering.RewritesListID,
rewrites: rewrites,
}
s.mu.Lock()
defer s.mu.Unlock()
err = s.resetRules()
if err != nil {
return nil, err
@@ -72,9 +56,9 @@ func NewDefaultStorage(listID int, rewrites []*Item) (s *DefaultStorage, err err
}
// 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) {
s.mu.RLock()
defer s.mu.RUnlock()
@@ -84,28 +68,32 @@ func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.
return nil
}
// TODO(a.garipov): Check cnames for cycles on initialisation.
// TODO(a.garipov): Check cnames for cycles on initialization.
cnames := stringutil.NewSet()
host := dReq.Hostname
var lastCNAMERule *rules.NetworkRule
for len(rrules) > 0 && rrules[0].DNSRewrite != nil && rrules[0].DNSRewrite.NewCNAME != "" {
rule := rrules[0]
rwAns := rule.DNSRewrite.NewCNAME
lastCNAMERule = rrules[0]
lastDNSRewrite := lastCNAMERule.DNSRewrite
rwAns := lastDNSRewrite.NewCNAME
log.Debug("rewrite: cname for %s is %s", host, 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.
log.Debug("rewrite: request for hostname itself for %q", dReq.Hostname)
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.
//
// 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) {
@@ -120,21 +108,28 @@ func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.
Hostname: rwAns,
DNSType: dReq.DNSType,
})
if drules != nil {
rrules = drules
if drules == nil {
break
}
rrules = drules
host = rwAns
}
return s.collectDNSRewrites(rrules, dReq.DNSType)
return s.collectDNSRewrites(rrules, lastCNAMERule, dReq.DNSType)
}
// collectDNSRewrites filters DNSRewrite by question type.
func (s *DefaultStorage) collectDNSRewrites(
rewrites []*rules.NetworkRule,
cnameRule *rules.NetworkRule,
qtyp uint16,
) (rws []*rules.DNSRewrite) {
if cnameRule != nil {
rewrites = append([]*rules.NetworkRule{cnameRule}, rewrites...)
}
for _, rewrite := range rewrites {
dnsRewrite := rewrite.DNSRewrite
if matchesQType(dnsRewrite, qtyp) {
@@ -152,8 +147,8 @@ func (s *DefaultStorage) rewriteRulesForReq(dReq *urlfilter.DNSRequest) (rules [
return res.DNSRewrites()
}
// Add implements the [Storage] interface for *DefaultStorage.
func (s *DefaultStorage) Add(item *Item) (err error) {
// Add implements the [RewriteStorage] interface for *DefaultStorage.
func (s *DefaultStorage) Add(item *filtering.RewriteItem) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -163,16 +158,16 @@ func (s *DefaultStorage) Add(item *Item) (err error) {
return s.resetRules()
}
// Remove implements the [Storage] interface for *DefaultStorage.
func (s *DefaultStorage) Remove(item *Item) (err error) {
// Remove implements the [RewriteStorage] interface for *DefaultStorage.
func (s *DefaultStorage) Remove(item *filtering.RewriteItem) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
arr := []*Item{}
arr := []*filtering.RewriteItem{}
// TODO(d.kolyshev): Use slices.IndexFunc + slices.Delete?
for _, ent := range s.rewrites {
if ent.equal(item) {
if ent.Equal(item) {
log.Debug("rewrite: removed element: %s -> %s", ent.Domain, ent.Answer)
continue
@@ -185,8 +180,8 @@ func (s *DefaultStorage) Remove(item *Item) (err error) {
return s.resetRules()
}
// List implements the [Storage] interface for *DefaultStorage.
func (s *DefaultStorage) List() (items []*Item) {
// List implements the [RewriteStorage] interface for *DefaultStorage.
func (s *DefaultStorage) List() (items []*filtering.RewriteItem) {
s.mu.RLock()
defer s.mu.RUnlock()
@@ -198,7 +193,7 @@ func (s *DefaultStorage) resetRules() (err error) {
// TODO(a.garipov): Use strings.Builder.
var rulesText []string
for _, rewrite := range s.rewrites {
rulesText = append(rulesText, rewrite.toRule())
rulesText = append(rulesText, toRule(rewrite))
}
strList := &filterlist.StringRuleList{
@@ -222,20 +217,60 @@ func (s *DefaultStorage) resetRules() (err error) {
// matchesQType returns true if dnsrewrite matches the question type qt.
func matchesQType(dnsrr *rules.DNSRewrite, qt uint16) (ok bool) {
// Add CNAMEs, since they match for all types requests.
if dnsrr.RRType == dns.TypeCNAME {
switch qt {
case dns.TypeA:
return dnsrr.RRType != dns.TypeAAAA
case dns.TypeAAAA:
return dnsrr.RRType != dns.TypeA
default:
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.
func isWildcard(pat string) (res bool) {
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"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/urlfilter"
"github.com/AdguardTeam/urlfilter/rules"
"github.com/miekg/dns"
@@ -12,32 +13,32 @@ import (
)
func TestNewDefaultStorage(t *testing.T) {
items := []*Item{{
items := []*filtering.RewriteItem{{
Domain: "example.com",
Answer: "answer.com",
}}
s, err := NewDefaultStorage(-1, items)
s, err := NewDefaultStorage(items)
require.NoError(t, err)
require.Len(t, s.List(), 1)
}
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.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)
require.NoError(t, err)
list := s.List()
require.Len(t, list, 1)
require.True(t, item.equal(list[0]))
require.True(t, item.Equal(list[0]))
err = s.Remove(item)
require.NoError(t, err)
@@ -45,7 +46,7 @@ func TestDefaultStorage_CRUD(t *testing.T) {
}
func TestDefaultStorage_MatchRequest(t *testing.T) {
items := []*Item{{
items := []*filtering.RewriteItem{{
// This one and below are about CNAME, A and AAAA.
Domain: "somecname",
Answer: "somehost.com",
@@ -101,7 +102,7 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
Answer: "sub.issue4016.com",
}}
s, err := NewDefaultStorage(-1, items)
s, err := NewDefaultStorage(items)
require.NoError(t, err)
testCases := []struct {
@@ -115,14 +116,39 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
wantDNSRewrites: nil,
dtyp: dns.TypeA,
}, {
name: "not_filtered_qtype",
host: "www.host.com",
wantDNSRewrites: nil,
dtyp: dns.TypeMX,
name: "other_qtype",
host: "www.host.com",
wantDNSRewrites: []*rules.DNSRewrite{{
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",
host: "www.host.com",
wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "host.com",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.IP{1, 2, 3, 4}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
@@ -138,6 +164,11 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
name: "rewritten_aaaa",
host: "www.host.com",
wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "host.com",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.ParseIP("1:2:3::4"),
NewCNAME: "",
RCode: dns.RcodeSuccess,
@@ -154,21 +185,30 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
RRType: dns.TypeA,
}},
dtyp: dns.TypeA,
//}, {
// TODO(d.kolyshev): This is about matching in urlfilter.
// name: "wildcard_override",
// host: "a.host.com",
// wantDNSRewrites: []*rules.DNSRewrite{{
// Value: net.IP{1, 2, 3, 4}.To16(),
// NewCNAME: "",
// RCode: dns.RcodeSuccess,
// RRType: dns.TypeA,
// }},
// dtyp: dns.TypeA,
}, {
name: "wildcard_override",
host: "a.host.com",
wantDNSRewrites: []*rules.DNSRewrite{{
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,
}},
dtyp: dns.TypeA,
}, {
name: "wildcard_cname_interaction",
host: "www.host2.com",
wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "host.com",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.IP{1, 2, 3, 4}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
@@ -184,6 +224,11 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
name: "two_cnames",
host: "b.host.com",
wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "somehost.com",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.IP{0, 0, 0, 0}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
@@ -194,6 +239,11 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
name: "two_cnames_and_wildcard",
host: "b.host3.com",
wantDNSRewrites: []*rules.DNSRewrite{{
Value: nil,
NewCNAME: "x.host.com",
RCode: dns.RcodeSuccess,
RRType: dns.TypeNone,
}, {
Value: net.IP{1, 2, 3, 5}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
@@ -221,10 +271,15 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
}},
dtyp: dns.TypeA,
}, {
name: "issue4008",
host: "somehost.com",
wantDNSRewrites: nil,
dtyp: dns.TypeHTTPS,
name: "issue4008",
host: "somehost.com",
wantDNSRewrites: []*rules.DNSRewrite{{
Value: net.IP{0, 0, 0, 0}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
RRType: dns.TypeA,
}},
dtyp: dns.TypeHTTPS,
}, {
name: "issue4016",
host: "www.issue4016.com",
@@ -256,7 +311,7 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
// Exact host, wildcard L2, wildcard L3.
items := []*Item{{
items := []*filtering.RewriteItem{{
Domain: "host.com",
Answer: "1.1.1.1",
}, {
@@ -267,7 +322,7 @@ func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
Answer: "3.3.3.3",
}}
s, err := NewDefaultStorage(-1, items)
s, err := NewDefaultStorage(items)
require.NoError(t, err)
testCases := []struct {
@@ -295,17 +350,21 @@ func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
RRType: dns.TypeA,
}},
dtyp: dns.TypeA,
//}, {
// TODO(d.kolyshev): This is about matching in urlfilter.
// name: "l3_match",
// host: "my.sub.host.com",
// wantDNSRewrites: []*rules.DNSRewrite{{
// Value: net.IP{3, 3, 3, 3}.To16(),
// NewCNAME: "",
// RCode: dns.RcodeSuccess,
// RRType: dns.TypeA,
// }},
// dtyp: dns.TypeA,
}, {
name: "l3_match",
host: "my.sub.host.com",
wantDNSRewrites: []*rules.DNSRewrite{{
Value: net.IP{3, 3, 3, 3}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
RRType: dns.TypeA,
}, {
Value: net.IP{2, 2, 2, 2}.To16(),
NewCNAME: "",
RCode: dns.RcodeSuccess,
RRType: dns.TypeA,
}},
dtyp: dns.TypeA,
}}
for _, tc := range testCases {
@@ -322,7 +381,7 @@ func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
// Wildcard and exception for a sub-domain.
items := []*Item{{
items := []*filtering.RewriteItem{{
Domain: "*.host.com",
Answer: "2.2.2.2",
}, {
@@ -330,10 +389,10 @@ func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
Answer: "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)
testCases := []struct {
@@ -356,12 +415,79 @@ func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
host: "sub.host.com",
wantDNSRewrites: nil,
dtyp: dns.TypeA,
//}, {
// TODO(d.kolyshev): This is about matching in urlfilter.
// name: "exception_wildcard",
// host: "my.sub.host.com",
// wantDNSRewrites: nil,
// dtyp: dns.TypeA,
}, {
name: "exception_wildcard",
host: "my.sub.host.com",
wantDNSRewrites: nil,
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 {
@@ -378,7 +504,7 @@ func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
func TestDefaultStorage_MatchRequest_ExceptionIP(t *testing.T) {
// Exception for AAAA record.
items := []*Item{{
items := []*filtering.RewriteItem{{
Domain: "host.com",
Answer: "1.2.3.4",
}, {
@@ -395,7 +521,7 @@ func TestDefaultStorage_MatchRequest_ExceptionIP(t *testing.T) {
Answer: "A",
}}
s, err := NewDefaultStorage(-1, items)
s, err := NewDefaultStorage(items)
require.NoError(t, err)
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"
)
// TODO(d.kolyshev): Use [rewrite.Item] instead.
type rewriteEntryJSON struct {
Domain string `json:"domain"`
Answer string `json:"answer"`
}
// handleRewriteList is the handler for the GET /control/rewrite/list HTTP API.
func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
arr := []*rewriteEntryJSON{}
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)
_ = aghhttp.WriteJSONResponse(w, r, d.rewriteStorage.List())
}
// handleRewriteAdd is the handler for the POST /control/rewrite/add HTTP API.
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
rwJSON := rewriteEntryJSON{}
err := json.NewDecoder(r.Body).Decode(&rwJSON)
rw := &RewriteItem{}
err := json.NewDecoder(r.Body).Decode(rw)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
rw := &LegacyRewrite{
Domain: rwJSON.Domain,
Answer: rwJSON.Answer,
}
err = rw.normalize()
err = d.rewriteStorage.Add(rw)
if err != nil {
// Shouldn't happen currently, since normalize only returns a non-nil
// error when a rewrite is nil, but be change-proof.
aghhttp.Error(r, w, http.StatusBadRequest, "normalizing: %s", err)
aghhttp.Error(r, w, http.StatusBadRequest, "add rewrite: %s", err)
return
}
log.Debug("rewrite: added element: %s -> %s", rw.Domain, rw.Answer)
d.confLock.Lock()
d.Config.Rewrites = append(d.Config.Rewrites, rw)
d.Config.Rewrites = d.rewriteStorage.List()
d.confLock.Unlock()
log.Debug("rewrite: added element: %s -> %s [%d]", rw.Domain, rw.Answer, len(d.Config.Rewrites))
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) {
jsent := rewriteEntryJSON{}
err := json.NewDecoder(r.Body).Decode(&jsent)
entDel := RewriteItem{}
err := json.NewDecoder(r.Body).Decode(&entDel)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
entDel := &LegacyRewrite{
Domain: jsent.Domain,
Answer: jsent.Answer,
err = d.rewriteStorage.Remove(&entDel)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "remove rewrite: %s", err)
return
}
arr := []*LegacyRewrite{}
d.confLock.Lock()
for _, ent := range d.Config.Rewrites {
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.Config.Rewrites = d.rewriteStorage.List()
d.confLock.Unlock()
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{
"||aus.social^",
"||awscommunity.social^",
"||dju.social^",
"||dresden.network^",
"||fedibird.com^",
"||fosstodon.org^",
@@ -261,11 +260,11 @@ var blockedServices = []blockedService{{
"||h4.io^",
"||hachyderm.io^",
"||hessen.social^",
"||hispagatos.space^",
"||home.social^",
"||hostux.social^",
"||ieji.de^",
"||indieweb.social^",
"||infosec.exchange^",
"||ioc.exchange^",
"||kolektiva.social^",
"||livellosegreto.it^",
@@ -287,9 +286,11 @@ var blockedServices = []blockedService{{
"||mastodon.nu^",
"||mastodon.nz^",
"||mastodon.online^",
"||mastodon.online^",
"||mastodon.scot^",
"||mastodon.sdf.org^",
"||mastodon.social^",
"||mastodon.social^",
"||mastodon.top^",
"||mastodon.uno^",
"||mastodon.world^",
@@ -309,7 +310,6 @@ var blockedServices = []blockedService{{
"||mstdn.social^",
"||muenchen.social^",
"||muenster.im^",
"||nerdculture.de^",
"||newsie.social^",
"||noc.social^",
"||norden.social^",
@@ -335,21 +335,21 @@ var blockedServices = []blockedService{{
"||social.vivaldi.net^",
"||sself.co^",
"||sueden.social^",
"||tech.lgbt^",
"||techhub.social^",
"||theblower.au^",
"||tkz.one^",
"||todon.eu^",
"||toot.aquilenet.fr^",
"||toot.community^",
"||toot.funami.tech^",
"||toot.wales^",
"||troet.cafe^",
"||uiuxdev.social^",
"||twingyeo.kr^",
"||union.place^",
"||universeodon.com^",
"||urbanists.social^",
"||vocalodon.net^",
"||wxw.moe^",
"||xarxa.cloud^",
},
}, {
ID: "minecraft",
@@ -540,6 +540,7 @@ var blockedServices = []blockedService{{
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>"),
Rules: []string{
"||pscp.tv^",
"||t.co^",
"||twimg.com^",
"||twitter.com^",

View File

@@ -278,15 +278,20 @@ var config = &configuration{
PortDNSOverTLS: defaultPortTLS, // needs to be passed through to dnsproxy
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{{
Filter: filtering.Filter{ID: 1},
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",
}, {
Filter: filtering.Filter{ID: 2},
Enabled: false,
URL: "https://adaway.org/hosts.txt",
URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt",
Name: "AdAway Default Blocklist",
}},
DHCP: &dhcpd.ServerConfig{

View File

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

View File

@@ -9,9 +9,12 @@ import (
"path/filepath"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rewrite"
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/dnsproxy/proxy"
@@ -39,17 +42,13 @@ func onConfigModified() {
}
}
// initDNSServer creates an instance of the dnsforward.Server
// Please note that we must do it even if we don't start it
// so that we had access to the query log and the stats
func initDNSServer() (err error) {
// initDNS updates all the fields of the [Context] needed to initialize the DNS
// server and initializes it at last. It also must not be called unless
// [config] and [Context] are initialized.
func initDNS() (err error) {
baseDir := Context.getDataDir()
var anonFunc aghnet.IPMutFunc
if config.DNS.AnonymizeClientIP {
anonFunc = querylog.AnonymizeIP
}
anonymizer := aghnet.NewIPMut(anonFunc)
anonymizer := config.anonymizer()
statsConf := stats.Config{
Filename: filepath.Join(baseDir, "stats.db"),
@@ -76,40 +75,57 @@ func initDNSServer() (err error) {
}
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 {
// Don't wrap the error, since it's informative enough as is.
return err
}
var privateNets netutil.SubnetSet
switch len(config.DNS.PrivateNets) {
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)
}
tlsConf := &tlsConfigSettings{}
Context.tls.WriteDiskConfig(tlsConf)
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{
DNSFilter: Context.filters,
Stats: Context.stats,
QueryLog: Context.queryLog,
DNSFilter: filters,
Stats: sts,
QueryLog: qlog,
PrivateNets: privateNets,
Anonymizer: anonymizer,
LocalDomain: config.DHCP.LocalDomainName,
DHCPServer: Context.dhcpServer,
DHCPServer: dhcpSrv,
}
Context.dnsServer, err = dnsforward.NewServer(p)
@@ -120,15 +136,15 @@ func initDNSServer() (err error) {
}
Context.clients.dnsServer = Context.dnsServer
var dnsConfig dnsforward.ServerConfig
dnsConfig, err = generateServerConfig()
dnsConf, err := generateServerConfig(tlsConf, httpReg)
if err != nil {
closeDNSServer()
return fmt.Errorf("generateServerConfig: %w", err)
}
err = Context.dnsServer.Prepare(&dnsConfig)
err = Context.dnsServer.Prepare(&dnsConf)
if err != nil {
closeDNSServer()
@@ -146,6 +162,32 @@ func initDNSServer() (err error) {
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 {
return Context.dnsServer != nil && Context.dnsServer.IsRunning()
}
@@ -193,7 +235,10 @@ func ipsToUDPAddrs(ips []netip.Addr, port int) (udpAddrs []*net.UDPAddr) {
return udpAddrs
}
func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
func generateServerConfig(
tlsConf *tlsConfigSettings,
httpReg aghhttp.RegisterFunc,
) (newConf dnsforward.ServerConfig, err error) {
dnsConf := config.DNS
hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()})
newConf = dnsforward.ServerConfig{
@@ -201,12 +246,10 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port),
FilteringConfig: dnsConf.FilteringConfig,
ConfigModified: onConfigModified,
HTTPRegister: httpRegister,
HTTPRegister: httpReg,
OnDNSRequest: onDNSRequest,
}
tlsConf := tlsConfigSettings{}
Context.tls.WriteDiskConfig(&tlsConf)
if tlsConf.Enabled {
newConf.TLSConfig = tlsConf.TLSConfig
newConf.TLSConfig.ServerName = tlsConf.ServerName
@@ -224,7 +267,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
}
if tlsConf.PortDNSCrypt != 0 {
newConf.DNSCryptConfig, err = newDNSCrypt(hosts, tlsConf)
newConf.DNSCryptConfig, err = newDNSCrypt(hosts, *tlsConf)
if err != nil {
// Don't wrap the error, because it's already
// wrapped by newDNSCrypt.
@@ -413,7 +456,11 @@ func startDNSServer() error {
func reconfigureDNSServer() (err error) {
var newConf dnsforward.ServerConfig
newConf, err = generateServerConfig()
tlsConf := &tlsConfigSettings{}
Context.tls.WriteDiskConfig(tlsConf)
newConf, err = generateServerConfig(tlsConf, httpRegister)
if err != nil {
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)
fatalOnError(err)
// TODO(e.burkov): This could be made earlier, probably as the option's
// effect.
cmdlineUpdate(opts)
if !Context.firstRun {
// Save the updated config
err = config.write()
@@ -522,7 +526,7 @@ func run(opts options, clientBuildFS fs.FS) {
fatalOnError(err)
if !Context.firstRun {
err = initDNSServer()
err = initDNS()
fatalOnError(err)
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()
// wait indefinitely for other go-routines to complete their job
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.
func startMods() error {
err := initDNSServer()
func startMods() (err error) {
err = initDNS()
if err != nil {
return err
}
@@ -927,8 +935,8 @@ func getHTTPProxy(_ *http.Request) (*url.URL, error) {
// jsonError is a generic JSON error response.
//
// TODO(a.garipov): Merge together with the implementations in .../dhcpd and
// other packages after refactoring the web handler registering.
// TODO(a.garipov): Merge together with the implementations in [dhcpd] and other
// packages after refactoring the web handler registering.
type jsonError struct {
// Message is the error message, an opaque string.
Message string `json:"message"`
@@ -940,30 +948,40 @@ func cmdlineUpdate(opts options) {
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("update not allowed on first run")
log.Info("cmdline update: performing update")
os.Exit(0)
}
_, err := Context.updater.VersionInfo(true)
updater := Context.updater
info, err := updater.VersionInfo(true)
if err != nil {
vcu := Context.updater.VersionCheckURL()
vcu := updater.VersionCheckURL()
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")
os.Exit(0)
}
err = Context.updater.Update()
err = updater.Update(Context.firstRun)
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)
}

View File

@@ -229,7 +229,7 @@ var cmdLineOpts = []cmdLineOpt{{
updateNoValue: func(o options) (options, error) { o.performUpdate = true; return o, nil },
effect: nil,
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",
shortName: "",
}, {

View File

@@ -159,6 +159,38 @@ func sendSigReload() {
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:
//
// - install: Installs a service/daemon.

View File

@@ -7,6 +7,8 @@ import (
"github.com/kardianos/service"
)
// chooseSystem checks the current system detected and substitutes it with local
// implementation if needed.
func chooseSystem() {
sys := service.ChosenSystem()
// 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.
const sysVersion = "openbsd-runcom"
// chooseSystem checks the current system detected and substitutes it with local
// implementation if needed.
func chooseSystem() {
service.ChooseSystem(openbsdSystem{})
}

View File

@@ -180,7 +180,7 @@ func withRecovered(orig *error) {
// type check
var _ Interface = (*StatsCtx)(nil)
// Start implements the Interface interface for *StatsCtx.
// Start implements the [Interface] interface for *StatsCtx.
func (s *StatsCtx) Start() {
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)
}
u.prevCheckTime = time.Now()
u.prevCheckTime = now
u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body)
return u.prevCheckResult, u.prevCheckError
@@ -92,7 +92,11 @@ func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) {
info.AnnouncementURL = versionJSON["announcement_url"]
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.packageURL = packageURL

View File

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

View File

@@ -136,10 +136,10 @@ func TestUpdate(t *testing.T) {
u.packageURL = fakeURL.String()
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.check())
require.NoError(t, u.backup())
require.NoError(t, u.backup(false))
require.NoError(t, u.replace())
u.clean()
@@ -215,10 +215,10 @@ func TestUpdateWindows(t *testing.T) {
u.packageURL = fakeURL.String()
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())
// assert.Nil(t, u.check())
require.NoError(t, u.backup())
require.NoError(t, u.backup(false))
require.NoError(t, u.replace())
u.clean()